Android StudioのActivityTemplate全部試してみた!(2/2)

前回に引き続き、AndroidStudioのActivityのサンプルを試してみます。 残り6個!

目次

  • LoginActivity
  • Master/Detail Flow
  • TabbedActivity
  • SettingsActivity
  • ScrollingActivity
  • NavigationDrawer

LoginActivity

f:id:t-miliya612:20180510205338p:plain

特徴

  • 一般的なログインフォーム
  • 補完機能などUIが充実

Source

xml

<!-- プログレスバー -->
<ProgressBar
    android:id="@+id/login_progress"
    style="?android:attr/progressBarStyleLarge"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:visibility="gone" />
<!-- メールアドレスの補完機能がついたTextView -->
<AutoCompleteTextView
    android:id="@+id/email"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/prompt_email"
    android:inputType="textEmailAddress"
    android:maxLines="1"
    android:singleLine="true" />

自動補完用のTextViewなんてあったんですね。

Activity

長いのでメソッド名だけ、

  • permission処理
    • mayRequestContacts()
    • onRequestPermissionResult(Int, Array, IntArray)
  • 連絡帳アクセス
    • onCreateLoader(Int, Bundle?)
    • onLoadFinished(Loader>Cursor>, Cursor)
  • textViewに補完
    • addEmailsToAutoComplete(List)
  • 通信処理
    • inner class UserLoginTask
  • プログレスダイアログ
    • showProgress(Boolean)

機能だけでなく、UX的な面でも参考になります……!

Master/Detail Flow

f:id:t-miliya612:20180510212411p:plain

特徴

  • リストをタップすると詳細画面が表示される
  • スマホだと画面遷移、タブレットだとペイン表示

Source

基本は一般的なActivityとFragmentです。特徴的なのは、出し分けのロジックですね。
今回はこれを2つのitem_list.xmlの呼び分けで実現しています。app/src/main/res/layoutにあるものが呼ばれていればスマホサイズ、app/src/main/res/layout-w900dpのものが呼ばれていればタブレットサイズとなります。

Activity

if (item_detail_container != null) {
    // The detail container view will be present only in the
    // large-screen layouts (res/values-w900dp).
    // If this view is present, then the
    // activity should be in two-pane mode.
    twoPane = true
}
onClickListener = View.OnClickListener { v ->
    val item = v.tag as DummyContent.DummyItem
    // タブレットかどうか
    if (twoPane) {
        val fragment = ItemDetailFragment().apply {
            arguments = Bundle().apply {
                putString(ItemDetailFragment.ARG_ITEM_ID, item.id)
            }
        }
        parentActivity.supportFragmentManager
                .beginTransaction()
                .replace(R.id.item_detail_container, fragment)
                .commit()
    } else {
        val intent = Intent(v.context, ItemDetailActivity::class.java).apply {
            putExtra(ItemDetailFragment.ARG_ITEM_ID, item.id)
        }
        v.context.startActivity(intent)
    }
}

TabbedActivity

特徴

3種類のスタイルがあります。

  • Swipe Views

f:id:t-miliya612:20180510204831p:plain

  • Action Bar Tabs

f:id:t-miliya612:20180510204917p:plain

  • Action Bar Spinner

f:id:t-miliya612:20180510204928p:plain

が、、、全体的に上手くいきませんでした。
SwipeViewsはデザインが違い、その他はそもそもビルドが出来ませんでした。
AndroidStudioのバージョンにもよるのでしょうが、別途自分で書いてみます。

SettingsActivity

f:id:t-miliya612:20180510205219p:plainf:id:t-miliya612:20180510205225p:plain

特徴

アプリの設定画面を生成してくれます。
アプリ内で完結したいものは、OSの設定画面へと遷移してくれます。

Source

AppCompatPreferenceAcrivityでPreferenceごとにFragmentを定義しています。

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
class GeneralPreferenceFragment : PreferenceFragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        addPreferencesFromResource(R.xml.pref_general)
        setHasOptionsMenu(true)

        // Bind the summaries of EditText/List/Dialog/Ringtone preferences
        // to their values. When their values change, their summaries are
        // updated to reflect the new value, per the Android Design
        // guidelines.
        bindPreferenceSummaryToValue(findPreference("example_text"))
        bindPreferenceSummaryToValue(findPreference("example_list"))
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        val id = item.itemId
        if (id == android.R.id.home) {
            startActivity(Intent(activity, SettingsActivity::class.java))
            return true
        }
        return super.onOptionsItemSelected(item)
    }
}

の繰り返しです。

また、xml/pref_xxxでpreferenceごとの画面を定義しています。

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- NOTE: Hide buttons to simplify the UI. Users can touch outside the dialog to
         dismiss it. -->
    <!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
    <ListPreference
        android:defaultValue="180"
        android:entries="@array/pref_sync_frequency_titles"
        android:entryValues="@array/pref_sync_frequency_values"
        android:key="sync_frequency"
        android:negativeButtonText="@null"
        android:positiveButtonText="@null"
        android:title="@string/pref_title_sync_frequency" />

    <!-- This preference simply launches an intent when selected. Use this UI sparingly, per
         design guidelines. -->
    <Preference android:title="@string/pref_title_system_sync_settings">
        <intent android:action="android.settings.SYNC_SETTINGS" />
    </Preference>

</PreferenceScreen>

こうやって使うんですね。

ScrollingActivity

f:id:t-miliya612:20180510205238p:plain

特徴

  • CollapsingToolbarLayoutの中に配置したAppBarの大きさが可変になる

Source

そのままじゃ上手く表示されなかったのでちょっと修正……

以前紹介した本を参考にしました。

t-miliya612.hatenablog.com

xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:elevation="10dp">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/toolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@color/colorPrimary"
                app:layout_collapseMode="pin" />

        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ScrollingActivity"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_mar[f:id:t-miliya612:20180510205238p:plain]gin="@dimen/text_margin"
            android:text="@string/large_text" />

    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>

app:layout_behavior="@string/appbar_scrolling_view_behavior"を忘れるとコンテントの文字が被ってしまうので要注意です。

NavigationDrawer

f:id:t-miliya612:20180510205432p:plain

特徴

  • 左側から出てくるタイプのナビゲーション

Source

設定は結構簡単でした。

Activity

