Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

@yt_hizi @yt-tkhs 髙橋 佑太 2018年4⽉, 株式会社サイバーエージェントに新卒⼊社 CATS(Client Advanced Technology Studio) に所属 MotionLayout と Navigation に注⽬ 技術書典5 で MotionLayout のことを書きました✏ Yuta Takahashi

Slide 3

Slide 3 text

ΞδΣϯμ • Navigation Architecture Component の概要 • 設計上の概念とデザイン原則 • 画⾯遷移を実装してみる • マルチモジュールにおける遷移を考える • まとめ

Slide 4

Slide 4 text

Navigation Architecture Component

Slide 5

Slide 5 text

Source: https://android.jlelse.eu/what-is-android-jetpack-737095e88161

Slide 6

Slide 6 text

Navigation Architecture Component • Google I/O 2018 で発表 • アプリケーション内における画⾯遷移を簡単に実装する
 ためのライブラリとそのツール群 • 現在の最新版は 1.0.0-beta01 2019/2/4 に beta になりました

Slide 7

Slide 7 text

画⾯遷移における問題 • FragmentTransaction • Deep Link • 画⾯間の引数渡し • Up と Back etc Navigation を使うことによって適切に制御できる

Slide 8

Slide 8 text

• FragmentTransaction を⾃動的にハンドリングする • Deep Link を⾃動的にハンドリングする • 画⾯遷移時に型安全に情報を渡すことができる Safe Args • アプリ内遷移を可視化・編集可能な Navigation Editor 主な特徴 etc

Slide 9

Slide 9 text

設計上の概念

Slide 10

Slide 10 text

Navigation Graph アプリ内における画⾯と 画⾯間の遷移のグラフ

Slide 11

Slide 11 text

Destination Fragment/Activity などの Actionによって遷移可能な画⾯

Slide 12

Slide 12 text

Start Destination アプリのエントリポイントとなる Destination (ホーム画⾯)

Slide 13

Slide 13 text

Action 任意の Destination から 他の Destination への遷移

Slide 14

Slide 14 text

Principles of navigation https://developer.android.com/topic/libraries/architecture/navigation/#principles デザイン原則

Slide 15

Slide 15 text

アプリは固定の "Start Destination" をもつ デザイン原則❶ A B C Start Destination

Slide 16

Slide 16 text

Login Splash A B C アプリは固定の "Start Destination" をもつ デザイン原則❶ Start Destination?

Slide 17

Slide 17 text

Login Splash 条件付/⼀時的な画⾯は Start Destination にならない A B C Start Destination アプリは固定の "Start Destination" をもつ デザイン原則❶

Slide 18

Slide 18 text

A B C A B C Navigation Stack == 遷移の状態はスタックで表現される デザイン原則❷

Slide 19

Slide 19 text

A B C Current Destination Start Destination 遷移の状態はスタックで表現される デザイン原則❷

Slide 20

Slide 20 text

A B C Current Destination Start Destination 遷移の操作は常に Current Destination で またはそれに対して⾏われるべき 遷移の状態はスタックで表現される デザイン原則❷

Slide 21

Slide 21 text

• Start destination(ホーム画⾯) にいるときは
 Toolbar に Upボタンを表⽰すべきではない • Toolbar と連携するための拡張機能があるので
 それを使うことで簡単に実現できる 参考: Back ボタンと Up ボタンを使⽤したナビゲーション - https://developer.android.com/design/patterns/navigation Up ボタンはアプリを終了しない デザイン原則❸

Slide 22

Slide 22 text

• "Start Destination" ではなく, ⾃⾝のアプリのタスクにいるとき
 Up と Back は同じ動作をするべき • Start Destination のときは, Up は使えない (❸で⽰した原則) • 他のアプリのタスクとして起動したとき Back は他のアプリに
 戻り, Up は⾃⾝のアプリの前の画⾯に戻る 参考: Reverse Navigation - https://material.io/design/navigation/understanding-navigation.html#reverse-navigation Up と Back は同じ動作をする デザイン原則❹

Slide 23

Slide 23 text

