hey :wave: Thank you for the great effort you put ...
# mvikotlin
w
hey 👋 Thank you for the great effort you put into creating all the libraries. I could use some help - I got stuck when trying to implement a simple screen using mvikotlin + decompose, I see only first emission from my component (it exposes
Flow<ViewState>
)
Copy code
class BacklogComponent(
    private val componentContext: ComponentContext,
    private val storeFactory: StoreFactory,
) : Backlog, ComponentContext by componentContext {

    private val backlogStore =
        instanceKeeper.getStore { BacklogStoreFactory(storeFactory).create() }

    override val models: Flow<BacklogViewState> = backlogStore.states

    override fun onEvent(event: BacklogEvent) {
        backlogStore.accept(event)
    }
}
Copy code
internal class BacklogStoreFactory(private val storeFactory: StoreFactory) {

    private val items = mutableListOf<String>()

    fun create(): BacklogStore =
        object : BacklogStore,
            Store<BacklogEvent, BacklogViewState, Nothing> by storeFactory.create(
                name = "ListStore",
                initialState = BacklogViewState(items),
                bootstrapper = SimpleBootstrapper(Unit),
                executorFactory = ::ExecutorImpl,
                reducer = ReducerImpl,
            ) {}

    private sealed interface Result {
        data class Updated(val items: List<String>) : Result
    }

    private inner class ExecutorImpl :
        CoroutineExecutor<BacklogEvent, Unit, BacklogViewState, Result, Nothing>() {

        override fun executeIntent(
            intent: BacklogEvent,
            getState: () -> BacklogViewState
        ) {
            when (intent) {
                is BacklogEvent.AddItem -> {
                    items.add(intent.item)
                    dispatch(Result.Updated(items))
                }
            }
        }
    }

    private object ReducerImpl : Reducer<BacklogViewState, Result> {
        override fun BacklogViewState.reduce(result: Result): BacklogViewState =
            when (result) {
                is Result.Updated -> copy(items = result.items)
            }
    }
}
Copy code
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val root = todoRoot(defaultComponentContext())

        setContent {
            ComposeAppTheme {
                Surface(color = MaterialTheme.colors.background) {
                    RootContent(root)
                }
            }
        }
    }

    private fun todoRoot(componentContext: ComponentContext): MainNavigation =
        MainNavigationComponent(
            componentContext = componentContext,
            storeFactory = DefaultStoreFactory(),
        )
}

@Composable
fun RootContent(component: MainNavigation) {
    Children(stack = component.childStack) {
        when (val child = it.instance) {
            is MainNavigation.Child.BacklogChild -> BacklogContent(child.component)
        }
    }
}

@Composable
fun BacklogContent(component: Backlog) {
    val model by component.models.collectAsState(BacklogViewState(emptyList()))

    Items(model, component::onEvent)
}

@Composable
private fun Items(
    model: BacklogViewState,
    onEvent: (BacklogEvent) -> Unit
) {
    Column {
        var clickNumber by remember { mutableStateOf(1) }
        Box(
            Modifier.fillMaxWidth().fillMaxHeight()
                .clickable {
                    onEvent(BacklogEvent.AddItem("Click $clickNumber"))
                    clickNumber += 1
                }) {
            val listState = rememberLazyListState()
            LazyColumn(state = listState) {
                items(model.items) {
                    Item(
                        item = it,
                    )
                    Divider()
                }
            }
        }

    }
}
so when I put a breakpoint on the store I see all items are added, but on the view side I can see only first emission (when list.size = 1)
a
Hey! Thanks for the feedback! Your issue is because you are using a mutable list. You should copy the State's list in the reducer instead. Like
state.copy(items = state.items + msg.item)
.
w
just want to understand why and where exactly it fails to not fall into this in the future
a
It's because of Compose, it doesn't recompose because the instance of the list is the same.
w
ok, makes sense
thank you very much 🙇
a
Btw, this would also break a RecyclerView with DiffUtils, as it wouldn't be able to compare previous and new lists.
w
you’re right, I missed that I passed the same instance all the way from beginning
is there any way to skip initial state when doing
collectAsState
?
Copy code
val model by component.models.collectAsState(BacklogViewState(emptyList()))
a
First of all, you can grab the initial value as follows - component.models.collectAsState(component.models.value) Also, you can expose StateFlow instead of Flow - but converting a Flow to a StateFlow would also require you to use the current value. Finally you can use Value instead of Flow - https://github.com/JetBrains/compose-jb/blob/master/examples/todoapp/common/utils/src/commonMain/kotlin/example/todo/common/utils/StoreExt.kt
w
Wow, this is awesome, thank you again
a
I will think about if store.states could return StateFlow instead of Flow.
w
hey Arkadii 👋 I have another problem I could use some help with 😅, when I use flow, UI is rendered just fine, but when I change to use
Value
instead nothing is rendered: this works fine:
a
Hey! The code looks correct, could you please show the asValue code?
w
Copy code
import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.rx.Disposable
typealias ValueObserver<T> = (T) -> Unit

