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

Navigation Componentを実践導入した際の感動、便利さ、そしてつまづき

Suyama
February 21, 2020

Navigation Componentを実践導入した際の感動、便利さ、そしてつまづき

概要:
DroidKaigi2020(開催中止)で発表予定だったNavigation Componentについての資料をアップロードします。Navigationを実践導入するのを躊躇っている方やどう導入すればわからない方に向けてまとめたのでよければ参考にしてください。

説明:
Android Architecture ComponentsのNavigation Componentは、Fragment間の遷移をよりシンプルに実装できるようにしたライブラリです。
既存のFragment遷移処理はFragmentManagerによるトランザクション処理を、バックスタックなども考慮しながらコード上で書いていました。Navigationはそれらのトランザクション処理をラップしているので、利用することでコードの簡略化が可能となります。それだけでなく、NavigationGraphと呼ばれるxmlによってFragmentの遷移図をグラフィカルに作成/表示することにより、画面遷移をよりわかりやすく実装することができる画期的なコンポーネントです。argumentsの型安全化(Safe Args)、ディープリンクによる遷移などの便利な機能も含まれています。

本セッションでは、スタディプラスアプリにおいてNavigation導入を行なった際に得られた知見と、それを基にプロジェクトに導入する際に考慮したいことなどを紹介します。また、内部実装をもとに、実際に内部でNavigationが行なっていることも紹介したいと思っております。本セッションが開発者の方々のNavigation導入のきっかけになれば幸いです。
内容としては以下のようなものを予定しております。
- 基本的な遷移の実装について
- NavHostFragmentとFragmentContainerView
- Fragmentの遷移とNavigator
- DialogFragment/BottomSheetDialogFragmentの遷移とNavigator
- BottomNavigationの連携
- ディープリンクに依る遷移
- NavigationGraphのネスト
- 遷移アニメーションの指定と注意点
- Fragment間のデータ受け渡しについて
- Safe Args機能
- ActivityViewModel、NavGraphViewModelに依るデータ受け渡し
- Safe ArgsとNavGraphViewModelの併用時の注意点
- バックキー制御方法について
- Fragment単位で制御する
- Activity全体で制御する
- 最初に表示されるFragmentの制御について
- NavigationGraphごと分ける
- NavigationGraphはそのままで開始Fragmentだけ変更する
- 利用する上で起こりうるクラッシュの原因と対応方法について
- CustomNavigatorの作成について
- 導入時の設計パターンについて
- アプリ全体を1Activityにまとめる場合
- 1機能ごとに1Activityにまとめる場合

Suyama

February 21, 2020
Tweet

More Decks by Suyama

Other Decks in Programming