val toggle = ActionBarDrawerToggle(
        this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()

nav_view.setNavigationItemSelectedListener(this)

activity_navigation_drawer.xml

...

    <android.support.design.widget.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/nav_header_navigation_drawer"
        app:menu="@menu/activity_navigation_drawer_drawer" />

...

layout/nav_header_navigation_drawer.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="@dimen/nav_header_height"
    android:background="@drawable/side_nav_bar"
    android:gravity="bottom"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:theme="@style/ThemeOverlay.AppCompat.Dark">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@string/nav_header_desc"
        android:paddingTop="@dimen/nav_header_vertical_spacing"
        app:srcCompat="@mipmap/ic_launcher_round" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="@dimen/nav_header_vertical_spacing"
        android:text="@string/nav_header_title"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/nav_header_subtitle" />

</LinearLayout>

menu/activity_navigation_drawer_drawer.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">

    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_camera"
            android:icon="@drawable/ic_menu_camera"
            android:title="Import" />
        <item
            android:id="@+id/nav_gallery"
            android:icon="@drawable/ic_menu_gallery"
            android:title="Gallery" />
        <item
            android:id="@+id/nav_slideshow"
            android:icon="@drawable/ic_menu_slideshow"
            android:title="Slideshow" />
        <item
            android:id="@+id/nav_manage"
            android:icon="@drawable/ic_menu_manage"
            android:title="Tools" />
    </group>

    <item android:title="Communicate">
        <menu>
            <item
                android:id="@+id/nav_share"
                android:icon="@drawable/ic_menu_share"
                android:title="Share" />
            <item
                android:id="@+id/nav_send"
                android:icon="@drawable/ic_menu_send"
                android:title="Send" />
        </menu>
    </item>

</menu>

まとめ

全体的に学習材料として面白かったです。ただ、Tab系のActivityは謎でした……結構使いそうな気がするんだけどなぁ。
初めて実装する際には、手直しのことも考えるとEmptyActivityで頑張った方がよさそうですね。

そして、 初めから全部で2記事って決めると大変つらいことになる という学びを得ました。以上です。

Android StudioのActivityTemplate全部試してみた!(1/2)

AndroidStudioでActivityを追加するとき、とりあえず何も考えずにEmptyActivityを選択していました。

f:id:t-miliya612:20180506150334p:plain

これ他の選ぶとどうなってるんだろう、とふと気になってしまい、、、
とりあえず全部遊んでみることにしました。

リポジトリはこちら

github.com

目次(1/2)

Basic Activity

f:id:t-miliya612:20180506150519p:plain

特徴

  • Toolbarが設定されている
  • FloatActionButtonが配置されている
    • onClickListenerでToastを表示
  • content_{ActivityName}の名前で空のConstraintLayoutがincludeされている

特に難しいものはないですね。ToolbarやFABを生成してくれるのは使い勝手が良さそうです。

BottomNavigationActivity

f:id:t-miliya612:20180506150751p:plain

  • BottomNavigationの配置
    • 遷移するとtextが変更
    • 選択したアイコンは色が変わってサイズが少し大きくなる
  • res/menuを使用したデータ受け渡し (あれ、menuなんて使ったことなかった……これめっちゃ便利……)

FullscreenActivity

f:id:t-miliya612:20180506150804p:plain
Fullscreen表示
f:id:t-miliya612:20180506150809p:plain
Fullscreen表示解除時

  • Fullscreen表示に対応
  • タップしたらFullscreenの解除、設定のトグル

面白そうなのでソースも追っていきます。分解して見ていきましょう

Source

  • onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_fullscreen)
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

    mVisible = true

    // Set up the user interaction to manually show or hide the system UI.
    fullscreen_content.setOnClickListener { toggle() }

    // Upon interacting with UI controls, delay any scheduled hide()
    // operations to prevent the jarring behavior of controls going away
    // while interacting with the UI.
    dummy_button.setOnTouchListener(mDelayHideTouchListener)
}
  • toggle()
private fun toggle() {
    if (mVisible) {
        hide()
    } else {
        show()
    }
}
  • hide()
private fun hide() {
    // Hide UI first
    supportActionBar?.hide()
    fullscreen_content_controls.visibility = View.GONE
    mVisible = false

    // Schedule a runnable to remove the status and navigation bar after a delay
    mHideHandler.removeCallbacks(mShowPart2Runnable)
    mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY.toLong())
}
  • mShowPart2Runnable
private val mShowPart2Runnable = Runnable {
    // Delayed display of UI elements
    supportActionBar?.show()
    fullscreen_content_controls.visibility = View.VISIBLE
}
  • mHidePart2Runnable
private val mHidePart2Runnable = Runnable {
    // Delayed removal of status and navigation bar

    // Note that some of these constants are new as of API 16 (Jelly Bean)
    // and API 19 (KitKat). It is safe to use them, as they are inlined
    // at compile-time and do nothing on earlier devices.
    fullscreen_content.systemUiVisibility =
            View.SYSTEM_UI_FLAG_LOW_PROFILE or
            View.SYSTEM_UI_FLAG_FULLSCREEN or
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
            View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
}

systemUiVisibilityについては、こちらの記事が詳しいです。Android4.xの時期、ハードの物理ボタンが無くなった時期に生まれたんですね。Xperia acroとか懐かしい……。

y-anz-m.blogspot.jp

GoogleAdMobAdsActivity

f:id:t-miliya612:20180506151209p:plain
Interstitialタイプ
f:id:t-miliya612:20180506151217p:plain
Bannerタイプ

特徴

  • GoogleのAdMobを使用する
  • Interstitial(全画面)タイプの広告、Bannerタイプの広告の2種類がある

Source

Interstitialの場合もBannerの場合も、呼んでるAPIは変わらないみたいです。以下、Interstitialの例です。

build.gradle

