I use Clean Architecture, LiveData, Navigation component & Bottom Navigation view.
I am creating a simple application with three tabs. By default, the First tab Fragment loads user data using some API. When i go to another tabs and then return to the First tab Fragment, i see, that observe return a new data!
I need observe not to return data again when I switch back to the first tab! what am I doing wrong? Could you help me please?
P.s. For navigation i use sample from navigation-advanced-sample and after switching tabs onDestroy is not called.
First solution in the article Observe LiveData from ViewModel in Fragment said:
One proper solution is to use getViewLifeCycleOwner() as LifeCycleOwer while observing LiveData inside onActivityCreated as follows.
I use following code, but it's not work for me:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
}
Second solution in the article Architecture Components pitfalls — Part 1 recommends using Resetting an existing observer and Manually unsubscribing the observer in onDestroyView(). But it doesn't work for me either...
ProfileFragment.kt
class ProfileFragment : DaggerFragment() {
#Inject
lateinit var viewModel: ProfileFragmentViewModel
private val observer = Observer<Resource<Profile>> {
when (it.status) {
Resource.Status.LOADING -> {
Timber.i("Loading...")
}
Resource.Status.SUCCESS -> {
Timber.i("Success: %s", it.data)
}
Resource.Status.ERROR -> {
Timber.i("Error: %s", it.message)
}
}
};
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("onCreate")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Timber.d("onCreateView")
return inflater.inflate(R.layout.fragment_profile, container, false)
}
fun <T> LiveData<T>.reObserve(owner: LifecycleOwner, observer: Observer<T>) {
removeObserver(observer)
observe(owner, observer)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.d("onViewCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
// viewModel.getProfileLive().reObserve(viewLifecycleOwner, observer)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
}
override fun onDestroyView() {
super.onDestroyView()
Timber.d("onDestroyView")
// viewModel.getProfileLive().removeObserver(observer)
}
override fun onDestroy() {
super.onDestroy()
Timber.d("onDestroy")
}
override fun onDetach() {
super.onDetach()
Timber.d("onDetach")
}
}
ProfileFragmentViewModel.kt
class ProfileFragmentViewModel #Inject constructor(
private val profileUseCase: ProfileUseCase
) : ViewModel() {
init {
Timber.d("Init profile VM")
}
fun getProfileLive() = profileUseCase.getProfile()
}
ProfileUseCase
class ProfileUseCase #Inject constructor(
private val profileRepository: ProfileRepository
) {
fun getProfile(): LiveData<Resource<Profile>> {
return profileRepository.getProfile()
}
}
ProfileRepository.kt.
class ProfileRepository #Inject constructor(
private val loginUserDao: LoginUserDao,
private val profileDao: ProfileDao,
) {
fun getProfile(): LiveData<Resource<Profile>> =
liveData(Dispatchers.IO)
{
emit(Resource.loading(data = null))
val profile = profileDao.getProfile()
// Emit Success result...
}
}
It's because of how Fragment Lifecycle works. When you move to and fro from a fragment onViewCreated() is called again. In onViewCreated you're calling viewModel.getProfileLive() which returns the livedata upto from the repository and observe to it.
Since onViewCreated() gets called everytime when you move back to the Fragment so is your call to viewModel.getProfileLive() and in turn the repository gets called again which again triggers the observe method in your Fragment.
In order to solve this problem,
create a LiveData variable in your ViewModel, set it to the returned Live Data from Repository.
In the Fragment observe to the LiveData variable of your ViewModel not the one returned from Repository.
That way, your observe method will get triggered on very first time and only when value of your data from repository changes.
Related
I have a view model that is data binded to a fragment. The view model is shared with the main activity.
I've button is binded to the view as follows:
<Button
android:id="#+id/startStopBtn"
android:text="#{dashboardViewModel.startStopText == null ? #string/startBtn : dashboardViewModel.startStopText}"
android:onClick = "#{() -> dashboardViewModel.onStartStopButton(context)}"
android:layout_width="83dp"
android:layout_height="84dp"
android:layout_gravity="center_horizontal|center_vertical"
android:backgroundTint="#{dashboardViewModel.isRecStarted == false ? #color/startYellow : #color/stopRed}"
tools:backgroundTint="#color/startYellow"
android:duplicateParentState="false"
tools:text="START"
android:textColor="#FFFFFF" />
What I expect to happen is that every time I press the button the function onStartStopButton(context) runs. This works fine as long as I don't rotate the device. When I rotate the device the function is run twice, if I rotate again the function is run 3 times and so on. This is not a problem if I go to another fragment and then back to the dashboard fragment. It looks like the live data observer is getting registered every time I rotate my screen, but not every time I detach and reattach the fragment.
This is true for all the elements in that fragment, whether they are data binded or I manually observe them.
Fragment code:
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private val dashboardViewModel: DashboardViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
val root: View = binding.root
binding.dashboardViewModel = dashboardViewModel
binding.lifecycleOwner = viewLifecycleOwner
dashboardViewModel.bleSwitchState.observe(viewLifecycleOwner, Observer { switchState -> handleBleSwitch(switchState) })
dashboardViewModel.yLims.observe(viewLifecycleOwner, Observer { yLims ->
updatePlotWithNewData(yLims.first, yLims.second)
})
Timber.i("Dahsboard on create: DashboardViewModel in fragment: $dashboardViewModel")
return root
}
}
The view model:
class DashboardViewModel : ViewModel() {
//region live data
private var _isRecStarted = MutableLiveData<Boolean>()
val isRecStarted: LiveData<Boolean> get() = _isRecStarted
//private var _bleSwitchState = MutableLiveData<Boolean>()
val bleSwitchState = MutableLiveData<Boolean>()
private var _startStopText = MutableLiveData<String>()
val startStopText: LiveData<String> get() = _startStopText
private var _yLims = MutableLiveData<Pair<kotlin.Float,kotlin.Float>>()
val yLims: LiveData<Pair<kotlin.Float,kotlin.Float>> get() = _yLims
//endregion
init {
Timber.d("DashboardViewModel created!")
bleSwitchState.value = true
}
//region start stop button
fun onStartStopButton(context: Context){
Timber.i("Start stop button pressed, recording data size: ${recordingRawData.size}, is started: ${isRecStarted.value}")
isRecStarted.value?.let{ isRecStarted ->
if (!isRecStarted){ // starting recording
_isRecStarted.postValue(true)
_startStopText.postValue(context.getString(R.string.stopBtn))
startDurationTimer()
}else{ // stopping recording
_isRecStarted.postValue(false)
_startStopText.postValue(context.getString(R.string.startBtn))
stopDurationTimer()
}
} ?: run{
Timber.e("Error! Is rec started is not there for some reason")
}
}
}
The view model is created the first time from the MainActivity as follows:
class MainActivity : AppCompatActivity() {
private val dashboardViewModel: DashboardViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("DashboardViewModel in main activity: $dashboardViewModel")
}
}
Edit explaining why the MainActivity is tided to the ViewModel:
The reason why the ViewModel is linked to the main activity is that the main activity handles some Bluetooth stuff for a stream of data, when a new sample arrives then the logic to handle it and update the UI of the dashboard fragment is on the DashboardViewModel. The data still needs to be handled even if the dashboard fragment is not there.
So I need to pass the new sample to the DashboardViewModel from the main activity as that is where I receive it. Any suggestions to make this work?
As you know, when you instantiate the ViewModel of a Fragment with activityViewModels, it means that the ViewModel will follow the lifecycle of the Activity containing that Fragment. Specifically here is MainActivity.
So what does ViewModel tied to Activity lifecycle mean in your case?
When you return to the Fragment, normally LiveData (with ViewModel attached to Fragment lifcycler) will trigger again.
But when that ViewModel is attached to the Activity's lifecycle, the LiveData will not be triggered when returning to the Fragment.
That leads to when you return to the Fragment, your LiveData doesn't trigger again.
And that LiveData only triggers according to the life cycle of the activity. That is, when you rotate the screen, the Activity re-initializes, now your LiveData is triggered.
EDIT:
Here, I will give you one way. Maybe my code below doesn't work completely for your case, but I think it will help you in how to control LiveData and ViewModel when you bind ViewModel to Activity.
First, I recommend that each Fragment should have its own ViewModel and it should not depend on any other Fragment or Activity. Here you should rename the DashboardViewModel initialized by activityViewModels() as ShareViewModel or whatever you feel it is related to this being the ShareViewModel between your Activity and Fragment.
class DashboardFragment : Fragment() {
// Change this `DashboardViewModel` to another class name. Could be `ShareViewModel`.
private val shareViewModel: ShareViewModel by activityViewModels()
// This is the ViewModel attached to the DashboardFragment lifecycle.
private val viewModel: DashboardViewModel by viewModels()
private lateinit var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.dashboardViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
Next, when there is data triggered by the ShareViewModel's LiveData, you will set the value for the LiveData in the ViewModel associated with your Fragment. As follows:
DashboardViewModel.kt
class DashboardViewModel: ViewModel() {
private val _blueToothSwitchState = MutableLiveData<YourType>()
val blueToothSwitchState: LiveData<YourType> = _blueToothSwitchState
private val _yLims = MutableLiveData<Pair<YourType, YourType>>()
val yLims: LiveData<Pair<YourType, YourType>> = _blueToothSwitchState
fun setBlueToothSwitchState(data: YourType) {
_blueToothSwitchState.value = data
}
fun setYLims(data: Pair<YourType, YourType>) {
_yLims.value = data
}
}
DashboardFragment.kt
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.run {
bleSwitchState.observe(viewLifeCycleOwner) {
viewModel.setBlueToothSwitchState(it)
}
yLims.observe(viewLifeCycleOwner) {
viewModel.setYLims(it)
}
}
viewModel.run {
// Here, LiveData fires observe according to the life cycle of `DashboardFragment`.
// So when you go back to `DashboardFragment`, the LiveData is re-triggered and you still get the observation of that LiveData.
blueToothSwitchState.observe(viewLifeCycleOwner, ::handleBleSwitch)
yLims.observe(viewLifeCycleOwner) {
updatePlotWithNewData(it.first, it.second)
}
}
}
...
}
Edit 2:
In case you rotate the device, the Activity and Fragment will be re-initialized. At that time, LiveData will fire observe. To prevent that, use Event. It will keep your LiveData from observing the value until you set the value again for LiveData.
First, let's create a class Event.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? = if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
fun peekContent(): T = content
}
Next, modify the return type of the LiveData that you want to trigger once.
ShareViewModel.kt
class ShareViewModel: ViewModel() {
private val _test = MutableLiveData<Event<YourType>>()
val test: LiveData<Event<YourType>> = _test
fun setTest(value: YourType) {
_test.value = Event(value)
}
}
Add this extension to easily get LiveData's observations.
LiveDataExt.kt
fun <T> LiveData<Event<T>>.eventObserve(owner: LifecycleOwner, observer: (t: T) -> Unit) {
this.observe(owner) { it?.getContentIfNotHandled()?.let(observer) }
}
Finally in the view, you get the data observed by LiveDatat.
class DashboardFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shareViewModel.test.eventObserve(viewLifeCycleOwner) {
Timber.d("This is test")
}
}
...
}
Note: When using LiveData with Event, make sure that LiveData is not reset when rotating the device. If LiveData is set to value again, LiveData will still trigger even if you use Event.
I am a newbie Android developer, and I am trying to observe a boolean set in the ViewModel from its parent's activity. I can observe its initial state as soon as the app launches, but any change applied later on doesn't seem to trigger the observer (i.e. when I switch the fragments).
Here is the code for my ViewModel:
class MyMusicViewModel : ViewModel() {
private var _MyMusicViewOn = MutableLiveData<Boolean>()
val MyMusicViewOn: LiveData<Boolean> get() = _MyMusicViewOn
init {
Timber.i("MyMusicViewModel Init Called!")
setMyMusicView(true)
}
override fun onCleared() {
super.onCleared()
Timber.i("MyMusicViewModel Cleared!")
setMyMusicView(false)
}
fun setMyMusicView(setter: Boolean) {
Timber.i("MyMusicViewModel setter called! %s", setter)
_MyMusicViewOn.value = setter
}
}
And here is its parent's activity:
class FullscreenActivity : AppCompatActivity() {
private val viewModel: MyMusicViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.MyMusicViewOn.observe(this, Observer { MyMusicViewOn ->
Timber.i("Observer called for MyMusicViewOn %s", MyMusicViewOn)
})
}
}
And in case you wanna see the ViewModel's related fragment, here it is:
class MyMusicFragment : Fragment() {
private lateinit var viewModel: MyMusicViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding = DataBindingUtil.inflate<FragmentMyMusicBinding>(
inflater,
R.layout.fragment_my_music,
container,
false
)
viewModel = ViewModelProvider(this).get(MyMusicViewModel::class.java)
return binding.root
}
override fun onResume() {
super.onResume()
Timber.i("MyMusicViewFragment resumed!")
viewModel.setMyMusicView(true)
}
}
What I am trying to accomplish is to observe the onResume(), onCleared() and init{} functions whenever they are called by changing the status of the MyMusicViewOn MutableLiveData Boolean. What I don't understand is why that boolean doesn't trigger the observer set in the parent activity whenever it changes.
Thankyou in advance for any thoughts!
All the best,
Fab.
I'm guessing that however you are populating that viewModel property in your Fragment, you are not using the Activity's ViewModel instance. The easiest way to get the same instance that the Activity is using would be to use the activityViewModels delegate:
private val viewModel: MyMusicViewModel by activityViewModels()
In my fragment, I have this code:
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
viewModel.state.observe(viewLifecycleOwner) {
//do something
}
}
And in my ViewModel:
class MyViewModel: ViewModel() {
val state = liveData {
val state = dataSource.getState()
emit(state)
}
}
When I navigate to another fragment or activity, and press back button, Fragment's onCreateView and onViewCreated methods are called, but viewModel.state has the same value. I mean, dataSource.getState() is not called again. I need state to be re-fetched from data source.
Is this possible using liveData builder? If not, how should I do it?
You need just cal load function every time when it needed. One of possible way to do it
ViewModel :
val stateLiveData = MutableLiveData<>()
fun loadData() {
viewModelScope.launch {
val state = dataSource.getState()
stateLiveData.setValue(state)
}
}
Fragment :
fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
viewModel.loadData()
viewModel.stateLiveData.observe(viewLifecycleOwner) {
//do something
}
}
I looked for many articles and tried to understand how Live Data is observe changes when MVVM architecture is used.
I have a Fragment A, ViewModel and Repository class.
ViewModel is initiated in onCreateView() method of the fragment.
Api call is initiated just after that in onCreateView() method of fragment.
Data from the Server is observed in onViewCreated method of the fragment.
For the first, it is running perfectly fine. But When I update the user name from another Fragment B and come back to Fragment A.
Api is called again in onResume() method of Fragment A to update UI. But here my Live Data is not observed again and UI is not updated
I didn't understand what I am doing wrong? Why observer is not triggering second time?
Below is the code
class FragmentA : Fragment(){
private lateinit var dealerHomeViewModel: DealerHomeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home_dealers, container, false)
val dealerHomeFactory = DealerHomeFactory(token!!)
dealerHomeViewModel = ViewModelProvider(this,dealerHomeFactory).get(DealerHomeViewModel::class.java)
dealerHomeViewModel.getDealerHomeData()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dealerHomeViewModel.dealerInfoLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {dealerInfo ->
// Update UI
tvDealerName.text = dealerInfo.name
})
}
override fun onResume() {
super.onResume()
dealerHomeViewModel.getDealerHomeData()
}
}
//=========================== VIEW MODEL ===================================//
class DealerHomeViewModel(val token:String) : ViewModel() {
var dealerInfoLiveData:LiveData<DealerInfo>
init {
dealerInfoLiveData = MutableLiveData()
}
fun getDealerHomeData(){
dealerInfoLiveData = DealerHomeRepo().getDealerHomePageInfo(token)
}
}
//======================== REPOSITORY ================================//
class DealerHomeRepo {
fun getDealerHomePageInfo(token:String):LiveData<DealerInfo>{
val responseLiveData:MutableLiveData<DealerInfo> = MutableLiveData()
val apiCall: ApiCall? = RetrofitInstance.getRetrofit()?.create(ApiCall::class.java)
val dealerInfo: Call<DealerInfo>? = apiCall?.getDealerInfo(Constants.BEARER+" "+token,Constants.XML_HTTP)
dealerInfo?.enqueue(object : Callback<DealerInfo>{
override fun onFailure(call: Call<DealerInfo>, t: Throwable) {
Log.d(Constants.TAG,t.toString())
}
override fun onResponse(call: Call<DealerInfo>, response: Response<DealerInfo>) {
if(response.isSuccessful){
when(response.body()?.status){
Constants.SUCCESS -> {
responseLiveData.value = response.body()
}
Constants.FAIL -> {
}
}
}
}
})
return responseLiveData
}
}
I think your problem is that you are generating a NEW mutableLiveData each time you use your getDealerHomePageInfo(token:String method.
First time you call getDealerHomePageInfo(token:String) you generate a MutableLiveData and after on onViewCreated you observe it, it has a value.
In onResume, you call again getDealerHomePageInfo(token:String) that generates a NEW MutableLiveData so your observer is pointing to the OLD one.
What would solve your problem is to pass the reference of your viewModel to your repository so it updates the MutableLiveData with each new value, not generate a new one each time.
Edited Answer:
I would do something like this for ViewModel:
class DealerHomeViewModel(val token:String) : ViewModel() {
private val _dealerInfoLiveData:MutableLiveData<DealerInfo> = MutableLiveData()
val dealerInfoLiveData:LiveData = _dealerInfoLiveData
fun getDealerHomeData(){
DealerHomeRepo().getDealerHomePageInfo(token, _dealerInfoLiveData)
}
}
And this for the DealerHomeRemo
class DealerHomeRepo{
fun getDealerHomePageInfo(token:String, liveData: MutableLiveData<DealerInfo>){
val apiCall: ApiCall? = RetrofitInstance.getRetrofit()?.create(ApiCall::class.java)
val dealerInfo: Call<DealerInfo>? = apiCall?.getDealerInfo(Constants.BEARER+" "+token,Constants.XML_HTTP)
dealerInfo?.enqueue(object : Callback<DealerInfo>{
override fun onFailure(call: Call<DealerInfo>, t: Throwable) {
Log.d(Constants.TAG,t.toString())
}
override fun onResponse(call: Call<DealerInfo>, response: Response<DealerInfo>) {
if(response.isSuccessful){
when(response.body()?.status){
Constants.SUCCESS -> {
liveData.value = response.body()
}
Constants.FAIL -> {
}
}
}
}
})
}
For Observers, use the LiveData as before:
dealerHomeViewModel.dealerInfoLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {dealerInfo ->
// Update UI
tvDealerName.text = dealerInfo.name
})
I have an activity, TabBarActivity that hosts a fragment, EquipmentRecyclerViewFragment. The fragment receives the LiveData callback but the Activity does not (as proofed with breakpoints in debugging mode). What's weird is the Activity callback does trigger if I call the ViewModel's initData method. Below are the pertinent sections of the mentioned components:
TabBarActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initVM()
setContentView(R.layout.activity_nav)
val equipmentRecyclerViewFragment = EquipmentRecyclerViewFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frameLayout, equipmentRecyclerViewFragment, equipmentRecyclerViewFragment.TAG)
.commit()
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
}
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, loadingObserver)// eVM?.initData() //TODO: Not calling this causes Activity to never receive the observed ∆
}
val loadingObserver = Observer<List<Gun>> { equipment ->
...}
EquipmentRecyclerViewFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
columnCount = 2
initVM()
}
//MARK: ViewModel Methods
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, equipmentObserver)
eVM?.initData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_equipment_list, container, false)
if (view is RecyclerView) { // Set the adapter
val context = view.getContext()
view.layoutManager = GridLayoutManager(context, columnCount)
view.adapter = adapter
}
return view
}
EquipmentViewModel
class EquipmentViewModel(application: Application) : AndroidViewModel(application), LifecycleObserver {
var equipment = MutableLiveData<List<Gun>>()
var isLoading = MutableLiveData<Boolean>()
fun initData() {
isLoading.setValue(true)
thread { Thread.sleep(5000) //Simulates async network call
var gunList = ArrayList<Gun>()
for (i in 0..100){
gunList.add(Gun("Gun "+i.toString()))
}
equipment.postValue(gunList)
isLoading.postValue(false)
}
}
The ultimate aim is to have the activity just observe the isLoading MutableLiveData boolean, but since that wasn't working I changed the activity to observe just the equipment LiveData to minimize the number of variables at play.
To get same reference of ViewModel of your Activity you need to pass the same Activity instance, you should use ViewModelProviders.of(getActivity). When you pass this as argument, you receive instance of ViewModel that associates with your Fragment.
There are two overloaded methods:
ViewModelProvider.of(Fragment fragment)
ViewModelProvider.of(FragmentActivity activity)
For more info Share data between fragments
I put this code inside the onActivityCreated fragment, don't underestimate getActivity ;)
if (activity != null) {
globalViewModel = ViewModelProvider(activity!!).get(GlobalViewModel::class.java)
}
globalViewModel.onStop.observe(viewLifecycleOwner, Observer { status ->
Log.d("Parent Viewmodel", status.toString())
})
This code helps me to listening Parent ViewModel changes in fragment.
Just for those who are confused between definitions of SharedViewModel vs Making two fragments use one View Model:
SharedViewModel is used to share 'DATA' (Imagine two new instances being created and data from view model is being send to two fragments) where it is not used for observables since observables look for 'SAME' instance to take action. This means you need to have one viewmodel instance being created for two fragments.
IMO: Google should somehow mention this in their documentation since I myself thought that under the hood they are same instance where it is basically not and it actually now makes sense.
EDIT : Solution in Kotlin: 11/25/2021
In Your activity -> val viewModel : YourViewModel by viewModels()
In Fragment 1 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
In Fragment 2 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
This Way 2 fragments share one instance of Activity viewmodel and both fragments can use listeners to observe changes between themselves.
When you create fragment instead of getting viewModel object by viewModels() get it from activityViewModels()
import androidx.fragment.app.activityViewModels
class WeatherFragment : Fragment(R.layout.fragment_weather) {
private lateinit var binding: FragmentWeatherBinding
private val viewModel: WeatherViewModel by activityViewModels() // Do not use viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentWeatherBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
// Observing for testing & Logging
viewModel.cityName.observe(viewLifecycleOwner, Observer {
Log.d(TAG, "onCreateView() | City name changed $it")
})
return binding.root
}
}
Kotlin Answer
Remove these two points in your function if you are using:
= viewModelScope.launch { }
suspend