5

I am currently trying to write an App for my thesis and currently, I am looking into different approaches. Since I really like Flutter and the Thesis requires me to use Java/Kotlin I would like to use Jetpack compose.

Currently, I am stuck trying to update ListElements.

I want to have a List that shows Experiments and their state/result. Once I hit the Button I want the experiments to run and after they are done update their state. Currently, the run Method does nothing besides setting the state to success. The problem is I don't know how to trigger a recompose from the viewModel of the ExperimentRow once an experiment updates its state.

ExperimentsActivity:

class ExperimentsActivity : AppCompatActivity() {

val exViewModel by viewModels<ExperimentViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    //For now this is just Dummy Data and will be replaced
    exViewModel.experiments += listOf(
        Experiment("Test1", exViewModel::experimentStateChanged),
        Experiment("Strongbox", exViewModel::experimentStateChanged)
    )

    setContent {
        TpmTheme {
            // A surface container using the 'background' color from the theme
            Surface {
                ExperimentScreen(
                    exViewModel.experiments,
                    exViewModel::startTests
                )
            }
        }
    }
}

}

ExperimentViewModel:

class ExperimentViewModel : ViewModel() {

    var experiments by mutableStateOf(listOf<Experiment>())



    fun startTests() {
        for (exp in experiments) {
            exp.run()
        }
    }

    fun experimentStateChanged(experiment: Experiment) {
        Log.i("ViewModel", "Changed expState of ${experiment.name} to ${experiment.state}")
        // HOW DO I TRIGGER A RECOMPOSE OF THE EXPERIMENTROW FOR THE experiment????
        //experiments = experiments.toMutableList().also { it.plus(experiment) }

        Log.i("Vi", "Size of Expirments: ${experiments.size}")
    }

}

ExperimentScreen:

@Composable
fun ExperimentScreen(
    experiments: List<Experiment>,
    onStartExperiments: () -> Unit
) {
    Column {
        LazyColumnFor(
            items = experiments,
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(top = 8.dp),
        ) { ep ->
            ExperimentRow(
                experiment = ep,
                modifier = Modifier.fillParentMaxWidth(),
            )
        }

        Button(
            onClick = { onStartExperiments() },
            modifier = Modifier.padding(16.dp).fillMaxWidth(),
        ) {
            Text("Run Tests")
        }
    }
}


@Composable
fun ExperimentRow(experiment: Experiment, modifier: Modifier = Modifier) {


    Row(
        modifier = modifier
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(experiment.name)
        Icon(
            asset = experiment.state.vAsset,
        )
    }

Experiment:

class Experiment(val name: String, val onStateChanged: (Experiment) -> Unit) {
    var state: ExperimentState = ExperimentState.DEFAULT
    set(value) {
        field = value
        onStateChanged(this)
    }


    fun run() {

        state = ExperimentState.SUCCESS;
    }

}

enum class ExperimentState(val vAsset: VectorAsset) {
    DEFAULT(Icons.Default.Info),
    RUNNING(Icons.Default.Refresh),
    SUCCESS(Icons.Default.Done),
    FAILED(Icons.Default.Warning),
}
Abhimanyu
  • 11,351
  • 7
  • 51
  • 121

1 Answers1

3

There's a few ways to address this but key thing is that you need to add a copy of element (with state changed) to experiments to trigger the recomposition.

One possible example would be

data class Experiment(val name: String, val state: ExperimentState,  val onStateChanged: (Experiment) -> Unit) {

    fun run() {
        onStateChanged(this.copy(state = ExperimentState.SUCCESS))
    }
}

and then

    fun experimentStateChanged(experiment: Experiment) {
        val index = experiments.toMutableList().indexOfFirst { it.name == experiment.name }
        experiments = experiments.toMutableList().also {
            it[index] = experiment
        }
    }

though I suspect there's probably cleaner way of doing this.

John O'Reilly
  • 10,000
  • 4
  • 41
  • 63
  • Thanks for your anwser. This works, but I wonder if I always have to update the complete Model if all I do is updating one value of it? – Patrick Schmidt Nov 23 '20 at 20:18
  • 1
    The general approach in Compose (and elsewhere) is to favour immutability where possible so I think this is expected approach. You can see something similar for example done in https://github.com/googlecodelabs/android-compose-codelabs/blob/main/StateCodelab/finished/src/main/java/com/codelabs/state/todo/TodoScreen.kt – John O'Reilly Nov 24 '20 at 08:49