Android/Kotlin replacement for EventBus postSticky() - android

I am searching for postSticky() method replacement. It is being used for simple passing value to previous Fragment, but thing is that I am using BackStackUtil for navigation so instance() method is being called when returning only if stack gets cleared somehow before getting back.Previous Fragment is holding List of items, when next Fragment can modify picked item and yet another one can do something else so it is chain of sticky events when each of these is being passed to previous Fragment. App structure won't let me to apply Coordinator pattern at current stage and also I don't want to attach Bundle to Fragments kept on stack. I was looking for solutions but I couldn't find any. I also don't want to store values in some static fields or SharedPreferences/Data storage.I was thinking about shared ViewModel but I don't really like this idea to be honest, so I would appreciate any ideas or just confirmation if shared VM is the only/best way.
Do you have any other ideas?

In your A fragment, before navigating to B fragment, listen to savedStateHandle:
findNavController()
.currentBackStackEntry
?.savedStateHandle?.getLiveData<Bundle>("DATA_KEY")
?.observe(viewLifecycleOwner) { result ->
// Result from fragment B
}
In your B fragment, before navigating back, set the data to pass to A fragment:
findNavController()
.previousBackStackEntry
?.savedStateHandle
?.set("DATA_KEY", result)
You can remove the observer using:
findNavController()
.currentBackStackEntry
?.savedStateHandle?.remove<Bundle>
Note that here the passed type is Bundle (the type in getLiveData<Bundle>) but you can use any type you want.

Related

Android: How do I trigger an event once a fragment is totally loaded?

