Best practice for hiltviewmodel in compose

Viewed 448

So i have a few questions about using hiltviewmodels, states and remember in compose.

For some context, i have a ViewPager set up

HorizontalPager(
                count = 4,
                modifier = Modifier.fillMaxSize(),
                state = pagerState,
            ) { page ->
                when (page) {
                    0 -> PagerOne()
                    1 -> PagerTwo()
                    2 -> PagerThree()
                    3 -> PagerFour()
                }
            }

Lets say i have a State in my viewmodel declared like this

private val _data: MutableState<DataClass> = mutableStateOf(DataClass())
var data: State<DataClass> = _data

First, where do i inject my viewmodel? Is it fine to do it in the constructor of my pager composable?

@Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {
...

And if i want to get the value from that viewmodel state, do i need to wrap it into a remember lambda?

@Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {

val myState = viewmodel.data or var myState by remember { viewmodel.data }

Next question about flow and .collectasstate. Lets say i have a function in my viewmodel which returns a flow of data from Room Database.

fun getRoomdata() = roomRepository.getLatesData()

Is it correct to get the data like this in my composable?

val roomData = viewmodel.getRoomdata().collectasState(initial = emptyRoomdata())

Everything is working like expected, but im not sure these are the best approaches.

1 Answers

About view model inject: it's fine instantiate a viewModel in the screen composable constructor, but only if you want this view model to be created as soon as your composable screen is created.
For example, if you have a view model for each composable screen on your pager, this is the way to go. But if you want all composable screens to share the same view model instance, you must instantiate it at the same level as your HorizontalPager and pass the view model to the composable screens.
I wrote an explanation with code example about view model instances a few days ago here

About value state from view model: you don't need and shouldn't wrap the state into a remember on composable screen.
The remember function is to keep state from previous recompose invocation (avoid resetting to default), and as the value is coming from the view model, it is already safe from recompositions.
Code example:

on ViewModel:

// first way: with a mutable private and an immutable public
private var _state = mutableStateOf(value = DataClass())
val state: State<DataClass> get() = _state

// second way: with one mutable public, but with private set
var state = mutableStateOf(value = DataClass())
    private set

on Screen:

@Composable
fun Screen(
    viewModel: ViewModel = hiltViewModel()
) {
    val state by viewModel.state
    // now you can get anything from state, composables will recompose on value change
}

About collecting flows: to have the separation of concepts applied in a better way in your project you should collect the flow inside the view model and delegate the results to some state class, like the one in the example I did above.
Code example:

ScreenState (data class to hold states)

data class ScreenState(
    val isLoading: Boolean = true,
    val strings: List<String> = emptyList(),
    @StringRes val stringError: Int? = null
)

ViewModel (just a view model)

@HiltViewModel
class ViewModel @Inject constructor(
    private val roomRepository: RoomRepository
) : ViewModel() {
    private var _state = mutableStateOf(value = ScreenState())
    val state: State<ScreenState> get() = _state

    init {
        getRoomData()
    }

    private fun getRoomData() = viewModelScope.launch {
        _state.value = _state.value.copy(isLoading = true)
        roomRepository.getData().collectLatest { result ->
            _state.value = _state.value.copy(
                isLoading = false,
                strings = result
            )
        }
    }
}

Note: in this scenario above I'm assuming that a supposed function getData from RoomRepository returns a simple list of strings just for a simple example. It would be more ideal to put a sealed class to encapsulate the data with possible return types, like success and error for example.

Screen (just a composable screen)

@Composable
fun Screen(
    viewModel: ViewModel = hiltViewModel()
) {
    val state by viewModel.state

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        if (state.isLoading) CircularProgressIndicator()
        LazyColumn(
            modifier = Modifier.fillMaxWidth(),
            contentPadding = PaddingValues(all = 16.dp),
            verticalArrangement = Arrangement.spacedBy(space = 16.dp)
        ) {
            if (state.strings.isEmpty()) item {
                Text(text = "nothing on string list to show")
            }
            else items(items = state.strings) { string ->
                Text(text = string)
            }
        }
    }
}

And so you have the display of data on a screen, whenever the state of the view model is changed, all composables that are using this state will receive the changes.

Here is a official doc about compose with view models and data class as state for source of truth that can help.

Related