Fragments observing livedata forever - android

The problem: I'm facing a problem with fragments and shared viewmodel LiveData ... The problem that is there is a FragmentA that update data in shared viewmodel and observe for it's changes then display the result for the user inside FragmentA, FragmentA can launch new instance of FragmentA to get new data and display it so the fragment launch it self and old instance gets added to the back stack, until here nothing is wrong the new instance updates LiveData in viewModel and displays the new data perfectly the problem that is when i popUpBackstack() return to FragmentA old instance the data displays in it is the data that new FragmentA instance gets it which means that old FragmentA instance still observing the data even if i remove the observers ... this is general overview about the problem now i will show you fragments structure, the code and what the solution's that i'v tried.
Expected behavior: what i want to achieve is that when FragmentA launch it self and gets added to back stack stop observing the data in viewModel and when i back to it displays the old data that's all.
Fragment structure: i use one activity to hold all the fragments ... MainActivity have FindMoviesFragment inside it there is a viewPager which holds FragmentMovies which launches MovieDetailsFragment and inside it there is ViewPager also which holds the fragments that displays the data from MoviesViewModel it will get clear when you see the code below.
This code shows how MovieDetailsFragment initialize MoviesViewModel and updates data in viewmodel:
class MovieDetailsFragment:Fragment(R.layout.movie_details_fragment) {
private val args: MovieDetailsFragmentArgs by navArgs()
private val fragmentList:ArrayList<Fragment> by lazy {
arrayListOf(MovieDetailsOneFragment(args.movieId), MovieDetailsTwoFragment(),MovieDetailsThreeFragment())
}
private lateinit var pagerAdapter: ViewPagerAdapter
private lateinit var moviesViewModel: MoviesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//these two lines updates the data in viewmodel
moviesViewModel.getMovieDetails(args.movieId)
moviesViewModel.changeMovieID(args.movieId)
}
}
//---------------------MoviesViewModel------------------//
class MoviesViewModel constructor(private val repo:Repository):ViewModel() {
private val _currentMovieDetails = MutableLiveData<Resource<MovieDetails>>()
val currentMovieDetails :LiveData<Resource<MovieDetails>>
get() = _currentMovieDetails
fun getMovieDetails(movieID:Int) = viewModelScope.launch {
_currentMovieDetails.postValue(Resource.loading(null))
val result = repo.getMovieDetails(movieID)
if(result.isSuccessful){
_currentMovieDetails.postValue(Resource.success(result.body()))
}
else{
_currentMovieDetails.postValue(Resource.error(result.errorBody().toString(),null))
}
}
}
And inside MovieDetailsOne which is inside viewpager in MovieDetailsFragment i observe the data like this:
class MovieDetailsOneFragment(private val movieId: Int):Fragment(R.layout.movie_details_one) {
private lateinit var moviesViewModel:MoviesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//this is how i define viewModel(global scope)
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//in this method i observe on changes in currentMovieDetils liveData
subscribeToObservers()
//i try to call this from on create and nothing changes too
}
}
Now What i tried is the following:
-Loacal scope for fragments
//Define viewModel like this in MovieDetilsFragment
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
//And in MovieDetailsOne like this
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
//and this doesn't work MovieDetailsOne don't observe any changes
-Observe once extinction function:
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
observe(lifecycleOwner, object : Observer<T> {
override fun onChanged(t: T?) {
observer.onChanged(t)
removeObserver(this)
}
})
}
//and this doesn't work MovieDetailsOne don't observe first time it's lanches
I know i took so long to explain :D but I'm trying to give you clear idea and sorry for you time <3 ... if you want any additional information about the code or the problem comment down below.

Finally i achieve the behavior that i want by doing some steps which is:
Attach ViewPager childs using childFragmentManager which means that android will treat viewpager fragments as childs for viewPager holder:
//instead of doing this
pagerAdapter = ViewPagerAdapter(activity?.supportragmentManager, lifecycle, fragmentList)
//do this
pagerAdapter = ViewPagerAdapter(childFragmentManager, lifecycle, fragmentList)
Define local viewModel for parent fragment like this:
//instead of doing this
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//do this
moviesViewModel = ViewModelProvider(this).get(MoviesViewModel::class.java)
Define parent fragment scope viewModel in childs fragment:
//instead of doing this
moviesViewModel = ViewModelProvider(requireActivity()).get(MoviesViewModel::class.java)
//do this
private val moviesViewModel:MoviesViewModel by viewModels(
{requireParentFragment()}
)
by doing these steps parent fragment will stop observing data when it's destroyed and child fragments also.

