26

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.

James Jordan Taylor
  • 1,560
  • 3
  • 24
  • 38
  • Try to pass ViewModelProviders.of(getActivity) in fragment istead of "this" – Pavel Poley Feb 12 '18 at 15:03
  • That did it! I changed my fragment `initVM()` code to `val myActivity = activity ?: return eVM = ViewModelProviders.of(myActivity).get(EquipmentViewModel::class.java)` and it triggered both. If you could explain why it works in an answer I'll mark it accepted & upvote. – James Jordan Taylor Feb 12 '18 at 15:10
  • How about using `viewLifecycleOwner`? – IgorGanapolsky Feb 25 '20 at 16:47

5 Answers5

44

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

Pavel Poley
  • 5,307
  • 4
  • 35
  • 66
  • I thought the point was there is only ever one ViewModel? Why is it a new instance? – Elliptica Jun 07 '18 at 18:07
  • The point is only ever one `ViewModel` per component, per `Fragment` or pet `Activity` – Pavel Poley Apr 12 '21 at 19:21
  • Thanks! I was going crazy with it. In my case I was using `viewModelStore` in the fragment, so all I needed to do was take it from the activity with `requireActivity().viewModelStore`. – Sergi Mascaró Sep 22 '21 at 09:25
3

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.

iamkdblue
  • 3,448
  • 2
  • 25
  • 43
3

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.

2

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
    }
}
Manisha
  • 813
  • 13
  • 24
0

Kotlin Answer

Remove these two points in your function if you are using:

  1. = viewModelScope.launch { }
  2. suspend
canerkaseler
  • 6,204
  • 45
  • 38