https://kotlinlang.org logo
#compose
Title
# compose
s

Srikrishna

08/02/2023, 7:05 AM
Hi All, collectAsStateWithLifecycle() returns old value from old recomposition. is there anyway to fix this? In my case, on every recomposition if data changes, i create new model object and that has initialized MutableStateFlow. on every recomposition collectAsStateWithLifecycle() return old object value instead of new flow . is there a way to fix this? (continuation in comments)
🧵 1
s

Srikrishna

08/02/2023, 8:31 AM
Copy code
import android.annotation.SuppressLint
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import kotlin.random.Random

fun timerRepeat(delay: Long, runnable: suspend () -> Unit) {
    Timer().scheduleAtFixedRate(object : TimerTask() {
        override fun run() {
            CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>).launch {
                runnable()
            }
        }
    }, 0, delay)
}

@Composable
fun MainScreen() {

//    Column(modifier = Modifier.fillMaxSize()) {
//        Navigator(ImageListScreen()) {
//            CurrentScreen()
//        }
//    }

    val flow = remember {
        val flow = MutableStateFlow(0)
        timerRepeat(1000) {
            flow.value = flow.value + 1
        }
        flow
    }

    TestContainer(flow = flow)
}

@Composable
fun TestContainer(flow: StateFlow<Int>) {
    val value = flow.collectAsStateWithLifecycle().value
    TestWidget.Widget(count = value)
}

open class Initializer {
    private var objectId = Random.nextInt(100, 200)
    private var update = 0
    val initialized: StateFlow<String> = MutableStateFlow("Initializer.$objectId.${update++}")

    fun initComplete() {
        (initialized as MutableStateFlow).value = "Initializer.$objectId.done"
    }

    fun isInitialized(): Boolean {
        return initialized.value.contains("done")
    }
}

abstract class Widget : Initializer() {
    private var objectId = Random.nextInt(100, 200)

    abstract fun init()

    @Composable
    protected abstract fun build()

    @SuppressLint("CoroutineCreationDuringComposition")
    @Composable
    fun Content() {
        val value = super.initialized.collectAsStateWithLifecycle().value
        Timber.v("[$objectId] initialized.value:$value, isInitialized():${super.isInitialized()}")

        val scope = rememberCoroutineScope()
        scope.launch {
            init()
            initComplete()
        }

        if (value.contains("done")) {
            this.build()
        }
    }
}

class TestWidget(private val count: Int) : Widget() {
    companion object {}

    override fun init() {
    }

    @Composable
    override fun build() {
        Text("Hello $count")
    }
}

@Composable
fun TestWidget.Companion.Widget(count: Int) {
    val widget = remember { mutableStateOf(TestWidget(count)) }

    LaunchedEffect(key1 = count, block = {
        widget.value = TestWidget(count)
    })

    widget.value.Content()
}
29426-29426 Widget in.cjcj.jcp V [196] initialized.value:Initializer.100.0, isInitialized():false 29426-29426 Widget in.cjcj.jcp V [127] initialized.value:Initializer.100.done, isInitialized():false 29426-29426 Widget in.cjcj.jcp V [173] initialized.value:Initializer.165.done, isInitialized():false
Copy code
val value = super.initialized.collectAsStateWithLifecycle().value
Timber.v("[$objectId] initialized.value:$value, isInitialized():${super.isInitialized()}")
initialization is considered done if initialized value contains done. calling manually .value return the right value but collectAsStateWithLifecycle return old object value
s

Stylianos Gakis

08/02/2023, 11:15 AM
Quite hard to follow this all together, but if there’s one thing I can see for sure is that you’re doing side effects in composition when you do this
Copy code
@Composable
    fun Content() {
        val value = super.initialized.collectAsStateWithLifecycle().value
        Timber.v("[$objectId] initialized.value:$value, isInitialized():${super.isInitialized()}")

        val scope = rememberCoroutineScope()
        scope.launch {
            init()
            initComplete()
        }

        if (value.contains("done")) {
            this.build()
        }
    }
you’re launching a coroutine in composition, and calling
build
in composition too. Don’t know if this is where the problem is, but I’d definitely fix those as a first step
s

Srikrishna

08/09/2023, 5:09 PM
@Stylianos Gakis thanks for your replay. build function is a compose function. build() function is basically once all the data is initialised, display UI. this build function will be implemented in child class and child class has the actual Ui. coming back to the question. I have solved it. collectAsStateWithLifecycle() is the culprit. I have replaced with following code
Copy code
fun <T> StateFlow<T>.state(scope: CoroutineScope): State<T> {
    val flow = this
    val state = mutableStateOf(flow.value, referentialEqualityPolicy())
    scope.launch {
        flow.collect {
            withContext(Dispatchers.Main) {
                state.value = it
            }
        }
    }
    return state
}
replacing collectAsStateWithLifecycle with state solved my problem.
@Stylianos Gakis
Copy code
scope.launch {
            init()
            initComplete()
        }
is this is a problem
s

Stylianos Gakis

08/09/2023, 5:14 PM
Ngl, this looks way too complicated whatever it is that you're doing 😅
3 Views