Sharing Data Between Fragments Using New Architecture Component Viewmodel

Sharing data between fragments using new architecture component ViewModel

Updated on 6/12/2017,

Android Official provide a simple, precise example to example how the ViewModel works on Master-Detail template, you should take a look on it first.Share data between fragments

As @CommonWare, @Quang Nguyen methioned, it is not the purpose for Yigit to make the call from master to detail but be better to use the Middle man pattern. But if you want to make some fragment transaction, it should be done in the activity. At that moment, the ViewModel class should be as static class in Activity and may contain some Ugly Callback to call back the activity to make the fragment transaction.

I have tried to implement this and make a simple project about this. You can take a look it. Most of the code is referenced from Google IO 2017, also the structure.
https://github.com/charlesng/SampleAppArch

I do not use Master Detail Fragment to implement the component, but the old one ( communication between fragment in ViewPager.) The logic should be the same.

But I found something is important using these components

  1. What you want to send and receive in the Middle man, they should be sent and received in View Model only
  2. The modification seems not too much in the fragment class. Since it only change the implementation from "Interface callback" to "Listening and responding ViewModel"
  3. View Model initialize seems important and likely to be called in the activity.
  4. Using the MutableLiveData to make the source synchronized in activity only.

1.Pager Activity

public class PagerActivity extends AppCompatActivity {
/**
* The pager widget, which handles animation and allows swiping horizontally to access previous
* and next wizard steps.
*/
private ViewPager mPager;
private PagerAgentViewModel pagerAgentViewModel;
/**
* The pager adapter, which provides the pages to the view pager widget.
*/
private PagerAdapter mPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pager);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
pagerAgentViewModel = new ViewModelProvider(this).get(PagerAgentViewModel.class);
pagerAgentViewModel.init();
}

/**
* A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
* sequence.
*/
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
...Pager Implementation
}

}

2.PagerAgentViewModel (It deserved a better name rather than this)

public class PagerAgentViewModel extends ViewModel {
private final SavedStateHandle state;
private final MutableLiveData<String> messageContainerA;
private final MutableLiveData<String> messageContainerB;

public PagerAgentViewModel(SavedStateHandle state) {
this.state = state;

messageContainerA = state.getLiveData("Default Message");
messageContainerB = state.getLiveData("Default Message");
}

public void sendMessageToB(String msg)
{
messageContainerB.setValue(msg);
}
public void sendMessageToA(String msg)
{
messageContainerA.setValue(msg);

}
public LiveData<String> getMessageContainerA() {
return messageContainerA;
}

public LiveData<String> getMessageContainerB() {
return messageContainerB;
}
}

3.BlankFragmentA

public class BlankFragmentA extends Fragment {

private PagerAgentViewModel viewModel;

public BlankFragmentA() {
// Required empty public constructor
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

viewModel = new ViewModelProvider(getActivity()).get(PagerAgentViewModel.class);


textView = (TextView) view.findViewById(R.id.fragment_textA);
// set the onclick listener
Button button = (Button) view.findViewById(R.id.btnA);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewModel.sendMessageToB("Hello B");
}
});

//setup the listener for the fragment A
viewModel.getMessageContainerA().observe(getViewLifecycleOwner(), new Observer<String>() {
@Override
public void onChanged(@Nullable String msg) {
textView.setText(msg);
}
});

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_a, container, false);
return view;
}

}

4.BlankFragmentB

public class BlankFragmentB extends Fragment {

public BlankFragmentB() {
// Required empty public constructor
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

viewModel = new ViewModelProvider(getActivity()).get(PagerAgentViewModel.class);

textView = (TextView) view.findViewById(R.id.fragment_textB);
//set the on click listener
Button button = (Button) view.findViewById(R.id.btnB);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
viewModel.sendMessageToA("Hello A");
}
});

//setup the listener for the fragment B
viewModel.getMessageContainerB().observe(getViewLifecycleOwner(), new Observer<String>() {
@Override
public void onChanged(@Nullable String msg) {
textView.setText(msg);

}
});
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_b, container, false);
return view;
}

}

How to Share Data between Activity and Fragment via ViewModel Class in Android?

It really depends on how do you create your ViewModel instance. Now you are creating ViewModel by its constructor, but that is not a proper way. You should use ViewModelProvider or extension methods that were created by Google team.

If you go with ViewModelProvider you should do it like this:

TimeTableViewModel viewModel = new ViewModelProvider(this).get(TimeTableViewModel.class);

It is important to pass the correct context to ViewModelProvider constructor call. If you are in fragment and you will just use getContext() instead of getActivity(), you will not get the same instance as it was created in Activity. You will create a new instance of ViewModel, that will be scoped only inside of fragment lifecycle. So it is important to use in both parts activity context to get the same instance.

Activity part:

TimeTableViewModel viewModel = new ViewModelProvider(this).get(TimeTableViewModel.class);

Fragment part:

TimeTableViewModel viewModel = new ViewModelProvider(getActivity()).get(TimeTableViewModel.class);

Is important that your fragment is located inside the same activity that is using this ViewModel.