• Deep Link で遷移したときと普通に遷移したときで
 同じ画⾯にいるなら, 同じスタックが形成されているべき • Navigation がもつ Deep Link の機能を使えば, Navigation Graph 
 から⾃動的にスタックを構築してくれるようになっている • ただし, 起動⽅法(既存のタスク or 新しいタスク)によって
 制御できない部分があるので注意が必要 Deep Link でも同じスタックを形成する デザイン原則❺

Slide 24

Slide 24 text

実装する上での知識

Slide 25

Slide 25 text

Navigation Safe Args Navigation Editor

Slide 26

Slide 26 text

Navigation XML を⽤いて Navigation Graph を記述していく

Slide 27

Slide 27 text

Navigation Navigation Graph XML を⽤いて Navigation Graph を記述していく

Slide 28

Slide 28 text

Navigation Destination Destination XML を⽤いて Navigation Graph を記述していく

Slide 29

Slide 29 text

Navigation Action Argument(遷移時の引数) XML を⽤いて Navigation Graph を記述していく Deep Link

Slide 30

Slide 30 text

Directions class Args class 型安全なデータ渡しを実現するための Gradle Plugin SafeArgs 遷移元からデータを渡すときに使う 遷移先でデータを受け取るときに使う

Slide 31

Slide 31 text

Navigation Editor XMLで定義したグラフを可視化および編集できるGUIツール

Slide 32

Slide 32 text

実装してみる

Slide 33

Slide 33 text

MainActivity UserDetailFragment User ID UserListFragment ❶ UserListFragment を表⽰ ❷ UserDetailFragment に遷移する ❸ 遷移時にデータ(User ID) を渡す

Slide 34

Slide 34 text

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 が必要 ⚠

Slide 35

Slide 35 text

res/navigation/graph_main.xml 最初のFragmentを表⽰する

Slide 36

Slide 36 text

res/navigation/graph_main.xml Destination を追加 最初のFragmentを表⽰する

Slide 37

Slide 37 text

res/navigation/graph_main.xml Start Destination の指定 最初のFragmentを表⽰する

Slide 38

Slide 38 text

res/navigation/graph_main.xml 最初のFragmentを表⽰する

Slide 39

Slide 39 text

res/layout/activity_main.xml 最初のFragmentを表⽰する

Slide 40

Slide 40 text

res/layout/activity_main.xml Destination をホストするための Fragment 最初のFragmentを表⽰する

Slide 41

Slide 41 text

res/layout/activity_main.xml 作成した Navigation XML を指定する 最初のFragmentを表⽰する

Slide 42

Slide 42 text

res/layout/activity_main.xml true のとき, システムのBackで前のFragmentに戻れる 最初のFragmentを表⽰する

Slide 43

Slide 43 text

res/navigation/graph_main.xml 最初のFragmentを表⽰する MainActivity NavHostFragment UserListFragment Layout xml を読み込んで表⽰ 指定した Graph の startDestination を表⽰

Slide 44

Slide 44 text

res/navigation/graph_main.xml 別のFragmentに遷移する

Slide 45

Slide 45 text

res/navigation/graph_main.xml 別のFragmentに遷移する

Slide 46

Slide 46 text

res/navigation/graph_main.xml 別のFragmentに遷移する

Slide 47

Slide 47 text

src/ /UserListFragment.kt findNavController().navigate(R.id.dest_user_detail, Bundle().apply { putString("userId", userId) }) 別のFragmentに遷移する

Slide 48

Slide 48 text

src/ /UserListFragment.kt findNavController().navigate(R.id.dest_user_detail, Bundle().apply { putString("userId", userId) }) 遷移するときは Safe Args を使う 別のFragmentに遷移する

Slide 49

Slide 49 text

