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

A guide to learning Fragment on Jetpack era

Avatar for Ryusuke NAKANO Ryusuke NAKANO
February 20, 2020

A guide to learning Fragment on Jetpack era

Jetpack時代のFragment再入門

Avatar for Ryusuke NAKANO

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