How to create separate ViewModels per list item when using Compose UI?

Viewed 792

I'm working on a trading app. I need to list the user stocks and their value (profit or loss) among with the total value of the portfolio.

For the holdings list, in an MVP architecture I would create a presenter for each list item but for this app I decided to use MVVM (Compose, ViewModels and Hilt ). My first idea was to create a different ViewModel for each list item. I'm using hiltViewModel() in the composable method signature to create instances of my ViewModel, however this gives me always the same instance and this is not what I want. When using MVVM architecture, is what I'm trying to do the correct way or I should use a single ViewModel? Are you aware about any project I could have a look at? The image below is a super simplification of my actual screen, each cell is complex and that's why I wanted to use a different ViewModel for each cell. Any suggestion is very welcome.

enter image description here

3 Answers

Hilt doesn't support keyed view models. There's a feature request for keyed view models in Compose, but we had to wait until Hilt supports it.

Here's a hacky solution on how to bypass it for now.

You can create a plain view model, which can be used with keys, and pass injections to this view model through Hilt view model:

class SomeInjection @Inject constructor() {
    val someValue = 0
}

@HiltViewModel
class InjectionsProvider @Inject constructor(
    val someInjection: SomeInjection
): ViewModel() {

}

class SomeViewModel(private val injectionsProvider: InjectionsProvider) : ViewModel() {
    val injectedValue get() = injectionsProvider.someInjection.someValue
    var storedValue by mutableStateOf("")
        private set

    fun updateStoredValue(value: String) {
        storedValue = value
    }
}

@Composable
fun keyedViewModel(key: String) : SomeViewModel {
    val injectionsProvider = hiltViewModel<InjectionsProvider>()
    return viewModel(
        key = key,
        factory = object: ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return SomeViewModel(injectionsProvider) as T
            }

        }
    )
}

@Composable
fun TestScreen(
) {
    LazyColumn {
        items(100) { i ->
            val viewModel = keyedViewModel("$i")
            Text(viewModel.injectedValue.toString())
            TextField(value = viewModel.storedValue, onValueChange = viewModel::updateStoredValue)
        }
    }
}

You have to use Dagger version 2.43 (or newer), it includes the feature/fix to support keys in Hilt ViewModels

https://github.com/google/dagger/releases/tag/dagger-2.43

From the release description:

Fixes #2328 and #3232 where getting multiple instances of @HiltViewModel with different keys would cause a crash.

Related