Shared ViewModels in modules - android

I have an Activity with multiple fragments under it. I want to share data with the Shared ViewModel, but they are all in different Modules. I can't get the ActivityViewModel Class. When I received activityViewModels() using CoreViewModel, the parent of ActivityViewModel, I got an object that was different from what I received with ActivityViewModel,How can I Share ViewModel in multiple Modules
This is the log I printed after I moved them to the same Module,But that's not the way I wanted it done
In MainActivity
extend ->MainViewModel:CoreViewModel:ViewModel
val mV0 by viewModels<MainViewModel>()
LogD("mViewModel0 = $mV0")
In MineFragment(The Fragment is MainActivity child,To get MainViewModel.class I moved MineFragment to the same module as MainActivity)
val mV1 by activityViewModels<MainViewModel>()
val mV2 by activityViewModels<CoreViewModel>()
LogD("mViewModel1 = $mV1")
LogD("mViewModel2 = $mV2")
Log info
mViewModel0 = com.nf.bitcoinexchange.activities.main.MainViewModel#6505362
mViewModel1 = by activityViewModels<MainViewModel>()= com.nf.bitcoinexchange.activities.main.MainViewModel#6505362
mViewModel2 = by activityViewModels<CoreViewModel>()= com.nf.corelib.viewmodel.CoreViewModel#3290057
The translation software I use may be a little unclear... sorry

Related

Android different ways to create viewModel object which one to use when?

I recently started with the ViewModel and AndroidViewModel, I see there are different approach to initialise a viewModel instance, for me all works fine, I just want to know which one to use when? and where should I initialise the viewModel object? following all are the different approach to get the viewModel instance and works for me:
val myViewModel1 = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(MyViewModel::class.java)
val myViewModel2 = ViewModelProvider.AndroidViewModelFactory(this.application).create(MyViewModel::class.java)
val myViewModel3 = ViewModelProvider(this).get(MyViewModel::class.java)
val myViewModel4: MyViewModel by viewModels()
val myViewModel5 by viewModels<MyViewModel>()
The easiest and most simple for me are 3rd, 4th and 5th, however I don't know what is the difference in all the five approaches, also please let me know if there any other way or optimal way to initialise my viewModel object, I do the initialisation on the global variable while declaring it, is it okay to initialise at the declaration time or it should be done inside some lifecycle method?
In case anyone looking for in depth answer, please check this, here we have the following way to create or get the viewModel object:
val myViewModel1 = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(MyViewModel::class.java)
myViewModel2 = ViewModelProvider.AndroidViewModelFactory(this.application).create(MyViewModel::class.java)
val myViewModel3 = ViewModelProvider(this).get(MyViewModel::class.java)
val myViewModel4: MyViewModel by viewModels()
val myViewModel5 by viewModels<MyViewModel>()
All do the same thing, the only two key differences is:
The viewModel initialisation with lazy loading and without lazy loading.
The viewModel with multiple parameter and no parameters.
Lets see this wrt the lazy loading and without lazy loading, the first three are without the delegate by that means there is no lazy loading of that object, so it's the developer
responsibility to create the viewModel object only when activity is created or the fragment is attached to the activity, that means the first three approach(1, 2, 3) can't be
used at global scope, if used at global scope the variable must be
a var with lateint or null initialisation, and the
initialisation(approach 1, 2, 3) must happen in the onCreate or
onViewCreated(in case of fragment).
Therefor the best way to create the viewModel object is using the delegate by(4, 5), both are same with a bit different syntax, I choose 4 because of it's simplicity and readability.
val myViewModel4: MyViewModel by viewModels()
The by delegate gives the flexibility to lazy load the instance and you can define the viewModel at global scope and get ride off the boilerplate code, if you try to initialise the viewModel at global scope without the delegate the app will crash since the viewModel will try to initialise before the activity is created(it will not lazy load the viewModel instance).
Now let's see how to lazy load with multiple parameters, the 6th approach not mention in the question.
If you have multiple parameters in your view model and not using any dependency injection, you can use a ViewModelFactory implementation and then lazy load it:
val myViewModelWithParm: MyViewModel by viewModels { MyViewModelFactory(application, "param1", "param2") }
ViewModelFactory implementation:
class MyViewModelFactory(val application: Application, val param1: String, val param2: String) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MyViewModel(application, param1, param2) as T
}
}
Till this point we are clear on the delegate initialisation(4, 5), and how it is different with(1, 2, 3) now let's see the difference on the top 3 approach(1, 2, 3).
Let's first check 1 and 2.
val myViewModel1 = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(MyViewModel::class.java)
myViewModel2 = ViewModelProvider.AndroidViewModelFactory(this.application).create(MyViewModel::class.java)
The key difference in them is one uses ViewModelProvider.NewInstanceFactory and other uses ViewModelProvider.AndroidViewModelFactory, so I checked the source code of both the classes and found that ViewModelProvider.AndroidViewModelFactory is actually the implementation of ViewModelProvider.NewInstanceFactory which override the create function that means both are doing the same stuff, preferable both approach should be chosen if we want multiple parameters however for that we have to override ViewModelProvider.NewInstanceFactory to create our own factory like it's done here
Now comes the third one:
val myViewModel3 = ViewModelProvider(this).get(MyViewModel::class.java)
This is the simple form of 1 and 2 when we don't have multiple parameters in our ViewModel and don't want to lazy load the object.
Note: I highly recommend the approach 4 or 5(both are same with different syntax), since this is the most suitable and optimal to write, if you don't have multiple arguments, in case you have multiple arguments you can use the approach 6 mentioned in the answer by implementing ViewModelProvider.Factory.
3 is the standard way to fetch (and create if necessary) a ViewModel with no constructor parameters. Internally, that's doing 1 to pass a factory that calls the empty constructor (NewInstanceFactory()).
An AndroidViewModel is a subclass of ViewModel that automatically passes in an application reference, in case you need access to things like the application context. So even if your AndroidViewModel has no parameters, the factory that creates it needs to pass in an application, which is what 2 is doing.
This is all taken care of for you by default using 3 - you only need to define and use a factory if your VM needs to be configured with some extra parameters.
4 and 5 are the same thing, just with the type specified in a different place (you only need one declaration and the other will be inferred). They're delegates from the KTX libraries, and they do the same thing as 3, but they're much more readable IMO - especially if you're mixing scopes, like using by viewModels to get a Fragment's own VM, and also by activityViewModels to get the Activity's VM to share data with that and other Fragments.
They're also lazy delegates (as far as I'm aware!) meaning the VM only gets instantiated when it's first accessed, which is generally going to happen later in the lifecycle (instead of when the object is first constructed). I'm not sure if there is a problem initialising the VM on construction, but all of the official examples I've seen seem to fetch it in onCreate (or thereabouts)