dependencies {
    implementation 'com.google.android.gms:play-services-ads:15.0.1'

Activity

// Create the InterstitialAd and set the adUnitId (defined in values/strings.xml).
interstitialAd = newInterstitialAd()
loadInterstitial()

...

private fun newInterstitialAd(): InterstitialAd {
    return InterstitialAd(this).apply {
        adUnitId = getString(R.string.interstitial_ad_unit_id)
        adListener = object : AdListener() {
            override fun onAdLoaded() {
                next_level_button.isEnabled = true
            }

            override fun onAdFailedToLoad(errorCode: Int) {
                next_level_button.isEnabled = true
            }

            override fun onAdClosed() {
                // Proceed to the next level.
                goToNextLevel()
            }
        }
    }
}

private fun showInterstitial() {
    // Show the ad if it's ready. Otherwise toast and reload the ad.
    if (interstitialAd?.isLoaded == true) {
        interstitialAd?.show()
    } else {
        Toast.makeText(this, "Ad did not load", Toast.LENGTH_SHORT).show()
        goToNextLevel()
    }
}

private fun loadInterstitial() {
    // Disable the next level button and load the ad.
    next_level_button.isEnabled = false
    val adRequest = AdRequest.Builder()
            .setRequestAgent("android_studio:ad_template")
            .build()
    interstitialAd?.loadAd(adRequest)
}
...

基本的な流れとしては、

  • InterstitialAd(this).applyでInterstitialの広告を生成
  • AdRequest.Builder()で広告リクエストを作成
  • interstitialAd?.loadAd(adRequest)で実際に広告をリクエス

となっているようです。

GoogleMapsActivity

f:id:t-miliya612:20180506151222p:plain

特徴

  • GoogleMapを表示
  • 事前にgoogle developer consoleでGoogle MapsAPI keyの取得が必須
    • res/values/google_maps_api.xmlを参照

Source

コードはそんなに複雑ではありません。

build.gradle

dependencies {
    implementation 'com.google.android.gms:play-services-maps:15.0.1'

Activity

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

    private lateinit var mMap: GoogleMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_maps)
        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        val mapFragment = supportFragmentManager
                .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    /**
     * Manipulates the map once available.
     * This callback is triggered when the map is ready to be used.
     * This is where we can add markers or lines, add listeners or move the camera. In this case,
     * we just add a marker near Sydney, Australia.
     * If Google Play services is not installed on the device, the user will be prompted to install
     * it inside the SupportMapFragment. This method will only be triggered once the user has
     * installed Google Play services and returned to the app.
     */
    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap

        // Add a marker in Sydney and move the camera
        val sydney = LatLng(-34.0, 151.0)
        mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
        mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
    }
}

一旦今回はここまで!
残り6つはまた次回。。。

kotlin-stdlib-jreがdeprecatedになっていたので深掘りしてみた

環境

  • AndroidStudio 3.1.2
  • Kotlin 1.2.41

起こったこと

AndroidStudioで新しくプロジェクトを作ってみたら、{project}/app.gradleorg.jetbrains.kotlin:kotlin-stdlib-jre7がdeprecatedになっていました。

kotlin-stdlib-jre7 is deprecated since 1.2.0 and should be replaced with kotlin-stdlib-jdk7

f:id:t-miliya612:20180502173641p:plain

言われた通りjre-7からjdk-7に変えたら直りました。よかったです。

……だけで終わるのもアレなので、しっかり調べてみました。

kotlin-stdlibについて

まさにStandardなlibraryで、Kotlinのコア部分の実装を担っています。

The Kotlin Standard Library provides living essentials for everyday work with Kotlin. These include:
- Higher-order functions implementing idiomatic patterns (let, apply, use, synchronized, etc).
- Extension functions providing querying operations for collections (eager) and sequences (lazy).
- Various utilities for working with strings and char sequences.
- Extensions for JDK classes making it convenient to work with files, IO, and threading.

kotlin-stdlib - Kotlin Programming Language

kotlin-stdlibはコア部分、kotlin-stdlib-jre/jdkはextension的な部分っぽいですね。
似たような議論がこちらに。

discuss.kotlinlang.org

変更理由

Java9、特にその中でもモジュールシステムのサポートのためみたいです。

The Kotlin standard library is now fully compatible with the Java 9 module system, which forbids split packages (multiple jar files declaring classes in the same package). In order to support that, new artifacts kotlin-stdlib-jdk7 and kotlin-stdlib-jdk8 are introduced, which replace the old kotlin-stdlib-jre7 and kotlin-stdlib-jre8.

The declarations in the new artifacts are visible under the same package names from the Kotlin point of view, but have different package names for Java. Therefore, switching to the new artifacts will not require any changes to your source code.

What's New in Kotlin 1.2 - Kotlin Programming Language

モジュールシステムについて

  • 別名 Project Jigsaw
  • Packageの上位概念としてモジュールを作る

実際にKotlin内のコードでも使われていますね。

github.com

目的

  • 構成の信頼性の向上
    • モジュール間の依存性を明示的に宣言
    • コンパイル時、実行時に依存性の認識が可能
  • 強力なカプセル化
    • モジュールは明示的にエクスポートしない限り外部からのアクセス不可
    • エクスポートは全開放するか特定のモジュールにのみ解放するか選べる
  • スケーラブルなJavaプラットフォーム
    • プラットフォームがモジュールで分割される
    • 必要なものだけを集めたカスタムランタイムを作れる
      • ランタイムのサイズを削減できる
  • プラットフォームの整合性向上
    • 内部APIを完全にカプセル化できる
    • プラットフォーム内でアプリに使われたくないAPIを隠蔽できる
  • パフォーマンスの向上
    • これちょっとわかりませんでした。。。

http://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-SO17-Modules.pdf

エクスポートを明示的にできるのは良さそうですね!openが必要なのはKotlinの影響もあるんでしょうか。

所感

Android-Java-Kotlinと変数が増えると最新版を追っかけるのは大変ですね……。
モジュールシステムに関しては、gradleを使ったモジュールとの今後が気になります。試しにこれ使ってマルチモジュールのアプリ書いてみたいなぁ(伏線)
また、stdlibに関して、ネーミングの理由はよくわからないままでした。何故拡張版をjreなんてネーミングにしたんでしょう。

おまけ

AndroidのJava7サポートって完全じゃなかったようなーと思い調べてみた所、こんな記事が見つかりました。

tech.speee.jp

Androidの開発環境における「Javaのバージョン」はいくつかの考え方があり、自明ではありません。ここでは3つに分けて考えます。

ひとつはAndroid端末にインストールされているAndroid OS、それにバンドルされているDalvik処理系(仮想マシン)がどのJVMバージョンに相当するかというものです。

もうひとつは、Android端末にインストールされているAndroid OS、それにバンドルされているJava標準ライブラリのバージョンです。

最後に、Androidアプリケーションを開発する際にJavaコンパイラに与えるバージョンです。

あれ、そしたら結局AndroidってどのJava使えるの……?

Java 8 言語機能の使用  |  Android Developers