Related

Sending Data between Fragments using Shared Viewmodel and SharedFlow

I'm very new to Kotlin Flows. As the title suggests I basically have 2 Fragments that Shares a ViewModel. I want to send data between them using SharedFlow as a substitute for LiveData without retaining it's state.
Fragment A
class FragmentA: Fragment() {
private lateinit var viewModelShared: SharedViewModel
//Others//
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
uper.onViewCreated(view, savedInstanceState)
viewModelShared = ViewModelProvider(requireActivity())[SharedViewModel::class.java]
someView.setOnClickListener{
viewModelShared.sendData("Hello")}
//Fragment Navigates From Fragment A to B using NavController
navController.navigate(some_action_id)
}
}
}
Fragment B
class FragmentB: Fragment() {
private lateinit var viewModelShared: SharedViewModel
//Others//
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModelShared = ViewModelProvider(requireActivity())[SharedViewModel::class.java]
lifecycleScope.launchWhenCreated {
viewModelMain.sharedFlow.collectLatest {
//Data or the word 'Hello' sent from Fragment A not being Received Here
}
}
}
}
SharedViewModel
class SharedViewModel:ViewModel() {
private val _sharedFlow= MutableSharedFlow<String>()
val sharedFlow= _sharedFlow.asSharedFlow()
fun sendData(data:String){
viewModelScope.launch {
_sharedFlow.emit(data)
}
}
}
Your FragmentB only collects when it is in started state or higher. This is correct because you don't want to be working with views when it's not visible and possibly doesn't currently have views.
However, since your SharedFlow has no replay history, this means there is nothing for FragmentB to collect when it is back on screen. Presumably it is off screen when FragmentA updates the flow.
So, your SharedFlow with no replay has nothing to do when it currently has no collectors, and the emitted value is thrown away. You need to have a replay of at least 1 for this to work.
private val _sharedFlow= MutableSharedFlow<String>(replay = 1)
You mentioned "without retaining its state", but this isn't possible without state if the two fragments are not on screen at the same time.
By the way, there is a simpler way to declare your shared ViewModel:
private val viewModelShared: SharedViewModel by activityViewModels()

How to pass data or add observer at onBackPressed event in FragmentActivity

I'm developing android app and I'm facing problem with passing data when user pressed back button (which means onBackPress event is fired).
I wanted to fire event with observer with viewmodel but it doesn't work.
like this.
// First Fragment
private val viewModel: MyViewModel by bindViewModel()
viewModel.currencyVal.observe { state ->
Timber.i("Event fired")
}
...
// Second fragment which was displayed with fragment transaction. This code is when user pressed back button. like override fun onBackPressed
private val viewModel: MyViewModel by bindViewModel()
viewModel.currencyVal(5)
// MyViewModel
...
val currencyVal = MutableLiveData<Int>()
...
fun setCurrencyVal(currencyVal: Int) {
currencyVal.value = currencyVal
}
Here's bindViewModel function
protected inline fun <reified T : ViewModel> bindViewModel(
crossinline initializer: T.() -> Unit = {}
): Lazy<T> = nonConcurrentLazy {
ViewModelProviders.of(requireActivity())
.get(T::class.java)
.also { it.initializer() }
}
And also passing data via fragment transaction doesn't work.
Could anyone please suggest how to pass data when user presses back button in FragmentActivity?
Thanks.
Please make sure your fragment instance is same.
If it's not same, you need to create view model by activity.
Hope this helps.
I am missing something. Is viewModel and firstViewModel the same object? Also if so are you sure that you are creating the ViewModel of the Activity, but not the Fragment?
mViewModel = ViewModelProviders.of(requireActivity()).get(YourViewModel.class);

RecyclerView loses state on rotation