Pass value of a MutableLiveData of ViewModels

I'm very very new to Android and I'm trying to understand/integrate Data Binding.
I think I'm missing something since it always displays null
Here is my ViewModel
class LockDetailsModel: ViewModel() {
var name = MutableLiveData<String>()
fun setName(name: String) {
this.name.value = name
}
In my LockActivity, I set the value of name based on a certain field.
val viewModel: LockDetailsModel by viewModels()
viewModel.setName(unitName.toString())
And in my other activity, which displays the layout, this is what I did to bind the model
val viewModel: LockDetailsModel by viewModels()
binding: ActivityConnectLockBinding = DataBindingUtil.setContentView(this, R.layout.activity_connect_lock)
binding.lockDetails = viewModel
binding.lifecycleOwner = this
It displays null :(
Thanks in advance for your help!
The reason behind you getting null value is because you are using different viewModel instances in both the activities , thus the value stored by you in first Activity cannot be obtained in the Second activity . You need to have SharedViewModel between both the activities , which is not possible due to Single Activity principle . So to implement logic that you want to implement you need to have Fragments and make use of fragment-ktx library to easily create SharedViewModel .If you want an example you can refer to my answer here :
Want to show progress bar while moving to the previous page from the current page
The above answer describes how to get progress of a download started in one activity into another .

Using Flow to "talk" between service and fragment

After more than 2 years, I am "updating" myself with android/kotlin changes, and boy, has it changed a lot.
Scenario
I have a main activity with MyFragment and a MyFragmentViewModel
I have a foreground service MyService
I have a repository that has a Flow<MyState> which should be collected by both MyFragmentViewModel and MySerice
Basically in the past, when I wanted to communicate between a not exported service and the main activity I've used LocalBroadCastReceiver which worked really well and removed the tight coupling between the two. Now that is deprecated so I thought why not have in the Repository a Flow that gets collected whenever it changes, so any client can react to changes.
Here is, for the sake of simplicity, some basic code related
enum class MyState{
STATE_LOADING,
STATE_NORMAL,
....
}
class MyRepository(){
//for simplicity there is no private immutable _state for now
val state:MutableStateFlow<MyState> = MutableStateFlow(MyState.STATE_NORMAL)
fun updateState(newState: MyState){
state.value = newState
}
}
class MyFragmentViewModel #Inject constructor(
private val myRepository: MyRepository
): ViewModel(){
fun updateCurrentState(){
myRepository.updateState(MyState.STATE_LOADING)
}
}
#AndroidEntryPoint
class MyService:Service(){
#Inject lateinitvar myRepository: MyRepository
private val myJob = SupervisorJob()
private val myServiceScope = CoroutineScope(Dispachers.IO+myJob)
fun listenForState(){
myServiceScope.launch{
myRepository.state.collect{
when(it)
....
}
}
}
}
What happens is that on starting, the collect in MyService does get the initial value STATE_NORMAL but when from MyFragmentViewModel I update the MyRepository state, this value is not received by the service.
My questions:
what am I doing wrong? Is something related to service scope/coroutines and how collect works?
is this a good approach, architecturally speaking or are there better way to do it?
Your Services should never communicate with the Repository , since it should come under the UI Module and thus it must communicate to the ViewModel which further communicates to the Repository .
You can read my answer on MVVM pattern here :
Is this proper Android MVVM design?
. I have explaind the MVVM pattern here .
Also for your specific useCase , I recommend you to check this github - project :
https://github.com/mitchtabian/Bound-Services-with-MVVM
In the ReadMe section there is a link to a Youtube video which will explain you in depth about how to use Services with MVVM .
Also in your code , you have made use of enum classes which is not wrong , but since you are using you can make use of Sealed Classes , which is built on top of Enums and provides to maintain strict hierarchy .Your enum class in the form of Sealed Class will look in the following manner :
sealed class MyState{
object State_Loading : MyState()
object State_Normal : MyState()
}
And for you issue about not able to update the data , I suggest you to try
fun updateState(newState: MyState){
state.emit( newState)
}
If this does not work , you need to debug at every step from where the data passes using Log and know where is the error taking place

How to separate data handling from Activity

I have a working Activity (TwalksRouteActivity) that accepts a record id (routeID) from a bundle (passed from a Fragment), pulls the associated record from my repository (routesRepository), and passes an associated value/column (routeName) to my UI. This works fine. However, as I understand best practice (I am learning Android development), the call to my Repository should be in a ViewModel, not an Activity. Is this correct? I have tried but failed to do this myself and would really appreciate some help in how to do this please.
TwalksRouteActivity:
class TwalksRouteActivity() : AppCompatActivity() {
private lateinit var viewModel: RouteViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Log.i("CWM","Called ViewModelProvider")
//viewModel = ViewModelProvider(this).get(RouteViewModel::class.java)
var bundle: Bundle? = intent.extras
var routeID = bundle?.getInt("routeID")
lifecycleScope.launch (Dispatchers.Main) {
val database = getDatabase(application)
val routesRepository = RoutesRepository(database)
val selectedRoute = routesRepository.getRoute(routeID)
val routeName = selectedRoute.routeName
Log.d("CWM", routeName.toString())
setContentView(R.layout.route_detail)
val routeName_Text: TextView = findViewById(R.id.routeName_text)
routeName_Text.text = routeName.toString()
val routeID_Text: TextView = findViewById(R.id.routeID)
routeID_Text.text = routeID.toString()
}
}
}
You are correct. Best practices include the idea of a ViewModel that handles communications between bussiness logic (your repository) and the activity or fragment which uses or/and dislpays the data. You should check Android Developers ViewModel's official documentation at: ViewModel Overview. Also the guide to app architecture. Check the following image:
As you can see, it describes the data-driven communication flow, and as you said, the ViewModel will call the repository functions that get the data. The ViewModel will then provide the activity with variables and / or functions that can be observed (such as: LiveData), and fire events that the activity will take to make its state changes / data presentation in the UI (this is call reactive pattern).
You should check these Codelabs (free lessons from Google): Incorporate Lifecycle-Aware Components and Android Room with a View - Kotlin (although it mainly covers Room Library, the codelab makes use of ViewModel and Android's best practices recommended by Google). Also, you could check this article: ViewModels and LiveData: Patterns + AntiPatterns.
I could write a lot of code but I think it is beyond the scope of this answer. I'm also learning, and my way was to first understand how these things work and why these things are called "best practices".

Koin: How to get the same Instance accoding to a given key

I used ViewModelProvider(this).get(myDataIdentifier, MyViewModel::class.java) to get the same viewmodel for each identifier.
Now I want to use Koin for dependency injection but I can't figure out how to get this working.
I can inject data via val viewModel by viewModel() but where am I able to make sure to get the same instance, identified by myDataIdentifier? I can't wrap my head around qualifier, parameter,....
Sorry, maybe this is a dumb question and i just overlooked something.
Try with named components, name your viewmodel (this may now need to be a singleton?)
val myModule = module {
viewModel(named("myViewModel")) { MyViewModel() }
}
...
val viewModel: MyViewModel by viewModel(named("myViewModel"))
https://engineering.bigshyft.com/koin-2-0-for-android/

Categories

Resources