Android Studio では、特定の Java 8 言語機能とそれらを使用するサードパーティ ライブラリのビルトイン サポートを提供しています。

し、知らなかった……。
こちらからは以上です。

公式のcodelabでFlutterに入門してみた

今度Flutter Meetup Tokyo #2に参加するので、事前にちょっと手を動かしてみました。

flutter-jp.connpass.com

What’s Flutter

Flutter - Beautiful native apps in record time

Googleが開発しているmobile appのSDKで、Dartという言語を用いて開発します。
本家サイトによると、特徴は以下の5つ。

  • Fast development
    • ホットリロードが可能
  • Expressive and Flexible UI
  • Modern, reactive framework
  • Access native features and SDKs
    • プラットフォーム(OS)のAPIや3rd partyのSDK、ネイティブのコードを呼び出せる(素敵)
  • Unified app development
    • AndroidiOSも、プラットフォームの差を意識することなく開発可能
    • Mobile Appの開発経験がなくても大丈夫!
    • 開発、テスト、デバッグコンパイル、配布に至るまでサポートするツールが充実している

環境構築

環境

1. install Dart

$ brew tap dart-lang/dart

==> Tapping dart-lang/dart
Cloning into '/usr/local/Homebrew/Library/Taps/dart-lang/homebrew-dart'...
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 5 (delta 0), reused 2 (delta 0), pack-reused 0
Unpacking objects: 100% (5/5), done.
Tapped 1 formula (30 files, 26.3KB)

$ brew install dart --with-dartium --with-content-shell

==> Installing dart from dart-lang/dart
==> Downloading https://storage.googleapis.com/dart-archive/channels/stable/release/1.24.3/sdk/dartsdk-macos-x64-release.zip
######################################################################## 100.0%
==> Downloading https://storage.googleapis.com/dart-archive/channels/stable/release/1.24.3/dartium/dartium-macos-x64-release.zip
######################################################################## 100.0%
==> Downloading https://storage.googleapis.com/dart-archive/channels/stable/release/1.24.3/dartium/content_shell-macos-x64-relea
######################################################################## 100.0%
==> Caveats
Please note the path to the Dart SDK:
  /usr/local/opt/dart/libexec
==> Summary
🍺  /usr/local/Cellar/dart/1.24.3: 3,039 files, 657.7MB, built in 21 seconds

2. install Flutter

$ git clone https://github.com/flutter/flutter.git

Cloning into 'flutter'...
remote: Counting objects: 120517, done.
remote: Total 120517 (delta 0), reused 0 (delta 0), pack-reused 120516
Receiving objects: 100% (120517/120517), 36.60 MiB | 3.11 MiB/s, done.
Resolving deltas: 100% (90418/90418), done.

$ git co v0.3.1 # flutter公式ページのzip最新版がv0.3.1だったため(2018/04/29現在)
$ vim ~/.zprofile

export PATH=$HOME/flutter/bin:$PATH 

3. Set up environment

$ flutter doctor

Downloading Dart SDK from Flutter engine 09d05a38912a3c1a906e95099cac9a7e14fae85f...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 43.9M  100 43.9M    0     0  9507k      0  0:00:04  0:00:04 --:--:-- 9508k
Building flutter tool...

  ╔════════════════════════════════════════════════════════════════════════════╗
  ║                 Welcome to Flutter! - https://flutter.io                   ║
  ║                                                                            ║
  ║ The Flutter tool anonymously reports feature usage statistics and crash    ║
  ║ reports to Google in order to help Google contribute improvements to       ║
  ║ Flutter over time.                                                         ║
  ║                                                                            ║
  ║ Read about data we send with crash reports:                                ║
  ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting        ║
  ║                                                                            ║
  ║ See Google's privacy policy:                                               ║
  ║ https://www.google.com/intl/en/policies/privacy/                           ║
  ║                                                                            ║
  ║ Use "flutter config --no-analytics" to disable analytics and crash         ║
  ║ reporting.                                                                 ║
  ╚════════════════════════════════════════════════════════════════════════════╝
  
Downloading Material fonts...                                0.9s
Downloading package sky_engine...                            0.5s
Downloading common tools...                                  1.1s
Downloading darwin-x64 tools...                              1.9s
Downloading android-arm-profile/darwin-x64 tools...          0.7s
Downloading android-arm-release/darwin-x64 tools...          0.8s
Downloading android-arm64-profile/darwin-x64 tools...        0.8s
Downloading android-arm64-release/darwin-x64 tools...        0.8s
Downloading android-x86 tools...                             1.8s
Downloading android-x64 tools...                             1.5s
Downloading android-arm tools...                             1.6s
Downloading android-arm-profile tools...                     1.0s
Downloading android-arm-release tools...                     1.2s
Downloading android-arm64 tools...                           1.3s
Downloading android-arm64-profile tools...                   1.1s
Downloading android-arm64-release tools...                   1.0s
Downloading ios tools...                                     2.2s
Downloading ios-profile tools...                             2.1s
Downloading ios-release tools...                             1.9s
Downloading Gradle Wrapper...                                0.2s
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel unknown, v0.3.1, on Mac OS X 10.13.4 17E199, locale ja-JP)
-Error executing simctl: -6
dyld: Symbol not found: _SimDeviceBootKeyDisabledJobs
  Referenced from: /Applications/Xcode.app/Contents/Developer/usr/bin/simctl
  Expected in: /Library/Developer/PrivateFrameworks/CoreSimulator.framework/Versions/A/CoreSimulator
 in /Applications/Xcode.app/Contents/Developer/usr/bin/simctl

[!] Android toolchain - develop for Android devices (Android SDK 27.0.3)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses
[!] iOS toolchain - develop for iOS devices (Xcode 9.3)
    ✗ Xcode requires additional components to be installed in order to run.
      Launch Xcode and install additional required components when prompted.
    ✗ libimobiledevice and ideviceinstaller are not installed. To install, run:
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
        For more info, see https://flutter.io/platform-plugins
      To install:
        brew install cocoapods
        pod setup
[✓] Android Studio (version 3.1)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[!] IntelliJ IDEA Community Edition (version 2017.2.6)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[✓] VS Code (version 1.22.2)
[!] Connected devices
    ! No devices available

! Doctor found issues in 4 categories.

いくつか引っかかりました。

Android license

  • Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

$ flutter doctor --android-licenses