I am making a note-taking app using MVVM design pattern, Room for persisting data and paging
The problem is when I rotate my device it appears like it keeps its state for less than one second, then the RecyclerView scrolls up
I've debugged my code and found that onChanged() is called multiple times
here is my code
NoteRepository
override fun loadPagedNotes() : LiveData<PagedList<Note>> {
val factory : DataSource.Factory<Int, Note> = mNotesDao.getNotes()
val mNotesList = MutableLiveData<PagedList<Note>>()
val notesList = RxPagedListBuilder(
factory, PagedList.Config
.Builder()
.setPageSize(20)
.setEnablePlaceholders(true)
.build()
).buildFlowable(BackpressureStrategy.LATEST)
mDisposables.add(
notesList.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
mNotesList.value = it
}
)
return mNotesList
}
NoteViewModel
fun loadPagedNotes() : LiveData<PagedList<Note>> {
return mNoteRepository.loadPagedNotes()
}
HomeActivity
class HomeActivity : AppCompatActivity() {
private lateinit var mBinding : ActivityHomeBinding
private val mViewModel : NoteViewModel by viewModel()
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_home)
loadNotes()
}
private fun loadNotes() {
mViewModel.loadPagedNotes()
.observe(this,
Observer {
if (it.isNotEmpty()) {
addNotesToRecyclerView(it)
}
})
}
private fun addNotesToRecyclerView(list : PagedList<Note>?) {
showRecyclerView()
val adapter = PagedNoteListAdapter(this#HomeActivity)
adapter.submitList(list)
mBinding.notesRecyclerView.adapter = adapter
}
}
When you rotate your device, the view (fragment) is destroyed and re-created, but the ViewModel remains. You're running into trouble because your ViewModel never leverages this feature: instead, on (re-)creation of your view, it causes the ViewModel to re-fetch the data all over again. Each invocation of the VM's loadPagedNotes will create a new LiveData and return it. You really want to have just one and return a reference to that one every single time.
Why don't you change your code to be something like this
class NoteViewModel: ViewModel() {
val pagedNotes = mNoteRepository.loadPagedNotes()
}
and in your activity
override fun onCreate(/*...*/) {
mViewModel.pagedNotes.observe(this, Observer { addNotesToRecyclerView(it)
})
Also, consider moving this val adapter = PagedNoteListAdapter(this#HomeActivity) to an instance member of the Activity: val mAdapter = .... That way, your addNotesToRecyclerView code will not create and bind a new adapter every time there is an update from your repository.
class HomeActivity: /*...*/ {
val mAdapter = PagedNoteListAdapter(this)
override fun onCreate(...) {
...
mBinding.notesRecyclerView.adapter = mAdapter
}
private fun addNotesToRecyclerView(list: PagedList<Note>) // You don't need the question mark since you're never calling the function except when you already know `list` isn't null) {
mAdapter.submitList(list)
}
That could be another reason you're experiencing this.
Also if you still have undesirable refreshing, look into DiffUtil. It enables you to update items of an adapter if they've individually changed, so even if the overall list changes but some elements stay the same, you'll only redraw the changed elements without redrawing the ones that haven't changed (e.g., if you've added one new item it won't redraw everything but just draw the one new item)
I'm not a RxJava expert so I can't speak to that too much but have you tried getting your ViewModel from the ViewModelProviders class instead:
myViewModel = ViewModelProviders.of(this)[MyViewModel::class.java]
I believe creating a ViewModel instance this way allows your ViewModel to live outside of your Activities lifecycle and may help with your state issue.

How to update a viewmodel from another fragment?

I'm trying to pass location data from one fragment to a ViewModel which updates my UI.
I have a fragment which contains a MapBoxCoordinatesListener. Every time, when the location is changed, I want to display the updated coordinates to the user. The labels to do this are on the another layout and I can access them via MainActivity's ViewModel.
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
(...)
showMapFragment()
initMainMenuBinding(activityMainBinding)
}
private fun initMainMenuBinding(activityMainBinding: ActivityMainBinding) {
mainMenuViewModel = MainMenuViewModel()
mainMenuViewModel.mainButtonsCallback = object : MainMenuViewModel.MainButtonsCallback {
(..)
}
activityMainBinding.mainMenu.viewModel = mainMenuViewModel
}
private fun showMapFragment() {
mapFragment = supportFragmentManager.findFragmentByTag(MapFragment.TAG)
?: MapFragment.newInstance()
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.map_container, mapFragment, MapFragment.TAG)
fragmentTransaction.commit()
}
MainMenuViewModel:
class MainMenuViewModel : BaseObservable(){
lateinit var mainButtonsCallback: MainButtonsCallback
(...)
#get:Bindable
var latLangModel = LatLangModel("latitude", "longitude")
}
I was thinking about sending coordinates from MapFragment to activity with a callback, but then I don't know how to pass data to ViewModel. Also I have doubts if this is a robust solution to archive what I want.
How can I achieve this in a robust way?

LiveData Observer not Called

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

Categories

Resources