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

Migrating from Fragments to Jetpack Compose wit...

Migrating from Fragments to Jetpack Compose with Nibel | droidcon NYC 2023

While the engineering community is excited about adopting Jetpack Compose, in reality, we all have existing codebases that rely on a legacy UI stack.

In this talk, I will share how we designed a Jetpack Compose architecture at Turo that provides seamless integration in the existing fragment-based codebase. It is now available as Nibel - an open-source navigation library.

I will cover how we leveraged the power of Kotlin Symbol Processing (KSP) API to build an abstraction over the navigation that enables easy multi-module navigation out-of-the-box and covers the following navigation scenarios:
• fragment to compose
• compose to compose
• compose to fragment

Pavlo Stavytskyi

September 19, 2023
Tweet

More Decks by Pavlo Stavytskyi

Other Decks in Programming

Transcript

  1. About me • Google Developer Expert - Android, Kotlin •

    Sr. Staff Software Engineer at Turo 2
  2. Problem? • You have a codebase that relies on Fragments

    • Adopting Jetpack Compose • Gradual transition 6
  3. Jetpack Compose architecture • Developed internally at Turo • Now,

    open-source: https://github.com/open-turo/nibel 13
  4. Navigating fragment to compose class BarFragment : Fragment() { requireActivity().supportFragmentManager.commit

    { val entry = FooScreenEntry.newInstance().fragment replace(android.R.id.content, entry.fragment) } } 17
  5. Navigating fragment to compose class BarFragment : Fragment() { requireActivity().supportFragmentManager.commit

    { val entry = FooScreenEntry.newInstance().fragment replace(android.R.id.content, entry.fragment) } } 18
  6. Navigating compose to compose @UiEntry(type = ImplementationType.Fragment) @Composable fun FooScreen()

    { ... } @UiEntry( type = ImplementationType.Composable, args = BarScreenArgs::class ) @Composable fun BarScreen(args: BarScreenArgs) { ... } 21
  7. Navigating compose to compose @UiEntry(type = ImplementationType.Fragment) @Composable fun FooScreen(navigator:

    NavigationController) { val args = BarArgs(...) navigator.navigateTo(BarScreenEntry.newInstance(args)) } 22
  8. Navigating compose to compose @UiEntry(type = ImplementationType.Fragment) @Composable fun FooScreen(navigator:

    NavigationController) { val args = BarArgs(...) navigator.navigateTo(BarScreenEntry.newInstance(args)) } 23
  9. Navigating compose to compose @UiEntry(type = ImplementationType.Fragment) @Composable fun FooScreen(navigator:

    NavigationController) { val args = BarArgs(...) navigator.navigateTo(BarScreenEntry.newInstance(args)) } 24
  10. Navigating compose to fragment 27 @UiEntry(type = ImplementationType.Fragment) @Composable fun

    FooScreen(navigator: NavigationController) { val fragment = BazFragment() navigator.navigateTo(FragmentEntry(fragment)) }
  11. Navigating compose to fragment 28 @UiEntry(type = ImplementationType.Fragment) @Composable fun

    FooScreen(navigator: NavigationController) { val fragment = BazFragment() navigator.navigateTo(FragmentEntry(fragment)) }
  12. Wrap composable with a fragment 30 class FooFragment: Fragment() {

    override fun onCreateView( ... ) = ComposeView(requireContext()).apply { setContent { AppTheme { FooScreen() // <-- our composable function } } } }
  13. Wrap composable with a fragment 31 class FooFragment: Fragment() {

    val someState: SomeState override fun onCreateView(...) { ... } fun someNonComposeFunction() { ... } fun anotherNonComposeFunction() { ... } }
  14. Base fragment class 32 class FooFragment : ComposableFragment() { @Composable

    override fun ComposableContent() { FooScreen() // <-- our composable function } }
  15. Generated fragment 34 // generated code class FooScreenEntry : ComposableFragment()

    { @Composable override fun ComposableContent() { FooScreen() // <-- our composable function } }
  16. Navigation 35 @UiEntry(type = ImplementationType.Fragment) @Composable fun FooScreen() { ...

    } @UiEntry(type = ImplementationType.Fragment) @Composable fun BarScreen() { ... }
  17. Navigation - fragment transaction 36 @UiEntry(type = ImplementationType.Fragment) @Composable fun

    FooScreen(navigator: NavigationController) { navigator.navigateTo(BarScreenEntry.newInstance()) } @UiEntry(type = ImplementationType.Fragment) @Composable fun BarScreen() { ... }
  18. Navigation - fragment transaction 37 @UiEntry(type = ImplementationType.Fragment) @Composable fun

    FooScreen(navigator: NavigationController) { navigator.navigateTo(BarScreenEntry.newInstance()) } @UiEntry(type = ImplementationType.Fragment) @Composable fun BarScreen() { ... }
  19. Generated composable wrapper 39 // generated code class BarEntry( override

    val args: SecondArgs, override val name: String, ) : ComposableEntry<BarArgs>(args, name) { @Composable override fun ComposableContent() { BarScreen() // <-- our composable function } }
  20. Generated composable wrapper 40 // generated code class BarEntry( override

    val args: SecondArgs, override val name: String, ) : ComposableEntry<BarArgs>(args, name) { @Composable override fun ComposableContent() { BarScreen() // <-- our composable function } }
  21. Implementation type 41 @UiEntry(type = ImplementationType.Fragment) @Composable fun FooScreen() {

    ... } @UiEntry(type = ImplementationType.Composable) @Composable fun BarScreen() { ... }
  22. Navigation under-the-hood 47 @Composable entry @Composable Fragment entry @Composable compose

    navigation @Composable entry @Composable type.composable type.composable
  23. Navigation under-the-hood 48 @Composable entry @Composable Fragment entry @Composable fragment

    transaction @Composable entry @Composable Fragment (legacy) type.composable
  24. Navigation under-the-hood 50 @Composable entry @Composable Fragment entry @Composable fragment

    transaction @Composable entry @Composable Fragment (legacy) type.composable
  25. Navigation under-the-hood 51 // base class for generated fragments abstract

    class ComposableFragment : Fragment() { @Composable abstract fun ComposableContent() override fun onCreateView(...) = ComposeView(...).apply { setContent { AppTheme { ... NavHost(..., startDestination = "@root") { composable("@root") { ComposableContent() } } } } } }
  26. Navigation under-the-hood 52 // base class for generated fragments abstract

    class ComposableFragment : Fragment() { @Composable abstract fun ComposableContent() override fun onCreateView(...) = ComposeView(...).apply { setContent { AppTheme { ... NavHost(..., startDestination = "@root") { composable("@root") { ComposableContent() } } } } } }
  27. Navigation under-the-hood 53 // base class for generated fragments abstract

    class ComposableFragment : Fragment() { @Composable abstract fun ComposableContent() override fun onCreateView(...) = ComposeView(...).apply { setContent { AppTheme { ... NavHost(..., startDestination = "@root") { composable("@root") { ComposableContent() } } } } } }
  28. Navigation under-the-hood 54 // base class for generated fragments abstract

    class ComposableFragment : Fragment() { @Composable abstract fun ComposableContent() override fun onCreateView(...) = ComposeView(...).apply { setContent { AppTheme { ... NavHost(..., startDestination = "@root") { composable("@root") { ComposableContent() } } } } } }
  29. Navigation under-the-hood 55 // base class for generated fragments abstract

    class ComposableFragment : Fragment() { @Composable abstract fun ComposableContent() override fun onCreateView(...) = ComposeView(...).apply { setContent { AppTheme { ... NavHost(..., startDestination = "@root") { composable("@root") { ComposableContent() } } } } } }
  30. Navigation under-the-hood 56 // base class for generated fragments abstract

    class ComposableFragment : Fragment() { @Composable abstract fun ComposableContent() override fun onCreateView(...) = ComposeView(...).apply { setContent { AppTheme { ... NavHost(..., startDestination = "@root") { composable("@root") { ComposableContent() } } } } } }
  31. Declaring a destination // :navigation module object FooScreenDestination : DestinationWithNoArgs

    data class BarScreenDestination( override val args: BarScreenArgs // Parcelable args ) : DestinationWithArgs<BarScreenArgs> 63
  32. Associating a destination with a screen @UiExternalEntry( type = ImplementationType.Fragment,

    destination = FooScreenDestination::class ) @Composable fun FooScreen() { ... } 64
  33. Navigation fragment to compose class BazScreenFragment : Fragment() { ...

    requireActivity().supportFragmentManager.commit { val entry = Nibel.newFragmentEntry(BarScreenDestination)!! replace(android.R.id.content, entry.fragment) } } 69
  34. Navigation fragment to compose class BazScreenFragment : Fragment() { ...

    requireActivity().supportFragmentManager.commit { val entry = Nibel.newFragmentEntry(BarScreenDestination)!! replace(android.R.id.content, entry.fragment) } } 70
  35. Navigation fragment to compose class BazScreenFragment : Fragment() { ...

    requireActivity().supportFragmentManager.commit { val entry = Nibel.newFragmentEntry(BarScreenDestination)!! replace(android.R.id.content, entry.fragment) } } 71
  36. To conclude... • Abstract navigation fragment → compose, compose →

    fragment • Simple API, no boilerplate • High configurability 91