How to get a result from fragment using Navigation Architecture Component? - android

Let's say that we have two fragments: MainFragment and SelectionFragment. The second one is build for selecting some object, e.g. an integer. There are different approaches in receiving result from this second fragment like callbacks, buses etc.
Now, if we decide to use Navigation Architecture Component in order to navigate to second fragment we can use this code:
NavHostFragment.findNavController(this).navigate(R.id.action_selection, bundle)
where bundle is an instance of Bundle (of course). As you can see there is no access to SelectionFragment where we could put a callback. The question is, how to receive a result with Navigation Architecture Component?

They have added a fix for this in the 2.3.0-alpha02 release.
If navigating from Fragment A to Fragment B and A needs a result from B:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<Type>("key")?.observe(viewLifecycleOwner) {result ->
// Do something with the result.
}
If on Fragment B and need to set the result:
findNavController().previousBackStackEntry?.savedStateHandle?.set("key", result)
I ended up creating two extensions for this:
fun Fragment.getNavigationResult(key: String = "result") =
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>(key)
fun Fragment.setNavigationResult(result: String, key: String = "result") {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}

Since Fragment KTX 1.3.6 Android supports passing data between fragments or between fragments and activities. It's similar to startActivityForResult logic.
Here is an example with Navigation Component. You can read more about it here
build.gradle
implementation "androidx.fragment:fragment-ktx:1.3.6"
FragmentA.kt
class FragmentA : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Step 1. Listen for fragment results
setFragmentResultListener(FragmentB.REQUEST_KEY) { key, bundle ->
// read from the bundle
}
// Step 2. Navigate to Fragment B
findNavController().navigate(R.id.fragmentB)
}
}
FragmentB.kt
class FragmentB : Fragment() {
companion object {
val REQUEST_KEY = "FragmentB_REQUEST_KEY"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
buttonA.setOnClickListener { view ->
// Step 3. Set a result
setFragmentResult(REQUEST_KEY, bundleOf("data" to "button clicked"))
// Step 4. Go back to Fragment A
findNavController().navigateUp()
}
}
}

Use these extension functions
fun <T> Fragment.getNavigationResult(key: String = "result") =
findNavController().currentBackStackEntry?.savedStateHandle?.get<T>(key)
fun <T> Fragment.getNavigationResultLiveData(key: String = "result") =
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
fun <T> Fragment.setNavigationResult(result: T, key: String = "result") {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}
So if you want to send result from Fragment B to fragment A
Inside Fragment B
setNavigationResult(false, "someKey")
Inside Fragment A
val result = fragment.getNavigationResultLiveData<Boolean>("someKey")
result.observe(viewLifecycleOwner){ booleanValue-> doSomething(booleanValue)
Important note
In the Fragment B you need to set the result (setNavigationResult())
in the started or resumed state (before onStop() or onDestroy()),
otherwise previousBackStackEntry will be already null.
Important note #2
If you’d only like to handle a result only once, you must call
remove() on the SavedStateHandle to clear the result. If you do not
remove the result, the LiveData will continue to return the last
result to any new Observer instances.
More information in the official guide.

According to Google: you should try to use shared ViewModel. Check below example from Google:
Shared ViewModel that will contain shared data and can be accessed from different fragments.
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
MasterFragment that updates ViewModel:
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
DetailsFragment that uses shared ViewModel:
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, item -> {
// Update the UI.
});
}
}

with a little improvement of #LeHaine 's answer, you can use these methods for navigation 2.3.0-alpha02 and above
fun <T> Fragment.getNavigationResult(key: String) =
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<T>(key)
fun <T> Fragment.setNavigationResult(result: T, key: String) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, result)
}

I created a wrapper function that's similar to LeHaine's answer, but it handles more cases.
To pass a result to a parent from a child:
findNavController().finishWithResult(PickIntervalResult.WEEKLY)
To get result from a child in a parent:
findNavController().handleResult<PickIntervalResult>(
viewLifecycleOwner,
R.id.navigation_notifications, // current destination
R.id.pickNotificationIntervalFragment // child destination
) { result ->
binding.textNotifications.text = result.toString()
}
My wrapper isn't so simple as LeHaine's one, but it is generic and handles cases as:
A few children for one parent
Result is any class that implements Parcelable
Dialog destination
See the implementation on the github or check out an article that explains how it works.