But guys at Google has make it easier for us with some extension methods. But as far as I know, they are working only in Kotlin classes. So if you have Kotlin code, you can declare your ViewModel simply like this:

private val quizViewModel: TimeTableViewModel by activityViewModels()

For Fragment scoped ViewModel you need to write something like this:

private val quizViewModel: TimeTableViewModel by viewModels()

But you have to add Kotlin ktx dependency to your project build.gradle file. For example like this:

implementation 'androidx.fragment:fragment-ktx:1.1.0'

Shared View Model between two fragments in the nav_graph

When you create a viewmodel by viewmodels() yout get a reference to the ViewModel scoped to the current Fragment.

So in your case you would be using private val viewModel: AuthViewModel by viewModels() in both the fragment which gives you two different instance of viewmodel tied to each fragment.

The concept of Shared Viewmodel need a Shared Scope like Activity or NavGraph.

  1. Using Activity

Just change

private val viewModel: AuthViewModel by viewModels()

to

private val viewModel: AuthViewModel by activityViewModels()

  1. Using NavGraph

Create another nav graph where you have two fragments which are used for authentication.

<navigation android:id="@+id/authenticationNavGraph" 
app:startDestination="@id/chooseRecipient">
<fragment
android:id="@+id/authentication1Fragment"
android:name="com.example.AuthFragment1"
android:label="Fragment 1">
</fragment>
<fragment
android:id="@+id/authentication2Fragment"
android:name="com.example.AuthFragment2"
android:label="Fragment 2" />
</navigation>

Now If you want to use same viewmodel AuthViewModel then you can create a viewmodel using:

private val viewModel: AuthViewModel by navGraphViewModels(R.id.authenticationNavGraph)

Android: One ViewModel for multiple Fragments possible?

It is technically possible to have one ViewModel for all Fragments.

However, since this one ViewModel would have to manage a number of very different use cases, it would be something like a god object. With 20 Fragments, it would have very many lines of code ...

Switching over to MVVM is generally worth the effort becasue in the long run the app is easier to test and to maintain.

It may be a good idea to have a BaseViewModel in your app: a ViewModel class which handles things which are similar in all use cases, like letting the Fragment know that it should show a loading indicator or an error message. The "normal" ViewModels could extend BaseViewModel and focus on their use cases.

It makes sense in some cases to have a shared ViewModel, for example when a Fragment shows a dialog with some EditTexts or when one has workflow with a sequence of 3-4 Fragments on a small device where on a larger device one or two Fragments would suffice.

Just in case, here's my favourite starting point for MVVM app architecture: Guide to app architecture

How to share view models between fragments using Google's GithubBrowserSample approach?

by viewModels() provides a ViewModel that is scoped to the individual Fragment. There's a separate by activityViewModels() that scopes the ViewModel to your Activity.

However, the direct replacement for ViewModelProviders.of(this) is simply ViewModelProvider(this) - you aren't required to switch to by viewModels() or by activityViewModels() when using lifecycle-extensions:2.2.0-alpha03

How to share data between two fragments? Having trouble with the MVVM architecture

Share a common view model between ForecastFragment and MapFragment using the activity scope

Take a look at the example provided here Share data between fragments

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

Note:

Both fragments must handle the scenario where the other fragment is
not yet created or visible.

Which is the better way to share data between Fragments?

It totally depends on your use case, what data you share between fragments and how it is used.

Even though both cases support passing custom objects you have to consider a few things.

In the case of the navigation component, regarding custom objects:

  • data must be either Parcelable (preferred) or Serializable;
  • all of the data passed through using Bundle object which has its own limitations and with large data can lead to TransactionTooLargeException (more about it). It can be easily avoided when using shared view model.

In the case of shared view models:

  • you have to define new classes to wrap your models;
  • you have to add new class members to hold shared view models;
  • a shared view model will take memory until Activity is finished or the view model store is cleared.

So what is the answer?

Such a question leads to opinionated answers, but I consider following a set of hints to use when choosing between safe argument and shared view model.

Use safe arguments when:

  1. data is small;
  2. when you do not need to return results to the previous fragment after using this data;
  3. you do not care if this data is removed from memory when you are done with it and you have to query it again each time.

Use a shared view model when:

  1. data is relatively large or you assume it could be large;
  2. you have multiple data models/sets;
  3. when you do need to return results to the previous fragment after using this data;
  4. when data is expensive to query. Shared view models typically live as long as the activity.

Should I share my ViewModel across two fragments?

The Android documentation suggested sharing a ViewModel for "a common case of master-detail fragments" so I decided to do the same for my case.

As suggested, I scoped the ViewModel to the Activity in both fragments:

entryViewModel = activity?.run {
ViewModelProviders.of(this, EntryViewModelFactory(this.application, EntryRepository(this.application))).get(EntryViewModel::class.java)
} ?: throw Exception("Invalid Activity")

Here's the commit where I made the change:
https://github.com/randroid88/TodayILearned/commit/e307bd3f238e68a399a2a1619438770d908a606d



Related Topics



Leave a reply



Submit