buildscript { dependencies { classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-beta01" } } /build.gradle Safe Args を導⼊する

Slide 50

Slide 50 text

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 から

Slide 51

Slide 51 text

res/navigation/graph_main.xml Safe Args で遷移する

Slide 52

Slide 52 text

res/navigation/graph_main.xml Safe Args で遷移する

Slide 53

Slide 53 text

res/navigation/graph_main.xml Safe Args で遷移する

Slide 54

Slide 54 text

res/navigation/graph_main.xml Safe Args で遷移する

Slide 55

Slide 55 text

res/navigation/graph_main.xml Safe Args で遷移する

Slide 56

Slide 56 text

res/navigation/graph_main.xml Safe Args で遷移する UserListFragmentDirections

Slide 57

Slide 57 text

res/navigation/graph_main.xml Safe Args で遷移する UserDetailFragmentArgs

Slide 58

Slide 58 text

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 で遷移する

Slide 59

Slide 59 text

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) } }

Slide 60

Slide 60 text

findNavController().navigate(
 UserListFragmentDirections.toUserDetail(userId)) src/ /UserListFragment.kt Safe Args で遷移する

Slide 61

Slide 61 text

src/ /UserDetailFragment.kt Safe Args でデータを受け取る private val args by navArgs() getUser(args.userId) Property Delegation alpha10 から

Slide 62

Slide 62 text

Deep Link を使って遷移する res/navigation/graph_main.xml

Slide 63

Slide 63 text

android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> Deep Link を使って遷移する res/navigation/graph_main.xml

Slide 64

Slide 64 text

android:id="@+id/dest_user_list" android:name="com.example.UserListFragment"> Deep Link を使って遷移する res/navigation/graph_main.xml argument にマッピング

Slide 65

Slide 65 text

Deep Link を使って遷移する app/AndroidManifest.xml

Slide 66

Slide 66 text

Deep Link を使って遷移する app/AndroidManifest.xml

Slide 67

Slide 67 text

Deep Link を使って遷移する app/AndroidManifest.xml Manifest Merger

Slide 68

Slide 68 text

Deep Link を使って遷移する https://example.com/user/U12345

Slide 69

Slide 69 text

Deep Link を使って遷移する https://example.com/user/U12345 MainActivity UserDetailFragment User ID

Slide 70

Slide 70 text

Deep Link を使って遷移する https://example.com/user/U12345 MainActivity UserDetailFragment User ID UserListFragment

Slide 71

Slide 71 text

res/navigation/graph_main.xml Navigation Graphを⾒てみる

Slide 72

Slide 72 text

Start Destination Deep Link Action Navigation Graphを⾒てみる res/navigation/graph_main.xml

Slide 73

Slide 73 text

実装のまとめ • Naviation XML によってアプリ内遷移を実装する • で Destination を定義する • で 遷移(Action) を定義する • で受け取る引数を定義する • Safe Args で型安全なデータの受け渡しを⾏うことができる • と で Deep Link を実装できる

Slide 74

Slide 74 text

その他の機能

Slide 75

Slide 75 text

Toolbar と連携する res/layout/activity_main.xml

Slide 76

Slide 76 text

Toolbar と連携する res/layout/MainActivity.kt toolbar.setupWithNavController( findNavController(R.id.navHostFragment)) • Start Destination のとき以外 Up ボタンを⾃動的に表⽰する • に指定した android:label を表⽰する

Slide 77

Slide 77 text

src/ /UserListFragment.kt Fragmentをもっと使う android:label Toolbarと連携したときに title を⾃動でセットする
 argument で置き換えることもできる

Slide 78

Slide 78 text

Actionをもっと使う app:launchSingleTop app:popUpTo app:popUpToInclusive app:enterAnim app:exitAnim app:popEnterAnim app:popExitAnim Fragment切り替え時のアニメーション 遷移時にスタックをどこまでPopするか SingleTop として起動するかどうか

Slide 79

Slide 79 text

Argumentをもっと使う app:nullable android:defaultValue Null値を許容するかどうか 引数のデフォルト値 app:argType に指定可能な型 integer integer[] long long[] float float[]
 boolean boolean[] reference reference[] string string[] Parcelable / Serializable を実装したクラスとͦͷArray型
 (独⾃のクラスを指定することも可能)

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

マルチモジュールにおける遷移

Slide 82

Slide 82 text

