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

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

179ba34ba8668d3d4323badc15c33b94?s=47 nacatl
February 21, 2020

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

DroidKaigi2020で発表予定だった資料です。マルチモジュールを採用した大規模アプリでのNavigation導入事例として、スタディプラス株式会社におけるプロダクトでの実装を紹介させていただきます。

179ba34ba8668d3d4323badc15c33b94?s=128

nacatl

February 21, 2020
Tweet

Transcript

  1. Navigation Component を実践導⼊した際の感動、 便利さ、そしてつまづき Yuzuru Nakashima, Junichiro Suyama 2020/02/21 13:00-13:40

    DroidKaigi2020 1
  2. ⾃⼰紹介 2

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

    (⼤会優勝を経験) 名前 :Yuzuru Nakashima GitHubID:@nacatl TwitterID:@affinity_robots 趣味 :Magic The Gathering
  4. 会社紹介 4

  5. 5 「毎⽇の勉強を習慣にできない」悩みを解決 勉強したら、スマホで記録し、グラフで可視化、勉強仲間で 励まし合うことで、勉強の習慣化を⽀援 累計会員数:500万⼈達成 ⼤学受験⽣の約40%が会員 アプリレビュー平均4.5以上 Google Playベストアプリ(2015, 2016)、

    ⽇本e-Learning⼤賞(最優秀賞)など受賞多数。
  6. ⽬次 • Navigationの説明 • Navigationとは • 基本的な遷移の実装について • NavigationUI •

    データ受け渡し • 導⼊事例紹介 • 導⼊⽅針 • 実際の導⼊事例(隅⼭) • 実際の導⼊事例(中島) 6
  7. 伝えたいこと • Navigationで画⾯遷移が簡単になったこと • ⼤規模アプリでもNavigationは導⼊できること • Navigationの各種便利機能の使いどころ 7

  8. 伝えたいこと Navigation導⼊の切っ掛けになれば嬉しい 8

  9. Navigationとは 9

  10. Navigationとは 10

  11. Navigationとは • Fragment遷移を簡単に実装可能 • Navigation EditorでGUI操作 11

  12. 使うことのメリット 12 今までの遷移 (FragmentManager) これからの遷移 (Navigation) 遷移の実装 ❌ 遷移をコード上で管理できない ⭕

    GUIで遷移を実装可能 バックスタック考慮 ❌ Fragmentスタック状態を コード上で管理 ⭕ Fragmentスタック状態を 考慮しなくていい データ受け渡し ❌ 型考慮、nullable考慮 ⭕ 型安全、null安全
  13. • Navigation ライブラリ • Navigation Editor • Navigation UI Navigationの構成要素

    13
  14. 基本的な遷移の実装について 14

  15. 既存のFragment遷移 • Fragment遷移図の把握が⾯倒 • 遷移時にstackの処理が必要 • アニメーション指定 • ActionBar.setTitle 15

  16. // ભҠ࣌ 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
  17. // ભҠ࣌ 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
  18. // ભҠ࣌ 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
  19. // ભҠ࣌ 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
  20. // ભҠ࣌ 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
  21. NavigationのFragment遷移 • NavController + xml(NavGraph) • NavGraphで視覚的に遷移図を管理 21

  22. NavigationのFragment遷移 22

  23. NavigationのFragment遷移 23

  24. 画⾯遷移をGUIで俯瞰できるのすごい! 24 NavigationのFragment遷移

  25. // ભҠॲཧ val navController = findNavController(R.id.nav_host_fragment) navController.navigate( HogeFragmentDirections.actionHogeToFuga() ) 25

  26. // 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
  27. // 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
  28. // 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
  29. // 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
  30. NavigationUI 30

  31. NavigationUIとは UIコンポーネントと連携させて更新可能 • Top App Bars(ActionBar, Toolbar) • Navigation Drawer

    • Bottom Navigation 31
  32. NavigationUIとは • Activity.setupActionBarWithNavController • Toolbar.setupWithNavController • NavigationView.setupWithNavController 32

  33. データ受け渡し 33

  34. Argumentの設定 データ受け渡しもNavGraphで実装可能 • プリミティブ型 • Parcelable • Serializable • Enumなど

    34
  35. 35 // nav_graph.xml <fragment android:id="@+id/secondFragment" android:name="com.example.navigationsample.SecondFragment"> // ࣗಈੜ੒ <argument android:name="hogeName"

    app:argType="string" android:defaultValue="hoge" /> </fragment>
  36. SafeArgsの利⽤ SafeArgsのプラグインを適⽤することで DirectionsクラスとArgsクラスが⾃動⽣成 • Directions:遷移する関数の引数でArgumentを 型安全に設定可能 • Args:by navArgs()で型安全に取得可能 36

  37. // 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
  38. // 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
  39. // 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
  40. // 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で指定した変数名をキー
  41. 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 ʙʙʙུʙʙʙ } }
  42. 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安全にデータ受け渡し可能
  43. ViewModelを⽤いる場合 ViewModelでデータ共有 • ActivityViewModel:Activityスコープ • NavGraphViewModel:NavGraphスコープ (NEW) 43

  44. 導⼊事例 44

  45. Studyplusアプリの概要 • アプリの歴史:2015年7⽉〜 • アプリサイズ:100000⾏弱 • Activity数:約200個 • Navigation導⼊開始:2019年7⽉〜 45

  46. 既存の作り • 画⾯全体がActivity構成 • マルチモジュール 46

  47. 47 モジュール図

  48. 48 モジュール図 app層

  49. 49 モジュール図 ui層

  50. 50 モジュール図 repository層

  51. 51 モジュール図 entity層

  52. Studyplusアプリへの導⼊⽅針 導⼊⽅針 • 機能モジュールごとにNavigation導⼊ 導⼊⽅法 • 機能モジュールを1ActivityマルチFragment化 • Navigationで画⾯遷移、データ受け渡し実現 52

  53. 実際の導⼊事例 (隅⼭) 53

  54. コミュニティ機能の全体構成 CommunityActivity(全11画⾯) 検索画⾯、検索結果画⾯、メンバー招待画⾯、 作成画⾯、詳細画⾯、メンバー管理画⾯、 編集画⾯、参加申請画⾯、トピック⼀覧画⾯、 トピック詳細画⾯、トピック作成画⾯ 54

  55. コミュニティ機能とは 55

  56. 56

  57. 導⼊事例 • 課題①:画⾯遷移 • 課題②:Toolbar • 注意事項:マルチクリックによるクラッシュ 57

  58. 導⼊事例 • 課題①:画⾯遷移 • 課題②:Toolbar • 注意事項:マルチクリックによるクラッシュ 58

  59. 課題①:画⾯遷移 他モジュールから遷移する場合、開始地点が異なる 1. 通常の開始地点:検索画⾯ 2. ユーザ情報からの遷移:検索結果画⾯ 3. 通知からの遷移:詳細画⾯ 4. 通知からの遷移:トピック詳細画⾯

    59
  60. 60 1 2 3 4

  61. 解決策:開始地点の変更⽅法 • StartDestinationを⽤いる • GlobalActionを⽤いる • NavGraphを分ける 61

  62. 解決策:開始地点の変更⽅法 • StartDestinationを⽤いる • GlobalActionを⽤いる • NavGraphを分ける →今回はNavGraphを分けることを選択 62

  63. StartDestinationの場合 63 NavGraphのsetStartDestinationで どの画⾯から始めるか指定できる 使い所  :開始地点が多い場合 メリット :nav_graphを変更する必要がない デメリット:特になし

  64. // 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
  65. 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 } } }
  66. 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 } } }
  67. GlobalActionの場合 67 どこからでも利⽤できる遷移 使い所  :汎⽤的な画⾯への遷移の場合 メリット :遷移が複雑でも遷移図がシンプル デメリット:GUI上で遷移関係が追いにくい

  68. GlobalActionのやり⽅ 68

  69. 69 <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/community_top_nav_graph" app:startDestination="@id/communitySearchFragment"> // ࣗಈੜ੒ <action android:id="@+id/actionToSearchResult"

    app:destination="@id/communitySearchResultFragment" /> </navigation>
  70. 70

  71. 71

  72. 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) ) } }
  73. 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) ) } }
  74. マルチモジュールでの GlobalActionの注意点 74 • NavGraphのstartDestinationの上に GlobalActionの遷移先が乗る • GlobalActionの遷移先をRemoveする とNavGraphのstartDestinationに戻る

  75. マルチモジュールでの GlobalActionの注意点 75 理想 トップ画⾯ コミュニティのNavGraph GlobalAction先

  76. マルチモジュールでの GlobalActionの注意点 76 トップ画⾯ コミュニティのNavGraph GlobalAction先 遷移 戻る 理想

  77. マルチモジュールでの GlobalActionの注意点 77 トップ画⾯ コミュニティのNavGraph GlobalAction先 startDestination 現実

  78.    遷移 マルチモジュールでの GlobalActionの注意点 78 トップ画⾯ コミュニティのNavGraph GlobalAction先 startDestination 戻る 戻る

    現実
  79. NavGraphを分ける場合 79 開始地点ごとにNavGraphを切り分けて それぞれのNavGraphをNestedGraphで遷移を実現 使い所  :ViewModelでデータ共有する場合 メリット :NavGraphViewModelが使える デメリット:GUI上で機能全体の遷移が追いにくい

  80. 80

  81. 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) } }
  82. 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) } }
  83. 補⾜:データ受け渡し実現 NestedGraphの場合 • NavGraphでデータ受け渡し • NavGraphViewModelでデータ共有 83

  84. 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) ) }
  85. 85 // NavGraphViewModelͰσʔλड͚౉͠ private val viewModel by navGraphViewModels<CommunityDetailViewModel>( R.navigation.community_detail_nav_graph )

  86. 導⼊事例 • 課題①:画⾯遷移 • 課題②:Toolbar • 注意事項:マルチクリックによるクラッシュ 86

  87. 課題②:Toolbar 画⾯仕様: • UpボタンのアイコンがCloseの画⾯が存在 • iOSとデザインを共通にする必要がある 87

  88. 解決策:Toolbarの作成⽅法 • NavigationUIでToolbarと連携 • ToolbarをActivityに持たせ、 addOnDestinationChangedListener()で変更 • Toolbarを各Fragmentに持たせる 88

  89. 解決策:Toolbarの作成⽅法 • NavigationUIでToolbarと連携 →アイコンが変えられない • ToolbarをActivityに持たせ、 addOnDestinationChangedListener()で変更 • Toolbarを各Fragmentに持たせる 89

  90. 解決策:Toolbarの作成⽅法 • NavigationUIでToolbarと連携 →アイコンが変えられない • ToolbarをActivityに持たせ、 addOnDestinationChangedListener()で変更 →アイコン変更のコード量が多い • Toolbarを各Fragmentに持たせる

    90
  91. 解決策:Toolbarの作成⽅法 • NavigationUIでToolbarと連携 →アイコンが変えられない • ToolbarをActivityに持たせ、 addOnDestinationChangedListener()で変更 →アイコン変更のコード量が多い • Toolbarを各Fragmentに持たせる→選択

    91
  92. // NavigationUIͰToolbarͱ࿈ܞ findViewById<Toolbar>(R.id.toolbar) .setupWithNavController( navController = findNavController(R.id.nav_host_fragment), configuration = AppBarConfiguration(emptySet())

    { onBackPressed() true } ) 92
  93. onDestinationChangedListener Destination • <fragment><activity><dialog_fragment> といった遷移先 onDestinationChangedListener • Navigationによる遷移時に呼び出される 93

  94. 94 // ListenerͰToolbarΛมߋ // ToolbarͷΞΠίϯ΍ϝχϡʔͳͲΧελϚΠζՄೳ val navController = findNavController(R.id.nav_host_fragment) navController.addOnDestinationChangedListener

    { _, destination, _ -> when (destination.id) { R.id.communitySearchFragment -> { // ॲཧ௥Ճ } R.id.communityCreateFragment -> { // ॲཧ௥Ճ } } }
  95. 補⾜:前画⾯に戻る挙動 popBackStack() • バックスタックからCurrentDestinationを削除 • バックスタックがCurrentのみの場合、 処理が⾏われずfalseを返却 95 private fun

    navigateBack() { if (findNavController().popBackStack().not()) activity?.finish() }
  96. 導⼊事例 • 課題①:画⾯遷移 • 課題②:Toolbar • 注意事項:マルチクリックによるクラッシュ 96

  97. マルチクリックによるクラッシュ エラー:xxx is unknown to this NavController 原因:マルチクリック時に複数回navigateが呼ばれ、 currentDestinationがずれるとクラッシュ 解決策:マルチクリックを無効にする

    97 // styles.xml <item name="android:splitMotionEvents">false</item>
  98. マルチクリック以外でも発⽣(発⽣条件は不明、⾮同期処理?) →ライブラリ側の不具合の可能性が⾼い →遷移時にcurrentDestinationのidを確認することで回避可能 参考資料:株式会社ZOZOテクノロジーズ TECH BLOG https://techblog.zozo.com/entry/android-jetpack-navigation 98 マルチクリックによるクラッシュ

  99. 実際の導⼊事例 (中島) 99

  100. 導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの導⼊経緯 •

    CustomNavigatorの留意点 100
  101. 導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの導⼊経緯 •

    CustomNavigatorの留意点 101
  102. Studyplus Pro 102 ⇄ 相互遷移 課⾦導線画⾯ 登録状況画⾯

  103. Studyplus Pro • 課⾦基盤関連画⾯(全4画⾯) • 課⾦導線画⾯(Fragment) • 機能⼀覧画⾯(BottomSheetDialog) • 登録完了画⾯(BottomSheetDialog)

    • 登録状況画⾯(別Activity) 103
  104. // 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" /> 104
  105. // 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 互いに遷移可能かつそれぞれ複数をスタックに積まない
  106. Studyplus Pro 106

  107. • 未課⾦時に課⾦機能を使おうとした時に 課⾦導線画⾯へ遷移させる • いろんな機能に散らばるためできる限り 共通に作りたい 107 課題:他モジュールから遷移

  108. • 既存の仕組み • ui層の下に置いたrouterモジュールに 遷移⽤のinterfaceを置く • 各機能モジュールで遷移interface実装 108 Studyplusのモジュール間遷移

  109. 109 再掲:モジュール図 ui層 routerモジュール

  110. // routerϞδϡʔϧͷinterface fun openHogeActivity(activity: Activity) // ֤uiϞδϡʔϧͰͷinterface࣮૷ override fun openHogeActivity(activity:

    Activity) { activity.startActivity(HogeActivity.createIntent(activity)) } 110
  111. 既存の仕組みでもいいけど Navigation導⼊済みの画⾯なら NavigationのDeepLink遷移で いいのではないか? 111 解決策:他モジュールから遷移

  112. // premium_nav_graph.xml <fragment android:id=“@+id/action_global_to_shipping_address” android:destination=“@id/premiumAppealFragment” > <deepLink app:uri="(scheme)://premium_appeal" />
 <fragment

    /> 112
  113. // 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
  114. Studyplus Pro 114

  115. Studyplus Pro 115

  116. // ભҠݩͷॲཧ startActivity(Intent( Intent.ACTION_VIEW, Uri.parse("(scheme)://premium_appeal") )) 116

  117. マルチモジュールでの遷移が⾮常に楽! 117 解決策:他モジュールから遷移

  118. つまづき:DeepLink遷移 118 → OK

  119. つまづき:DeepLink遷移 119 ← × アプリが 閉じる

  120. タスクのバックスタックが全部消えた…? 120 つまづき:DeepLink遷移

  121. • Navigationの暗黙的DeepLinkは Intent.FLAG_ACTIVITY_NEW_TASK の有無でバックスタックの挙動が変わる • 明記はされていないが、 launchMode=“singleTask”も同様の結果に 121 原因 https://developer.android.com/guide/navigation/navigation-deep-link#implicit

  122. // 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" /> 122 !!
  123. • モジュール間の画⾯遷移⾃体には ⾮常に使いやすく、有⽤ • ただし、遷移先ActivityのLaunchMode には注意… (SingleTask指定はかなり特殊だと思いますが) 123 DeepLink遷移を試してみて

  124. 導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの利⽤経緯 •

    CustomNavigatorの留意点 124
  125. CustomNavigatorとは • 遷移管理クラス(Navigator)を⾃分で カスタマイズして追加できる • 公式にないDestinationを追加したいとき • 遷移時に共通で何か処理が必要なとき 125

  126. Navigationの拡張性の⾼さに感動 126 CustomNavigatorとは

  127. 導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの利⽤経緯 •

    CustomNavigatorの留意点 127
  128. ⼤学資料機能 128 検索して → 選択して →

  129. ⼤学資料機能 • CollegeDocumentActivity(全7画⾯) • 検索条件指定画⾯ • 検索結果画⾯ • おすすめ資料画⾯ •

    請求画⾯ • 請求完了画⾯ • 請求履歴画⾯ • 住所⼊⼒画⾯ 129
  130. ⼤学資料機能 130

  131. ⼤学資料機能 131 DialogFragment

  132. Studyplusでの利⽤経緯 • 実装当時(ver2.0.0)DialogFragmentは 公式でサポートされていなかった • 2.1.0-alphaでは実装済みだったが リリース時期的に待てなかった 132

  133. Studyplusでの利⽤経緯 alphaの内部コードなどを参考に DialogFragmentNavigatorを⾃作した 133

  134. // 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
  135. // 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
  136. // 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
  137. // 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
  138. // 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
  139. // college_document_nav_graph.xml <dialog-fragment android:id="@+id/collegeDocumentRecommendFragment" android:name="~~.CollegeDocumentRecommendFragment" tools:layout=“@layout/dialog_college_document_recommend" /> ここの指定以外は同じ 139

  140. // CollegeDocumentActivity.kt // onCreate() // `dialog-fragment` λάΛ࢖༻͢ΔͨΊʹΧελϜNavigatorΛ௥Ճ val navController =

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

    findNavController(R.id.nav_host_fragment) navController.navigatorProvider += DialogFragmentNavigator(~~) 141 インスタンス作って追加する
  142. 補⾜:公式のDialog対応 • DialogFragmentNavigatorは ver2.1.0で追加されている • BottomSheetDialogFragmentも対応 • なお、ライブラリ更新時に ⾃作クラスは削除 142

  143. 導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの利⽤経緯 •

    CustomNavigatorの留意点 143
  144. CustomNavigatorの留意点 • 欲しい機能は公式alphaに追加されてないか • 作っても⼀時的なコードになる • stableを待てるかリリース⽇の相談 144

  145. CustomNavigatorの留意点 • 作成後、Navigatorクラス周りに変更はないか • 公式の変更に追従する必要性 • メンテナンスコストの検討 145

  146. CustomNavigatorの留意点 • 仕様の⾒直しで解決できないか • 特殊なことをしないで済まないか相談 146

  147. CustomNavigatorの留意点 便利で強⼒な拡張機能だが使いどころは⾒極めよう! 147

  148. 余談 Navigatorクラスが遷移に必要な処理を ラップしているということは… 通常のFragmentからDialogFragmentに変更、 みたいな仕様変更に強い 148

  149. まとめ 149

  150. まとめ • Navigationで画⾯遷移が簡単になった! • ⼤規模アプリでもNavigationは導⼊できる! • Navigationの機能使いこなすと便利! 150

  151. まとめ • Navigationで画⾯遷移が簡単になった! • ⼤規模アプリでもNavigationは導⼊できる! • Navigationの機能使いこなすと便利! 遷移図の俯瞰やGUI操作、遷移処理の簡易化など 151

  152. まとめ • Navigationで画⾯遷移が簡単になった! • ⼤規模アプリでもNavigationは導⼊できる! • Navigationの機能使いこなすと便利! 1機能1Activity⽅針で⼗分効果的 マルチモジュールでも導⼊は容易かつ有⽤ 152

  153. まとめ • Navigationで画⾯遷移が簡単になった! • ⼤規模アプリでもNavigationは導⼊できる! • Navigationの機能使いこなすと便利! 様々な便利機能、拡張性 153

  154. まとめ Navigation導⼊の切っ掛けになれば嬉しい 154

  155. ご静聴ありがとうございました Thank you for listening. 155