3 of 6 SDK package licenses not accepted. 100% Computing updates...             
Review licenses that have not been accepted (y/N)? y

1/3: License android-googletv-license:
2/3: License google-gdk-license:
3/3: License mips-android-sysimage-license:

iOS

  • Xcodeのcomponents
  • gem
    • libimobiledevice
    • ideviceinstaller
    • ios-deploy
    • CocoaPods

CocoaPods入れてなかったのがつらい。。。30分休み。

Android Studio

  • Flutter plugin
  • Dart plugin

Device

  • Device繋がってないよ!はい。

対応し、再挑戦してみます。

$ flutter doctor

master
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel unknown, v0.3.1, on Mac OS X 10.13.4 17E199, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK 27.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.3)
[✓] Android Studio (version 3.1)
[✓] VS Code (version 1.22.2)
[✓] Connected devices (2 available)

いい感じです。これで開発環境が整いました。

codelabについて

flutter.io

スタートアップの会社名候補を生成するシンプルなモバイルアプリを作っていきます。お気に入り機能があり、別のページでお気に入りの名前リストを表示することもできます。
f:id:t-miliya612:20180429233102p:plain

開発はAndroidStudioを使いましたが、VScodeなどを使っても良いそうです。

Step 1: Create the starting Flutter app

Flutterのプロジェクトを新規生成します。
Android Studioを使う場合はウィザードから、VScodeを使う場合はコマンドからアプリの雛形を生成できます。

f:id:t-miliya612:20180429124315p:plain
f:id:t-miliya612:20180429124335p:plain
f:id:t-miliya612:20180429124343p:plain
f:id:t-miliya612:20180429124355p:plain

ファイル構成はこんな感じです。

├── README.md
├── android # androidネイティブのコード
├── build
├── ios # iOSネイティブのコード
├── lib
│   └── main.dart # 手を加えていくファイル
├── pubspec.lock
├── pubspec.yaml # 依存関係などなどを管理するファイル
└── test
    └── widget_test.dart

終わったら、codelabにあるコードでlib/main.dartを置き換えましょう。

Step 2: Use an external package

english_wordsというプラグインを入れます。プラグインの追加は、以下の手順で行います。

  1. pubspec.yamlに追記
    • english_words: ^3.1.0
  2. Android Studioのタブに表示されているPackages Getをクリック f:id:t-miliya612:20180429124552p:plain
    • 以下が表示されればOK
${PATH_TO_FLUTTER}/flutter/bin/flutter --no-color packages get
Running "flutter packages get" in ${YOUR_APP_NAME}...
Process finished with exit code 0
  1. lib/main.dartにimportを追記
    • import 'package:english_words/english_words.dart';

後はいつも通り使うだけです。特に難しいところはなさそうです。

Step 3: Add a Stateful widget

WidgetにはStatefulとStatelessの2種類があります。画面を表示している間にWidgetを書き換えるかどうかで使い分けます。今回のアプリでは無限スクロールのListを描画するので、StatefulWidgetを拡張したクラスを使用します。
まずは中央に表示されたtextを試しにstatefulにしてみます。

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          child: new RandomWords(),
        ),
      ),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  createState() => new RandomWordsState();
}

class RandomWordsState extends State<RandomWords> {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return new Text(wordPair.asPascalCase);
  }
}

Step 4: Create an infinite scrolling ListView

いよいよ無限スクロールのListViewを生成します。ListView.builder()を返すWidgetを定義し、builderの引数として渡すitemBuilderプロパティの中でリスト生成のロジックを記述します。

(省略...)
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );
  }
  Widget _buildSuggestions() {
    return new ListView.builder( // ListView.builderを生成
      padding: const EdgeInsets.all(16.0),
      itemBuilder: (context, i){ // itemBuilderのプロパティにリストを描画する無名関数を定義
        if (i.isOdd) return new Divider();
        final index = i ~/ 2;
        if (index >= _suggestions.length) {
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      },
    );
  }

  // リスト1行をセットする
  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }
}

Step 5: Add interactivity

ハート型アイコンをセットして、タップするとお気に入り登録できるようにします。Iconsクラスがすごく充実していて、アイコン追加がかなり楽そうです。

docs.flutter.io

Androidで言うonClickメソッドは、onTapプロパティに対応します。

(省略...)
  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: new Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _saved.remove(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }

Step 6: Navigate to a new screen

画面遷移のコードを書いていきます。Flutterでは新しい画面のことをrouteと言うみたいですね。
routeのスタックは、Navigationクラスを使って管理していきます。

(省略...)
  void _pushSaved(){
    Navigator.of(context).push(
      new MaterialPageRoute(
          builder: (context) {
            // お気に入り登録されたリストを各行にマップ
            final tiles = _saved.map( 
                (pair) {
                  return new ListTile(
                    title: new Text(
                      pair.asPascalCase,
                      style: _biggerFont,
                    ),
                  );
                },
            );
            // リストを生成
            final divided = ListTile
              .divideTiles(
                  context: context,
                  tiles: tiles,
              )
              .toList();
            // appBarと画面のコンテンツ(リスト)をセット
            return new Scaffold(
              appBar: new AppBar(
                title: new Text('Saved Suggestions'),
              ),
              body: new ListView(children: divided),
            );
          },
      ),
    );
  }

Step 7: Change the UI using Themes

UIの色をいじってみます。今回はひとまず、primaryColorを変更してappBarの色を変えてみます。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      theme: new ThemeData(
        primaryColor: Colors.white,
      ),
      home: new RandomWords(),
    );
  }
}

簡単ですね!ただ、結構大胆にカスタマイズをしようとすると大変なことになりそうな。。。
MaterialDesignからどうしても離れなきゃいけないときは厳しそうですね。

作ったもの

github.com

f:id:t-miliya612:20180429233141p:plain
f:id:t-miliya612:20180429233147p:plain

まとめ

一通りアプリを完成させるところまでやってみました。かなり簡単にそれなりのUIを作成することができました。
イメージとしてはAnkoのような感じかなーと思っていたのですが、ちょっと違う印象でした。ReactやVueなどのライブラリは使ったことがないのですが、そういったjsのライブラリで培ったノウハウが必要になりそうです。

とはいえ、これでFlutterのことが何となく完全に理解できました。今からmeetupが楽しみです。