マルチモジュール • ここ最近の Android におけるトレンド • Instant Apps や Dynamic Feature Module を使うには
 マルチモジュール構成が必須となる • ここで扱うのは, 機能ごとにモジュールが分かれており
 Fragment が各モジュールに分散しているような状態の構成

Slide 83

Slide 83 text

First Second Third Base (Dynamic) Feature Modules

Slide 84

Slide 84 text

First Second Third

Slide 85

Slide 85 text

Feature modules 間の依存関係が複雑になる
 循環参照が発⽣する First Second Third

Slide 86

Slide 86 text

nickbutcher/plaid • デザイン関連ニュースのフィードアプリ • Navigation を使⽤していないマルチモジュールプロジェクト • 遷移は主に Activity 間で⾏われる https://github.com/nickbutcher/plaid どうやって遷移している?

Slide 87

Slide 87 text

• モジュール "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 } }

Slide 88

Slide 88 text

fun intentTo(addressableActivity: AddressableActivity): Intent { return Intent(Intent.ACTION_VIEW).setClassName( PACKAGE_NAME, addressableActivity.className ) } val intent = intentTo(Activities.Search) ActivityHelper.kt HomeActivity.kt

Slide 89

Slide 89 text

DroidKaigi/droidkaigi-2019-app https://github.com/DroidKaigi/droidkaigi-2019-app • DroidKaigi 2019 公式アプリ • Navigation を使⽤しているマルチモジュールプロジェクト • Safe Args も使⽤している • 遷移は主に Fragment 間で⾏われる

Slide 90

Slide 90 text

Navigation をどう使っているか • 各 feature module が共通で依存するモジュールに
 Navigation XML を配置している • コードは共通モジュールに⽣成され, それを各モジュールが
 参照するようになっている

Slide 91

Slide 91 text

Navigation をどう使っているか ……

Slide 92

Slide 92 text

Navigation をどう使っているか …… 別モジュールにあるため参照できない

Slide 93

Slide 93 text

Safe Args がクラスを⽣成するとき • クラス⽣成時は android:name が指定されているかどうかチェック • 参照が正しいかどうかはチェックしていないため正しく⽣成できる https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/navigation/safe-args-generator/ src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt NavParser.kt Safe Args に含まれる, XMLをパースするためのクラス

Slide 94

Slide 94 text

⽣成されたクラスを使って遷移するとき https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/navigation/fragment/ src/main/java/androidx/navigation/fragment/FragmentNavigator.java FragmentNavigator.kt Fragmentの遷移処理を受け持つクラス @NonNull public Fragment instantiateFragment( @NonNull Context context, @SuppressWarnings("unused") @NonNull FragmentManager fragmentManager, @NonNull String className, @Nullable Bundle args) { return Fragment.instantiate(context, className, args); }

Slide 95

Slide 95 text

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のインスタンスを⽣成
 実⾏時には各モジュールがマージされているため参照可能 ⽣成されたクラスを使って遷移するとき

Slide 96

Slide 96 text

Navigation — 別のアプローチ ❶ Dagger で interface のみを Feature Modules に提供して
 実際の遷移コードを application module で記述する (記述量が増える)
 ❷ 実⾏時だけ android:name が使われるなら実⾏時に
 クラスの参照を渡すこともできそう (ワークアラウンド的) Feature module が Application module の
 依存関係にあるときに限られる ⚠

Slide 97

Slide 97 text

マルチモジュールまとめ • マルチモジュールで遷移するときはリフレクションを使う
 のが最も現実的な⽅法だと考えられる • Navigation を使うときは Navigation Graph を
 共通モジュールに配置するのが現状の解決策
 (droidkaigi-app-2019 の⽅法)

Slide 98

Slide 98 text

まとめ • Navigation の登場によって, 画⾯遷移に関わる様々な機能を
 容易に実装できるようになった
 • Safe Args によってNavigationをもっと便利に使える • マルチモジュールにおける利⽤はワークアラウンド的である
 ことを理解した上で使ったほうがよい

Slide 99

Slide 99 text

ありがとうございました @yt_hizi @yt-tkhs