Two-way data binding in Jetpack Compose

Jetpack Compose uses a pattern called state hoisting to make stateless composables and move state managing to its parent. The state is being represented by two parameters:
value: T
: the current valueonValueChange: (T) -> Unit
: an event that requests the value to change, whereT
is proposed as the new value
To bind values to ViewModel
, it is common to use libraries that follow Observer pattern (such as Flow
in Kotlin). Jetpack Compose offers extension functions that convert Observable
into State
type supported in composables:
Flow.collectAsState()
LiveData.observeAsState()
Observable.subscribeAsState()
One-way binding
The typical ViewModel
with binding may look as follows:
Composable functions can use such ViewModel
to get current value and notify it about value changes:
It is nice but requires 3 class members to support binding: callback function, public observable field, and backing field. This type of data binding is called one-way binding, as both callback function and public observable communicate one way — can only read value or change it. I thought that it can be improved by implementing extensions that will allow using two-way data binding similar to Google’s Data Binding Library, which works for XML files.
Solution: Two-way binding
I’ve implemented MutableStateAdapter
class that allows to convert State<T>
to MutableState<T>
by adding mutate
function:
By using this class, creating any MutableState
extensions is very easy. For MutableStateFlow<T>
:
LiveData
andRxJava
extensions can be found here
Let’s go back to the example. Now 3 class members can be replaced with only one MutableStateFlow<String>
field:
In a composable, the new extension can be used to add two-way binding. It works perfectly with Kotlin’s destructing declarations:
As a result, boilerplate code responsible for data binding is greatly reduced in a ViewModel
.
Next steps
I thought about releasing code as a library, but as Adapter
and extension are less than 20 lines, I think it would be overkill. If you prefer using a library, let me know in the comments — I could release it ;) It would be nicer though to have those extensions included in androidx.compose.runtime:runtime*
artifacts in my opinion.