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

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

    View full-size slide

  2. ⾃⼰紹介
    2

    View full-size slide

  3. 3
    ⾃⼰紹介
    名前 :Junichiro Suyama
    GitHubID:@JASON13F
    TwitterID:@JasonAndroidDev
    趣味 :ぷよぷよ
          (⼤会優勝を経験)
    名前 :Yuzuru Nakashima
    GitHubID:@nacatl
    TwitterID:@affinity_robots
    趣味 :Magic The Gathering

    View full-size slide

  4. 会社紹介
    4

    View full-size slide

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

    View full-size slide

  6. ⽬次
    • Navigationの説明
    • Navigationとは
    • 基本的な遷移の実装について
    • NavigationUI
    • データ受け渡し
    • 導⼊事例紹介
    • 導⼊⽅針
    • 実際の導⼊事例(隅⼭)
    • 実際の導⼊事例(中島)
    6

    View full-size slide

  7. 伝えたいこと
    • Navigationで画⾯遷移が簡単になったこと
    • ⼤規模アプリでもNavigationは導⼊できること
    • Navigationの各種便利機能の使いどころ
    7

    View full-size slide

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

    View full-size slide

  9. Navigationとは
    9

    View full-size slide

  10. Navigationとは
    10

    View full-size slide

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

    View full-size slide

  12. 使うことのメリット
    12
    今までの遷移
    (FragmentManager)
    これからの遷移
    (Navigation)
    遷移の実装

    遷移をコード上で管理できない

    GUIで遷移を実装可能
    バックスタック考慮

    Fragmentスタック状態を
    コード上で管理

    Fragmentスタック状態を
    考慮しなくていい
    データ受け渡し

    型考慮、nullable考慮

    型安全、null安全

    View full-size slide

  13. • Navigation ライブラリ
    • Navigation Editor
    • Navigation UI
    Navigationの構成要素
    13

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  21. NavigationのFragment遷移
    • NavController + xml(NavGraph)
    • NavGraphで視覚的に遷移図を管理
    21

    View full-size slide

  22. NavigationのFragment遷移
    22

    View full-size slide

  23. NavigationのFragment遷移
    23

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  26. // navGraphͷaction
    android:id=“@+id/hogeFragment"
    android:name="~~.HogeFragment"
    android:label="@string/title_fragment_hoge"
    tools:layout="@layout/hoge_fragment" >
    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" />

    android:id=“@+id/hogeFragment”
    26

    View full-size slide

  27. // navGraphͷaction
    android:id=“@+id/hogeFragment"
    android:name="~~.HogeFragment"
    android:label="@string/title_fragment_hoge"
    tools:layout="@layout/hoge_fragment" >
    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" />

    android:id=“@+id/hogeFragment”
    遷移アニメーションの指定
    27

    View full-size slide

  28. // navGraphͷaction
    android:id=“@+id/hogeFragment"
    android:name="~~.HogeFragment"
    android:label="@string/title_fragment_hoge"
    tools:layout="@layout/hoge_fragment" >
    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" />

    android:id=“@+id/hogeFragment”
    遷移先の指定
    28

    View full-size slide

  29. // navGraphͷaction
    android:id=“@+id/hogeFragment"
    android:name="~~.HogeFragment"
    android:label="@string/title_fragment_hoge"
    tools:layout="@layout/hoge_fragment" >
    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" />

    android:id=“@+id/hogeFragment”
    タイトル⽂字列の設定
    29

    View full-size slide

  30. NavigationUI
    30

    View full-size slide

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

    View full-size slide

  32. NavigationUIとは
    • Activity.setupActionBarWithNavController
    • Toolbar.setupWithNavController
    • NavigationView.setupWithNavController
    32

    View full-size slide

  33. データ受け渡し
    33

    View full-size slide

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

    View full-size slide

  35. 35
    // nav_graph.xml
    android:id="@+id/secondFragment"
    android:name="com.example.navigationsample.SecondFragment">
    // ࣗಈੜ੒
    android:name="hogeName"
    app:argType="string"
    android:defaultValue="hoge" />

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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で指定した変数名をキー

    View full-size slide

  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
    ʙʙʙུʙʙʙ
    }
    }

    View full-size slide

  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安全にデータ受け渡し可能

    View full-size slide

  43. ViewModelを⽤いる場合
    ViewModelでデータ共有
    • ActivityViewModel:Activityスコープ
    • NavGraphViewModel:NavGraphスコープ
    (NEW)
    43

    View full-size slide

  44. 導⼊事例
    44

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  47. 47
    モジュール図

    View full-size slide

  48. 48
    モジュール図
    app層

    View full-size slide

  49. 49
    モジュール図
    ui層

    View full-size slide

  50. 50
    モジュール図
    repository層

    View full-size slide

  51. 51
    モジュール図
    entity層

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  59. 60




    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  63. // 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

    View full-size slide

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

    View full-size slide

  65. 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 }
    }
    }

    View full-size slide

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

    View full-size slide

  67. GlobalActionのやり⽅
    68

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  71. マルチモジュールでの
    GlobalActionの注意点
    74
    • NavGraphのstartDestinationの上に
    GlobalActionの遷移先が乗る
    • GlobalActionの遷移先をRemoveする
    とNavGraphのstartDestinationに戻る

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  79. 補⾜:データ受け渡し実現
    NestedGraphの場合
    • NavGraphでデータ受け渡し
    • NavGraphViewModelでデータ共有
    83

    View full-size slide

  80. 84
    // ผͷNavGraph΁ͷσʔλड͚౉͠
    android:id="@+id/action_search_to_search_result"
    app:destination="@id/community_search_result_nav_graph">
    // actionʹ௥Ճ͢Δ͜ͱͰDirectionsͷҾ਺ͱͯ͠ೝࣝ
    android:name="keyword"
    app:argType="string" />

    // Fragment.kt
    private fun navigateToSearchResult(word: String) {
    findNavController().navigate(
    CommunitySearchFragmentDirections.actionSearchToSearchResult(keyword = word)
    )
    }

    View full-size slide

  81. 85
    // NavGraphViewModelͰσʔλड͚౉͠
    private val viewModel by navGraphViewModels(
    R.navigation.community_detail_nav_graph
    )

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  88. // NavigationUIͰToolbarͱ࿈ܞ
    findViewById(R.id.toolbar)
    .setupWithNavController(
    navController = findNavController(R.id.nav_host_fragment),
    configuration = AppBarConfiguration(emptySet()) {
    onBackPressed()
    true
    }
    )
    92

    View full-size slide

  89. onDestinationChangedListener
    Destination

    といった遷移先
    onDestinationChangedListener
    • Navigationによる遷移時に呼び出される
    93

    View full-size slide

  90. 94
    // ListenerͰToolbarΛมߋ
    // ToolbarͷΞΠίϯ΍ϝχϡʔͳͲΧελϚΠζՄೳ
    val navController = findNavController(R.id.nav_host_fragment)
    navController.addOnDestinationChangedListener { _, destination, _ ->
    when (destination.id) {
    R.id.communitySearchFragment -> {
    // ॲཧ௥Ճ
    }
    R.id.communityCreateFragment -> {
    // ॲཧ௥Ճ
    }
    }
    }

    View full-size slide

  91. 補⾜:前画⾯に戻る挙動
    popBackStack()
    • バックスタックからCurrentDestinationを削除
    • バックスタックがCurrentのみの場合、
    処理が⾏われずfalseを返却
    95
    private fun navigateBack() {
    if (findNavController().popBackStack().not()) activity?.finish()
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  98. Studyplus Pro
    102

    相互遷移
    課⾦導線画⾯ 登録状況画⾯

    View full-size slide

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

    View full-size slide

  100. // AndroidManifest
    android:name=".status.PremiumStatusActivity"
    android:screenOrientation="portrait"
    android:launchMode="singleTask"
    android:theme="@style/Studyplus.NoActionBar" />
    android:name=".plan.PremiumPlanActivity"
    android:screenOrientation="portrait"
    android:launchMode="singleTask"
    android:theme="@style/Studyplus.Premium.NoActionBar" />
    104

    View full-size slide

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

    View full-size slide

  102. Studyplus Pro
    106

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  107. 既存の仕組みでもいいけど
    Navigation導⼊済みの画⾯なら
    NavigationのDeepLink遷移で
    いいのではないか?
    111
    解決策:他モジュールから遷移

    View full-size slide

  108. // premium_nav_graph.xml
    android:id=“@+id/action_global_to_shipping_address”
    android:destination=“@id/premiumAppealFragment” >


    112

    View full-size slide

  109. // AndroidManifest
    android:name=".status.PremiumStatusActivity"
    android:screenOrientation="portrait"
    android:launchMode="singleTask"
    android:theme="@style/Studyplus.NoActionBar" />
    android:name=".plan.PremiumPlanActivity"
    android:screenOrientation="portrait"
    android:launchMode="singleTask"
    android:theme="@style/Studyplus.Premium.NoActionBar"


    113

    View full-size slide

  110. Studyplus Pro
    114

    View full-size slide

  111. Studyplus Pro
    115

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  114. つまづき:DeepLink遷移
    118

    OK

    View full-size slide

  115. つまづき:DeepLink遷移
    119

    ×
    アプリが
    閉じる

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  118. // AndroidManifest
    android:name=".status.PremiumStatusActivity"
    android:screenOrientation="portrait"
    android:launchMode="singleTask"
    android:theme="@style/Studyplus.NoActionBar" />
    android:name=".plan.PremiumPlanActivity"
    android:screenOrientation="portrait"
    android:launchMode="singleTask"
    android:theme="@style/Studyplus.Premium.NoActionBar" />
    122
    !!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  124. ⼤学資料機能
    128
    検索して

    選択して

    View full-size slide

  125. ⼤学資料機能
    • CollegeDocumentActivity(全7画⾯)
    • 検索条件指定画⾯
    • 検索結果画⾯
    • おすすめ資料画⾯
    • 請求画⾯
    • 請求完了画⾯
    • 請求履歴画⾯
    • 住所⼊⼒画⾯
    129

    View full-size slide

  126. ⼤学資料機能
    130

    View full-size slide

  127. ⼤学資料機能
    131
    DialogFragment

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  130. // DialogFragmentNavigator.kt
    @Navigator.Name("dialog-fragment")
    class DialogFragmentNavigator(
    private val context: Context,
    private val manager: FragmentManager
    ) : Navigator() {
    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

    View full-size slide

  131. // DialogFragmentNavigator.kt
    @Navigator.Name("dialog-fragment")
    class DialogFragmentNavigator(
    private val context: Context,
    private val manager: FragmentManager
    ) : Navigator() {
    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

    View full-size slide

  132. // DialogFragmentNavigator.kt
    @Navigator.Name("dialog-fragment")
    class DialogFragmentNavigator(
    private val context: Context,
    private val manager: FragmentManager
    ) : Navigator() {
    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

    View full-size slide

  133. // DialogFragmentNavigator.kt
    @Navigator.Name("dialog-fragment")
    class DialogFragmentNavigator(
    private val context: Context,
    private val manager: FragmentManager
    ) : Navigator() {
    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

    View full-size slide

  134. // DialogFragmentNavigator.kt
    @Navigator.Name("dialog-fragment")
    class DialogFragmentNavigator(
    private val context: Context,
    private val manager: FragmentManager
    ) : Navigator() {
    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で使うタグ()
    138

    View full-size slide

  135. // college_document_nav_graph.xml
    android:id="@+id/collegeDocumentRecommendFragment"
    android:name="~~.CollegeDocumentRecommendFragment"
    tools:layout=“@layout/dialog_college_document_recommend" />
    ここの指定以外は同じ
    139

    View full-size slide

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

    View full-size slide

  137. // CollegeDocumentActivity.kt
    // onCreate()
    // `dialog-fragment` λάΛ࢖༻͢ΔͨΊʹΧελϜNavigatorΛ௥Ճ
    val navController = findNavController(R.id.nav_host_fragment)
    navController.navigatorProvider += DialogFragmentNavigator(~~)
    141
    インスタンス作って追加する

    View full-size slide

  138. 補⾜:公式のDialog対応
    • DialogFragmentNavigatorは
    ver2.1.0で追加されている
    • BottomSheetDialogFragmentも対応
    • なお、ライブラリ更新時に
    ⾃作クラスは削除
    142

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  145. まとめ
    149

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide