How can I have different states with different viewmodels? - android

I am making an app where the user first need to login to be able to get alot of different data from a backend. (many endpoints)
So I have one viewmodel for the login, and I have alot of viewmodels for all the other data.
The other viewmodels require the token from the first viewmodel to be able to get data from the backend.
I don't know how I can do this.
I was thinking that I can have my login screen in a kind of state manager which will direct the UI to the correct screen like this
#ExperimentalComposeUiApi
#Composable
fun LoginState(vm: AuthViewModel, nc: NavController) {
val token by vm.token.collectAsState()
when (token) {
is Resource.Loading -> {
LoadingScreen()
}
is Resource.Success -> {
Scaffold(vm = vm)
}
is Resource.Error -> {
LoginScreen(vm = vm)
}
}
}
But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
Another thought was to use Hilt to do some kind of magic dependency injection, and then put all the viewmodels into a ViewModelManager in the MainActivity and then inject the Token into the repositories of each viewmodel when login is successfull.
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val authViewModel: AuthViewModel by viewModels()
private val userViewModel: UserViewModel by viewModels()
private val venueViewModel: VenueViewModel by viewModels()
private val eventViewModel: EventViewModel by viewModels()
private val viewModelManager = ViewModelManager(
userViewModel = userViewModel,
authViewModel = authViewModel,
venueViewModel = venueViewModel,
eventViewModel = eventViewModel,
)
#ExperimentalMaterialApi
#ExperimentalComposeUiApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MoroAdminTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
ScaffoldExample(viewModelManager)
}
}
}
}
}
However I have no idea how to do this or if it is even possible - or a good solution.

Problem: you want to share a value (token) to all of your view model
your token retrieved in AuthViewModel and need to share it to the other viewModels
you can make your data in the other viewModels changes when the token changes
by using datastore Preferences see implementation
Datastore preferences provides you with a flow of values whenever the value changes
Create a DatastoreManager Class
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
#Singleton
class DatastoreManager #Inject constructor(#ApplicationContext context: Context) {
private val dataStore = context.dataStore
val dataFlow = dataStore.data
.map { preferences ->
val token = preferences[PreferencesKeys.TOKEN]
}
suspend fun updateToken(token: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.TOKEN] = token
}
}
private object PreferencesKeys {
val TOKEN = preferencesKey<String>("token")
}
}
In AuthViewModel
Inject the DatastoreManager and set the token after login
datastore.updateToken(newToken)
In other ViewModels
Inject the DatastoreManager and use it's value
//this is a flow of tokens and will recive the token when you set it
val token = datastore.token
// if you are not familiar with flows and using only LiveData
val token = datastore.token.asLiveData()
// use the token to get the data from backend
val data = token.map {
// this code will trigger every time the token changes
yourGetDataFromBackendFunction(it)
}

But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
This is not true. You don't have to create view models in your Activity.
In any composable you can use viewModel()
Returns an existing ViewModel or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided by LocalViewModelStoreOwner.
So you don't need any ViewModelManager. Inside any composable you can use viewModel() with the corresponding class. In your case you're using Hilt, you should use hiltViewModel() instead: it'll also initialize your injections.
#Composable
fun AuthScreen(viewModel: AuthViewModel = hiltViewModel()) {
}
Or like this:
#Composable
fun VenueScreen() {
val viewModel: VenueViewModel = hiltViewModel()
}
First approach will allow you to easily test your screen with mock view model, without passing any arguments in your production code.
Check out more about view models in view models documentation and hilt documentation
As to your token question, you can pass it with injections. I don't think that your view model really needs the token, probably you should have some network manager which will use the token to make requests. And this network manager should use injection of some token provider.

Related

How to pass arguments to ViewModel from Fragment and inject dependencies at the same time?

I need to open a Compose component with its own ViewModel and pass arguments to it, but at the same time I inject dependencies to this ViewModel. How can I achieve this? Can I combine ViewModel factory and Dependency Injection (Hilt)?
Yes. you can..
Have your component be like this:
#Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
...
}
and in your viewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: MyRepository,
... //If you have any other dependencies, add them here
): ViewModel() {
...
}
When you pass arguments to the ViewModel, make sure that Hilt knows where to get that dependency. If you follow the MVVM architecture, then the ViewModel should handle all the data and the composable all the ui related components. So usually, you only need the ViewModel injection into the composable and all the other data injected dependencies into the ViewModel.
The composable should only care about the data that it gets from the ViewModel. Where the ViewModel gets that data and the operations it does on that data, it does not care.
Lemme know if this is what you meant..
Check out the official website for more:
Hilt-Android
Yes, you can. This is called "Assisted Inject" and it has it's own solutions in Hilt, Dagger(since version 2.31) and other libraries like AutoFactory or square/AssistedInject.
In this article, you can find an example of providing AssistedInject in ViewModel for Composable with Hilt Entry points.
Here is some code from article in case if article would be deleted:
In the main Activity, we’ll need to declare EntryPoint interface which will provide Factory for creating ViewModel:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#EntryPoint
#InstallIn(ActivityComponent::class)
interface ViewModelFactoryProvider {
fun noteDetailViewModelFactory(): NoteDetailViewModel.Factory
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotyTheme {
NotyNavigation()
}
}
}
}
We get Factory from Activity and instantiating our ViewModel with that Factory and assisted some field:
#Composable
fun noteDetailViewModel(noteId: String): NoteDetailViewModel {
val factory = EntryPointAccessors.fromActivity(
LocalContext.current as Activity,
MainActivity.ViewModelFactoryProvider::class.java
).noteDetailViewModelFactory()
return viewModel(factory = NoteDetailViewModel.provideFactory(factory, noteId))
}
Now just go to your navigation components and use this method to provide ViewModel to your Composable screen as following:
NavHost(navController, startDestination = Screen.Notes.route, route = NOTY_NAV_HOST_ROUTE) {
composable(
Screen.NotesDetail.route,
arguments = listOf(navArgument(Screen.NotesDetail.ARG_NOTE_ID) { type = NavType.StringType })
) {
val noteId = it.arguments?.getString(Screen.NotesDetail.ARG_NOTE_ID)!!
NoteDetailsScreen(navController, noteDetailViewModel(noteId))
}
}

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

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.
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)
}
}
}
Unfortunately, HiltViewModelFactory is not a KeyedFactory. So as of now it does not support same viewModel with multiple instances.
Tracking: https://github.com/google/dagger/issues/2328
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.

