KoMPose Kit: KaMP Kit goes Jetpack Compose
Tags: kotlin, compose, android
Published: 2021-08-24 16:38:22

## KaMP Kit and Jetpack Compose
Hey folks, Brady from Touchlab here. I've only been at Touchlab since the beginning of the year, but [KaMP Kit](https://github.com/touchlab/KaMPKit), our simple-but-not-too-simple sample project to help those considering Kotlin Multiplatform, started way back at the end of 2019. Back then, Jetpack Compose had just been announced in May. It was a time full of optimism about the modern mobile UI development experience, but also of wild instability. The first [method](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-navigation-release/ui/README.md) of getting Jetpack Compose to work on your machine involved pulling down the AndroidX development toolchain, and running a special version of Android Studio via terminal commands. Eventually, preview, alpha, and beta version of Compose could be used in the canary version of Android Studio (more [history](https://youtu.be/U5BwfqBpiWU?t=1324)). Starting July 28, 2021, Compose went [stable](https://android-developers.googleblog.com/2021/07/jetpack-compose-announcement.html), and a version of Android Studio Arctic Fox, which supports Compose, was released in the stable channel shortly thereafter. Now we can use a stable version of Compose with a stable version of Android Studio. We at Touchlab have been excited about Compose for a long time; you can watch us geek out about it [here](https://www.youtube.com/watch?v=CA6KYa0MvPo). And though the community has been interested in Compose for KaMP Kit since at least [May 2020](https://github.com/touchlab/KaMPKit/issues/105), we didn't want folks who are trying out Kotlin Multiplatform with KaMP Kit to also have to learn a changing Jetpack Compose API, and require them to use a special version of Android Studio. Now that these obstacles have been removed, we feel comfortable fully endorsing Jetpack Compose in KMM.

## Cutting Code
After moving to Jetpack Compose, we were able to remove a lot of things that clutter the KMP learning experience. XML views are gone, which means developers don't need to worry about context switching between XML and Kotlin, `findViewById()`, or configure `viewBinding` or `dataBinding`. Removing the `RecyclerView`, its `ViewHolder`, and `Adapter`, and replacing it with a `LazyColumn` from Compose simplifies the sample considerably. Although we had to bring in Compose dependencies, @russhwolf noticed that by abandoning `AppCompatActivity` in favor of `ComponentActivity`, we were able to remove the large AppCompat library from our dependencies entirely.

## Exposing a Flaw in our State Management
If you're now converting an app to use Jetpack Compose, you may have noticed that modeling your view state using a `sealed class` may not work as well as it used to in the `View` world. That's because `View`s implicitly kept state that we relied on. Compose made this more apparent, and forced us to stop relying on our UI for any state whatsoever. 

For example, let's say we have `Loading`, `Success`, and `Error` states to describe our UI, and that we are currently showing the `Success` state to describe a list of items in our UI, while fetching more data. In the `View` world, we emit a `Loading` state, which just makes the loading spinner visible, in addition to the stale list, while fetching a fresh list. It just comes down to showing what we're already showing, and then making a loading spinner visible.

However, in the Compose world, we don't have all possible views on the screen, only toggling some as visible. Instead, we need to emit all of the UI we want to show whenever the State changes. In our example, when we emit the `Loading` state, the success UI with our list of data goes away, and only the loading spinner is visible. This is very jarring, and not a great user experience. This is because we're using a `sealed class` for something that's not mutually exclusive. `Success` and `Loading` are not mutually exclusive, unless `Loading` only describes an empty screen with a loading spinner. Ryan Harter has [written](https://ryanharter.com/blog/2019/07/encapsulating-view-state/#a-note-about-sealed-classes) about this issue, and Android GDE @ditn [Adam Bennett](https://twitter.com/iateyourmic) told me that his team at Cuvva also had this discussion. Perhaps the simplest solution is to have a `data class` with nullable fields:

```kotlin
data class DataState<out T>(
    val data: T? = null,
    val exception: String? = null,
    val empty: Boolean = false,
    val loading: Boolean = false
)
```
This covers the only `Loading`, only `Success`, only `Error`, `Loading` and `Success`, and `Loading` and `Error` possibilities. It harkens back to the old Android architecture components samples' `Resource<T>` [class](https://github.com/android/architecture-components-samples/blob/88747993139224a4bb6dbe985adf652d557de621/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt). 

Though, some [argue](https://twitter.com/JorgeCastilloPr/status/1428731347888230402) that those State combinations should all be mutually exclusive `sealed class`es, which is also a great approach that avoids the nullability issues. If `Loading` is the only State that can coexist with other States, we can also just add a boolean field. 

Whichever way you go, you should make sure not to model any of your UI state as mutually exclusive of others unless it actually *is* mutually exclusive of others. 

## Swipe To Refresh
Swipe-to-refresh functionality is an extremely common UI element, and as such, it is available on Android's legacy View system in [SwipeRefreshLayout](https://developer.android.com/training/swipe/add-swipe-interface). The Compose equivalent isn't part of the core Compose UI, but there is a solid solution.

To get this same functionality in Compose without implementing swipe-to-refresh yourself, you'll want to use [Accompanist-SwipeRefresh](https://github.com/google/accompanist/tree/main/swiperefresh), which is a Google library, but isn't officially part of Jetpack. You'll also need to make sure that any content inside the `SwipeRefresh` Composable is scrollable. You may have to wrap some content in a `Column` with a `verticalScroll` modifier per the [documentation](https://google.github.io/accompanist/swiperefresh/). If you miss this step, you could emit a non-scrollable `Error` state, and be unable to swipe to refresh again.

Given its popularity, it seems a little strange that swipe-to-refresh isn't a core part of Compose. But this brings us to another way that Compose shines. Compose, and its 1st party associated libraries, are completely unbundled from the operating system. This means that Compose can run on any device running Android API [21 (Lollipop) and newer](https://developer.android.com/jetpack/compose/interop/adding#anchor). 

## Transforming Flows
Compose uses a [special observable type](https://developer.android.com/jetpack/compose/state) to know when to update UI. In Compose, this is the `State<T>` class. When `State` changes, all `@Composable` functions dependent on that `State` are reinvoked, and emit the corresponding UI. By exposing data as `StateFlow`s from our KMM module, we can use the `Flow` extension function `collectAsState()` and clean it up even more with delegate syntax. We want to collect the `Flow` safely, avoiding collection when the view goes to the background, and restarting it when it comes back to the foreground. We'll use Manuel Vivo's [post](https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda#c5fd), "A safer way to collect flows from Android UIs" as a guide to create a lifecycle-aware `Flow`.
```kotlin
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleAwareDogsFlow = remember(
        viewModel.breedStateFlow, 
        lifecycleOwner
    ) {
    viewModel.breedStateFlow
        .flowWithLifecycle(lifecycleOwner.lifecycle)
}
val dogsState by lifecycleAwareDogsFlow
    .collectAsState(viewModel.breedStateFlow.value)
```
The delegate syntax is nice because we get best of `State` and its backing data. Our `dogsState` is actually not a `State`, so we don't need to put `.value` to get the value, but because it delegates its `get()`s to a `State`, `@Composable` functions that take it as a parameter are still invoked whenever its value changes.

## Conclusion
Jetpack Compose has been an exciting project to follow, and it's clear that it has a bright future for reactive and declarative UI. Updating to Compose has simplified KaMP Kit, and exposed a flaw in our previous state management approach, forcing us to become better developers. Our goal with the [KaMP Kit](https://github.com/touchlab/KaMPKit) project is to give folks interested in Kotlin Multiplatform the easiest way to get started, and now that Compose is stable, it makes learning KMM easier than ever.