fun <Type> BaseNavigationActivity<*,*,*>.getNavigationResult(key : String, #IdRes viewId: Int) =
this.findNavController(viewId).currentBackStackEntry?.savedStateHandle?.getLiveData<Type>(key)
fun <Type> BaseNavigationActivity<*,*,*>.setNavigationResult(result: Type, key: String, #IdRes viewId: Int){
this.findNavController(viewId).previousBackStackEntry?.savedStateHandle?.set<Type>(key, result)
}

I would suggest you to use NavigationResult library which is an add-on for JetPack's Navigation Component and lets you to navigateUp with Bundle.
I've also wrote a blog post about this on Medium.

Just an alternative to the other answers...
EventBus with MutableShareFlow as it core in a shared object (ex: repo) and an observer describe in here
Looks like things are moving away from LiveData and going in Flow direction.
Worth to have a look.

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

How should i use ViewModel in two fragments?

I have an app with one activity and two fragments, in the first fragment, I should be able to insert data to the database, in the second I should be able to see the added items in a recyclerView.
So I've made the Database, my RecyclerView Adapter, and the ViewModel,
the issue is now how should I manage all that?
Should I initialize the ViewModel in the activity and call it in some way from the fragment to use the insert?
Should I initialize the viewmodel twice in both fragments?
My code looks like this:
Let's assume i initialize the viewholder in my Activity:
class MainActivity : AppCompatActivity() {
private val articoliViewModel: ArticoliViewModel by viewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
}
Then my FirstFragments method where i should add the data to database using the viewModel looks like this:
class FirstFragment : Fragment() {
private val articoliViewModel: ArticoliViewModel by activityViewModels()
private fun addArticolo(barcode: String, qta: Int) { // function which add should add items on click
// here i should be able to do something like this
articoliViewModel.insert(Articolo(barcode, qta))
}
}
And my SecondFragment
class SecondFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private val articoliViewModel: ArticoliViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recyclerView)
val adapter = ArticoliListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
// HERE I SHOULD BE ABLE DO THIS
articoliViewModel.allWords.observe(viewLifecycleOwner) { articolo->
articolo.let { adapter.submitList(it) }
}
}
}
EDIT:
My ViewModel looks like this:
class ArticoliViewModel(private val repository: ArticoliRepository): ViewModel() {
val articoli: LiveData<List<Articolo>> = repository.articoli.asLiveData()
fun insert(articolo: Articolo) = viewModelScope.launch {
repository.insert(articolo)
}
}
class ArticoliViewModelFactory(private val repository: ArticoliRepository): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ArticoliViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ArticoliViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Whether multiple fragments should share a ViewModel depends on whether they are showing the same data. If they show the same data, I think it usually makes sense to share a ViewModel so the data doesn't have to be pulled from the repository when you switch between them, so the transition is faster. If either of them also has significant amount of unique data, you might consider breaking that out into a separate ViewModel so it doesn't take up memory when it doesn't need to.
Assuming you are using a shared ViewModel, you can do it one of at least two different ways, depending on what code style you prefer. There's kind of a minor trade-off between encapsulation and code duplication, although it's not really encapsulated anyway since they are looking at the same instance. So personally, I prefer the second way of doing it.
Each ViewModel directly creates the ViewModel. If you use by activityViewModels(), then the ViewModel will be scoped to the Activity, so they will both receive the same instance. But since your ViewModel requires a custom factory, you have to specify it in both Fragments, so there is a little bit of code duplication:
// In each Fragment:
private val articoliViewModel: ArticoliViewModel by activityViewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
Specify the ViewModel once in the MainActivity and access it in the Fragments by casting the activity.
// In Activity: The same view model code you already showed in your Activity, but not private
// In Fragments:
private val articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
Or to avoid code duplication, you can create an extension property for your Fragments so they don't have to have this code duplication:
val Fragment.articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel

Pass data/bundle using navigateUp in Android Navigation Component