Pass arguments from fragment to viewmodel function

Can you tell me if my approach is right? It works but I don't know if it's correct architecture. I read somewhere that we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request but I really need to pass arguments from one viewmodel to another one. Important thing is I'm using Dagger Hilt dependency injection so creating factory for each viewmodel isn't reasonable?
Assume I have RecyclerView of items and on click I want to launch new fragment with details - common thing. Because logic of these screens is complicated I decided to separate single viewmodel to two - one for list fragment, one for details fragment.
ItemsFragment has listener and launches details fragment using following code:
fun onItemSelected(item: Item) {
val args = Bundle().apply {
putInt(KEY_ITEM_ID, item.id)
}
findNavController().navigate(R.id.action_listFragment_to_detailsFragment, args)
}
Then in ItemDetailsFragment class in onViewCreated function I receive passed argument, saves it in ItemDetailsViewModel itemId variable and then launch requestItemDetails() function to make api call which result is saved to LiveData which is observed by ItemDetailsFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
val itemId = arguments?.getInt(KEY_ITEM_ID, -1) ?: -1
viewModel.itemId = itemId
viewModel.requestItemDetails()
//...
}
ItemDetailsViewModel
class ItemDetailsViewModel #ViewModelInject constructor(val repository: Repository) : ViewModel() {
var itemId: Int = -1
private val _item = MutableLiveData<Item>()
val item: LiveData<Item> = _item
fun requestItemDetails() {
if (itemId == -1) {
// return error state
return
}
viewModelScope.launch {
val response = repository.getItemDetails(itemId)
//...
_item.postValue(response.data)
}
}
}
Good news is that this is what SavedStateHandle is for, which automatically receives the arguments as its initial map.
#HiltViewModel
class ItemDetailsViewModel #Inject constructor(
private val repository: Repository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val itemId = savedStateHandle.getLiveData(KEY_ITEM_ID)
val item: LiveData<Item> = itemId.switchMap { itemId ->
liveData(viewModelScope.coroutineContext) {
emit(repository.getItemDetails(itemId).data)
}
}
we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request
Yes, in your example a request will be executed whenever ItemDetailsFragment's view is created.
Take a look at this GitHub issue about assisted injection support in Hilt. The point of assisted injection is to pass additional dependencies at object's creation time.
This will allow you to pass itemId through the constructor, which then will allow you to access it in ViewModel's init block.
class ItemDetailsViewModel #HiltViewModel constructor(
private val repository: Repository,
#Assisted private val itemId: Int
) : ViewModel() {
init {
requestItemDetails()
}
private fun requestItemDetails() {
// Do stuff with itemId.
}
}
This way the network request will be executed just once when ItemDetailsViewModel is created.
By the time the feature is available you can either try workarounds suggested in the GitHub issue or simulate the init block with a flag:
class ItemDetailsViewModel #ViewModelInject constructor(
private val repository: Repository
) : ViewModel() {
private var isInitialized = false
fun initialize(itemId: Int) {
if (isInitialized) return
isInitialized = true
requestItemDetails(itemId)
}
private fun requestItemDetails(itemId: Int) {
// Do stuff with itemId.
}
}

Android ViewModelProvider() parameter error

I am trying to get a value from the SharedViewModel class but the ViewModelProvider() is giving a parameter error when i am passing requireActivity() although the same initilization and assignment works in my fragments.
It is requiring "ViewModelStoreOwner" to be passed.
class CourseRepository(val app: Application) {
private var viewModel: SharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
if (Utility.networkAvailable(app)) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(viewModel.pathName).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
}
The purpose of the ViewModel here is because i am storing the Id of the selected RecyclerView item in order to send it to a server
ViewModel instances are scoped to Fragments or Activities (or anything with a similar lifecycle), which is why you need to pass in a ViewModelStoreOwner to the provider to get a ViewModel from it. The point of ViewModels is that they will exist until the store they belong to is destroyed.
The requireActivity method doesn't work here, because you're not inside a Fragment.
Some things to consider here:
Do you really need ViewModel in this use case? Could you perhaps use just a regular class that you can create by calling its constructor?
Could you call this Repository from your ViewModel, and pass in any parameters you need from there?

Edit Text and pass string to the ViewModel?

I'm creating the app uses weather API and I need to get name of the place from Edit Text in UI to the ViewModel and there is val which gets method from Repository. How to correctly communicate with ViewModel in my case? There is some magic spell in LiveData or I need Databinding?
ViewModel:
class MainViewModel(
private val weatherRepository: WeatherRepository
) : ViewModel() {
val metric: String = "metric"
val currentWeatherByCoordinates by lazyDeferred {
weatherRepository.getCurrentWeather() }
val forecastByCoordinates by lazyDeferred {
weatherRepository.getForecast() }
val currentWeatherByCity by lazyDeferred { //This is what I'm writing about
weatherRepository.getCurrentWeatherByCity("London", metric)
}
}
use live data. observe some live data method1() in ui -> change the livedata state in viewmodel by method2(city)

Categories

Resources