Getting FragmentResultListener triggered without popbackstack - android

Currently I'm facing a problem getting data from a child fragment to it's parent fragment, I saw that the best option is using setFragmentResult but because setFragmentResultListener needs to be in STARTED state at parentFragment(doen't happen because it is stoped when replaced by another fragment) I see that the only option is to use popBackStack() and then the listener gets triggered. The thing is that I don't wanna use popBackStack()
Can anyone help me?
PS: No, I don't want to use viewModel in this case to keep data.
Listener:
class ResultListenerFragment : Fragment() {
val viewModel : SomeViewModel by viewModels()
var result : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("requestKey") { requestKey, bundle ->
//verify values
viewModel.repeat()
}
//in case of an error
viewModel.getError().observe(viewLifecycleOwner,{
requireActivity()
.supportFragmentManager
.beginTransaction()
.replace(R.id.framelayout, ErrorFragment.newInstance(it).commit()
}
}
}
Triggerer:
class ErrorFragment: Fragment(R.layout.fragment_error) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById(R.id.some_tv).text = arguments.getString(keyError)
view.findViewById(R.id.result_button).setOnClickListener {
val result = "retry"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
companion object {
private const val keyError = "errorKey"
fun newInstance(error:String): ErrorFragment{
val args = Bundle().apply {
putString(keyError, error)
}
}
return ErrorFragment().apply{arguments = args}
}
}

Related

Fragment. getViewLifeCycleOwner doesn't prevent multiple calls of LiveData Observer

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.

LiveData is not re-emitting when fragment is resumed

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
}
}

How to know when `navController.popBackStack()` was called?

I am popping the backstack on my nav controller on some point in my code -
navController.popBackStack()
The fragment that added that following fragment to the backstack needs to know exactly when that one was popped in order to trigger code following that.
How do I make the first fragment know about it?
I thought about adding a callback as an argument but I doubt it's a good practice.
If you use Koin you can do something like:
class MyActivity : AppCompatActivity(){
// Lazy inject MyViewModel
val model : MySharedViewModelby sharedViewModel()
override fun onCreate() {
super.onCreate()
model.isFragmentPopped.observe(this, Observe{
if(it){
doSomething()
}
}
}
}
Fragment:
class MyFragment : Fragment(){
// Lazy inject MyViewModel
val model : MySharedViewModel by sharedViewModel()
override fun onCreate() {
super.onCreate()
var fragmentX = model.isFragmentXPopped
}
fun backstackPopped{
model.fragmentPopped()
navController.popBackStack()
}
}
ViewModel:
var _isFragmentPopped = MutableLiveData<Boolean>(false)
val isFragmentPopped : LiveData<Boolean>
get = _isFragmentPopped
fun fragmentPopped(){
_isFragmentPopped.value = true
}
Keep in mind that you should keep sharedViewModels as small as possible as they do not get destroyed until the activity is destroyed.
we can create observer for return values from second fragment using popBackStack()
In firstFragment use this for observer :-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController()
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")
?.observe(viewLifecycleOwner) {
}
}
In secondFragment use this code
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("key", "you press back button")
navController.popBackStack()

Why my ViewModel is still alive after I replaced current fragment in Android?

Example, If I replaced 'fragmentA' with 'fragmentB', the 'viewModelA' of fragmentA is still live. why ?
onCreate() of Fragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider.NewInstanceFactory().create(InvoicesViewModel::class.java)
}
ViewModel
class InvoicesViewModel : ViewModel() {
init {
getInvoices()
}
private fun getInvoices() {
viewModelScope.launch {
val response = safeApiCall() {
// Call API here
}
while (true) {
delay(1000)
println("Still printing although the fragment of this viewModel destroied")
}
if (response is ResultWrapper.Success) {
// Do work here
}
}
}
}
This method used to replace fragment
fun replaceFragment(activity: Context, fragment: Fragment, TAG: String) {
val myContext = activity as AppCompatActivity
val transaction = myContext.supportFragmentManager.beginTransaction()
transaction.replace(R.id.content_frame, fragment, TAG)
transaction.commitNow()
}
You will note the while loop inside the Coroutine still work although after replace fragment to another fragment.
this is about your implementation of ViewModelProvider.
use this way for creating your viewModel.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(InvoicesViewModel::class.java)
}
in this way you give your fragment as live scope of view model.
Check, if you have created the ViewModel in Activity passing the context of activity or fragment.

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