今後やりたいこと

  • ファイル構成を考える
    • ファイルってどうやって分けていくんだろ
    • いい加減Reactやらねば。。。
  • OS依存の機能を使う
    • network通信
    • GPS
    • camera
    • SMS(出来るかな……?)

#技術書典 に行ってきたからまずは買ったものをまとめておく

昨日、技術書典4に行ってきました。前回に引き続き2回目の参加です。
今回は晴れだったこともあり、恐ろしいほどの参加者数に。

twitter.com

だいぶ気合いの入った入場待ちの列に感化されて、合計12冊の収穫を得ました。熱かったししょうがないです。
もちろんまだ読み終わってないのですが、一旦出会い等をまとめておきます。

目次

  • DNSをはじめよう ~基礎からトラブルシューティングまで~
  • Now and Future
  • SVGで作ろうローディングアニメーション
  • こうしてぼくらは、書籍を売るアプリを作った
  • こうしてぼくらは、書籍を売るアプリを作った2.0.1
  • Gopherの休日
  • 大きめのAndroidアプリでの設計を考えてみる〜pocket〜
  • Excel働き方改革(仮)
  • Vue.jsとFirebaseで作るミニWEBサービス 初めてのサーバーレスシングルアプリケーション
  • ざっクリわかる Ubuntu 18.04 LTS
  • Nature Design with Houdini 自然界のアルゴリズムを利用したデザインレシピ集
  • Android Things with Kotlin Hands-on Book

DNSをはじめよう ~基礎からトラブルシューティングまで~

techbookfest.org

気になった理由

  • 学生時代、wordpress独自ドメインを当てるときまさに「数時間かかるんだよねー」とか知ったかぶりをしてた
  • 会社入れば研修とかでやるじゃろ、って思ってたらあんまやらなかった
  • 今後自分でサービスを立ち上げたりするときにアワアワしたくない
  • ドリル付き←最高
  • 半透明の遊び紙で製本してあり、本自体のクオリティが高い

Now and Future

techbookfest.org

気になった理由

第1章 Android通知最前線 What's new in Android P

  • 普段Androidの開発をする上で、新しいOSの機能ってシェアの都合上すぐに触ることがない(アプリに求められるのはある程度色々なデバイスで動くこと)
  • 何だかんだ理由をつけて調べてなかったが、Twitterで情報収集するようにしてから気になって仕方がない

第2章 Fit API for Androidを使ってみよう

  • 筋肉になりたい
  • 最近乗ってないけどロードバイク持ってる
  • Garmin高いから自分でやっていきたい

第7章 Androidライブラリ実践入門

  • Androidライブラリ、業務でやる必要があった
  • 断片的な知識しかなかったのでまとめて頭に入れたかった
  • OSS公開、貢献に上手く繋げていきたい

SVGで作ろうローディングアニメーション

techbookfest.org

気になった理由

  • 俺たちは雰囲気でSVGをやっている
  • 学生時代にイラレで書き出しとかやってたけど、中身よく分かってない
  • 目的は多画面サイズ対応くらいだった

こうしてぼくらは、書籍を売るアプリを作った

techbookfest.org

気になった理由

  • 目当ての章
    • 第3章 今日から始めるChatOpsの勘どころ
    • 第4章 Debug Menuはじめました。
    • 第5章 実践Go言語のインタフェース
  • 特に第4章の学びが濃そう
    • debugメニュー、「絶対触っちゃいけないゾーン」になってしまったり
    • productionを想定してちゃんと管理しているのが流石

こうしてぼくらは、書籍を売るアプリを作った2.0.1

techbookfest.org

気になった理由

  • 目当ての章
    • 第2章 Google App EngineでGoのバージョンアップを行う
    • 第4章 キンパツは企画を考え、どんなことをしてきたのか
  • キンパツこと@operandosさんの話は特にきになる
  • ようやくキャリア周りの話に敏感になるようになってきた

Gopherの休日

techbookfest.org

気になった理由

  • お世話になっているgolang.tokyoの頒布物
  • 表紙が最高
  • @tenntennさんが売り子してるのすごい

大きめのAndroidアプリでの設計を考えてみる〜pocket〜

techbookfest.org

気になった理由

Excel働き方改革(仮)

techbookfest.org

気になっていること

  • 自動化自動化と叫ばれているだけあって、いわゆる神Excelはほとんど姿を見かけない(見かけたことがない)
  • ただ、たまに他の人のスプレッドシートを見ると「オッ」となることが
  • 我々は神Excelの悲劇を忘れてはならない
  • (正直何が出来て何がつらいのかよくわかってないので、雰囲気でしか喋れない)

Vue.jsとFirebaseで作るミニWEBサービス 初めてのサーバーレスシングルアプリケーション

techbookfest.org

作者様はこちら nabettu.hatenablog.com

気になった理由

  • SPA作ったことない!
  • Vue.jsもいい加減触りたい!
  • しっかりWebサービス立てるのも大事だけど、Firebase等使ってライトにサクッと立てる技術も大事だと思った

ざっクリわかる Ubuntu 18.04 LTS

techbookfest.org

気になった理由

  • これ学生時代に欲しかったなぁとの思いから
  • 大学の授業や先輩の宗教上の理由から、やたらとCentじゃなくてUbuntuを触る機会が多かった
  • やっぱり初めに触れるなら、CUIだけのサーバー用途よりもGUIを含めたデスクトップ環境の方が楽でいい
  • 「ざっクリ」というだけあってそんなに濃い内容が詰まってるわけではないけれど、聞かれたときにスッと出したい本

Nature Design with Houdini 自然界のアルゴリズムを利用したデザインレシピ集

techbookfest.org

気になった理由

  • これぞ技術書典に来た理由
  • webとかAndroidとか、普段の業務や趣味の領域じゃ出会わない分野と出くわすことができる
  • アルゴリズミックデザインの本
    • アルゴリズムを用いて形状のデザインをする」

    • 自分の手入力により形を作り上げていくデータが重くなりがちな作り方ではなく、数理的なルールを記述するだけで少ないデータで複雑な形状を作ることができる

  • SVGと繋がる点がありそう
  • 対象読者は、アルゴリズミックデザインについて全く知らない人、ではないので、ちょっとずつ勉強しながら読んでいきたい

Android Things with Kotlin Hands-on Book

techbookfest.org

