Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Navigation Architecture Component によるアプリ内遷移の管理

Navigation Architecture Component によるアプリ内遷移の管理

2019/02/08(金) に DroidKaigi 2019 で使用したスライドです

Management of In-App Transition using Navigation Architecture Component

Yuta Takahashi

February 08, 2019
Tweet

More Decks by Yuta Takahashi

Other Decks in Programming

Transcript

  1. @yt_hizi @yt-tkhs 髙橋 佑太 2018年4⽉, 株式会社サイバーエージェントに新卒⼊社 CATS(Client Advanced Technology Studio)

    に所属 MotionLayout と Navigation に注⽬ 技術書典5 で MotionLayout のことを書きました✏ Yuta Takahashi
  2. Navigation Architecture Component • Google I/O 2018 で発表 • アプリケーション内における画⾯遷移を簡単に実装する


    ためのライブラリとそのツール群 • 現在の最新版は 1.0.0-beta01 2019/2/4 に beta になりました
  3. 画⾯遷移における問題 • FragmentTransaction • Deep Link • 画⾯間の引数渡し • Up

    と Back etc Navigation を使うことによって適切に制御できる
  4. Login Splash 条件付/⼀時的な画⾯は Start Destination にならない A B C Start

    Destination アプリは固定の "Start Destination" をもつ デザイン原則❶
  5. A B C Current Destination Start Destination 遷移の操作は常に Current Destination

    で またはそれに対して⾏われるべき 遷移の状態はスタックで表現される デザイン原則❷
  6. • Start destination(ホーム画⾯) にいるときは
 Toolbar に Upボタンを表⽰すべきではない • Toolbar と連携するための拡張機能があるので


    それを使うことで簡単に実現できる 参考: Back ボタンと Up ボタンを使⽤したナビゲーション - https://developer.android.com/design/patterns/navigation Up ボタンはアプリを終了しない デザイン原則❸
  7. • "Start Destination" ではなく, ⾃⾝のアプリのタスクにいるとき
 Up と Back は同じ動作をするべき •

    Start Destination のときは, Up は使えない (❸で⽰した原則) • 他のアプリのタスクとして起動したとき Back は他のアプリに
 戻り, Up は⾃⾝のアプリの前の画⾯に戻る 参考: Reverse Navigation - https://material.io/design/navigation/understanding-navigation.html#reverse-navigation Up と Back は同じ動作をする デザイン原則❹
  8. • Deep Link で遷移したときと普通に遷移したときで
 同じ画⾯にいるなら, 同じスタックが形成されているべき • Navigation がもつ Deep

    Link の機能を使えば, Navigation Graph 
 から⾃動的にスタックを構築してくれるようになっている • ただし, 起動⽅法(既存のタスク or 新しいタスク)によって
 制御できない部分があるので注意が必要 Deep Link でも同じスタックを形成する デザイン原則❺
  9. Navigation XML を⽤いて Navigation Graph を記述していく <navigation> <fragment> <action />

    </fragment> <fragment> <argument /> <argument /> <deepLink /> </fragment> </navigation>
  10. Navigation Navigation Graph XML を⽤いて Navigation Graph を記述していく <navigation> <fragment>

    <action /> </fragment> <fragment> <argument /> <argument /> <deepLink /> </fragment> </navigation>
  11. <navigation> <fragment> <action /> </fragment> <fragment> <argument /> <argument />

    <deepLink /> </fragment> </navigation> Navigation Destination Destination XML を⽤いて Navigation Graph を記述していく
  12. Navigation Action Argument(遷移時の引数) XML を⽤いて Navigation Graph を記述していく Deep Link

    <navigation> <fragment> <action /> </fragment> <fragment> <argument /> <argument /> <deepLink /> </fragment> </navigation>
  13. <navigation> <fragment> <action /> </fragment> <fragment> <argument /> <argument />

    <deepLink /> </fragment> </navigation> Directions class Args class 型安全なデータ渡しを実現するための Gradle Plugin SafeArgs 遷移元からデータを渡すときに使う 遷移先でデータを受け取るときに使う
  14. app/build.gradle Navigation を導⼊する dependencies { implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-beta01" implementation "android.arch.navigation:navigation-ui-ktx:1.0.0-beta01" }

    Toolbar, BottomNavigation との連携を⾏うためのツール Navigation で Fragment を扱うための拡張 AndroidXを使⽤している場合は Jetifier が必要 ⚠
  15. app/build.gradle Safe Args を導⼊する apply plugin: 'com.android.application' apply plugin: 'kotlin-android'

    apply plugin: 'androidx.navigation.safeargs.kotlin' android { … } 'androidx.navigation.safeargs' 'androidx.navigation.safeargs.kotlin' Kotlin のコードが⽣成される Java のコードが⽣成される Javaで出来ることがKotlinで出来ないことがあるので注意 ⚠ alpha10 から
  16. <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/graph_main" app:startDestination="@id/dest_user_list"> <fragment android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail"

    app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"/> </navigation> res/navigation/graph_main.xml Safe Args で遷移する
  17. <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/graph_main" app:startDestination="@id/dest_user_list"> <fragment android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail"

    app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"/> </navigation> res/navigation/graph_main.xml Safe Args で遷移する
  18. <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/graph_main" app:startDestination="@id/dest_user_list"> <fragment android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail"

    app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"> <argument android:name="userId" app:argType="string" /> </fragment> </navigation> res/navigation/graph_main.xml Safe Args で遷移する
  19. <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/graph_main" app:startDestination="@id/dest_user_list"> <fragment android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail"

    app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"> <argument android:name="userId" app:argType="string" /> </fragment> </navigation> res/navigation/graph_main.xml Safe Args で遷移する
  20. <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/graph_main" app:startDestination="@id/dest_user_list"> <fragment android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail"

    app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"> <argument android:name="userId" app:argType="string" /> </fragment> </navigation> res/navigation/graph_main.xml Safe Args で遷移する UserListFragmentDirections
  21. <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/graph_main" app:startDestination="@id/dest_user_list"> <fragment android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail"

    app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"> <argument android:name="userId" app:argType="string" /> </fragment> </navigation> res/navigation/graph_main.xml Safe Args で遷移する UserDetailFragmentArgs
  22. class UserListFragmentDirections private constructor() { private data class ToUserDetail(val userId:

    String) : NavDirections { override fun getActionId(): Int = com.example.R.id.toUserDetail override fun getArguments(): Bundle { val result = Bundle() result.putString("userId", this.userId) return result } } companion object { fun toUserDetail(userId: String): NavDirections = ToUserDetail(userId) } } app/build/ /UserListFragmentDirections.kt Safe Args で遷移する
  23. app/build/ /UserDetailFragmentArgs.kt Safe Args で遷移する data class UserDetailFragmentArgs(val userId: String)

    : NavArgs { fun toBundle(): Bundle { val result = Bundle() result.putString("userId", this.userId) return result } companion object { @JvmStatic fun fromBundle(bundle: Bundle): UserDetailFragmentArgs { bundle.setClassLoader(UserDetailFragmentArgs::class.java.classLoader) val __userId : String? if (bundle.containsKey("userId")) { __userId = bundle.getString("userId") if (__userId == null) { throw IllegalArgumentException("Argument \"userId\" is marked as non-null but } } else { throw IllegalArgumentException("Required argument \"userId\" is missing and does } return UserDetailFragmentArgs(__userId) } }
  24. <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/graph_main" app:startDestination="@id/dest_user_list"> <fragment android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail"

    app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"> <argument android:name="userId" app:argType="string" /> </fragment> </navigation> Deep Link を使って遷移する res/navigation/graph_main.xml
  25. android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> <action android:id="@+id/toUserDetail" app:destination="@id/dest_user_detail"/> </fragment> <fragment android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment"> <argument

    android:name="userId" app:argType="string" /> <deepLink app:uri="example.com/user/{userId}"/> </fragment> </navigation> Deep Link を使って遷移する res/navigation/graph_main.xml argument にマッピング
  26. Deep Link を使って遷移する app/AndroidManifest.xml <manifest …> <application …> <activity android:name=".MainActivity">

    <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
  27. Deep Link を使って遷移する app/AndroidManifest.xml <manifest …> <application …> <activity android:name=".MainActivity">

    <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <nav-graph android:value="@navigation/graph_main" /> </activity> </application> </manifest>
  28. 実装のまとめ • Naviation XML によってアプリ内遷移を実装する • <fragment> で Destination を定義する

    • <action> で 遷移(Action) を定義する • <argument> で受け取る引数を定義する • Safe Args で型安全なデータの受け渡しを⾏うことができる • <deepLink> と <nav-args> で Deep Link を実装できる
  29. Toolbar と連携する res/layout/activity_main.xml <androidx.constraintlayout.widget.ConstraintLayout...> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar"
 … /> <fragment android:id="@+id/navHostFragment"

    android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/graph_main"
 … /> </androidx.constraintlayout.widget.ConstraintLayout>
  30. src/ /UserListFragment.kt Fragmentをもっと使う android:label Toolbarと連携したときに title を⾃動でセットする
 argument で置き換えることもできる <fragment

    android:id="@+id/dest_user_detail" android:name="com.example.UserDetailFragment" android:label="User: {userId}"> <argument android:name="userId" app:argType="string" /> </fragment>
  31. Argumentをもっと使う app:nullable android:defaultValue Null値を許容するかどうか 引数のデフォルト値 app:argType に指定可能な型 integer integer[] long

    long[] float float[]
 boolean boolean[] reference reference[] string string[] Parcelable / Serializable を実装したクラスとͦͷArray型
 (独⾃のクラスを指定することも可能)
  32. SafeArgs — Java と Kotlin の違い findNavController().navigate( FirstFragmentDirections .toSecond(123) //

    argA .setArgB("arg_b") .setArgC("arb_c") ) findNavController().navigate( UserListFragmentDirections.toUserDetail( argA = 123, argB = "test", argC = "test")) Java
 Builder pattern Kotlin Named Argument
  33. マルチモジュール • ここ最近の Android におけるトレンド • Instant Apps や Dynamic

    Feature Module を使うには
 マルチモジュール構成が必須となる • ここで扱うのは, 機能ごとにモジュールが分かれており
 Fragment が各モジュールに分散しているような状態の構成
  34. • モジュール "core" に各モジュールを依存させている • core内の ActivityHelper.kt で各Activityのクラス名をハードコーディング object Activities

    { …… object Search : AddressableActivity { override val className = "$PACKAGE_NAME.search.ui.SearchActivity" const val EXTRA_QUERY = "EXTRA_QUERY" const val EXTRA_SAVE_DRIBBBLE = "EXTRA_SAVE_DRIBBBLE" const val EXTRA_SAVE_DESIGNER_NEWS = "EXTRA_SAVE_DESIGNER_NEWS" const val RESULT_CODE_SAVE = 7 } }
  35. Navigation をどう使っているか • 各 feature module が共通で依存するモジュールに
 Navigation XML を配置している

    • コードは共通モジュールに⽣成され, それを各モジュールが
 参照するようになっている
  36. public static Fragment instantiate(Context context, String fname, @Nullable Bundle args)

    { try { Class<?> clazz = sClassMap.get(fname); if (clazz == null) { // Class not found in the cache, see if it's real, and try to add it clazz = context.getClassLoader().loadClass(fname); if (!Fragment.class.isAssignableFrom(clazz)) { throw new InstantiationException("Trying to instantiate a class " + fname + " that is not a Fragment", new ClassCastException()); } sClassMap.put(fname, clazz); } Fragment f = (Fragment) clazz.getConstructor().newInstance(); if (args != null) { args.setClassLoader(f.getClass().getClassLoader()); f.setArguments(args); } return f; } catch (ClassNotFoundException e) { リフレクションによってFragmentのインスタンスを⽣成
 実⾏時には各モジュールがマージされているため参照可能 ⽣成されたクラスを使って遷移するとき
  37. Navigation — 別のアプローチ ❶ Dagger で interface のみを Feature Modules

    に提供して
 実際の遷移コードを application module で記述する (記述量が増える)
 ❷ 実⾏時だけ android:name が使われるなら実⾏時に
 クラスの参照を渡すこともできそう (ワークアラウンド的) Feature module が Application module の
 依存関係にあるときに限られる ⚠
  38. まとめ • Navigation の登場によって, 画⾯遷移に関わる様々な機能を
 容易に実装できるようになった
 • Safe Args によってNavigationをもっと便利に使える

    • マルチモジュールにおける利⽤はワークアラウンド的である
 ことを理解した上で使ったほうがよい