Jetpack Compose: A use case for view interop migration strategy
Tags: compose, android, mobile, jetpackcompose
Published: 2023-02-13 17:59:33

## Approach

Recently, we had an opportunity to redesign the sample app for an SDK we're developing. We were only redesigning one screen (home page), so we decided to introduce `Jetpack Compose` in the app.

Based on what we read, most migration or view interop posts are about keeping `Fragments` and `fragment navigation` as they are and moving the content of the fragments into `ComposeView`. In fact, [official docs also mention that strategy.](https://developer.android.com/jetpack/compose/interop/migration-strategy) 

For that, we would still have to touch other fragments in the app rather than just the home page. So it felt like more work than what we intended to do. On top of that, the sample app extensively uses `PreferenceFragmentCompat` for various setting screens. To move its content to `ComposeView`, we would have to re-create a kind of preferences screen from scratch because compose doesn't have a straight replacement for `PreferenceFragment`.

So we decided to go with the [Fragments in Compose](https://developer.android.com/jetpack/compose/interop/interop-apis#fragments-in-compose) approach. Along with that, we also moved our navigation using [compose navigation component](https://developer.android.com/jetpack/compose/navigation).

## Fragments in Compose

### Layout

We created new layout files for each fragment with [FragmentContainerView](https://developer.android.com/reference/androidx/fragment/app/FragmentContainerView) in each.

```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view_events"
    android:name="com.example.fragment.analytics.AnalyticsEventsFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

</androidx.fragment.app.FragmentContainerView>
```

### Composable

We wrote a generic `FragmentHolder` composable with [AndroidViewBinding](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidViewBinding(kotlin.Function3,androidx.compose.ui.Modifier,kotlin.Function1)). It composes an Android layout resource. The layout files we created above would have their `Binding` class generated that we can inflate and pass as `BindingFactory` in the `AndroidViewBinding`

```kotlin
@Composable
fun <T : ViewBinding> FragmentHolderScreen(
    topBarTitle: String,
    androidViewBindingFactory: (inflater: LayoutInflater, parent: ViewGroup, attachToParent: Boolean) -> T,
    onBackPress: () -> Unit = {},
    androidViewBindingUpdate: T.() -> Unit = {},
) {
    Scaffold(
        topBar = {
            TopBar(title = topBarTitle, onBackPress = onBackPress)
        },
        content = { paddingValues ->
            AndroidViewBinding(
                factory = androidViewBindingFactory,
                modifier = Modifier.padding(paddingValues),
                update = androidViewBindingUpdate,
            )
        },
    )
}
```

### Navigation

After creating the composable, we defined a navigation component with a `Screen` enum.

With above defined layout binding and `FragmentHolderScreen` for `AnalyticsEventsFragment`, the minimized `Scaffold` composables look like this, 

```kotlin
Scaffold(
    content = { contentPadding ->
        NavHost(
            navController = navController,
            startDestination = Screen.Settings.route,
            Modifier.padding(contentPadding),
        ) {
            composable(Screen.Events.route) {
                FragmentHolderScreen(
                    topBarTitle = getString(R.string.toolbar_title_events),
                    androidViewBindingFactory = ComposeFragmentEventsBinding::inflate,
                    androidViewBindingUpdate = {
                        with(fragmentContainerViewEvents.getFragment<AnalyticsEventsFragment>()) {
                            // Reference of AnalyticsEventsFragment is available here
                        }
                    },
                    onBackPress = {
                        onBackPress(navController)
                    },
                )
            }
        }
    },
)
```

### Inner/child fragments

Some of our fragments internally had navigation where a user would go to child fragments. While doing the compose-based navigation, we added those child fragments as part of our `Screen/route` enum. Then, we set a lambda function (`val navigateTo: (Screen) -> Unit`) in the `base` fragment class to handle the click actions to move to child fragments.

## SharedPreferences

The sample app uses `SharedPreferences`, and we didn't have time to move to something like `DataStore`. It still worked out well after moving one screen to `Compose`. We just made sure to define the reference of `SharedPreferences` in `onCreate` of the main activity and then pass the reference down to composable. Since we didn't touch existing fragments, they use the `SharedPreferences` as defined already.

## UI Testing

We have extensive UI testing in the sample app that tests the SDK. After moving the sample app's home screen to `Compose`, we broke the entry point of all our tests.

Luckily, compose UI testing and espresso testing work well with each other. You can write a compose test in one-line testing the compose-based screen, and the second line can be an espresso test step looking at the view layer.

The biggest problem we faced around updating unit tests was the delay between the two systems. After clicking on a button on the compose-based screen, the app would go to SDK (a view-based UI). Many tests failed because the view would not be available on the screen when a test executes. After trying out several things, nothing worked well except adding `Thread.sleep()` just before and after we handle button clicks on the compose-based screen. The delay probably gave espresso enough time to find the correct view-based screen.

## Conclusion 

Overall, we're satisfied with how the whole experiment went. We learned a bit about compose-view interop and got the opportunity to share our findings via this blog post.

---
Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at [@shaktiman_droid](https://twitter.com/shaktiman_droid) on Twitter, [LinkedIn](https://www.linkedin.com/in/shaktiman-droid/) or [Kotlin Slack](https://slack.kotlinlang.org/). And if you find all this interesting, maybe you'd like to [work with](https://touchlab.co/contact-us/) or [work at](https://touchlab.co/careers-3/) Touchlab.