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

Tim Malseed

12/21/2021, 10:40 PM
A follow up from a previous question.. I’m trying to figure out how to share a ViewModel across multiple composable screens, but it doesn’t feel quite right. I’ve written some psuedo code to try to explain the approach..
Copy code
sealed class ScreenAState() {
    object Loading : ScreenAState()
    object Ready : ScreenAState()
}

sealed class ScreenBState() {
    object Loading : ScreenAState()
    object Ready : ScreenAState()
}

sealed class Event {
    object DataSuccessfullyRetrieved : Event()
}

data class BackendDataObject(val stuff: Int)

class SharedViewModel {

    val screenAState = MutableStateFlow<ScreenAState>(ScreenAState.Loading)

    val screenBState = MutableStateFlow<ScreenAState>(ScreenAState.Loading)

    val events = MutableSharedFlow<Event>(
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    // Some data that is set in Screen A, but retrieved in Screen B
    val someSharedObject = MutableStateFlow<BackendDataObject?>(null)

    fun submitThingFromScreenA(thing: String) {
        screenAState.value = ScreenAState.Loading
        backend.getDataObject(thing)
            .onSuccess {
                someSharedObject.value = it
                events.emit(Event.DataSuccessfullyRetrieved)
            }
    }
}

@Composable
fun ScreenA(viewModel: SharedViewModel, onNavigateToScreenB: () -> Unit) {

    LaunchedEffect(viewModel) {
        viewModel.events
            .onEach {
                onNavigateToScreenB()
            }
            .launchin(this)
    }

    Button(
        onClick = {
            viewMode.submitThingFromScreenA("thing")
        }
    )
}

@Composable
fun ScreenB(viewModel: SharedViewModel) {

    val sharedObject by viewModel.someSharedObject.collectAsState()
    
    ...
}
Basically, you submit some data from ScreenA, which sets a value on the VM, and fires an event. The user is then navigated to Screen B, which reads that data.
The reason I don’t love this approach, is because the
screenBState
is not scoped to the lifecycle of ScreenB. If you navigate to Screen B and then press back,
screenBState
will not change, and the
someSharedObject
will still be valid (though technically stale)
But, I’m struggling to think of another way to pass this
BackendDataObject
from ScreenA to ScreenB. I’m using Compose Navigation. Serializing/Parcelizing the object and passing it that way doesn’t seem right. I could create some singleton repository and hold the
BackendDataObject
there, but then that repository object will outlive the scope of both these screens.
Maybe there should be 3 ViewModels. One for ScreenA and its state, one for Screen B and its state, and a shared one that just holds the
BackendDataObject
. That would look like this:
Copy code
class ScreenAViewModel {
    val screenAState = MutableStateFlow<ScreenAState>(ScreenAState.Loading)

    val events = MutableSharedFlow<Event>(
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    fun submitThingFromScreenA(thing: String) {
        screenAState.value = ScreenAState.Loading
        backend.getDataObject(thing)
            .onSuccess {
                someSharedObject.value = it
                events.emit(Event.DataSuccessfullyRetrieved)
            }
    }
}

class ScreenBViewModel {

    val screenBState = MutableStateFlow<ScreenAState>(ScreenAState.Loading)
}

class SharedViewModel {

    // Some data that is set in Screen A, but retrieved in Screen B
    val someSharedObject = MutableStateFlow<BackendDataObject?>(null)
}

@Composable
fun ScreenA(screenAViewModel: ScreenAViewModel, sharedViewModel: SharedViewModel, onNavigateToScreenB: () -> Unit) {

    LaunchedEffect(screenAViewModel) {
        screenAViewModel.events
            .onEach { event ->
                when (event) {
                    is Event.DataSuccessfullyRetrieved -> {
                        sharedViewModel.someSharedObject = event.data
                        onNavigateToScreenB()
                    }
                }
            }
            .launchin(this)
    }

    Button(
        onClick = {
            screenAViewModel.submitThingFromScreenA("thing")
        }
    )
}
Hmm, actually I think this works..
👍 1
a

Alex Vanyo

12/21/2021, 11:10 PM
One thing to be careful of here is process death. As written, if you navigate to Screen B, then background the app, your app’s process might be killed. If you then return to your app, your backstack will be restored, so you’ll still be navigate to Screen B. However, since the app’s process was killed, the
SharedViewModel
will be recreated from scratch, so you’ll find that its shared object will be
null
in that case. If the data is sufficiently small, a
SavedStateHandle
could be used to persist the shared object. Otherwise, you may need to have
ScreenB
navigate back to
ScreenA
if the shared object is
null
.
t

Tim Malseed

12/21/2021, 11:11 PM
Ah yes, this is a very good point and one that I completely forgot to consider. Thanks!
👍 1
2 Views