Transcript

  1. 3 ⾃⼰紹介 名前 :Junichiro Suyama GitHubID:@JASON13F TwitterID:@JasonAndroidDev 趣味 :ぷよぷよ      

    (⼤会優勝を経験) 名前 :Yuzuru Nakashima GitHubID:@nacatl TwitterID:@affinity_robots 趣味 :Magic The Gathering
  2. ⽬次 • Navigationの説明 • Navigationとは • 基本的な遷移の実装について • NavigationUI •

    データ受け渡し • 導⼊事例紹介 • 導⼊⽅針 • 実際の導⼊事例(隅⼭) • 実際の導⼊事例(中島) 6
  3. 使うことのメリット 12 今までの遷移 (FragmentManager) これからの遷移 (Navigation) 遷移の実装 ❌ 遷移をコード上で管理できない ⭕

    GUIで遷移を実装可能 バックスタック考慮 ❌ Fragmentスタック状態を コード上で管理 ⭕ Fragmentスタック状態を 考慮しなくていい データ受け渡し ❌ 型考慮、nullable考慮 ⭕ 型安全、null安全
  4. // ભҠ࣌ supportFragmentManager.commit { add( R.id.fragment_container, ~~Fragment.newInstance() ) addToBackStack(null) setCustomAnimations(

    R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right ) } supportActionBar?.setTitle(~~) 16
  5. // ભҠ࣌ supportFragmentManager.commit { add( R.id.fragment_container, ~~Fragment.newInstance() ) addToBackStack(null) setCustomAnimations(

    R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right ) } supportActionBar?.setTitle(~~) 17
  6. // ભҠ࣌ supportFragmentManager.commit { add( R.id.fragment_container, ~~Fragment.newInstance() ) addToBackStack(null) setCustomAnimations(

    R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right ) } supportActionBar?.setTitle(~~) 18
  7. // ભҠ࣌ supportFragmentManager.commit { add( R.id.fragment_container, ~~Fragment.newInstance() ) addToBackStack(null) setCustomAnimations(

    R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right ) } supportActionBar?.setTitle(~~) 19
  8. // ભҠ࣌ supportFragmentManager.commit { add( R.id.fragment_container, ~~Fragment.newInstance() ) addToBackStack(null) setCustomAnimations(

    R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right ) } supportActionBar?.setTitle(~~) 20
  9. // navGraphͷaction <fragment android:id=“@+id/hogeFragment" android:name="~~.HogeFragment" android:label="@string/title_fragment_hoge" tools:layout="@layout/hoge_fragment" > <action android:id="@+id/action_hogeFragment_to_fugaFragment"

    app:destination="@id/fugaFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> <fragment android:id=“@+id/hogeFragment” 26
  10. // navGraphͷaction <fragment android:id=“@+id/hogeFragment" android:name="~~.HogeFragment" android:label="@string/title_fragment_hoge" tools:layout="@layout/hoge_fragment" > <action android:id="@+id/action_hogeFragment_to_fugaFragment"

    app:destination="@id/fugaFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> <fragment android:id=“@+id/hogeFragment” 遷移アニメーションの指定 27
  11. // navGraphͷaction <fragment android:id=“@+id/hogeFragment" android:name="~~.HogeFragment" android:label="@string/title_fragment_hoge" tools:layout="@layout/hoge_fragment" > <action android:id="@+id/action_hogeFragment_to_fugaFragment"

    app:destination="@id/fugaFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> <fragment android:id=“@+id/hogeFragment” 遷移先の指定 28
  12. // navGraphͷaction <fragment android:id=“@+id/hogeFragment" android:name="~~.HogeFragment" android:label="@string/title_fragment_hoge" tools:layout="@layout/hoge_fragment" > <action android:id="@+id/action_hogeFragment_to_fugaFragment"

    app:destination="@id/fugaFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> <fragment android:id=“@+id/hogeFragment” タイトル⽂字列の設定 29
  13. // DirectionsΫϥε(ࣗಈੜ੒) class FirstFragmentDirections private constructor() { private data class

    ActionFirstToSecond( val hogeName: String = "hoge" ) : NavDirections { override fun getActionId(): Int = R.id.action_first_to_second override fun getArguments(): Bundle { val result = Bundle() result.putString("hogeName", this.hogeName) return result } } companion object { fun actionFirstToSecond(hogeName: String = "hoge"): NavDirections = ActionFirstToSecond(hogeName) } } 37
  14. // DirectionsΫϥε(ࣗಈੜ੒) class FirstFragmentDirections private constructor() { private data class

    ActionFirstToSecond( val hogeName: String = "hoge" ) : NavDirections { override fun getActionId(): Int = R.id.action_first_to_second override fun getArguments(): Bundle { val result = Bundle() result.putString("hogeName", this.hogeName) return result } } companion object { fun actionFirstToSecond(hogeName: String = "hoge"): NavDirections = ActionFirstToSecond(hogeName) } } 38
  15. // ArgsΫϥε(ࣗಈੜ੒) data class SecondFragmentArgs(val hogeName: String = "hoge") :

    NavArgs { fun toBundle(): Bundle { val result = Bundle() result.putString("hogeName", this.hogeName) return result } companion object { @JvmStatic fun fromBundle(bundle: Bundle): SecondFragmentArgs { bundle.setClassLoader(SecondFragmentArgs::class.java.classLoader) val __hogeName : String? If (bundle.containsKey("hogeName")) { __hogeName = bundle.getString("hogeName") if (__hogeName == null) { throw IllegalArgumentException("Argument is marked as non-null but was passed a null value.") } } else { __hogeName = "hoge" } return SecondFragmentArgs(__hogeName) } } } 39
  16. // ArgsΫϥε(ࣗಈੜ੒) data class SecondFragmentArgs(val hogeName: String = "hoge") :

    NavArgs { fun toBundle(): Bundle { val result = Bundle() result.putString("hogeName", this.hogeName) return result } companion object { @JvmStatic fun fromBundle(bundle: Bundle): SecondFragmentArgs { bundle.setClassLoader(SecondFragmentArgs::class.java.classLoader) val __hogeName : String? If (bundle.containsKey("hogeName")) { __hogeName = bundle.getString("hogeName") if (__hogeName == null) { throw IllegalArgumentException("Argument is marked as non-null but was passed a null value.") } } else { __hogeName = "hoge" } return SecondFragmentArgs(__hogeName) } } } 40 NavGraphで指定した変数名をキー
  17. 41 class SecondFragment : Fragment(R.layout.fragment_second) { private val args: SecondFragmentArgs

    by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val hogeName: String = args.hogeName ʙʙʙུʙʙʙ } }
  18. 42 class SecondFragment : Fragment(R.layout.fragment_second) { private val args: SecondFragmentArgs

    by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val hogeName: String = args.hogeName ʙʙʙུʙʙʙ } } 型安全・null安全にデータ受け渡し可能
  19. 56

  20. // StartDestinationͷ΍Γํ private enum class Action { TOP, SEARCH_RESULT, DETAIL,

    TOPIC_DETAIL } companion object { fun createTopIntent(context: Context) = Intent(context, CommunityActivity::class.java).apply { putExtra(CommunityActivity::action.name, Action.TOP) } fun createSearchResultIntent(context: Context) = Intent(context, CommunityActivity::class.java).apply { putExtra(CommunityActivity::action.name, Action.SEARCH_RESULT) } fun createDetailIntent(context: Context) = Intent(context, CommunityActivity::class.java).apply { putExtra(CommunityActivity::action.name, Action.DETAIL) } fun createTopicDetailIntent(context: Context) = Intent(context, CommunityActivity::class.java).apply { putExtra(CommunityActivity::action.name, Action.TOPIC_DETAIL) } } 64
  21. 65 // StartDestinationͷ΍Γํ override fun onCreate(savedInstanceState: Bundle?) { ʙʙʙུʙʙʙ val

    navController = findNavController(R.id.nav_host_fragment) val navGraph = navController.navInflater.inflate(R.navigation.community_nav_graph) when (action) { Action.TOP -> navController.graph = navGraph Action.SEARCH_RESULT -> navController.graph = navGraph.apply { startDestination = R.id.communitySearchResultFragment } Action.DETAIL -> navController.graph = navGraph.apply { startDestination = R.id.communityDetailFragment } Action.TOPIC_DETAIL -> navController.graph = navGraph.apply { startDestination = R.id.communityTopicDetailFragment } } }
  22. 66 // StartDestinationͷ΍Γํ override fun onCreate(savedInstanceState: Bundle?) { ʙʙʙུʙʙʙ val

    navController = findNavController(R.id.nav_host_fragment) val navGraph = navController.navInflater.inflate(R.navigation.community_nav_graph) when (action) { Action.TOP -> navController.graph = navGraph Action.SEARCH_RESULT -> navController.graph = navGraph.apply { startDestination = R.id.communitySearchResultFragment } Action.DETAIL -> navController.graph = navGraph.apply { startDestination = R.id.communityDetailFragment } Action.TOPIC_DETAIL -> navController.graph = navGraph.apply { startDestination = R.id.communityTopicDetailFragment } } }
  23. 70

  24. 71

  25. 72 // GlobalActionͷ΍Γํ override fun onCreate(savedInstanceState: Bundle?) { ʙʙʙུʙʙʙ val

    navController = findNavController(R.id.nav_host_fragment) navController.setGraph(R.navigation.community_nav_graph) if (action == Action.SEARCH_RESULT) { navController.navigate( ActionOnlyNavDirections(R.id.actionToSearchResult) ) } }
  26. 73 // GlobalActionͷ΍Γํ override fun onCreate(savedInstanceState: Bundle?) { ʙʙʙུʙʙʙ val

    navController = findNavController(R.id.nav_host_fragment) navController.setGraph(R.navigation.community_nav_graph) if (action == Action.SEARCH_RESULT) { navController.navigate( ActionOnlyNavDirections(R.id.actionToSearchResult) ) } }
  27. 80

  28. 81 // NestedGraphͷ΍Γํ override fun onCreate(savedInstanceState: Bundle?) { ʙʙʙུʙʙʙ val

    navController = findNavController(R.id.nav_host_fragment) when (action) { Action.TOP -> navController.setGraph(R.navigation.community_top_nav_graph) Action.SEARCH_RESULT -> navController.setGraph(R.navigation.community_search_result_nav_graph) Action.DETAIL -> navController.setGraph(R.navigation.community_detail_nav_graph) Action.TOPIC_DETAIL -> navController.setGraph(R.navigation.community_topic_detail_nav_graph) } }
  29. 82 // NestedGraphͷ΍Γํ override fun onCreate(savedInstanceState: Bundle?) { ʙʙʙུʙʙʙ val

    navController = findNavController(R.id.nav_host_fragment) when (action) { Action.TOP -> navController.setGraph(R.navigation.community_top_nav_graph) Action.SEARCH_RESULT -> navController.setGraph(R.navigation.community_search_result_nav_graph) Action.DETAIL -> navController.setGraph(R.navigation.community_detail_nav_graph) Action.TOPIC_DETAIL -> navController.setGraph(R.navigation.community_topic_detail_nav_graph) } }
  30. 84 // ผͷNavGraph΁ͷσʔλड͚౉͠ <action android:id="@+id/action_search_to_search_result" app:destination="@id/community_search_result_nav_graph"> // actionʹ௥Ճ͢Δ͜ͱͰDirectionsͷҾ਺ͱͯ͠ೝࣝ <argument android:name="keyword"

    app:argType="string" /> </action> // Fragment.kt private fun navigateToSearchResult(word: String) { findNavController().navigate( CommunitySearchFragmentDirections.actionSearchToSearchResult(keyword = word) ) }
  31. 94 // ListenerͰToolbarΛมߋ // ToolbarͷΞΠίϯ΍ϝχϡʔͳͲΧελϚΠζՄೳ val navController = findNavController(R.id.nav_host_fragment) navController.addOnDestinationChangedListener

    { _, destination, _ -> when (destination.id) { R.id.communitySearchFragment -> { // ॲཧ௥Ճ } R.id.communityCreateFragment -> { // ॲཧ௥Ճ } } }
  32. // AndroidManifest <activity android:name=".status.PremiumStatusActivity" android:screenOrientation="portrait" android:launchMode="singleTask" android:theme="@style/Studyplus.NoActionBar" /> <activity android:name=".plan.PremiumPlanActivity"

    android:screenOrientation="portrait" android:launchMode="singleTask" android:theme="@style/Studyplus.Premium.NoActionBar" /> 105 互いに遷移可能かつそれぞれ複数をスタックに積まない
  33. // AndroidManifest <activity android:name=".status.PremiumStatusActivity" android:screenOrientation="portrait" android:launchMode="singleTask" android:theme="@style/Studyplus.NoActionBar" /> <activity android:name=".plan.PremiumPlanActivity"

    android:screenOrientation="portrait" android:launchMode="singleTask" android:theme="@style/Studyplus.Premium.NoActionBar" <nav-graph android:value=“@navigation/premium_nav_graph" /> </activity> 113
  34. // DialogFragmentNavigator.kt @Navigator.Name("dialog-fragment") class DialogFragmentNavigator( private val context: Context, private

    val manager: FragmentManager ) : Navigator<DialogFragmentNavigator.DialogDestination>() { override fun createDestination() = DialogDestination(this) override fun navigate( destination: DialogDestination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras? ): NavDestination? { val fragment = destination.createFragment(args) fragment.show(manager, TAG) return destination } override fun popBackStack(): Boolean { val existingFragment = manager.findFragmentByTag(TAG) if (existingFragment != null) { (existingFragment as DialogFragment).dismiss() } return true } 134
  35. // DialogFragmentNavigator.kt @Navigator.Name("dialog-fragment") class DialogFragmentNavigator( private val context: Context, private

    val manager: FragmentManager ) : Navigator<DialogFragmentNavigator.DialogDestination>() { override fun createDestination() = DialogDestination(this) override fun navigate( destination: DialogDestination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras? ): NavDestination? { val fragment = destination.createFragment(args) fragment.show(manager, TAG) return destination } override fun popBackStack(): Boolean { val existingFragment = manager.findFragmentByTag(TAG) if (existingFragment != null) { (existingFragment as DialogFragment).dismiss() } return true } Destinationの作成(クラスも⾃作必要) 135
  36. // DialogFragmentNavigator.kt @Navigator.Name("dialog-fragment") class DialogFragmentNavigator( private val context: Context, private

    val manager: FragmentManager ) : Navigator<DialogFragmentNavigator.DialogDestination>() { override fun createDestination() = DialogDestination(this) override fun navigate( destination: DialogDestination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras? ): NavDestination? { val fragment = destination.createFragment(args) fragment.show(manager, TAG) return destination } override fun popBackStack(): Boolean { val existingFragment = manager.findFragmentByTag(TAG) if (existingFragment != null) { (existingFragment as DialogFragment).dismiss() } return true } 遷移する時の処理( dialog.show() ) 136
  37. // DialogFragmentNavigator.kt @Navigator.Name("dialog-fragment") class DialogFragmentNavigator( private val context: Context, private

    val manager: FragmentManager ) : Navigator<DialogFragmentNavigator.DialogDestination>() { override fun createDestination() = DialogDestination(this) override fun navigate( destination: DialogDestination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras? ): NavDestination? { val fragment = destination.createFragment(args) fragment.show(manager, TAG) return destination } override fun popBackStack(): Boolean { val existingFragment = manager.findFragmentByTag(TAG) if (existingFragment != null) { (existingFragment as DialogFragment).dismiss() } return true } バックキー時の処理( dialog.dismiss() ) 137
  38. // DialogFragmentNavigator.kt @Navigator.Name("dialog-fragment") class DialogFragmentNavigator( private val context: Context, private

    val manager: FragmentManager ) : Navigator<DialogFragmentNavigator.DialogDestination>() { override fun createDestination() = DialogDestination(this) override fun navigate( destination: DialogDestination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Extras? ): NavDestination? { val fragment = destination.createFragment(args) fragment.show(manager, TAG) return destination } override fun popBackStack(): Boolean { val existingFragment = manager.findFragmentByTag(TAG) if (existingFragment != null) { (existingFragment as DialogFragment).dismiss() } return true } NavGraphで使うタグ(<dialog-fragment/>) 138
  39. // CollegeDocumentActivity.kt // onCreate() // `dialog-fragment` λάΛ࢖༻͢ΔͨΊʹΧελϜNavigatorΛ௥Ճ val navController =

    findNavController(R.id.nav_host_fragment) navController.navigatorProvider += DialogFragmentNavigator(~~) 140
  40. // CollegeDocumentActivity.kt // onCreate() // `dialog-fragment` λάΛ࢖༻͢ΔͨΊʹΧελϜNavigatorΛ௥Ճ val navController =

    findNavController(R.id.nav_host_fragment) navController.navigatorProvider += DialogFragmentNavigator(~~) 141 インスタンス作って追加する