気になった理由

  • 今のところAndroidアプリ開発者としてお仕事をしている
  • PWAなどが流行っている中、今やっているネイティブアプリの知識って今後どこまで通用するのかちょっと不安になる
  • この本のことを知ってからAndroid Thingsのことも知ったんだけど、こういった道で自分の経験を活かしていけるのであれば……という気持ち。

以上です! (長かった……)
GWに向けて、最高の遊び道具が揃った感じですね!やっていきましょう!

ちなみに

買ったものはTrelloでまとめてみるトライアルをしてみています。

Go Conference 2018 Springに参加してきた感動をなんとか言葉にしたい

gocon.connpass.com

1週間経ってしまいましたが、Go Conference2018 Springに参加してきました。やっていくぞという気持ちの昂ぶりが止まらないためちょっと書き残しておきます。
資料やセッション等はconnpassのページをどうぞ。

gocon.connpass.com

 この1週間、会社で「GoCon行ってきた!スライド見て!ほら!」と騒いでおりました。「GoCon良かったなぁ」といい思い出で終わらせないうちに、特に面白かったものをまとめておきます。

Testing with microservices in merpay

speakerdeck.com

概要

  • merpayで使っているgRPCとGoの話
  • 決済システムを扱う上でのテストについて
  • マイクロサービスアーキテクチャを使用する上で、一貫性をどう保っていくか

感想

Go Conferenceでスカラーシップを提供してくれたmercari, merpayのスポンサー枠セッションです。

tech.mercari.com

 今業務でマイクロサービス(的なもの)のTransactionalな処理についてちょうど考えていたところだったので、いいタイミングで発表を聞けました!実際、コンポーネントとその接点が増えれば増えるほどテスト項目は増えるので、そこを完璧にテストするのって難しいよなぁ……などと思っていいたのですが、やらなきゃ駄目ですよねごめんなさい。
 特に、gRPCに関するテストの話題ということで、主題とは外れますが「ああ、gRPCってもうこんなレベルで動いているんだ」などと今更ながら感慨にふけっていました。学習コストも踏まえると、すぐにチームで導入!というのは正直難しい点もあります。だからといって知らないでいい、というのは問題外ですが、一方業務で必要に駆られる環境に身を置くことって大事だなぁと改めて感じました。業務でGo触りたいなぁ……。

How to write Go code

speakerdeck.com

概要

www.youtube.com

感想

 あるものを学ぶ際、公式のドキュメントを読んで勉強しようというのはどの言語、ライブラリでも変わらないことですよね。ただ、頭では分かっていてもGoogle検索で初心者向けの記事を探ってしまう……というのが恥ずかしながら現状でした。アーキテクチャや書き方の好みなども相まって、「公式にはこうあるけどほんとは……」みたいな情報ばかり出回ってしまったりするとなかなかつらいところです。
 Goの場合、書き方がある程度統一されている点もあり、ドキュメントがある程度Godとなりえます。もちろん、発表者のkaneshinさんもコメントされていましたが、開発していく中の応用的な書き方として、個人のブログを参照することも価値があります。一方、バージョンや有用性を判断する基準を持ち合わせていない時期に急に応用面に触れてしまうと、という点もあり。
 こういう勉強の仕方の部分をクローズアップしてくれる発表はめちゃくちゃ有難かったです……!「Goの命名ってどうすればいいんだっけ」とか聞かれた際にポンってURLを投げられるような場所として、覚えるくらい読み込んでおきたいです。

所感

 資料も上がるのに勉強会わざわざ何で行くの、と聞かれることがあります。同じ問題を考えている人と実際に会って話せる、というのがその時の答えでした。加えて、一つ大きな理由として、自分が興味のあるテーマの中で知らなかったことの存在を知り、それについてある程度時間を取って考えられる、というものがあるなぁと思うわけです。

 改めてスケジュールを全体的に眺めてみると、本当に被りのないテーマばかりだったなと思います。これだけ様々なレイヤー、領域が何か1つのテーマを持って集まれる場ってそんなに無いんじゃないかなと。
 大げさに言えばGo言語の共通言語的な側面が好きです。これってコンピュータサイエンスの基礎が無い僕のような人間にとってすごく有難くて、例えばOS関連のシステムプログラミングを勉強したい、といった際、じゃあそのためにC言語を勉強しようとなると、それだけでまた一つハードルが上がってしまいます。一つの言語でアプリケーションからOSのAPIを直で触るような開発まで実際に使われているとなると、また1つ知れることが増えてきます。(文句を言わずCやれ、という話もありますが……)
 次はこの本に真面目に取り組んで行きたいです。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

 また、このカンファレンスを通してもう一つ。会社で社内勉強会を運営したりしているのですが、気になっている問題がありました。それはテーマ設定の部分で、オンプレで動いてるシステムがあるような会社だと、なかなか全員を巻き込むような大きなものって開催がしづらいと感じています。ちゃんとテーマを絞った方が面白いものになるというのも理解はしているのですが、横断的な会も一回やってみたいなぁなんて思ってみたりしています。Goを社内でどれだけ導入しているところがあるかは分かりませんが……ちょっと調べてみます。

 長々としたポエムになってしまいましたが、締めくくります。3倍近い倍率にもなったカンファレンスに参加できたのはめちゃくちゃ幸運でした。ブログだけではなく、コードのアウトプットもしなくては……!

おまけ

keynoteで発表してくれたdaveから、突然AirdropでみんなにGopherの画像が飛んでくるサプライズが。

とのことでした。素敵。

Android App Linksを試してみた

 会社で資料を眺めていたら、今更ながらAndroid App Linksのことを知ったのでまとめてみます。
 みなさんも、バックエンドチームが作ったシステム構成図に知らないAndroidの機能が書いてあって震えることありますよね

Android App Links

 「外部のリンクを踏んだら特定のアプリを立ち上げたい」、なんてのはよくあることだと思います。入門書などでもActivity連携などで取り上げられてるテーマですし、そこで大体Intent(特に暗黙的Intent)の話につながっていく流れですよね。それを別の方向から実現するのが今回のAndroid App Linksです。
 Android6.0で登場した機能なので、全世界で見るとちょっとまだ導入には厳しそうですが……。
f:id:t-miliya612:20180418233137p:plain

何が出来るの

  • deeplinkよりも起動するアプリを指定できます
  • 立ち上げるアプリを事前に認証できます
  • これ全部をAndroidStudioのウィザードで片付けることができます

詳しく見ていきましょう

