DEV Community

Cover image for Jetpack Compose: A use case for view interop migration strategy
Jigar Brahmbhatt for Touchlab

Posted on • Updated on • Originally published at touchlab.co

Jetpack Compose: A use case for view interop migration strategy

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.

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 approach. Along with that, we also moved our navigation using compose navigation component.

Fragments in Compose

Layout

We created new layout files for each fragment with FragmentContainerView in each.

<?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>
Enter fullscreen mode Exit fullscreen mode

Composable

We wrote a generic FragmentHolder composable with AndroidViewBinding. 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

@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,
            )
        },
    )
}
Enter fullscreen mode Exit fullscreen mode

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,

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)
                    },
                )
            }
        }
    },
)
Enter fullscreen mode Exit fullscreen mode

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 on Twitter, LinkedIn or Kotlin Slack. And if you find all this interesting, maybe you'd like to work with or work at Touchlab.

Top comments (0)