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

A guide to learning Fragment on Jetpack era

Ryusuke NAKANO
February 20, 2020

A guide to learning Fragment on Jetpack era

Jetpack時代のFragment再入門

Ryusuke NAKANO

February 20, 2020
Tweet

More Decks by Ryusuke NAKANO

Other Decks in Technology

Transcript

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

    mixi • Android developer of 6gram • I’d been avoiding Fragments until one year ago
  2. 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. 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. Problems with Fragment • Testing ◦ Troublesome preparation • FragmentTransaction

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

    How to write safety and maintainable Fragments with Jetpack ◦ Testing ◦ FragmentTransaction ◦ Lifecycle
  6. My reasons not to write unit tests for Fragment •

    It's too much bother to … ◦ prepare Activity ◦ prepare view test. It needs androidTest ◦ ...
  7. @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
  8. @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()
  9. @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
  10. @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
  11. 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?
  12. class BlankFragment( private var repository: Repository ) : Fragment(R.layout.fragment_blank) //

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

    constructor • You can customize Fragment’s constructor
  14. 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
  15. class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    supportFragmentManager.fragmentFactory = MyFragmentFactory() super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) } FragmentFactory
  16. @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
  17. class TestFragmentFactory<F : Fragment>( private val initializer: () -> F

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

    = mockk<Repository>() val scenario = launchFragmentInContainer<BlankFragment>( factory = TestFragmentFactory { BlankFragment(repository) } ) Test with FragmentFactory
  19. 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
  20. Navigation between fragments How to switch from A_Fragment to B_Fragment

    on MainActivity? MainActivity A_Fragment B_Fragment
  21. 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
  22. 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
  23. @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?
  24. Navigation Component • Unifying and Simplifying Android Navigation • Navigate

    between Activities, Fragments, etc. • Can be used as FragmentTransaction wrapper
  25. 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
  26. 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
  27. 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
  28. 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
  29. Navigation Graph • XML file that defines Fragments and it’s

    transition • Integrated with AndroidStudio
  30. 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)
  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() } } } How to use Navigation: A_Fragment (before)
  32. 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)
  33. 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
  34. 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
  35. @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?
  36. @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. Summary: Navigation Component • No more FragmentTransaction • Eliminate boilerplate

    codes • Navigation Graph is good document • Advanced topic: ◦ safe-args ◦ dynamic-features-fragment
  38. 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
  39. onCreate() onStart() onResume() onPause() onStop() onDestroy() onCreateView() onDestroyView() MainActivity Backstack

    A_Fragment When back key is pressed again, view is destroyed in onDestroyView()
  40. 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
  41. 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)
  42. Problems with lifecycle • onViewCreated()/onDestroyView() becomes longer • We tend

    to forget to call stop() 
 • Fragment needs to call start/stop with correct lifecycle
  43. Lifecycle-aware Component • Make lifecycle managements lighter-weight and maintainable •

    4 key classes: ◦ Lifecycle ◦ LifecycleOwner ◦ LifecycleEvent ◦ LifecycleObserver
  44. class MyLocationListener( private val context: Context, private val callback: (Location)

    -> Unit ) { fun start() { ... } fun stop() { ... } } Make LifecycleObserver (before)
  45. 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)
  46. 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)
  47. override fun onViewCreated() { val locationListener = MyLocationListener(context) { location

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

    -> // do something } lifecycle.addObserver(locationListener) } Add observer (bug)
  49. 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
  50. Be careful with other components as well • CameraX: bindToLifecycle()

    • ViewDataBinding: setLifecycleOwner() • LiveData: observe() • ...
  51. // 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
  52. I talked about • How to write safety and maintainable

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