TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

DroidKaigi 2019 official appを読んでみる②:Navigation

背景

  • DroidKaigi 2019 のofficial appが公開されている:参照
  • せっかくだしコントリビュートしたいなと思ったけど思った以上に皆新しい技術使っててよくわからない。。
  • とりあえず一つずつ理解することにする。

アプリ起動から順に追っていく

DroidKaigi 2019のReadmeに記載されている通り、 Fluxベースのマルチモジュール構成になっているよう。

https://github.com/DroidKaigi/conference-app-2019/blob/master/README.md

早速起動アクティビティを確認しようとManifestを確認していくと、ActivityはMainActivity一つしかなく、表示画面はすべてFragmentで構成されている模様。FragmentManagerに殺された経験がある身としてはなんて構成なんだ...と震えていたのですが、どうやらNavigationという物を使っているようです。今回はこのNavigationを調査していきます。

まずはNavigationの公式ドキュメントを見てみる

Navigationはアプリの目的地を案内するためのフレームワークらしいです。要するに画面遷移系を楽にしてくれるみたい。 名前からするとNavigation Drowerの親戚かと思ったけどどちらかというとiOSのSegueに近いのかな。

公式サイトには以下の利点が書いてありました。

Navigation Architecture Componentには、次のような他の多くの利点があります。

- フラグメントトランザクションの処理
- デフォルトでUpとBackアクションを正しく処理する
- アニメーションとトランジションのための標準化されたリソースを提供する
- 第一級の操作としてディープリンクを扱う
- 最小限の追加作業で、ナビゲーションパネルや下部ナビゲーションなどのナビゲーションUIパターンを含める
- ナビゲート中に情報を渡すときにタイプセーフを提供する
- Android StudioのNavigation Editorを使用したナビゲーショングラフの視覚化と編集

・・・もうフラグメントトランザクションを楽にしてくれるというのであればその時点で使いたくなりますよね! とりあえず実装してみないことには理解が進まないので、早速実装してみることにします。

Navigation Code Labなるものが存在しているみたいなので、これ通りに順を追って開発してみます。

Navigationを実装してみる(サンプル)

build.gradleにdependenciesを追加する。

以下のように追加します。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    // Navigation
    implementation Dep.AndroidX.Navigation.ui
    implementation Dep.AndroidX.Navigation.uiKtx
    implementation Dep.AndroidX.Navigation.fragment
    implementation Dep.AndroidX.Navigation.fragmentKtx
    implementation Dep.AndroidX.Navigation.runtime
    implementation Dep.AndroidX.Navigation.runtimeKtx
}
    object AndroidX {
        object Navigation {
            val version = "1.0.0-alpha08"
            val runtime = "android.arch.navigation:navigation-runtime:$version"
            val runtimeKtx = "android.arch.navigation:navigation-runtime-ktx:$version"
            val fragment = "android.arch.navigation:navigation-fragment:$version"
            val ui = "android.arch.navigation:navigation-ui:$version"
            val fragmentKtx = "android.arch.navigation:navigation-fragment-ktx:$version"
            val uiKtx = "android.arch.navigation:navigation-ui-ktx:$version"
        }
    }

公式を見ると、common, ui, fragment, runtimeとあるようですが、commonとruntimeの違いがわからない。。
とりあえずDroidKaigi2019で選択されているruntimeを追加しました。

Navigationの基本的な3つの要素

Navigationは以下の3つの要素で構成されるようです。

  1. Navigation Graph (XML Resource) : すべての遷移先が記載されているxml
  2. NavHostFragment (Layout XML View) : レイアウトに追加する特殊なwidgetだそうです。コンテナFragmentみたいな感じです。
  3. NavController(Kotlin/Java object) : Navigation Graph内のどこにいるか、currentPositionを持つクラスだそうです。

このNavigationの基本的な考え方は以下のようです。

  1. NavControllerで次の遷移先をNavigation Graphから示す
  2. それをNavHostFragmentが受け取って画面遷移する

Navigation Graph

アプリ内における対象の目的地(Destination)から到達可能な目的地を視覚的に表示できる新しいリソースタイプです。 ここでいう目的地というのはActivityやFragmentが該当するようです。要するにiOSのstoryboard的なやつだと思います。

navigation.xmlAndroid Studio -> Design で表示すると以下のような感じです。
注:Navigation Code LabにはAndroid Studio 3.2 以上ならNavigation ツールが使えると書いてあったけど、3.2では表示されませんでした。Android Studio 3.3 にあげたほうが無難かもしれません。(Android Studio 3.3のWhats new に書いてあったし。)

f:id:off2white:20190115204314p:plain

画面のようなものがDestination、矢印がActionというそうです。

