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

A guide to learning Fragment on Jetpack era

7b1243134c5358540e24a3f3a1f5f441?s=47 Ryusuke NAKANO
February 20, 2020

A guide to learning Fragment on Jetpack era

Jetpack時代のFragment再入門

7b1243134c5358540e24a3f3a1f5f441?s=128

Ryusuke NAKANO

February 20, 2020
Tweet

Transcript

  1. A guide to learning Fragment on Jetpack era Ryusuke Nakano

    @rnakano
  2. About me • Ryusuke Nakano (@rnakano) • Software Engineer at

    mixi • Android developer of 6gram • I’d been avoiding Fragments until one year ago
  3. Single Activity • Use Activity as an entry point for

    app • Create screens and transitions with custom components Benefits • Control over transitions within app • Improve UX
  4. Single Activity • Use Activity as an entry point for

    app • Create screens and transitions with custom components Benefits • Control over transitions within app • Improve UX Fragment
  5. But Fragment was …

  6. Problems with Fragment • Testing ◦ Troublesome preparation • FragmentTransaction

    ◦ Difficult and many boilerplate codes • Lifecycle ◦ More complex than Activity
  7. I’ll talk about • Jetpack has improved these problems •

    How to write safety and maintainable Fragments with Jetpack ◦ Testing ◦ FragmentTransaction ◦ Lifecycle
  8. Testing

  9. class BlankFragment : Fragment(R.layout.fragment_blank) BlankFragment

  10. Do you write unit tests for Fragment?

  11. My reasons not to write unit tests for Fragment •

    It's too much bother to … ◦ prepare Activity ◦ prepare view test. It needs androidTest ◦ ...
  12. fragment-testing • Jetpack testing library • Testing fragments in isolation

    • Integrated with Espresso and Robolectric
  13. @RunWith(AndroidJUnit4::class) class BlankFragmentTest { @Test fun testShowTextView() { val scenario:

    FragmentScenario<BlankFragment> = launchFragmentInContainer<BlankFragment>() onView(withId(R.id.textView)) .check(matches(withText("Hello blank fragment"))) } } fragment-testing
  14. @RunWith(AndroidJUnit4::class) class BlankFragmentTest { @Test fun testShowTextView() { val scenario:

    FragmentScenario<BlankFragment> = launchFragmentInContainer<BlankFragment>() onView(withId(R.id.textView)) .check(matches(withText("Hello blank fragment"))) } } fragment-testing 1. Lunch empty Activity 2. Instantiate Fragment 3. Attach to Activity 4. Change state to onResume()
  15. @RunWith(AndroidJUnit4::class) class BlankFragmentTest { @Test fun testShowTextView() { val scenario:

    FragmentScenario<BlankFragment> = launchFragmentInContainer<BlankFragment>() onView(withId(R.id.textView)) .check(matches(withText("Hello blank fragment"))) } } fragment-testing
  16. @RunWith(AndroidJUnit4::class) class BlankFragmentTest { @Test fun testShowTextView() { val scenario:

    FragmentScenario<BlankFragment> = launchFragmentInContainer<BlankFragment>() onView(withId(R.id.textView)) .check(matches(withText("Hello blank fragment"))) scenario.onFragment { fragment -> // call fragment method directly } scenario.recreate() // recreate host Activity } } fragment-testing
  17. class BlankFragment : Fragment(R.layout.fragment_blank) { @Inject lateinit var repository: Repository

    override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) super.onAttach(context) } } How to test with DI?
  18. Another approach: using constructor injection

  19. class BlankFragment( private var repository: Repository ) : Fragment(R.layout.fragment_blank) Fragment

    and constructors
  20. class BlankFragment( private var repository: Repository ) : Fragment(R.layout.fragment_blank) //

    Unable to instantiate fragment BlankFragment // : could not find Fragment constructor Fragment and constructors
  21. FragmentFactory • FragmentFactory is a bridge between FragmentManager and your

    constructor • You can customize Fragment’s constructor
  22. class MyFragmentFactory : FragmentFactory() { private val repository = Repository()

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment { return when (className) { BlankFragment::class.java.name -> BlankFragment(repository) else -> super.instantiate(classLoader, className) } } } FragmentFactory
  23. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    supportFragmentManager.fragmentFactory = MyFragmentFactory() super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) } FragmentFactory
  24. @RunWith(AndroidJUnit4::class) class BlankFragmentTest { @Test fun testShowTextView() { val scenario

    = launchFragmentInContainer<BlankFragment>( factory = MyFragmentFactory() ) onView(withId(R.id.textView)) .check(matches(withText("Hello blank fragment"))) } } Test with FragmentFactory
  25. class TestFragmentFactory<F : Fragment>( private val initializer: () -> F

    ) : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { return initializer() } } Test helper for FragmentFactory
  26. @RunWith(AndroidJUnit4::class) class BlankFragmentTest { @Test fun testShowTextView() { val repository

    = mockk<Repository>() val scenario = launchFragmentInContainer<BlankFragment>( factory = TestFragmentFactory { BlankFragment(repository) } ) Test with FragmentFactory
  27. Summary: Testing • Easy to test fragments with fragment-testing and

    FragmentFactory ◦ If you can write tests with Dagger, of course it is OK ◦ The most important thing is to write Fragment unit tests ◦ Let's write Fragment tests more and more • Advanced topic: sharedTest
  28. FragmentTransaction

  29. Navigation between fragments How to switch from A_Fragment to B_Fragment

    on MainActivity? MainActivity A_Fragment B_Fragment
  30. class MainActivity : AppCompatActivity(R.layout.main_activity) { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .add<A_Fragment>(R.id.container) .commit() } } override fun onBackPressed() { val fm = supportFragmentManager if (fm.backStackEntryCount > 0) { fm.popBackStack() } else { super.onBackPressed() } MainActivity
  31. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { parentFragmentManager.beginTransaction() .replace<B_Fragment>(R.id.container) .addToBackStack(null) .commit() } } } A_Fragment
  32. Problems with FragmentTransaction • Too many boilerplate codes • Hard

    to test
  33. @RunWith(AndroidJUnit4::class) class A_FragmentTest { @Test fun testGotoB() { val scenario

    = launchFragmentInContainer<A_Fragment>() onView(withId(R.id.gotoButton)).perform(click()) // How to verify transaction here? } } How to test FragmentTransaction?
  34. Jetpack: Navigation Component

  35. Navigation Component • Unifying and Simplifying Android Navigation • Navigate

    between Activities, Fragments, etc. • Can be used as FragmentTransaction wrapper
  36. Key concepts of Navigation Component • Navigation Graph • NavHost

    • NavController
  37. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { parentFragmentManager.beginTransaction() .replace<B_Fragment>(R.id.container) .addToBackStack(null) .commit() } } } Key concepts of Navigation Component
  38. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { parentFragmentManager.beginTransaction() .replace<B_Fragment>(R.id.container) .addToBackStack(null) .commit() } } } Key concepts of Navigation Component Navigation Graph
  39. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { parentFragmentManager.beginTransaction() .replace<B_Fragment>(R.id.container) .addToBackStack(null) .commit() } } } Key concepts of Navigation Component Navigation Graph NavHost
  40. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { parentFragmentManager.beginTransaction() .replace<B_Fragment>(R.id.container) .addToBackStack(null) .commit() } } } Key concepts of Navigation Component Navigation Graph NavHost NavController
  41. Navigation Graph • XML file that defines Fragments and it’s

    transition • Integrated with AndroidStudio
  42. <fragment android:id="@+id/container" android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/nav_main" app:defaultNavHost="true" android:layout_width="match_parent" android:layout_height="match_parent" /> NavHost: activity_main.xml

  43. <fragment android:id="@+id/container" android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/nav_main" app:defaultNavHost="true" android:layout_width="match_parent" android:layout_height="match_parent" /> NavHost: activity_main.xml

  44. class MainActivity : AppCompatActivity(R.layout.main_activity) { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .replace<A_Fragment>(R.id.container) .commit() } } override fun onBackPressed() { val fm = supportFragmentManager if (fm.backStackEntryCount > 0) { fm.popBackStack() } else { super.onBackPressed() } NavHost: MainActivity (before)
  45. class MainActivity : AppCompatActivity(R.layout.main_activity) { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) } } NavHost: MainActivity (after)
  46. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { parentFragmentManager.beginTransaction() .replace<B_Fragment>(R.id.container) .addToBackStack(null) .commit() } } } How to use Navigation: A_Fragment (before)
  47. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { findNavController().navigate(R.id.action_a_Fragment_to_b_Fragment) } } } How to use Navigation: A_Fragment (after)
  48. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { findNavController().navigate(R.id.action_a_Fragment_to_b_Fragment) } } } How to use Navigation: A_Fragment returns NavController with navigation graph
  49. class A_Fragment : Fragment(R.layout.fragment_a) { override fun onViewCreated(...) { gotoButton.setOnClickListener

    { findNavController().navigate(R.id.action_a_Fragment_to_b_Fragment) } } } How to use Navigation: A_Fragment
  50. @RunWith(AndroidJUnit4::class) class A_FragmentTest { @Test fun testGotoB() { val scenario

    = launchFragmentInContainer<A_Fragment>() val mockNavController = mockk<NavController>(relaxed = true) scenario.onFragment { fragment -> Navigation.setViewNavController( fragment.requireView(), mockNavController) } onView(withId(R.id.gotoButton)).perform(click()) verify { mockNavController.navigate(R.id.action_a_Fragment_to_b_Fragment) } How to test with Navigation Component?
  51. @RunWith(AndroidJUnit4::class) class A_FragmentTest { @Test fun testGotoB() { val scenario

    = launchFragmentInContainer<A_Fragment>() val mockNavController = mockk<NavController>(relaxed = true) scenario.onFragment { fragment -> Navigation.setViewNavController( fragment.requireView(), mockNavController) } onView(withId(R.id.gotoButton)).perform(click()) verify { mockNavController.navigate(R.id.action_a_Fragment_to_b_Fragment) } How to test with Navigation Component?
  52. Summary: Navigation Component • No more FragmentTransaction • Eliminate boilerplate

    codes • Navigation Graph is good document • Advanced topic: ◦ safe-args ◦ dynamic-features-fragment
  53. Lifecycle

  54. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() Backstack A_Fragment

    MainActivity A_Fragment B_Fragment Let’s review Fragment’s lifecycle again Focus on A_Fragment
  55. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment First, A_Fragment is instantiated
  56. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment Next, view is inflated in onCreateView()
  57. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment State changes to onResume()
  58. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment When button is pressed, view is destroyed by onDestroyView()
  59. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment Push A_Fragment to backstack
  60. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment B is displayed on the screen
  61. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment When back key is pressed
  62. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment Pop A_Fragment from backstack
  63. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment View is inflated in onCreateView() again
  64. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment State changes to onResume()
  65. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment When back key is pressed again, view is destroyed in onDestroyView()
  66. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    Finally, A_Fragment is destroyed
  67. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() Fragment’s lifecycle

    View’s lifecycle
  68. private lateinit var locationListener: MyLocationListener override fun onViewCreated() { locationListener

    = MyLocationListener(context) { location -> // do something } locationListener.start() } override fun onDestroyView() { super.onDestroyView() locationListener.stop() } Lifecycle example
  69. private lateinit var locationListener: MyLocationListener override fun onViewCreated() { locationListener

    = MyLocationListener(context) { location -> // do something } locationListener.start() } override fun onDestroy() { super.onDestroy() locationListener.stop() } Lifecycle example (bug)
  70. Problems with lifecycle • onViewCreated()/onDestroyView() becomes longer • We tend

    to forget to call stop() 
 • Fragment needs to call start/stop with correct lifecycle
  71. Jetpack: Lifecycle-aware Component

  72. Lifecycle-aware Component • Make lifecycle managements lighter-weight and maintainable •

    4 key classes: ◦ Lifecycle ◦ LifecycleOwner ◦ LifecycleEvent ◦ LifecycleObserver
  73. Lifecycle-aware Component Lifecycle LifecycleOwner LifecycleObserver ON_CREATED ON_STARTED ... ON_RESUMED

  74. Lifecycle-aware Component Lifecycle LifecycleOwner LifecycleObserver ON_CREATED ON_STARTED ... ON_RESUMED MyLocationListener

  75. class MyLocationListener( private val context: Context, private val callback: (Location)

    -> Unit ) { fun start() { ... } fun stop() { ... } } Make LifecycleObserver (before)
  76. class MyLocationListener( private val context: Context, private val callback: (Location)

    -> Unit ) : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun start() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun stop() { ... } } Make LifecycleObserver (after)
  77. Lifecycle-aware Component Lifecycle LifecycleOwner LifecycleObserver ON_CREATED ON_STARTED ... ON_RESUMED

  78. Lifecycle LifecycleOwner Activity’s lifecycle Lifecycle LifecycleOwner Fragment’s lifecycle Lifecycle LifecycleOwner

    Fragment View’s lifecycle
  79. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() Fragment’s lifecycle

    View’s lifecycle
  80. onCreate() onStart() onResume() onPause() onStop() onDestroy() Fragment (this) onCreateView() onStart()

    onResume() onPause() onStop() onDestroyView() viewLifecycleOwner
  81. private lateinit var locationListener: MyLocationListener override fun onViewCreated() { locationListener

    = MyLocationListener(context) { location -> // do something } locationListener.start() } override fun onDestroyView() { super.onDestroyView() locationListener.stop() } Add observer (before)
  82. override fun onViewCreated() { val locationListener = MyLocationListener(context) { location

    -> // do something } viewLifecycleOwner.lifecycle.addObserver(locationListener) } Add observer (after)
  83. override fun onViewCreated() { val locationListener = MyLocationListener(context) { location

    -> // do something } lifecycle.addObserver(locationListener) } Add observer (bug)
  84. Be careful about using Lifecycle in Fragment • Think carefully

    which LifecycleOwner you should use • When call addObserver() in onCreate() ◦ should use this • When call addObserver() in onViewCreated() ◦ Should use viewLifecycleOwner
  85. Be careful with other components as well • CameraX: bindToLifecycle()

    • ViewDataBinding: setLifecycleOwner() • LiveData: observe() • ...
  86. // Don’t do this: override fun onViewCreated(...) { liveData.observe(this) {

    value -> // do something }) } // Do this: override fun onViewCreated(...) { liveData.observe(viewLifecycleOwner) { value -> // do something }) } Example: LiveData
  87. New lint suggestions (fragment:1.2.2)

  88. Summary: Lifecycle-aware Component • Removes lifecycle management code from Fragment

    • Be careful which LifecycleOwner you should use
  89. I talked about • How to write safety and maintainable

    Fragments with Jetpack ◦ Testing-> fragment-testing ◦ FragmentTransaction -> Navigation Component ◦ Lifecycle -> Lifecycle-aware Component