Share
Embed
Navigation Componentを実戦投入した際の感動、便利さ、そしてつまづき
DroidKaigi2020で発表予定だった資料です。マルチモジュールを採用した大規模アプリでのNavigation導入事例として、スタディプラス株式会社におけるプロダクトでの実装を紹介させていただきます。
nacatl
February 21, 2020
Transcript
155 slides
-
3 ⾃⼰紹介 名前 :Junichiro Suyama GitHubID:@JASON13F TwitterID:@JasonAndroidDev 趣味 :ぷよぷよ
(⼤会優勝を経験) 名前 :Yuzuru Nakashima GitHubID:@nacatl TwitterID:@affinity_robots 趣味 :Magic The Gathering -
⽬次 • Navigationの説明 • Navigationとは • 基本的な遷移の実装について • NavigationUI •
データ受け渡し • 導⼊事例紹介 • 導⼊⽅針 • 実際の導⼊事例(隅⼭) • 実際の導⼊事例(中島) 6 -
使うことのメリット 12 今までの遷移 (FragmentManager) これからの遷移 (Navigation) 遷移の実装 ❌ 遷移をコード上で管理できない ⭕
GUIで遷移を実装可能 バックスタック考慮 ❌ Fragmentスタック状態を コード上で管理 ⭕ Fragmentスタック状態を 考慮しなくていい データ受け渡し ❌ 型考慮、nullable考慮 ⭕ 型安全、null安全 -
// ભҠ࣌ 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 -
// ભҠ࣌ 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 -
// ભҠ࣌ 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 -
// ભҠ࣌ 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 -
// ભҠ࣌ 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 -
// 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 -
// 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 -
// 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 -
// 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 -
NavigationUIとは UIコンポーネントと連携させて更新可能 • Top App Bars(ActionBar, Toolbar) • Navigation Drawer
• Bottom Navigation 31 -
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> -
// 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 -
// 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 -
// 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 -
// 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 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 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安全にデータ受け渡し可能 -
// 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 // 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 // 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 } } } -
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> -
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 // 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) ) } } -
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 // 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) } } -
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) ) } -
94 // ListenerͰToolbarΛมߋ // ToolbarͷΞΠίϯϝχϡʔͳͲΧελϚΠζՄೳ val navController = findNavController(R.id.nav_host_fragment) navController.addOnDestinationChangedListener
{ _, destination, _ -> when (destination.id) { R.id.communitySearchFragment -> { // ॲཧՃ } R.id.communityCreateFragment -> { // ॲཧՃ } } } -
補⾜:前画⾯に戻る挙動 popBackStack() • バックスタックからCurrentDestinationを削除 • バックスタックがCurrentのみの場合、 処理が⾏われずfalseを返却 95 private fun
navigateBack() { if (findNavController().popBackStack().not()) activity?.finish() } -
マルチクリックによるクラッシュ エラー:xxx is unknown to this NavController 原因:マルチクリック時に複数回navigateが呼ばれ、 currentDestinationがずれるとクラッシュ 解決策:マルチクリックを無効にする
97 // styles.xml <item name="android:splitMotionEvents">false</item> -
導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの導⼊経緯 •
CustomNavigatorの留意点 100 -
導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの導⼊経緯 •
CustomNavigatorの留意点 101 -
// 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 -
// 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 互いに遷移可能かつそれぞれ複数をスタックに積まない -
// routerϞδϡʔϧͷinterface fun openHogeActivity(activity: Activity) // ֤uiϞδϡʔϧͰͷinterface࣮ override fun openHogeActivity(activity:
Activity) { activity.startActivity(HogeActivity.createIntent(activity)) } 110 -
// 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 -
// 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 !! -
導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの利⽤経緯 •
CustomNavigatorの留意点 124 -
導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの利⽤経緯 •
CustomNavigatorの留意点 127 -
⼤学資料機能 • CollegeDocumentActivity(全7画⾯) • 検索条件指定画⾯ • 検索結果画⾯ • おすすめ資料画⾯ •
請求画⾯ • 請求完了画⾯ • 請求履歴画⾯ • 住所⼊⼒画⾯ 129 -
// 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 -
// 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 -
// 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 -
// 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 -
// 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 -
// CollegeDocumentActivity.kt // onCreate() // `dialog-fragment` λάΛ༻͢ΔͨΊʹΧελϜNavigatorΛՃ val navController =
findNavController(R.id.nav_host_fragment) navController.navigatorProvider += DialogFragmentNavigator(~~) 140 -
// CollegeDocumentActivity.kt // onCreate() // `dialog-fragment` λάΛ༻͢ΔͨΊʹΧελϜNavigatorΛՃ val navController =
findNavController(R.id.nav_host_fragment) navController.navigatorProvider += DialogFragmentNavigator(~~) 141 インスタンス作って追加する -
導⼊事例 • DeepLinkの留意点 • CustomNavigator • CustomNavigatorとは • Studyplusでの利⽤経緯 •
CustomNavigatorの留意点 143