$30 off During Our Annual Pro Sale. View Details »

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

nacatl
February 21, 2020

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

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

nacatl

February 21, 2020
Tweet

More Decks by nacatl

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 インスタンス作って追加する