I am trying to load initial data onto a fragment in my application. In the following lines of code (located at the very end of onCreate, I attempt retrieve the fragment and load data onto it:
// Display the current list of matches to the user
var eventsFragment: EventsFragment =
supportFragmentManager.findFragmentByTag("eventsFragment") as EventsFragment
eventsFragment.displaySchedule(currList)
However, I receive the following error:
Caused by: kotlin.TypeCastException: null cannot be cast to non-null
type com.example.alarmfornbamatches.ui.main.EventsFragment
My guess is that the fragment hasn't loaded at the end of onCreate. So how do I execute the displaySchedule function once the fragment is fully loaded and available to be referenced for UI updates?
You can solve the TypeCastException by using this:
// Display the current list of matches to the user
var eventsFragment: EventsFragment? = supportFragmentManager.findFragmentByTag("eventsFragment") as EventsFragment
eventsFragment?.displaySchedule(currList)
You are looking for a fragment that you haven't created, in a place you haven't put it yet. You must first create the fragment and add it to the supportFragmentManager using transactions. If you want do pass data to the fragment you should do so by add this to your fragment (if your list is not string, then it must be serializable and use putSerializable instead.
companion object{
fun newInstance(list: ArrayList<String>) = EventsFragment().apply{
arguments = Bundle().apply {
putStringArrayList(KEY, list)
}
}
}
and then pull the data out of the arguments bundle in the onCreate method in your fragment.
Now in the onCreate in your activity you will create the fragment and add it like so
eventsFragment = EventsFragment.newInstance(list)
supportFragmentManager.beginTransaction().add(R.id.frameLayoutId, eventsFragment, fragmentTag).commit()
only now, if necessary can you use the code you have to get the fragment again, and you should always do it expecting ti to possibly come back as null:
eventsFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as? EventsFragment ?: EventsFragment.newInstance(list)
The problem with your code is you are trying to find a fragment which is not there that's why its null. Not sure where you made the Fragment transaction.
To fix this There can be two cases:
If you already have the data in your Activity its better you pull it inside Fragment by using getActivity() in #onViewCreated() or by a Shared ViewModel.
If you are loading data from a source(Network/IO) then you can use a Shared ViewModel or Get fragment from back stack and call its method or you can use conventional callback stuff.
I can add some sample code but i am not sure which option is best fit for you since its not clear from question that u are using MVVM or not.

Android navigation component view decision

I am using Android Navigation Component in my project. What I am not satisfied is the fragment making the decision to do fragment transition to next fragment transition i.e
In my LoginFragment I have this -
viewModel.onLoginPressed(email, password)
.observe(viewLifecycleOwner, Observer {
if (it.userLoggedIn) {
activity?.findNavController(R.id.nav_host_fragment)
?.navigate(R.id.action_loginFragment_to_productsFragment)
}
})
According to me, the view must be dummy and must not do any such decisions of what to do on loginSuccess for example. The viewModel must be responsible for this.
How do I use this navigation component inside viewModel?
The ViewModel doesn't need to know about navigation, but it knows about events and state.
The communication should be more like:
NavHostActivity -> Fragment -> ViewModel
Your Fragment has Views, and click listeners, and state. The user types a user/password and presses a button. This onClick listener will tell the view model -> (pseudo-code) onUserPressedTheLoginButtonWith(username, password)
The ViewModel in turn will receive this, do what it needs (like check if you're already logged or whatever, perhaps the final decision is to navigate to another fragment).
What the ViewModel will do is expose a LiveData like
val navigationEvent = LiveData<...>
So the viewModel will navigationEvent.postValue(...)
The Fragment should observe this viewModel.navigationEvent.observe(...) { }
And in THERE in the fragment it can either navigate directly or -if you have an Interface- use it like:
yourNavigator.navigateTo(...) //either your VM knows the destination or the yourNavitagor has a navigateToLogin() concrete method, this is all "it depends what you like/prefer/etc.".
In summary
Activity Host contains Nav code, irrelevant.
Fragment(s) can communicate to a NavDelegate created by you (and likely injected) or Fragments simply know the details and do it by themselves.
Fragment(s) observe the navigation state/event from the viewModel.
Fragment(s) push events from the UI (clicks, actions, etc.) to this viewModel
The ViewModel decides what to do and updates the "liveData"(s) (there can be more than one type of thing you want to observe, not only navigation).
The Fragment(s) react to this observation and act accordingly, either doing it themselves or delegating (step 2 ^).
That's how I'd do it.

Android Navigation - Is it possible to restore a bundle from upper fragment with popBackStack method?

I was trying to pass data between fragments while I was passing from upper fragment to down fragment, but I don't want to use the navigate method. It adds a new destination to the process history, but I want back to the previous fragment, restore it data and refresh the view.
Is it possible to do it with NavController and popBackStack method?
After few hours I found a strange solution. findNavController().popBackStack() cannot pass data to the previous fragment, but we can do it using the findNavController().graph.defaultArguments, set the data in one fragment, and get the data in the second one. I believe someone will find the better solution.
I also got same problem and I have used below solution which is working, But i am not sure its correct way or not.
val navOptions = NavOptions.Builder().setPopUpTo(R.id.id_of_fragment_one, true).build()
view?.findNavController()?.navigate(R.id.id_of_fragment_one, bundleOf("id" to model), navOptions)

Shared ViewModel to help communication between fragments and parent activity

While Navigation component of JetPack looks pretty promising I got to a place where I could not find a way to implement something I wanted.
Let's take a look at a sample app screen:
The app has one main activity, a top toolbar, a bottom toolbar with fab attached.
There are 2 challenges that I am facing and I want to make them the right way.
1. I need to implement fragment transactions in order to allow replacing the fragment on the screen, based on the user interaction.
There are three ways I can think of and have this implemented:
the callbacks way. Having a interface onFragmentAction callback in fragment and have activity implement it. So basically when user presses a button in FragmentA I can call onFragmentAction with params so the activity will trigger and start for example transaction to replace it with FragmentB
implement Navigation component from JetPack. While I've tried it and seems pretty straightforward, I had a problem by not being able to retrieve the current fragment.
Use a shared ViewModel between fragment and activity, update it from the fragment and observe it in the activity. This would be a "replacement" of the callbacks
2. Since the FAB is in the parent activity, when pressed, I need to be able to interact with the current visible fragment and do an action. For instance, add a new item in a recyclerview inside the fragment. So basically a way to communicate between the activity and fragment
There are two ways I can think of how to make this
If not using Navigation then I can use findFragmentById and retrieve the current fragment and run a public method to trigger the action.
Using a shared 'ViewMode' between fragment and activity, update it from activity and observe it in the fragment.
So, as you can see, the recommended way to do navigation would be to use the new 'Navigation' architecture component, however, at the moment it lacks a way to retrieve the current fragment instance so I don't know how to communicate between the activity and fragment.
This could be achieved with shared ViewModel but here I have a missing piece: I understand that fragment to fragment communication can be made with a shared ViewModel. I think that this makes sense when the fragments have something in common for this, like a Master/Detail scenarion and sharing the same viewmodel is very useful.
But, then talking between activity and ALL fragments, how could a shared ViewModel be used? Each fragment needs its own complex ViewModel. Could it be a GeneralViewModel which gets instantiated in the activity and in all fragments, together with the regular fragment viewmodel, so have 2 viewmodels in each fragment.
Being able to talk between fragments and activity with a viewmodel will make the finding of active fragment unneeded as the viewmodel will provide the needed mechanism and also would allow to use Navigation component.
Any information is gladly received.
Later edit. Here is some sample code based on the comment bellow. Is this a solution for my question? Can this handle both changes between fragments and parent activity and it's on the recommended side.
private class GlobalViewModel ():ViewModel(){
var eventFromActivity:MutableLiveData<Event>
var eventFromFragment:MutableLiveData<Event>
fun setEventFromActivity(event:Event){
eventFromActivity.value = event
}
fun setEventFromFragment(event:Event){
eventFromFragment.value = event
}
}
Then in my activity
class HomeActivity: AppCompatActivity(){
onCreate{
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromFragment.observe(){
//based on the Event values, could update toolbar title, could start
// new fragment, could show a dialog or snackbar
....
}
//when need to update the fragment do
viewModel.setEventFromActivity(event)
}
}
Then in all fragments have something like this
class FragmentA:Fragment(){
onViewCreated(){
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromActivity.observe(){
// based on Event value, trigger a fun from the fragment
....
}
viewModelFragment = ViewModelProviders.of(this, factory)
.get(FragmentAViewModel::class.java)
viewModelFragment.some.observe(){
....
}
//when need to update the activity do
viewModel.setEventFromFragment(event)
}
}

Equivalent of startActivityForResult() with Android Architecture Navigation

I have a workflow with 3 screens. From "screen 1" to access to "screen 2", the user must accept some kind of terms and conditions that I call in my picture "modal". But he only has to accept those conditions once. The next time he is on the first screen, he can go directly to screen 2. The user can chose to NOT accept the terms, and therefore we go back to "screen 1" and do not try to go to "screen 2".
I am wondering how to do it with the new navigation component.
Previously, what I would do it:
On screen 1, check if the user must accept the conditions
If no, start "screen 2" activity
If yes, use startActivityForResult() and wait result from the modal. Mark the terms as accepted. Start "screen 2"
But with the navigation graph, there is no way to start a Fragment to obtain a result.
I could mark the terms as accepted in the "modal" screen and start the "screen 2" from there. The thing is that to access to the screen 2, I need to do a network request. I do not want to duplicate the call to the API and processing its outcome in both "screen 1" and "modal".
Is there a way to go back from "modal" to "screen 1" with some information (user accepted the terms), using Jetpack navigation?
Edit: I currently get around it by using the same flow that Yahya suggests below: using an Activity just for the modal and using startActivityForResult from the "screen 1". I am just wondering if I could continue to use the navigation graph for the whole flow.
In the 1.3.0-alpha04 version of AndroidX Fragment library they introduced new APIs that allow passing data between Fragments.
Added support for passing results between two Fragments via new APIs on FragmentManager. This works for hierarchy fragments (parent/child), DialogFragments, and fragments in Navigation and ensures that results are only sent to your Fragment while it is at least STARTED. (b/149787344)
FragmentManager gained two new methods:
FragmentManager#setFragmentResult(String, Bundle) which you can treat similiarly to the existing Activity#setResult ;
FragmentManager#setFragmentResultListener(String, LifecycleOwner, FragmentResultListener) which allows you to listen/observe result changes.
How to use it?
In FragmentA add FragmentResultListener to the FragmentManager in the onCreate method:
setFragmentResultListener("request_key") { requestKey: String, bundle: Bundle ->
val result = bundle.getString("your_data_key")
// do something with the result
}
In FragmentB add this code to return the result:
val result = Bundle().apply {
putString("your_data_key", "Hello!")
}
setFragmentResult("request_key", result)
Start FragmentB e.g.: by using:
findNavController().navigate(NavGraphDirections.yourActionToFragmentB())
To close/finish FragmentB call:
findNavController().navigateUp()
Now, your FragmentResultListener should be notified and you should receive your result.
(I'm using fragment-ktx to simplify the code above)
There are a couple of alternatives to the shared view model.
fun navigateBackWithResult(result: Bundle) as explained here https://medium.com/google-developer-experts/using-navigation-architecture-component-in-a-large-banking-app-ac84936a42c2
Create a callback.
ResultCallback.kt
interface ResultCallback : Serializable {
fun setResult(result: Result)
}
Pass this callback as an argument (note it has to implement Serializable and the interface needs to be declared in its own file.)
<argument android:name="callback"
app:argType="com.yourpackage.to.ResultCallback"/>
Make framgent A implement ResultCallback, fragment B by will get the arguments and pass the data back through them, args.callback.setResult(x)
It looks like there isn't equivalent for startActivityForResult in Navigation Component right now. But if you're using LiveData and ViewModel you may be interested in this article. Author is using activity scoped ViewModel and LiveData to achieve this for fragments.
Recently (in androidx-navigation-2.3.0-alpha02 ) Google was released a correct way for achieve this behaviour with fragments.
In short: (from release note)
If Fragment A needs a result from Fragment B..
A should get the savedStateHandle from the currentBackStackEntry, call getLiveData providing a key and observe the result.
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Type>("key")?.observe(
viewLifecycleOwner) {result ->
// Do something with the result.
}
B should get the savedStateHandle from the previousBackStackEntry, and set the result with the same key as the LiveData in A
findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result)
Related documentation
There is another alternative workaround. You can use another navigation action from your modal back to screen1, instead of using popBackStack(). On that action you can send whatever data you like to screen one. Use this strategy to make sure the modal screen isn't then kept in the navigation back stack: https://stackoverflow.com/a/54015319/4672107.
The only issue I see with this strategy is that pressing the back button will not send any data back, however most use cases require navigation after a specific user action and and in those situations, this workaround will work.
To get the caller fragment, use something like fragmentManager.putFragment(args, TargetFragment.EXTRA_CALLER, this), and then in the target fragment get the caller fragment using
if (args.containsKey(EXTRA_CALLER)) {
caller = fragmentManager?.getFragment(args, EXTRA_CALLER)
if (caller != null) {
if (caller is ResultCallback) {
this.callback = caller
}
}
}

Categories

Resources