A guide to learning Fragment on Jetpack era

A guide to learning Fragment on Jetpack era

Jetpack時代のFragment再入門

7b1243134c5358540e24a3f3a1f5f441?s=128

Ryusuke NAKANO

February 20, 2020
Tweet

Transcript

  1. 2.

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

    mixi • Android developer of 6gram • I’d been avoiding Fragments until one year ago
  2. 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
  3. 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
  4. 6.

    Problems with Fragment • Testing ◦ Troublesome preparation • FragmentTransaction

    ◦ Difficult and many boilerplate codes • Lifecycle ◦ More complex than Activity
  5. 7.

    I’ll talk about • Jetpack has improved these problems •

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

    My reasons not to write unit tests for Fragment •

    It's too much bother to … ◦ prepare Activity ◦ prepare view test. It needs androidTest ◦ ...
  8. 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
  9. 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()
  10. 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
  11. 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
  12. 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?
  13. 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
  14. 21.

    FragmentFactory • FragmentFactory is a bridge between FragmentManager and your

    constructor • You can customize Fragment’s constructor
  15. 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
  16. 23.

    class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    supportFragmentManager.fragmentFactory = MyFragmentFactory() super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) } FragmentFactory
  17. 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
  18. 25.

    class TestFragmentFactory<F : Fragment>( private val initializer: () -> F

    ) : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { return initializer() } } Test helper for FragmentFactory
  19. 26.

    @RunWith(AndroidJUnit4::class) class BlankFragmentTest { @Test fun testShowTextView() { val repository

    = mockk<Repository>() val scenario = launchFragmentInContainer<BlankFragment>( factory = TestFragmentFactory { BlankFragment(repository) } ) Test with FragmentFactory
  20. 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
  21. 29.

    Navigation between fragments How to switch from A_Fragment to B_Fragment

    on MainActivity? MainActivity A_Fragment B_Fragment
  22. 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
  23. 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
  24. 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?
  25. 35.

    Navigation Component • Unifying and Simplifying Android Navigation • Navigate

    between Activities, Fragments, etc. • Can be used as FragmentTransaction wrapper
  26. 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
  27. 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
  28. 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
  29. 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
  30. 41.

    Navigation Graph • XML file that defines Fragments and it’s

    transition • Integrated with AndroidStudio
  31. 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)
  32. 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)
  33. 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)
  34. 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
  35. 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
  36. 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?
  37. 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?
  38. 52.

    Summary: Navigation Component • No more FragmentTransaction • Eliminate boilerplate

    codes • Navigation Graph is good document • Advanced topic: ◦ safe-args ◦ dynamic-features-fragment
  39. 53.
  40. 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
  41. 58.
  42. 65.

    onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment When back key is pressed again, view is destroyed in onDestroyView()
  43. 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
  44. 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)
  45. 70.

    Problems with lifecycle • onViewCreated()/onDestroyView() becomes longer • We tend

    to forget to call stop() 
 • Fragment needs to call start/stop with correct lifecycle
  46. 72.

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

    4 key classes: ◦ Lifecycle ◦ LifecycleOwner ◦ LifecycleEvent ◦ LifecycleObserver
  47. 75.

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

    -> Unit ) { fun start() { ... } fun stop() { ... } } Make LifecycleObserver (before)
  48. 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)
  49. 80.
  50. 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)
  51. 82.

    override fun onViewCreated() { val locationListener = MyLocationListener(context) { location

    -> // do something } viewLifecycleOwner.lifecycle.addObserver(locationListener) } Add observer (after)
  52. 83.

    override fun onViewCreated() { val locationListener = MyLocationListener(context) { location

    -> // do something } lifecycle.addObserver(locationListener) } Add observer (bug)
  53. 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
  54. 85.

    Be careful with other components as well • CameraX: bindToLifecycle()

    • ViewDataBinding: setLifecycleOwner() • LiveData: observe() • ...
  55. 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
  56. 89.

    I talked about • How to write safety and maintainable

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