I found the question but does not have solution in code
I want to have data when backpress/manual back happens. I am using navigateUp() to go back. How can I pass data to previous fragment? navigateUp() does not have any facility to pass data to previous fragment. Even I did not find solution using Safe Args. It's passing data forward. I want to have in backward Frad B -> Frag A.
My code to go back to previous fragment
Navigation.findNavController(view).navigateUp()
My question is, How can i get data in previous fragment. I can navigate to Frag A from Frag B using
According to developer.android.com, you can use common for fragments where you want to share data ViewModel using their activity scope.
Here are steps:
Create view model which will keep the data:
class SharedViewModel : ViewModel() {
val dataToShare = MutableLiveData<String>()
fun updateData(data: String) {
dataToShare.value = data
}
}
Observe data changes in Fragment1:
class Fragment1 : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(SharedViewModel::class.java)
viewModel.dataToShare.observe(this, Observer<String> { dataFromFragment2 ->
// do something with data
})
}
}
Update data in Fragment2 and if you're handling navigation properly, now, you should be able to receive data changes on Fragment1:
class Fragment2 : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(SharedViewModel::class.java)
updateDataButton.setOnClickListener { v ->
viewModel.updateData("New data for fragment1")
}
}
}
I hope answer helps.
You can use NavigationResult library. Basically it's startActivityForResult but for Fragments in Navigation component.
Please use the OFFICIAL androidx's components. setFragmentResultListener() and setFragmentResult() methods:
implementation "androidx.fragment:fragment-ktx:1.3.5"
Cheers ;)
You should use static variables/companion objects, because it is better than shared viewmodel as it is not simple/nice architecture. As it it not straightforward, I think it is the best way.
To navigateUp From FragmentB to FragmentA
FragmentB:
isBackpressed = true
findNavController().navigateUp()
FragmentA:
onViewCreated() {
// todo
if(isBackpressed) {
isBackpressed = false
// do whatever you want
}
}
To pop destinations when navigating from one destination to another, add an app:popUpTo attribute to the associated <action> element.
To navigate from fargment2 to Fragment1 with arguments, specify in the navigation graph the action of the caller fragment and the arguments of the destination fragment :
<fragment
android:id="#+id/fragment2"
android:name="com.example.myapplication.Fragment2"
android:label="fragment_2"
tools:layout="#layout/fragment_2">
<action
android:id="#+id/action_2_to_1"
app:destination="#id/fragment1"
app:popUpTo="#+id/fragment1"/>
</fragment>
<fragment
android:id="#+id/fragment1"
android:name="com.example.myapplication.Fragment1"
android:label="fragment_1"
tools:layout="#layout/fragment_1">
<argument
android:name="someArgument"
app:argType="string"
app:nullable="false"
android:defaultValue="Hello Word"/>
</fragment>
In your Fragment2 class, you call your action and pass your argument:
val action = Fragment2Directions.action2To1("MY_STRING_ARGUMENT")
findNavController().navigate(action)
You can just call
findNavController().navigate(R.id.fragment1, args)
where args is your bundle.
In fragment1, fetch the data from the arguments

How to use android navigation without binding to UI in ViewModel (MVVM)?

I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.

Android MVVM: Activity with multiple Fragments - Where to put shared LiveData?

I have an architectural question about the android ViewModels:
Let's say that in my App I have an Activity with two Fragments inside (using a Viewpager). The two fragments do different things (therefore may have their own ViewModel?), but they also both need various data that is similar.
This is for example the state if a network connection is available or not (and both fragments show different error UIs if there is no connection), or some user setting that comes via a Push from a server and affects both fragments equally.
This looks something like this:
Now my question is how to deal with that situation when using ViewModels?
Is it good that a view observes multiple ViewModels, like it would be if I have a ViewModel for the Activity (holding the state that both need equally) and one for each fragment, like this:
This was hinted here for example, but it is not a good practice, as the relationship in MVVM generally is
View n - 1 ViewModel n - 1 Model
But I am not sure where the right place for such shared LiveData is in my case?
Late answer but I asked myself the same question and found the answer in Google guide.
Especially for fragments, it is mentioned on Google Documentations explicitly here
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
class DetailFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
I think the concept behind the ViewModel was that it is supposed to be related to a single "Screen" rather than a "View". So going by that logic, I think you can use the same ViewModel if multiple fragments reference the same ViewModel because they technically belong to the same "Screen".
In the fragments, you could request the activity for the ViewModel which holds the instance of LiveData and could give you the updates as needed.
Hope this answers your question.
Update: I found a link to a sample fragment in Google samples. Check out onCreateView() method. Pasting code below for reference:
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View root = inflater.inflate(R.layout.addtask_frag, container, false);
if (mViewDataBinding == null) {
mViewDataBinding = AddtaskFragBinding.bind(root);
}
mViewModel = AddEditTaskActivity.obtainViewModel(getActivity());
mViewDataBinding.setViewmodel(mViewModel);
setHasOptionsMenu(true);
setRetainInstance(false);
return mViewDataBinding.getRoot();
}
P.S. If you have found a better solution/answer/practice, lemme know.

Categories

Resources