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 value
onValueChange: (T) -> Unit: an event that requests the value to change, where
T
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:
class ViewModel { | |
private val _password = MutableStateFlow<String>("") | |
val password: StateFlow<String> = _password | |
fun onPasswordChanged(password: String) { | |
_password.value = password | |
} | |
} |
Composable functions can use such ViewModel
to get current value and notify it about value changes:
@Composable | |
fun Screen() { | |
val viewModel = remember { ViewModel() } // or viewModel() etc. | |
val password by viewModel.password.collectAsState() | |
TextField( | |
value = password, | |
onValueChange = viewModel::onPasswordChanged | |
) | |
} |
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:
class MutableStateAdapter<T>( | |
private val state: State<T>, | |
private val mutate: (T) -> Unit | |
) : MutableState<T> { | |
override var value: T | |
get() = state.value | |
set(value) { | |
mutate(value) | |
} | |
override fun component1(): T = value | |
override fun component2(): (T) -> Unit = { value = it } | |
} |
By using this class, creating any MutableState
extensions is very easy. For MutableStateFlow<T>
:
@Composable | |
fun <T> MutableStateFlow<T>.collectAsMutableState( | |
context: CoroutineContext = EmptyCoroutineContext | |
): MutableState<T> = MutableStateAdapter( | |
state = collectAsState(context), | |
mutate = { value = it } | |
) |
Job Offers
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:
class ViewModel { | |
val password = MutableStateFlow("") | |
} |
In a composable, the new extension can be used to add two-way binding. It works perfectly with Kotlin’s destructing declarations:
@Composable | |
fun Screen() { | |
val viewModel = remember { ViewModel() } // or viewModel() etc. | |
val (password, setPassword) = viewModel.password.collectAsMutableState() | |
TextField( | |
value = password, | |
onValueChange = setPassword | |
) | |
} |
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.
This article was originally published on proandroiddev.com on April 10, 2022