fun <T : Any> Store<*, T, *>.asValue(): Value<T> =
    object : Value<T>() {
        override val value: T get() = state
        private var disposables = emptyMap<ValueObserver<T>, Disposable>()

        override fun subscribe(observer: ValueObserver<T>) {
            val disposable = states(com.arkivanov.mvikotlin.rx.observer(onNext = observer))
            this.disposables += observer to disposable
        }

        override fun unsubscribe(observer: ValueObserver<T>) {
            val disposable = disposables[observer] ?: return
            this.disposables -= observer
            disposable.dispose()
        }
    }
a
I can't see what could be an issue. I would recommend adding logs to verify that 1. The store actually produces new states -
Copy code
override fun subscribe(observer: ValueObserver<T>) {
            val disposable = states(com.arkivanov.mvikotlin.rx.observer { 
                println(it)
                observer(it)
            })
            this.disposables += observer to disposable
        }
2. The Composable recomposes - add
println
in BackologContent , right before
Items
call. I would need a reproducer to check what's wrong.
Also the new state must not be
equal
to the previous state in order for the composable function to recompose. So it must be a different instance and with different data.
w
thanks!
Also the new state must not be
equal
to the previous state in order for the composable function to recompose. So it must be a different instance and with different data.
each time it’s a new data class obtained with
copy
(data class contains only list of strings)
so composable is getting only first emission (with empty list)
a
So it looks like something is wrong around the first line of the Composable. Maybe the
by
operator references a wrong thing (wrong import?). Also worth checking inside observeAsState. You could cope the method to your project and add logs there as well.
w
hmm, I think I may imported wrong one here:
Copy code
package com.arkivanov.decompose.extensions.compose.jetpack

@androidx.compose.runtime.Composable public fun <T : kotlin.Any> com.arkivanov.decompose.value.Value<T>.subscribeAsState(): androidx.compose.runtime.State<T> { /* compiled code */ }
there are two separate extensions one for jetbrains jetpack and second one for google, correct? I imported all google deps in android module, can it be that this ☝️ is a wrong import?
I can also mumble, not knowing what am I talking about 😄
a
I'm not sure why you have both variants. Normally there should be only one variant, depending on what dependency you added. But you can try copying the function from here - https://github.com/arkivanov/Decompose/blob/master/extensions-compose-jetbrains/sr[…]anov/decompose/extensions/compose/jetbrains/SubscribeAsState.kt And add logs in the lambda, before
state.value = it
.
And also could you please Ctrl+Click on the
by
operator? Wondering what it leads to.
w
copying the function behaves the same, it’s not it
by operator:
I also tried without
by
only using
=
and then
.value
, with same result
a
This is very weird. I would need a reproducer for this.
w
thank you very much for the help, I will dig into it later, if I find anything I will update the thread here, if not I will create a simple repro app
I am having a new instance of list using
toList
that calls
ArrayList(collection)
, it works fine with flow, but not with value,
if instead I create new immutable list with
listOf
it starts to work
a
Glad that you found the cause, but I still don't understand why it doesn't work. I will try to reproduce this later. Meantime, you don't have to maintain the mutable list. Just send the new item to the reducer and let it copy the satet and the list
w
the mutable list is instead of persistent storage/backend api there, just wanted to kick start it and remove it as soon as I get more real with it
I also don’t understand why it doesn’t work with
toList