image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Approach to manage synchronisation of model and view

Example: we want to compute the Body Mass Index (BMI) of a person to determine if this person is underweight or obese. For this, we use:

First naive approach: using a single component managing all with mutable variables

@Composable
fun BMIGlobal() {
    Log.v("BMIGlobal", "Starting the composition")
    var mass by remember { mutableStateOf(70f) } // in kg
    var height by remember { mutableStateOf(1.70f) } // in meters
    Column() {
        TextField("$mass", { t -> (t.toFloatOrNull() ?: 0f).let { mass = it } }, label = { Text("Mass in Kg") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
        TextField("${height * 100}", { t -> (t.toFloatOrNull() ?: 0f).let { height = it / 100 } }, label = { Text("Height in cm")}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
        Text("You weigh $mass Kg and your height is ${height} meters")
        Text("Your BMI is ${(mass / height / height)}")
    }
}

Variables are declared with var x by remember { mutableStateOf(initialValue) }:

mutableStateOf is useful for a value of an immutable type that can change (e.g. if a mutable state of a String changes, a new String will be used since a String is immutable and cannot be changed in-situ; idem for an Int, Float...).
If the type of variable handled by mutableStateOf is mutable, no change will be detected if we change a property of the object.

Let's replace:

And change the first item of the list: mass[0] = newValue. The change will not be detected since the composer cannot watch an internal change in an object (only a change of the reference of the object). To overcome this problem, special mutable state structures have been introduced to track internal changes:

Sometimes a value must be computed using several mutable state variable. For this we can use derivedStateOf. For example if we want to declare a centimeterHeight that is computed from the height in meters:

val centimeterHeight by remember { derivedStateOf { height * 100 } }

This value will be automatically updated when height changes.

Creating a single global composable for an activity is not a good practice. We must cut this global composable in several smaller composables, each being responsible of a chunk of the GUI. It promotes code reused and also allow to have composables relying on immutable parameters. Let's try for our example with :

data class BodyParams(val mass: Float, val height: Float)

val BodyParams.bodyMassIndex get() = mass / height / height

@Composable
fun BodyParamsInput(bodyParams: BodyParams, onChange: (BodyParams) -> Unit, modifier: Modifier = Modifier) {
    Column(modifier) {
        TextField("${bodyParams.mass}", { t -> onChange(bodyParams.copy(mass= (t.toFloatOrNull() ?: 0f))) }, label = { Text("Mass in Kg") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
        TextField("${bodyParams.height * 100}", { t -> (t.toFloatOrNull() ?: 0f).let { bodyParams.copy(height = it / 100) } }, label = { Text("Height in cm")}, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
    }
}

@Composable
fun BMIResult(bodyParams: BodyParams, modifier: Modifier = Modifier) {
    Column(modifier) {
        Text("You weigh ${bodyParams.mass} Kg and your height is ${bodyParams.height} meters")
        Text("Your BMI is ${bodyParams.bodyMassIndex}")
    }
}

@Composable
fun BMI() {
    var bodyParams by remember { mutableStateOf(BodyParams(70f, 1.7f))}
    Column() {
        BodyParamsInput(bodyParams, onChange = {bodyParams = it})
        BMIResult(bodyParams, modifier=Modifier.border(1.dp, color=Color.Red))
    }
}

Approach used:

What are the chain of events when we input a digit into one of the text field?

  1. The onChange callback given to the TextField is called
  2. It changes the value of the bodyParams state in the parent composable BMI
  3. The composer detects that a new BodyParams object is assigned. It triggers the recomposition of BMI.
    • → The composer knows that BMI is dependent on bodyParams (because this variable is read during the composition), that's why the recomposition is done

Sometimes it is difficult to represent an entire model in a single immutable class for performance reasons (e.g. if we deal with large lists or maps). In this case we can use mutableStateListOf or mutableStateMapOf.

It is recommanded to gather all the state data inside a single class (inheriting from the ViewModel class) with several State fields (that can be obtained with the mutableStateOf(...) API). One can also use elements of type LiveData, Flow or Observable that can emit a stream of values; elements of these types can easily be converted to a State with the extension methods observeAsState.

Management of side effects

Coroutine scope

Example: we create a component that increment a counter not immediately but after a delay:

@Composable
fun DelayedIncrementation() {
    var counter by rememberSaveable { mutableStateOf(0) }
    val coroutineScope = rememberCoroutineScope()
    Text("$counter", fontSize = 40.sp, modifier = Modifier.clickable {
        coroutineScope.launch {
            delay(1000)
            counter++
        }
    })
}

It is also possible to cancel the previous job (if we click several times in a row in less than one second, only the last incrementation will be done):

@Composable
fun DelayedIncrementation() {
    var counter by rememberSaveable { mutableStateOf(0) }
    var lastJob by remember { mutableStateOf<Job?>(null) }
    val coroutineScope = rememberCoroutineScope()
    Text("$counter", fontSize = 40.sp, modifier = Modifier.clickable {
        lastJob?.cancel()
        lastJob = coroutineScope.launch {
            delay(1000)
            counter++
        }
    })
}

SideEffect composable

@Composable
fun SideEffectStorm() {
    var counter by rememberSaveable { mutableStateOf(0) }
    Text("$counter", fontSize = 40.sp)
    SideEffect { counter++ } // bad idea!
}

LaunchedEffect composable

LaunchedEffect is the counterpart in Jetpack Compose of the useEffect hook in React

Example: a counter that is incremented periodically, the increment period being set with a slider between 0 and 1s; we use the period as key of the LaunchedEffect; thus if the period changes, the previous instance of the LaunchedEffect will be cancelled, and a new one using the new period will be used.

@Composable
fun PeriodicIncrementation() {
    var counter by rememberSaveable { mutableStateOf(0) }
    var incrementPeriod by rememberSaveable() { mutableStateOf(0f) }
    Column() {
        Slider(value = incrementPeriod, onValueChange = { incrementPeriod = it }, valueRange = 0f..1f)
        Text("$counter", fontSize = 40.sp, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
    }
    if (incrementPeriod > 0f)
        LaunchedEffect(incrementPeriod) {
            while (true) {
                delay((incrementPeriod * 1000).toLong()) // convert to milliseconds
                counter++
            }
        }
}

produceState allow to produce a watchable State object that is updated using the coroutine passed as parameter (we use assignment with value = ... to modify the state in the coroutine). It is implemented using a LaunchedEffect (implementations exist for zero, one, two or three keys), here is the implementation with two keys :

@Composable
fun <T> produceState(
    initialValue: T,
    key1: Any?,
    key2: Any?,
    @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
    val result = remember { mutableStateOf(initialValue) }
    LaunchedEffect(key1, key2) {
        ProduceStateScopeImpl(result, coroutineContext).producer()
    }
    return result
}

We can rewrite the PeriodIncrementation using produceState:

@Composable
fun getIncrementingState(incrementPeriod: Float) = produceState(initialValue = 0, incrementPeriod) {
    if (incrementPeriod > 0f)
        while (true) {
            delay((incrementPeriod * 1000).toLong())
            value++
        }
}

@Composable
fun PeriodicIncrementationWithState() {
    var incrementPeriod by rememberSaveable() { mutableStateOf(0f) }
    val state = getIncrementingState(incrementPeriod)
    Column() {
        Slider(value = incrementPeriod, onValueChange = { incrementPeriod = it }, valueRange = 0f..1f)
        Text("${state.value}", fontSize = 40.sp, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
    }
}

⚠ Note that produceState uses remember to save the variable and not rememberSaveable: thus the value will be lost when we destroy/recreate the activity by rotating the screen.

⚠ Lambdas employed for side effects uses a closure with the value of parameters of the parent composable. Execution of the lambda occurs after the composition, and maybe after a recomposition. However the recomposition can be done with new values for the parameters: in this case the old values from the closure at the time of the first composition will be used. To avoid this one can declare val param by rememberUpdatedState(funParam).

Exemple: we display the value of the counter one second after the first composition

@Composable
fun CounterDisplay(value: Int) {
    val rememberedValue by rememberUpdatedState(value)
    var initialValue by remember { mutableStateOf<Int?>(null) }
    var afterSecondValue by remember { mutableStateOf<Int?>(null) }
    Column() {
        Text("Current value: $value")
        val iv = initialValue
        val asv = afterSecondValue
        if (iv != null && asv != null)
          Text("Initial value: $iv, value one second after the first composition: $asv")
        else
          Text("First second not elapsed")
    }
    LaunchedEffect(true) { // effect launched only once
        delay(1000L) // convert to milliseconds
        initialValue = value
        afterSecondValue = rememberedValue
    }
}

DisposableEffect composable

Example: a component that display the acceleration magnitude; it requires to register a listener with the sensor API; the listener must be unregistered when we leave the composition (i.e. the composable has been removed from the screen).

@Composable
fun Accelerometer() {
    val acceleration = remember { mutableStateListOf(Float.NaN, Float.NaN, Float.NaN) }
    val context = LocalContext.current
    val sensorManager = remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }
    val accelerometer = remember { sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) }

    val listener = remember {
        object: SensorEventListener {
            override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}

            override fun onSensorChanged(event: SensorEvent) {
                event.values.forEachIndexed { index, value -> acceleration[index] = value }
            }
        }
    }

    DisposableEffect(true) {
        sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_UI)
        onDispose { sensorManager.unregisterListener(listener) }
    }

    Column() {
        Text("Accelerometer")
        acceleration.forEach {
            Text("$it")
        }
    }
}

⚠ The composition is not leaved when the activity is put into the background; in this case we will continue to watch for acceleration (however the screen is not refreshed on the background)

Lifecycle of the activity

Example: a State publishing a negative value of the activity is stopped and a positive value when it is started

@Composable
fun getLifecycleStartedState(): State<Int> {
    val lc = LocalLifecycleOwner.current.lifecycle
    val initial = if (lc.currentState.isAtLeast(Lifecycle.State.STARTED)) 1 else -1
    return produceState(initial, lc) {
        val observer = LifecycleEventObserver { _, event ->
            when(event) {
                Lifecycle.Event.ON_START -> value = kotlin.math.abs(value) + 1
                Lifecycle.Event.ON_STOP -> value = -(kotlin.math.abs(value) + 1)
                else -> {}
            }
            Log.d("getLifecycleStartedState", "New event: $event, new value=$value")
        }
        lc.addObserver(observer)
        try {
            delay(Long.MAX_VALUE) // infinite wait (or almost)
        } finally {
            Log.d("getLifecycleStartedState", "Remove observer")
            lc.removeObserver(observer)
        }
    }
}

Creating a state from a long running task

Example: let's write a composable that displays the prime factors of a number; this task can be long for a large integer, especially if it is prime (it will require to examine O(sqrt(n)) factors). We use produceState to convert the coroutine to a state and we publish regularly the progress done in the computation.

data class FactorizationState(val progress: Float, val factors: List<BigInteger>, val cancelled: Boolean = false)

@Composable
fun getFactorizerState(number: BigInteger, cancelled: Boolean) = produceState(FactorizationState(0f, listOf()), number, cancelled) {
    if (! cancelled)
        withContext(Dispatchers.Default) {
            value = FactorizationState(0f, listOf())
            val two = BigInteger.valueOf(2)
            var n = number
            var nsqrt = sqrt(number.toDouble())
            if (n < BigInteger.valueOf(2)) {
                value = FactorizationState(1f, listOf()) // terminated, no factor found
            } else {
                var factors = listOf<BigInteger>()
                var divider = two
                var iterationCounter = 0
                while (divider * divider <= n) {
                    val dividing = n % divider == BigInteger.ZERO
                    if (dividing) {
                        factors += divider
                        n /= divider
                        nsqrt = sqrt(n.toDouble())
                        value =
                            FactorizationState(divider.toDouble().div(nsqrt).toFloat(), factors)
                    } else {
                        divider += if (divider == two) BigInteger.ONE else two
                    }
                    if (iterationCounter % 100 == 0) {
                        value = value.copy(progress = divider.toDouble().div(nsqrt).toFloat())
                        if (!isActive) {
                            value = value.copy(cancelled = true)
                            break // exit since the coroutine has been cancelled
                        }
                    }
                    iterationCounter++
                }
                if (! value.cancelled) {
                    if (n > BigInteger.ONE)
                        factors += n
                    value = FactorizationState(1f, factors)
                }
            }
        }
}

@Composable
fun Factorizer(number: BigInteger, onCancellationRequested: () -> Unit, cancelled: Boolean, modifier: Modifier = Modifier) {
    var state = getFactorizerState(number, cancelled)
    Card(modifier = modifier) {
        Column() {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text("$number")
                if (! state.value.cancelled && state.value.progress != 1f)
                    IconButton(onClick = { onCancellationRequested() }) { Icon(painter = painterResource(android.R.drawable.ic_delete), contentDescription = "Cancel") }
            }
            when {
                state.value.progress < 1f -> {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        LinearProgressIndicator(progress = state.value.progress, modifier=Modifier.weight(1f))
                        if (state.value.cancelled) Text("Cancelled")
                    }
                }
                state.value.progress == 1f -> Text("Computation terminated.")
            }
            FlowRow() {
                state.value.factors.forEach {
                    Text("$it", modifier= Modifier
                        .padding(5.dp)
                        .border(width = 1.dp, color = Color.Black)
                        .padding(5.dp))
                }
            }
        }
    }
}

@Composable
fun ComposeFactorizer() {
    var typedNumber by remember { mutableStateOf(BigInteger.ONE) }
    var validatedNumber by remember { mutableStateOf<BigInteger?>(null) }
    var cancelled by remember { mutableStateOf(false) }
    Column() {
        Row(verticalAlignment = Alignment.CenterVertically) {
            TextField(value="$typedNumber", onValueChange = { typedNumber = BigInteger(it) }, modifier = Modifier.weight(1f), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
            Button(onClick = { validatedNumber = typedNumber; cancelled = false; }) { Text("Validate") }
        }
        val number = validatedNumber
        if (number != null)
            Factorizer(number, onCancellationRequested = {cancelled = true}, cancelled, modifier = Modifier.fillMaxWidth())
        else {
            Text("No number validated yet.")
        }
    }

}

Transversal states with CompositionLocal

Principle of use of CompositionLocal:

  1. We create a class with the data (e.g. class MyData(val value: Int))
  2. We create one instance by default of the data (that will be used inside all the composables as a singleton unless otherwise specified):
    • val LocalMyData = compositionLocalOf { MyData(1) }
  3. We can also use blocks to specify an instance that must be used in some parts of the composition tree instead of the default data:
    • CompositionLocalProvider(LocalMyData provides MyData(2))
  4. We can obtain the current instance with LocalMyData.current
// with inline the class behaves as a primitive type in memory
inline class Temperature(val celsius: Float) {
    val farhenheit get() = celsius * 9f / 5f + 32f
    val kelvin get() = celsius + 273.15f
}

sealed class TemperatureScale(open val unit: String, open val extractValue: (Temperature) -> Float)
object CelsiusScale: TemperatureScale("C", { it.celsius })
object FahrenheitScale: TemperatureScale("F", { it.farhenheit })
object KelvinScale: TemperatureScale("K", { it.kelvin })

// default value is Celsius
val LocalTemperatureMode = compositionLocalOf<TemperatureScale> { CelsiusScale }

@Composable
fun TemperatureDisplayer(label: String, value: Temperature) {
    val t = "${LocalTemperatureMode.current.extractValue(value)} ${LocalTemperatureMode.current.unit}"
    Row {
        Text(label, Modifier.padding(5.dp))
        Text(t, Modifier.padding(5.dp))
    }
}

@Composable
fun TemperaturesDisplayer(temperatures: Map<String, Temperature>) {
    LazyColumn {
        items(temperatures.entries.toList(), { it.key }) { entry ->
            TemperatureDisplayer(entry.key, entry.value)
        }
    }
}

@Composable
fun TemperatureScaleSelector(scale: TemperatureScale, onScaleChanged: (TemperatureScale) -> Unit) {
    Row {
        // to use subclasses, the reflect dependency must be installed
        TemperatureScale::class.sealedSubclasses.forEach { s ->
            Row(Modifier.padding(10.dp)) {
                RadioButton(s == scale::class, onClick={ s.objectInstance?.let { onScaleChanged(it) } })
                Text(s.objectInstance?.unit ?: "")
            }
        }
    }
}

@Composable
fun EnhancedTemperaturesDisplayer(temperatures: Map<String, Temperature>) {
    var temperatureScale by remember { mutableStateOf<TemperatureScale>(CelsiusScale) }
    Column {
        TemperatureScaleSelector(temperatureScale) { temperatureScale = it }
        CompositionLocalProvider(LocalTemperatureMode provides temperatureScale) {
            TemperaturesDisplayer(temperatures)
        }
    }
}

@Composable
fun SampleTemperaturesDisplayer() {
    val map = mapOf(
        "Absolute zero" to Temperature(-273.15f),
        "Freezing point of water" to Temperature(0f),
        "Human body temperature" to Temperature(37f),
        "Boiling point of water" to Temperature(100f),
        "Burning point of paper" to Temperature(218f)
    )
    EnhancedTemperaturesDisplayer(temperatures = map)
}

⚠ It is not recommended to put a ViewModel inside a CompositionLocal to allow all the components of the hierarchy to pick the elements they are interested in. It is better to supply as arguments of the functions only the parts of the ViewModel that are required by the component.

Using a ViewModel

Example : a composable using a ViewModel for a counter incremented by clicks

class CrazyClickingViewModel: ViewModel() {
    private var _counter by mutableStateOf(1)

    val counter get() = _counter

    fun incrementCounter() {
        _counter++
    }

    val tick = liveData<Int> {
        var i = 0
        while (true) {
            emit(i++)
            delay(100L)
        }
    }

    private var backgroundColorIndex by mutableStateOf(0)

    companion object {
        val ALL_COLORS = arrayOf(Color.Blue, Color.White, Color.Red)
    }

    fun changeBackgroundColor() {
        backgroundColorIndex = (backgroundColorIndex + 1) % ALL_COLORS.size
    }

    val backgroundColor by derivedStateOf { ALL_COLORS[backgroundColorIndex] }
}

@Composable
fun CrazyClicking() {
    // dependency to use the viewModel function: implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
    val vm: CrazyClickingViewModel = viewModel()
    val tick by vm.tick.observeAsState()
    LaunchedEffect(tick?.div(100) ?: 0) { vm.changeBackgroundColor() }
    Column(
        Modifier
            .background(color = vm.backgroundColor)
            .clickable { vm.incrementCounter() }) {
        Text("${vm.counter}")
        Text("${tick}")
    }
}