Search code examples
androidkotlinandroid-viewmodelviewmodel-savedstate

Is it possible to save and extract a savedStateHandle (viewmodel) without using NavHost?


I am using NavigableListDetailPaneScaffold, to show list of items and then it's details when clicked.

The screen is accompanied by its viewModel and I would love to run the fetching code in the constructor. But I can't because;

  1. I lack a way to get the argument within the class (probably from savedStateHandle)
  2. The class is initialized immediately the ListDetailPane is created (not when navigated to the detail pain)
@HiltViewModel
class DetailsScreenViewModel
@Inject constructor(
    private val repository: ItemRepository,
//    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    init {
        //run fetching code here once
    }
}

Although, I can achieve this using a member function together with LauchedEffect(Unit) to ensure it ran once but I am not satisfied using it.

fun fetch(index: Int? = null) {
    val combinedFlow: Flow<EditDetailUiState> = combine(
        repository.requestAllItemDetails(index),
        repository.requestAllUnits()
    ) { item, allUnits ->
            
    }

    viewModelScope.launch {
        combinedFlow.collect { state ->
            editDetailUiState = state
        }
    }
}

I understand that when using NavHost, arguments could be extracted through the savedStateHandle. Is there a way to achieve this using the NavigableListDetailPaneScaffold or any advice about it?


Solution

  • You should not collect flows in your view model.

    Instead, use a pattern like this:

    private val selectedItem: MutableStateFlow<Int?> = MutableStateFlow(null)
    
    val editDetailUiState: StateFlow<EditDetailUiState> = selectedItem
        .flatMapLatest(repository::requestAllItemDetails)
        .combine(repository.requestAllUnits()) { item, allUnits ->
            EditDetailUiState(item, allUnits)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = EditDetailUiState(),
        )
    
    fun selectItem(index: Int? = null) {
        selectedItem.value = index
    }
    

    Now you only transform the flows in the view model and finally expose them as a single, combined StateFlow of EditDetailUiState. In your composables you can then retrieve the state like this:

    val state by viewModel.editDetailUiState.collectAsStateWithLifecycle()
    

    Make sure you use the gradle dependency androidx.lifecycle:lifecycle-runtime-compose.

    Please note that I used EditDetailUiState() as the initial value of the StateFlow. This value is used until all flows provided their first value. Adapt the value as needed, you can also use null.

    You can now easily add additional flows to combine, for example the flows returned from a DataStore or a database like Room. Then use this same view model for both the list pane and the detail pane.


    You can read more about the collection of StateFlows here: https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3

    There is also a codelab for ViewModel and State in Compose: https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state