このxmlは以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2018 The Android Open Source Project
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<navigation 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"
    app:startDestination="@+id/home_dest">
    <fragment
        android:id="@+id/home_dest"
        android:name="com.example.android.codelabs.navigation.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/home_fragment">

        <!-- TODO STEP 7.1 - Add action with transitions -->
        <!--<action-->
            <!--android:id="@+id/next_action"-->
            <!--app:destination="@+id/flow_step_one_dest"-->
            <!--app:enterAnim="@anim/slide_in_right"-->
            <!--app:exitAnim="@anim/slide_out_left"-->
            <!--app:popEnterAnim="@anim/slide_in_left"-->
            <!--app:popExitAnim="@anim/slide_out_right" />-->
        <!-- TODO END STEP 7.1 -->

    </fragment>

    <fragment
        android:id="@+id/flow_step_one_dest"
        android:name="com.example.android.codelabs.navigation.FlowStepFragment"
        tools:layout="@layout/flow_step_one_fragment">
        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="1"/>

        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_two_dest">
        </action>
    </fragment>

    <fragment
        android:id="@+id/flow_step_two_dest"
        android:name="com.example.android.codelabs.navigation.FlowStepFragment"
        tools:layout="@layout/flow_step_two_fragment">

        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="2"/>

        <action
            android:id="@+id/next_action"
            app:popUpTo="@id/home_dest">
        </action>
    </fragment>

    <!-- TODO STEP 4 Create a new navigation destination pointing to SettingsFragment -->
    <!--<fragment-->
        <!--android:id="@+id/settings_dest"-->
        <!--android:name="com.example.android.codelabs.navigation.SettingsFragment"-->
        <!--android:label="@string/settings"-->
        <!--tools:layout="@layout/settings_fragment" />-->
    <!-- TODO END STEP 4 -->

    <fragment
        android:id="@+id/deeplink_dest"
        android:name="com.example.android.codelabs.navigation.DeepLinkFragment"
        android:label="@string/deeplink"
        tools:layout="@layout/deeplink_fragment">

        <argument
            android:name="myarg"
            android:defaultValue="Android!"/>
        <!-- TODO STEP 12.1 - Add a deep link to www.example.com/{myarg}/ -->

        <!--<deepLink app:uri="www.example.com/{myarg}" />-->

        <!-- TODO END STEP 12.1 -->
    </fragment>
</navigation>

属性については以下のようになっています。

  • navigation: Navigation Graphのルート
    • navigationは少なくともActivityかFragmentのどちらか一つは含む
    • app:startDestination には起動時に表示する目的地を指定する
  • android:id : 対象目的地のid
  • android:name : 対象目的地が選択された際にインスタンス化するActivity/Fragmentのクラス名を指定する
  • tools:layout : Designに表示するレイアウトファイルの指定する

NavHostFragment

画面遷移の設計図のようなNavigation Graphはできたけれど、それを使うためにはNavHostFragmentという特別なWidgetが必要です。
NavHostFragmentはコンテナみたいなもので、このNavHostFragmentの中身が入れ替わっていくことで目的地の移動が表現されるみたい。 このNavHostFragmentの推奨利用方法は、一つのActivityにNavHostFragmentを含めることとされています。
以下の図(Navigation Gode Labから引用)のような感じです。

f:id:off2white:20190115213724p:plain

これを満たすActivityの単純なレイアウトは以下のようになるそうです。

<LinearLayout
    .../>
    <androidx.appcompat.widget.Toolbar
        .../>
    <fragment
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/mobile_navigation"
        app:defaultNavHost="true"
        />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        .../>
</LinearLayout>
  • android:name="androidx.navigation.fragment.NavHostFragment" でこのFragmentはNavHostFragmentとなる
  • app:NavGraph : ここに先ほど作成したnavigation.xmlを指定する
  • app:defaultNavHost : これはバックキーの動作をNavHostFragmentに紐づけるかどうかを指定するフラグ。

NavController

ここまで用意すればあとはユーザーアクションに応じて移動処理を実施するのみ。以下のように記載できます。

   val button = view.findViewById<Button>(R.id.navigate_destination_button)
   button?.setOnClickListener {
         findNavController().navigate(R.id.flow_step_one_dest)
   }

これだけです。まじかよって気分です。
仮にnavigateがActivityだった場合は、startActivity()と同じような処理がされるそうです。すごいですね。

ただActivity, Fragment, ViewのどこでNavController()を呼ぶかについてメソッドが変わるので注意してね、だそうです。

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: Int)

感想

  • 画面遷移がグッと楽になるNavigation。今後ぜひ使っていきたい。けどまだalpha版。。
  • やたら画面数のある大規模アプリを開発することになったらどうなるんだろう。storyBoardみたいに重くて開けないとかないといいな。。
  • あとは記載してないけどsafeArgsとかアニメーションとかDeepLinkとかの機能がある。つよい。
  • Code Labがすごいわかりやすかった。ありがとうございます。