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

Fragments V2 + Navigation Component

Ziem
October 04, 2021

Fragments V2 + Navigation Component

Ziem

October 04, 2021
Tweet

More Decks by Ziem

Other Decks in Programming

Transcript

  1. Old screen architecture • multiple Activities • each screen had

    a separate Activity • no Fragments in the project • OK, we had like one or two (e.g. PreferenceFragment)
  2. Why did we decide to migrate to Fragments? Activity has

    some limitations • It doesn’t support fl exible UI for tablets (e.g.: two pane layout) • It doesn’t allow to move views between Activities (e.g.: video in AB)
  3. Why did we decide to migrate to Fragments? Activity has

    some limitations • It doesn’t support fl exible UI for tablets (e.g.: two pane layout) • It doesn’t allow to move views between Activities (e.g.: video in AB) Nav Component solves some problems • We don’t have to reinvent the wheel • Easier onboarding
  4. <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http: // schemas.android.com/apk/res/android" xmlns:app="http: // schemas.android.com/apk/res-auto"

    android:id="@+id/nav_graph" app:startDestination="@id/router"> <fragment android:id="@+id/article" android:name=".ArticleFragment" android:label="Article"> <argument android:name="articleId" app:argType="string" app:nullable="true" /> </ fragment> <action android:id="@+id/action_global_article" app:destination="@id/article" / > ... </ navigation>
  5. <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http: // schemas.android.com/apk/res/android" xmlns:app="http: // schemas.android.com/apk/res-auto"

    android:id="@+id/nav_graph" app:startDestination="@id/router"> <fragment android:id="@+id/article" android:name=".ArticleFragment" android:label="Article"> <argument android:name="articleId" app:argType="string" app:nullable="true" /> </ fragment> <action android:id="@+id/action_global_article" app:destination="@id/article" / > ... </ navigation>
  6. <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http: // schemas.android.com/apk/res/android" xmlns:app="http: // schemas.android.com/apk/res-auto"

    android:id="@+id/nav_graph" app:startDestination="@id/router"> <fragment android:id="@+id/article" android:name=".ArticleFragment" android:label="Article"> <argument android:name="articleId" app:argType="string" app:nullable="true" /> </ fragment> <action android:id="@+id/action_global_article" app:destination="@id/article" / > ... </ navigation>
  7. <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http: // schemas.android.com/apk/res/android" xmlns:app="http: // schemas.android.com/apk/res-auto"

    android:id="@+id/nav_graph" app:startDestination="@id/router"> <fragment android:id="@+id/article" android:name=".ArticleFragment" android:label="Article"> <argument android:name="articleId" app:argType="string" app:nullable="true" /> </ fragment> <action android:id="@+id/action_global_article" app:destination="@id/article" / > ... </ navigation>
  8. 1. Creating a Fragment for an Activity 2. Creating the

    whole navigation graph 3. Deleting all Activities 4. Clean up, fi xing Migration process
  9. Back press handling private val backButtonCallback = object : OnBackPressedCallback(true)

    { override fun handleOnBackPressed() { if (webView.canGoBack()) { webView.goBack() } else { remove() requireActivity().onBackPressed() } } } override fun onCreate(savedInstanceState: Bundle?) { ... requireActivity().onBackPressedDispatcher .addCallback(this, backButtonCallback) }
  10. Back press handling private val backButtonCallback = object : OnBackPressedCallback(true)

    { override fun handleOnBackPressed() { if (webView.canGoBack()) { webView.goBack() } else { remove() requireActivity().onBackPressed() } } } override fun onCreate(savedInstanceState: Bundle?) { ... requireActivity().onBackPressedDispatcher .addCallback(this, backButtonCallback) }
  11. Back press handling private val backButtonCallback = object : OnBackPressedCallback(true)

    { override fun handleOnBackPressed() { if (webView.canGoBack()) { webView.goBack() } else { remove() requireActivity().onBackPressed() } } } override fun onCreate(savedInstanceState: Bundle?) { ... requireActivity().onBackPressedDispatcher .addCallback(this, backButtonCallback) }
  12. Getting a result from a fragment • scoped ViewModel •

    currentBackStackEntry & previousBackStackEntry & 
 SavedStateHandle combo • More in https://stackover fl ow.com/q/50702643/759007
  13. Getting a result from a fragment setFragmentResultListener("request_key") { requestKey: String,

    bundle: Bundle -> { val result = bundle.getString("your_data_key") // do something with the result } } findNavController().navigate(…) FirstFragment
  14. Getting a result from a fragment setFragmentResultListener("request_key") { requestKey: String,

    bundle: Bundle -> { val result = bundle.getString("your_data_key") // do something with the result } } findNavController().navigate(…) FirstFragment
  15. Getting a result from a fragment SecondFragment val result =

    Bundle().apply { putString("your_data_key", "Hello!") } setFragmentResult("request_key", result) findNavController().navigateUp()
  16. Getting a result from a fragment SecondFragment val result =

    Bundle().apply { putString("your_data_key", "Hello!") } setFragmentResult("request_key", result) findNavController().navigateUp()
  17. Getting a result from a fragment setFragmentResultListener("request_key") { requestKey: String,

    bundle: Bundle -> { val result = bundle.getString("your_data_key") // do something with the result } } findNavController().navigate(…) FirstFragment
  18. Getting a result from a fragment setFragmentResultListener("request_key") { requestKey: String,

    bundle: Bundle -> { val result = bundle.getString("your_data_key") // do something with the result } } findNavController().navigate(…) FirstFragment
  19. Deep links • Manually by passing data through Intent •

    By using NavDeepLinkBuilder which creates PendingIntent (explicit)
  20. Deep links (explicit) val pendingIntent = NavDeepLinkBuilder(context) .setComponentName(MainActivity :: class.java)

    .setGraph(R.navigation.nav_graph) .setDestination(R.id.newsfeed_web) .setArguments( ... ) .createPendingIntent()
  21. Deep links • Manually by passing data through Intent •

    By using NavDeepLinkBuilder which creates PendingIntent (explicit deep link) • By using Uri (implicit deep link)
  22. Deep links (implicit) <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http: // schemas.android.com/apk/res/android">

    <fragment android:id="@+id/article" android:name=".ArticleFragment"> <argument android:name="articleId" /> <deepLink app:uri="vg: // article/{articleId}" /> ... </ navigation>
  23. SafeArgs val articleId = "kRmLdL" val dir = NavGraphDirections.actionGlobalArticle(articleId) val

    fragmentArgs = ArticleFragmentArgs(articleId) val bundle = fragmentArgs.toBundle() private val args: ArticleFragmentArgs by navArgs()
  24. SafeArgs val articleId = "kRmLdL" val dir = NavGraphDirections.actionGlobalArticle(articleId) val

    fragmentArgs = ArticleFragmentArgs(articleId) val bundle = fragmentArgs.toBundle() private val args: ArticleFragmentArgs by navArgs()
  25. SafeArgs val articleId = "kRmLdL" val dir = NavGraphDirections.actionGlobalArticle(articleId) val

    fragmentArgs = ArticleFragmentArgs(articleId) val bundle = fragmentArgs.toBundle() private val args: ArticleFragmentArgs by navArgs()
  26. SafeArgs public data class ArticleFragmentArgs( public val articleId: String, )

    : NavArgs { @Suppress("CAST_NEVER_SUCCEEDS") public fun toBundle(): Bundle { val result = Bundle() result.putString("articleId", this.articleId) return result } ... }
  27. SafeArgs public data class ArticleFragmentArgs( public val articleId: String, )

    : NavArgs { @Suppress("CAST_NEVER_SUCCEEDS") public fun toBundle(): Bundle { val result = Bundle() result.putString("articleId", this.articleId) return result } ... }
  28. Basic Fragment test @RunWith(AndroidJUnit4 :: class) class PodcastFragmentTest { @Test

    fun start() { val args = PodcastFragmentArgs(Referrer.Test()).toBundle() val scenario = launchFragmentInContainer<PodcastFragment>(args) onView(withId( ... ) .check(matches(isDisplayed())) } }
  29. Basic Fragment test @RunWith(AndroidJUnit4 :: class) class PodcastFragmentTest { @Test

    fun start() { val args = PodcastFragmentArgs(Referrer.Test()).toBundle() val scenario = launchFragmentInContainer<PodcastFragment>(args) onView(withId( ... ) .check(matches(isDisplayed())) } }
  30. Basic Fragment test @RunWith(AndroidJUnit4 :: class) class PodcastFragmentTest { @Test

    fun start() { val args = PodcastFragmentArgs(Referrer.Test()).toBundle() val scenario = launchFragmentInContainer<PodcastFragment>(args) onView(withId( ... ) .check(matches(isDisplayed())) } }
  31. TestNavHostController val navController = TestNavHostController( ApplicationProvider.getApplicationContext() ) navController.setGraph(R.navigation.navigation_graph) val fragmentScenario

    = launchFragmentInContainer<PodcastFragment>( ... ) fragmentScenario.onFragment { fragment -> Navigation.setViewNavController(fragment.requireView(), navController) }
  32. TestNavHostController val fragmentScenario = launchFragmentInContainer<PodcastFragment>( ... ) val fragmentScenario =

    launchFragmentInContainer { PodcastFragment( ... ).also { fragment -> fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner -> if (viewLifecycleOwner != null) { Navigation.setViewNavController( fragment.requireView(), navController ) } } } }
  33. Problems • Nav component destroys Fragment’s view when navigating to

    the new Fragment • We discovered leaking RecyclerView’s adapters • Testing is sometimes tricky • Not everything is supported
  34. Summary • https://github.schibsted.io/smp-distribution/android-hermes-app/pull/2520 • Negative delta (+3,040 −3,562) • 221

    commits • About two months of work of two people • Jetpack/AndroidX alpha and beta versions worked really well • Jetpack/AndroidX is the future