badoo / Decompose

Kotlin Multiplatform lifecycle-aware business logic components (aka BLoCs) with routing functionality and pluggable UI (Jetpack Compose, SwiftUI, JS React, etc.), inspired by Badoos RIBs fork of the Uber RIBs framework
https://arkivanov.github.io/Decompose
Apache License 2.0
814 stars 41 forks source link

Missing `lifecycle` parameter for `observe` function (jetbrains-compose.v020-build128) #13

Closed xetra11 closed 3 years ago

xetra11 commented 3 years ago

This is the decompose setup I use (following the guide in the readme):

import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import com.arkivanov.decompose.value.MutableValue
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.observe

class WithCharacterState {
    private val _value = MutableValue(mutableListOf<Character>())
    val state: Value<List<Character>> = _value

    fun addCharacter(character: Character) {
        _value.value.add(character)
    }
}

@Composable
fun WithCharacterState.render() {
    state.observe { state ->
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Text(text = state.toString())

            Button(onClick = { addCharacter(CharacterTemplate.DEFAULT_CHARACTER)} ) {
                Text("Add Character")
            }
        }
    }
}

However I receive the following two errors:

e: /xetra11/app/module/character/WithCharacterState.kt: (25, 11): No value passed for parameter 'lifecycle'
e: /xetra11/app/module/character/WithCharacterState.kt: (26, 9): @Composable invocations can only happen from the context of a @Composable function

my build.gradle.kts:

import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.4.20"
    id("org.jetbrains.compose") version "0.2.0-build128"
}

group = "com.github.xetra11"
version = "0.0.1"

repositories {
    jcenter()
    mavenCentral()
    google()
    maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
    maven { url = uri("https://dl.bintray.com/arkivanov/maven") }
}

dependencies {
    implementation(compose.desktop.currentOs)
    implementation("org.slf4j:slf4j-simple:2.0.0-alpha1")

    val decomposeVersion = "0.1.2"
    implementation("com.arkivanov.decompose:decompose:$decomposeVersion")
    implementation("com.arkivanov.decompose:extensions-compose-jetbrains:$decomposeVersion")

    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
    testImplementation("org.assertj:assertj-core:3.18.1")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "11"
}

compose.desktop {
    application {
        mainClass = "MainKt"
        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            packageName = "app"
        }
    }
}
arkivanov commented 3 years ago

@xetra11 in Composable functions you should prefer Composable Value.observe extension function. An example using Jetpack Compose can be found here, for JetBrains Compose it will be the same.

arkivanov commented 3 years ago

Also you may like to use Value.asState extension function:

@Composable
fun WithCharacterState.render() {
    val value by state.asState()

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = value.toString())

        Button(onClick = { addCharacter(CharacterTemplate.DEFAULT_CHARACTER)} ) {
            Text("Add Character")
        }
    }
}

Probably it is worth to deprecate the Composable Value.observe extension, will think about it.

xetra11 commented 3 years ago

Ok I give it a new try. Thanks for the quick reply

xetra11 commented 3 years ago

Sorry to bother you again but I have a hard time grasping the concept and usage of Decompose. I read over it several times now but still can not apply it to my use case properly.

See this is my CharacterTable component in which I'd like to inject the observable state into and read and write to it:

@Composable
fun CharacterTable(
    modifier: Modifier = Modifier,
    characters: List<Character>
) {
    TableHeader(
        modifier.then(Modifier.background(Color.LightGray)),
        items = listOf(
            "name",
            "dna",
            "dynasty",
            "culture",
            "religion",
            "birth",
            "death"
        )
    )

    characters.map {
        Row {
            Box(modifier) { Text(it.name) }
            Box(modifier) { Text(it.dna) }
            Box(modifier) { Text(it.dynasty) }
            Box(modifier) { Text(it.culture) }
            Box(modifier) { Text(it.religion) }
            Box(modifier) { Text(it.birth) }
            Box(modifier) { Text(it.death) }
        }
    }
}

Now I have this state here defined:

class CharacterState{
    private val _value = MutableValue(mutableListOf<Character>())
    val state: Value<List<Character>> = _value

    fun addCharacter(character: Character) {
        _value.value.add(character)
    }
}

Can you give me an example with the provided code if you don't mind?

arkivanov commented 3 years ago

Hey, I'm here to help! I think you can just write as follows:

@Composable
fun CharacterState.render() {
    val state by state.asState()
    CharacterTable(characters = state)

    // You can now call this.addCharacter(...) 
}

You may want to add a callback to the CharacterTable (or to any other Composable depending on your requirements):

@Composable
fun CharacterTable(
    modifier: Modifier = Modifier,
    characters: List<Character>,
    addCharacter: (Character) -> Unit
) {
    // Omitted code
}

And then your render function will become:

@Composable
fun CharacterState.render() {
    val state by state.asState()
    CharacterTable(characters = state, addCharacter = ::addCharacter)
}
xetra11 commented 3 years ago

Ok I see. However I still can't figure out how to "use" this composable with state in other composable components then.

This is my existing code with the CharacterTable function call. I try to use the given information. Can you verify that I am using this the right way coping with the concept?

@Composable
fun CharacterList(
    characters: List<Character>
) {
    Column(modifier = Modifier.fillMaxWidth()) {
        val characterState = CharacterState()
        characterState.render()
    }
}

@Composable
private fun CharacterState.render() {
    val state = state.asState()
    CharacterTable(characters = state, addCharacter = ::addCharacter)
}

Reading over that that way I don't like the naming of render here since I have no clue that this is my CharacterTable. Renaming characterState and .render() to withCharacterState and onCharacterTable at least brings um something like:

@Composable
fun CharacterList(
    characters: List<Character>
) {
    Column(modifier = Modifier.fillMaxWidth()) {
        val withCharacterState = CharacterState()
        withCharacterState.onCharacterTable()
    }
}

@Composable
private fun CharacterState.onCharacterTable() {
    val state = state.asState()
    CharacterTable(characters = state, addCharacter = ::addCharacter)
}
arkivanov commented 3 years ago

Something wrong in your example. E.g. the characters argument is not used. What are you trying to achieve? Maybe I could provide a complete code snipped based on your idea?

xetra11 commented 3 years ago

@arkivanov Thanks for your time but I think I mixed up some concepts here and there. Having a hard time grasping the state concepts with Compose for Desktop. Completely new to the field ;)