deeplinkよりも細かく起動するアプリを指定できます

 外部アプリを起動するとき、custom url schemeを使ってintent filterを設置するのがよくやる方法です。例えば、地図アプリを起動するときはgeo:など、Androidデフォルトのアプリが提供しているものもあります。
 Androidのユーザー側が自分で使用するアプリを決められるのがメリットですが、正直そんなに……という気持ちになります。それよりも、複数アプリが同じschemeを待ち受けている際に出てくるchooserのストレスの方が大きいような。
f:id:t-miliya612:20180418233025p:plain
 ここで「ALWAYS」を選んだばっかりに「アプリが立ち上がらない!」と問合せを受けるのは誰もが通る道だと思います。

f:id:t-miliya612:20180418233055p:plain
いらすとやにはなかった

 そこで他と被らない独自のcustom url schemeを作り始めるのですが、httpスキーマじゃないとリンクとして認識してくれないメールやSMSクライアントがあったりしてちょっとつらいのが現状です。仕方ないのでこれまではWebサーバーにリダイレクトを設置して頑張っていたのですが、どう見てもコレジャナイ感があります。

 一方、Android App Linksではhttpスキーマを使いつつ、パスまでしっかり使って起動するアプリを細かく指定できます。これで、chooserに悩まされるユーザーを減らすことができます。

立ち上げるアプリを事前に認証できます

 Android App Linksとして指定するURLは、個人で所有するドメインを使う必要があります。正確に言うと個人で契約していなくてもいいのですが、事前にドメインに紐付いているサーバーにアプリ情報を含むjsonを上げておき、認証するというプロセスが必須です。これによって、得体の知れないアプリがlinkを侵食してくる問題を予防ことができます。
 例えば、悪意あるアプリが決済系アプリと同じURLで待ち受けていた場合、ユーザーの選択によってはきれいに情報を引っこ抜かれてしまう恐れがあります。これを事前に防げるわけですね。

これ全部をAndroidStudioのウィザードで片付けることができます

 (実は一部出来ていないようなところがあるのですが…)App Linksを設定するには、AndroidStudioのウィザードに沿って作業を進めるだけでOKです。公式ページ動画にもやり方は載っているのですが、以下で簡単にまとめておきます。

どうやるの

 動画を参考に進めていきます。

 まずは適当にprojectを生成しましょう。注意点は、minSdkVersionを23以上にすることくらいです。今回はkotlinを選択しました。

f:id:t-miliya612:20180418233427p:plain
f:id:t-miliya612:20180418233446p:plain

 次に、AndroidStudioのTools->App Links Assistantを選択します。

f:id:t-miliya612:20180418234040p:plain

 すると、こんな感じのメニューが出てきます。

f:id:t-miliya612:20180418234215p:plain

 この4ステップでやっていきましょう。

①Add URL intent filters

f:id:t-miliya612:20180418234336p:plain

 Host, Path, 起動したいActivityを設定します。  Hostにはリンクに使用したいwebサイトのURLのhost部分のみを入力してください。今回はgithub.ioを使用します。理由は後述します。
 また、PATHのプルダウンメニューでは、今回とりあえずpathPrefixを選んでおきましょう。pathの末尾の値を取得するために必要です。

f:id:t-miliya612:20180418234355p:plain

 Check URL Mappingに試しにURLを入力してみましょう。うまく設定できていれば、先程指定したActivityが出てくるはずです。
 また、この時点で自動的にAndroidManifest.xmlにintent filterが設定されています。Previewを参考にしてください。

②Add logic to handle the intent

 次にIntentを受け取って処理するロジックを、選択したActivityに自動で記述してくれる……はずなのですが、

f:id:t-miliya612:20180418234409p:plain

 駄目でした。。。Javaだと通ったのですが、Kotlinはまだ未対応のようです。
 仕方ないので手で書いていきます。

MainActivity.kt

package tech.miliya.applinkssample

import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        handleIntent()
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        setIntent(intent)
        handleIntent()
    }

    private fun handleIntent() {
        val appLinkAction = intent.action
        val appLinkData = intent.data
        if (Intent.ACTION_VIEW == appLinkAction && appLinkData != null) {
            val comment = appLinkData.lastPathSegment
            tv_comment.text = comment
        }
    }

}

 ついでに、xmlも少し弄っておきます。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_comment" <!-- この行を追加 -->
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

 一旦ここで、アプリを起動できるかテストしておきましょう。④Test on device or emulatorを選択します。

f:id:t-miliya612:20180419000511p:plain

 先程入力した、{HOST}/{PATH}/適当なコメントを入力し、テストを実行します。

f:id:t-miliya612:20180418234416p:plain

 無事、準備したアプリが起動候補に出てきました。
 いよいよここからアプリをドメインと紐付けていきます。

③Associate website

f:id:t-miliya612:20180418234423p:plain

 Site Domain(リンクとしたいURLのホスト)とApplication ID(アプリのパッケージ)を入力し、Generate Digital Asset Links Fileを選択します。
 すると、Previewjsonファイルが表示されます。これを指定されたURLに配置していきましょう。

 ここだけはHTTPSが必須です。準備するのが面倒なので、ここは動画の通りにgithub.ioを使ってホストしていきます。

github.com

 Github{USERNAME}.github.ioの名前でリポジトリを作成し、指定された階層の通りに/.well-known/assetlinks.jsonを作成します。
 作業が完了したら、下の方にあるLink and Verifyをクリックしましょう。

note

 github.ioでは、そのままだと.well-known以下が見えないので、下記のファイルをリポジトリのルートに配置します。

  • .nojekyll(空ファイル)
  • _config.yml
include: [".well-known"]

④Test on device or emulator

 最終動作確認です。

f:id:t-miliya612:20180418234427p:plain

 無事設定できていれば、先程のようにchooserが出てくることなく、いきなりアプリが起動します。
 テキストが反映されていることも確認しておきましょう。お疲れ様でした。

まとめ

 AndroidStudioにあるアシスタントを使いながら、Android App Linksを使ってアプリの外部起動のフローを実装してみました。
 起動用URLの使い勝手の良さ、セキュリティ面ももちろんですが、chooserが出ないことをひたすら祈らなくてよくなるのは有り難いです。
 しかもアシスタントが優秀で素晴らしい……!
 Android6.0がもうちょっと広まれば、もっと積極的に使っていきたいです。

参考

developer.android.com developer.android.com www.youtube.com stackoverflow.com github.com