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

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

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

    View full-size slide

  2. Problem?
    ● You have a codebase that relies on Fragments
    4

    View full-size slide

  3. Problem?
    ● You have a codebase that relies on Fragments
    ● Adopting Jetpack Compose
    5

    View full-size slide

  4. Problem?
    ● You have a codebase that relies on Fragments
    ● Adopting Jetpack Compose
    ● Gradual transition
    6

    View full-size slide

  5. Problem?
    class FooFragment : Fragment() { ... }
    @Composable
    fun BarScreen() { ... }
    7

    View full-size slide

  6. Navigation scenarios
    ● fragment → compose
    8

    View full-size slide

  7. Navigation scenarios
    ● fragment → compose
    ● compose → compose
    9

    View full-size slide

  8. Navigation scenarios
    ● fragment → compose
    ● compose → compose
    ● compose → fragment
    10

    View full-size slide

  9. Problem?
    ● Unified navigation mechanism
    ● Proper Jetpack Compose experience
    11

    View full-size slide

  10. Jetpack Compose
    architecture

    View full-size slide

  11. Jetpack Compose architecture
    ● Developed internally at Turo
    ● Now, open-source: https://github.com/open-turo/nibel
    13

    View full-size slide

  12. Basic navigation

    View full-size slide

  13. Declaring an entry
    @UiEntry(type = ImplementationType.Fragment)
    @Composable
    fun FooScreen() { ... }
    15

    View full-size slide

  14. Navigating
    fragment to compose

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  17. Navigating
    compose to compose

    View full-size slide

  18. Navigating compose to compose
    @UiEntry(type = ImplementationType.Fragment)
    @Composable
    fun FooScreen() { ... }
    20

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  23. Navigating
    compose to fragment

    View full-size slide

  24. Navigating compose to fragment
    26
    class BazFragment : Fragment() { ... }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  27. Thought process

    View full-size slide

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

    View full-size slide

  29. Wrap composable with a fragment
    31
    class FooFragment: Fragment() {
    val someState: SomeState
    override fun onCreateView(...) { ... }
    fun someNonComposeFunction() { ... }
    fun anotherNonComposeFunction() { ... }
    }

    View full-size slide

  30. Base fragment class
    32
    class FooFragment : ComposableFragment() {
    @Composable
    override fun ComposableContent() {
    FooScreen() // <-- our composable function
    }
    }

    View full-size slide

  31. ImplementationType.Fragment
    33
    @UiEntry(type = ImplementationType.Fragment)
    @Composable
    fun FooScreen() { ... }

    View full-size slide

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

    View full-size slide

  33. Navigation
    35
    @UiEntry(type = ImplementationType.Fragment)
    @Composable
    fun FooScreen() { ... }
    @UiEntry(type = ImplementationType.Fragment)
    @Composable
    fun BarScreen() { ... }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. Optimization

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  39. Implementation type
    41
    @UiEntry(type = ImplementationType.Fragment)
    @Composable
    fun FooScreen() { ... }
    @UiEntry(type = ImplementationType.Composable)
    @Composable
    fun BarScreen() { ... }

    View full-size slide

  40. Implementation type
    42
    navigator.navigateTo(BarScreenEntry.newInstance())

    View full-size slide

  41. Implementation type
    43
    navigator.navigateTo(BarScreenEntry.newInstance())
    ???

    View full-size slide

  42. Abstraction over
    navigation

    View full-size slide

  43. Navigation under-the-hood
    45
    Fragment entry
    @Composable
    type.fragment

    View full-size slide

  44. Navigation under-the-hood
    46
    compose
    navigation
    @Composable entry
    @Composable
    Fragment entry
    @Composable
    type.fragment type.composable

    View full-size slide

  45. Navigation under-the-hood
    47
    @Composable entry
    @Composable
    Fragment entry
    @Composable
    compose
    navigation
    @Composable entry
    @Composable
    type.composable type.composable

    View full-size slide

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

    View full-size slide

  47. Navigation under-the-hood
    49
    Fragment entry
    @Composable
    fragment
    transaction
    Fragment
    (legacy)

    View full-size slide

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

    View full-size slide

  49. 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()
    }
    }
    }
    }
    }
    }

    View full-size slide

  50. 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()
    }
    }
    }
    }
    }
    }

    View full-size slide

  51. 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()
    }
    }
    }
    }
    }
    }

    View full-size slide

  52. 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()
    }
    }
    }
    }
    }
    }

    View full-size slide

  53. 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()
    }
    }
    }
    }
    }
    }

    View full-size slide

  54. 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()
    }
    }
    }
    }
    }
    }

    View full-size slide

  55. Multi-module navigation

    View full-size slide

  56. Navigation module
    59
    feature A
    module
    feature B
    module

    View full-size slide

  57. Navigation module
    60
    navigation
    module
    feature A
    module
    feature B
    module
    depends depends

    View full-size slide

  58. 61
    screen 2
    destination
    screen 1 screen 2
    navigate to
    Navigation module

    View full-size slide

  59. Declaring a destination
    // :navigation module
    object FooScreenDestination : DestinationWithNoArgs
    62

    View full-size slide

  60. Declaring a destination
    // :navigation module
    object FooScreenDestination : DestinationWithNoArgs
    data class BarScreenDestination(
    override val args: BarScreenArgs // Parcelable args
    ) : DestinationWithArgs
    63

    View full-size slide

  61. Associating a destination with a screen
    @UiExternalEntry(
    type = ImplementationType.Fragment,
    destination = FooScreenDestination::class
    )
    @Composable
    fun FooScreen() { ... }
    64

    View full-size slide

  62. Navigating
    compose to compose

    View full-size slide

  63. Navigation compose to compose
    navigator.navigateTo(BarScreenDestination)
    66

    View full-size slide

  64. Navigation compose to compose
    val args = BarScreenArgs(...)
    navigator.navigateTo(BarScreenDestination(args))
    67

    View full-size slide

  65. Navigating
    fragment to compose

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. Navigating
    compose to fragment

    View full-size slide

  70. Navigation fragment to compose
    @LegacyExternalEntry(destination = QuxScreenDestination::class)
    class QuxScreenFragment : Fragment() { ... }
    73

    View full-size slide

  71. Navigation fragment to compose
    @LegacyExternalEntry(destination = BasScreenDestination::class)
    class BazScreenFragment : Fragment() { ... }
    navigator.navigateTo(BasScreenDestination)
    74

    View full-size slide

  72. Jetpack Compose
    adoption scenarios

    View full-size slide

  73. Scenario 1:
    Brand new feature
    module

    View full-size slide

  74. 77
    Scenario 1
    fragment
    @UiEntry
    type.composable
    @UiEntry
    type.composable
    new feature module
    @UiExternalEntry
    type.fragment

    View full-size slide

  75. 78
    Scenario 1
    fragment
    @UiEntry
    type.composable
    @UiEntry
    type.composable
    new feature module
    @UiExternalEntry
    type.fragment

    View full-size slide

  76. 79
    Scenario 1
    fragment
    @UiEntry
    type.composable
    @UiEntry
    type.composable
    new feature module
    @UiExternalEntry
    type.fragment

    View full-size slide

  77. 80
    Scenario 1
    fragment
    @UiEntry
    type.composable
    @UiEntry
    type.composable
    new feature module
    @UiExternalEntry
    type.fragment

    View full-size slide

  78. 81
    Scenario 1
    @UiEntry
    type.composable
    @UiEntry
    type.composable
    @UiExternalEntry
    type.fragment
    @LegacyExternalEntry
    fragment
    new feature module

    View full-size slide

  79. 82
    Scenario 1
    @UiEntry
    type.composable
    @UiEntry
    type.composable
    @UiExternalEntry
    type.fragment
    @LegacyExternalEntry
    fragment
    new feature module

    View full-size slide

  80. 83
    Scenario 1
    @UiEntry
    type.composable
    @UiEntry
    type.composable
    @UiExternalEntry
    type.fragment
    @LegacyExternalEntry
    fragment
    new feature module

    View full-size slide

  81. Scenario 2
    Expanding existing
    feature

    View full-size slide

  82. 85
    Scenario 3
    fragment
    @UiEntry
    type.composable
    fragment
    old feature module
    @UiEntry
    type.fragment

    View full-size slide

  83. 86
    Scenario 3
    fragment
    @UiEntry
    type.composable
    fragment
    old feature module
    @UiEntry
    type.fragment

    View full-size slide

  84. Scenario 3
    Standalone screens

    View full-size slide

  85. 88
    Scenario 3
    fragment fragment
    old feature module
    @UiEntry
    type.fragment
    @UiEntry
    type.fragment

    View full-size slide

  86. 89
    Scenario 3
    fragment fragment
    old feature module
    @UiEntry
    type.fragment
    @UiEntry
    type.fragment

    View full-size slide

  87. To conclude…

    View full-size slide

  88. To conclude...
    ● Abstract navigation fragment → compose, compose → fragment
    ● Simple API, no boilerplate
    ● High configurability
    91

    View full-size slide

  89. 93
    Blog posts
    medium.com/turo-engineering/
    b11ee5f19ba8

    View full-size slide

  90. 94
    Blog posts
    medium.com/turo-engineering/
    b11ee5f19ba8
    medium.com/turo-engineering/
    541c7b2f3f84

    View full-size slide