Sharing data between fragments using new architecture component ViewModel
C

8

76

On Last Google IO, Google released a preview of some new arch components, one of which, ViewModel.

In the docs google shows one of the possible uses for this component:

It is very common that two or more fragments in an activity need to communicate with each other. This is never trivial as both fragments need to define some interface description, and the owner activity must bind the two together. Moreover, both fragments must handle the case where the other fragment is not yet created or not visible.

This common pain point can be addressed by using ViewModel objects. Imagine a common case of master-detail fragments, where we have a fragment in which the user selects an item from a list and another fragment that displays the contents of the selected item.

These fragments can share a ViewModel using their activity scope to handle this communication.

And shows a implementation example:

public class SharedViewModel extends ViewModel {
    private final SavedStateHandle state;

    public SharedViewModel(SavedStateHandle state) {
        this.state = state;
    }

    private final MutableLiveData<Item> selected = state.getLiveData("selected");

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class MasterFragment extends Fragment {
    private SharedViewModel model;

    @Override
    protected void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        model = new ViewModelProvider(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    @Override
    protected void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SharedViewModel model = new ViewModelProvider(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // update UI
        });
    }
}

I was quite excited about the possibility of not needing those interfaces used for fragments to communicate through the activity.

But Google's example does not show exactly how would I call the detail fragment from master.

I'd still have to use an interface that will be implemented by the activity, which will call fragmentManager.replace(...), or there is another way to do that using the new architecture?

Capwell answered 30/5, 2017 at 22:30 Comment(4)
I didn't interpret it that way. I interpreted it as one fragment (detail) can find out about data changes from another fragment (master) via the shared ViewModel, not that the fragments would be in direct communication ("call the detail fragment from master"). You specifically don't want to do that direct communication, for the reasons outlined in the quoted passage ("both fragments must handle the case where the other fragment is not yet created or not visible").Blakney
Hmm.. I thought the viewmodel would allow us just attack the problem explained in that paragraph, using the viewmodel for communication, and not the activity, as was said in this video: youtu.be/bEKNi1JOrNs?t=2005. But I think you're right, I still have call it using activity.Capwell
Sharing data between fragments is super easy if you use Navigation Architecture Component in your project. In the Navigation component, you can initialize a ViewModel with a navigation graph scope. This means all the fragments in the same navigation graph and their parent Activity share the same ViewModel.Superficies
yes, it become much easier after the release of navigation components.Capwell
P
67

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

}
Proscription answered 20/6, 2017 at 14:32 Comment(8)
LifecycleFragment is deprecatedCelestinacelestine
is there a solution for ViewModels with parameters, injected in cunstructor? I want my Activity to create an instance of ViewModel, providing a set of parameters through a Factory. Then i want to get the instance same instance of this viewmodel without having to pass the same parameters from activity to fragments. Is that even possible?Periapt
@ЄвгенГарастович 1. You should implement ViewModelProvider.Factory like this medium.com/@dpreussler/… so that you can make your own ViewModelProvider to create instance of viewmodel.Proscription
2. Get the same instance of the viewmodel you can just put getActivity() inside the ViewModelProvider, then it will get the instance from the getactivity if it is created.Proscription
@Long Ranger. This means i'd have to pass the instance of the Factory to my Fragments as well to get the same instance of the ViewModel which is basically the same thing as passing the parameters. It just feels wrong, so I wondered if there was a clean way of doing thisPeriapt
@ЄвгенГарастович did you figure out a clean way of passing the factories to the fragment?Wilton
yeah. It turned out that you don't need a factory for the fragments. Just using the default one is enough to get you the instance of the viewModel in fragments assuming that the parent activity has already created it using the proper factoryPeriapt
@ЄвгенГарастович it is, assuming you create the viewModel before the Activity's super.onCreate.Succentor
M
42

As written in the official Google tutorial now you may obtain a shared view model with by activityViewModels()

// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
Marcimarcia answered 20/4, 2020 at 14:13 Comment(2)
Much needed solution for Kotlin. Data is not shared if viewModels() is used instead of activityViewModels().Grotius
It doesn't say anywhere in the doc, but do we have to initialize the ViewModel in the container activity first? Just adding SharedViewModel by activityViewModels() in both fragments isn't creating the ViewModel for me.Undertrump
C
20

I have found a similar solution as others according to google codelabs example. I have two fragments where one of them wait for an object change in the other and continues its process with updated object.

for this approach you will need a ViewModel class as below:

import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import yourPackage.YourObjectModel;

public class SharedViewModel extends ViewModel {

   public MutableLiveData<YourObjectModel> item = new MutableLiveData<>();

   public YourObjectModel getItem() {
      return item.getValue();
   }

   public void setItem(YourObjectModel item) {
      this.item.setValue(item);
   }

}

and the listener fragment should look like this:

public class ListenerFragment extends Fragment{
   private SharedViewModel model;
  @Override
  public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);

    model.item.observe(getActivity(), new Observer<YourObjectModel>(){

        @Override
        public void onChanged(@Nullable YourObjectModel updatedObject) {
            Log.i(TAG, "onChanged: recieved freshObject");
            if (updatedObject != null) {
                // Do what you want with your updated object here. 
            }
        }
    });
}
}

finally, the updater fragment can be like this:

public class UpdaterFragment extends DialogFragment{
    private SharedViewModel model;
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
   }
   // Call this method where it is necessary
   private void updateViewModel(YourObjectModel yourItem){
      model.setItem(yourItem);
   }
}

It is good to mention that the updater fragment can be any form of fragments(not DialogFragment only) and for using these architecture components you should have these lines of code in your app build.gradle file. source

dependencies {
  def lifecycle_version = "1.1.1"
  implementation "android.arch.lifecycle:extensions:$lifecycle_version"
}
Confiding answered 2/10, 2018 at 15:26 Comment(1)
model.item.observe(getActivity(), new Observer<YourObjectModel>(){ } getActivity was the right thing to use. I was using viewLifecycleOwnerIntrospection
B
7

I implemented something similar to what you want, my viewmodel contains LiveData object that contains Enum state, and when you want to change the fragment from master to details (or in reverse) you call ViewModel functions that changing the livedata value, and activity know to change the fragment because it is observing livedata object.

TestViewModel:

public class TestViewModel extends ViewModel {
    private MutableLiveData<Enums.state> mState;

    public TestViewModel() {
        mState=new MutableLiveData<>();
        mState.setValue(Enums.state.Master);
    }

    public void onDetail() {
        mState.setValue(Enums.state.Detail);
    }

    public void onMaster() {
        mState.setValue(Enums.state.Master);
    }

    public LiveData<Enums.state> getState() {

        return mState;
    }
}

Enums:

public class Enums {
    public enum state {
        Master,
        Detail
    }
}

TestActivity:

public class TestActivity extends LifecycleActivity {
    private ActivityTestBinding mBinding;
    private TestViewModel mViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding=DataBindingUtil.setContentView(this, R.layout.activity_test);
        mViewModel=ViewModelProviders.of(this).get(TestViewModel.class);
        mViewModel.getState().observe(this, new Observer<Enums.state>() {
            @Override
            public void onChanged(@Nullable Enums.state state) {
                switch(state) {
                    case Master:
                        setMasterFragment();
                        break;
                    case Detail:
                        setDetailFragment();
                        break;
                }
            }
        });
    }

    private void setMasterFragment() {
        MasterFragment masterFragment=MasterFragment.newInstance();
        getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, masterFragment,"MasterTag").commit();
    }

    private void setDetailFragment() {
        DetailFragment detailFragment=DetailFragment.newInstance();
        getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, detailFragment,"DetailTag").commit();
    }

    @Override
    public void onBackPressed() {
        switch(mViewModel.getState().getValue()) {
            case Master:
                super.onBackPressed();
                break;
            case Detail:
                mViewModel.onMaster();
                break;
        }
    }
}

MasterFragment:

public class MasterFragment extends Fragment {
    private FragmentMasterBinding mBinding;


    public static MasterFragment newInstance() {
        MasterFragment fragment=new MasterFragment();
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_master, container, false);
        mBinding.btnDetail.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
                viewModel.onDetail();
            }
        });

        return mBinding.getRoot();
    }
}

DetailFragment:

public class DetailFragment extends Fragment {
    private FragmentDetailBinding mBinding;

    public static DetailFragment newInstance() {
        DetailFragment fragment=new DetailFragment();
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_detail, container, false);
        mBinding.btnMaster.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
                viewModel.onMaster();
            }
        });
        return mBinding.getRoot();
    }
}
Biles answered 26/6, 2017 at 10:15 Comment(2)
Initializing viewmodel inside onCreateView will cause NPE when orientation of fragment changes .Ramah
Another similar approach instead of enum states is to have a method called navigate() in ViewModel, which will emit any value and in activity find out which fragment is on the top and navigate to next fragment depending on that. (or any fragment transaction)Casillas
B
6

Before you are using a callback which attaches to Activity which is considered as a container.
That callback is a middle man between two Fragments. The bad things about this previous solution are:

  • Activity has to carry the callback, it means a lot of work for Activity.
  • Two Fragments are coupled tightly, it is difficult to update or change logic later.

With the new ViewModel (with support of LiveData), you have an elegant solution. It now plays a role of middle man which you can attach its lifecycle to Activity.

  • Logic and data between two Fragments now lay out in ViewModel.
  • Two Fragment gets data/state from ViewModel, so they do not need to know each other.
  • Besides, with the power of LiveData, you can change detail Fragment based on changes of master Fragment in reactive approach instead of previous callback way.

You now completely get rid of callback which tightly couples to both Activity and related Fragments.
I highly recommend you through Google's code lab. In step 5, you can find an nice example about this.

Berky answered 31/5, 2017 at 0:24 Comment(0)
C
2

I end up using the own ViewModel to hold up the listener that will trigger the Activity method. Similar to the old way but as I said, passing the listener to ViewModel instead of the fragment. So my ViewModel looked like this:

public class SharedViewModel<T> extends ViewModel {

    private final MutableLiveData<T> selected = new MutableLiveData<>();
    private OnSelectListener<T> listener = item -> {};

    public interface OnSelectListener <T> {
        void selected (T item);
    }


    public void setListener(OnSelectListener<T> listener) {
        this.listener = listener;
    }

    public void select(T item) {
        selected.setValue(item);
        listener.selected(item);
    }

    public LiveData<T> getSelected() {
        return selected;
    }

}

in StepMasterActivity I get the ViewModel and set it as a listener:

StepMasterActivity.class:

SharedViewModel stepViewModel = ViewModelProviders.of(this).get("step", SharedViewModel.class);
stepViewModel.setListener(this);

...

@Override
public void selected(Step item) {
    Log.d(TAG, "selected: "+item);
}

...

In the fragment I just retrieve the ViewModel

stepViewModel = ViewModelProviders.of(getActivity()).get("step", SharedViewModel.class);

and call:

stepViewModel.select(step);

I tested it superficially and it worked. As I go about implementing the other features related to this, I will be aware of any problems that may occur.

Capwell answered 21/6, 2017 at 2:55 Comment(1)
The problem with most solutions suggested here, including this one, is that, if there are more than two shared Fragments, there is a good chance of showing the wrong data, since LiveData will always show the latest data posted.Counterclockwise
P
1

For those using Kotlin out there try the following approach:

  • Add the androidx ViewModel and LiveData libraries to your gradle file

  • Call your viewmodel inside the fragment like this:

      class MainFragment : Fragment() {
    
          private lateinit var viewModel: ViewModel
    
          override fun onActivityCreated(savedInstanceState: Bundle?) {
              super.onActivityCreated(savedInstanceState)
    
              // kotlin does not have a getActivity() built in method instead we use activity, which is null-safe
              activity?.let {
                  viemModel = ViewModelProvider(it).get(SharedViewModel::class.java)
              }
          }
      }
    

The above method is a good practice since it will avoid crashes due to null pointer exceptions

Edit: As btraas complemented: activity is compiled into getActivity() which is marked as @Nullable in the android SDK. activity and getActivity() are both accessible and equivalent.

Papilla answered 20/11, 2020 at 13:44 Comment(2)
activity is compiled into getActivity() which is marked as @Nullable in the android SDK. activity and getActivity() are both accessible and equivalent.Consumptive
Yeah, I may have expressed myself badly. you can use getActivity(), but it's not the kotlin way to d it. Nice comment, btraas.Papilla
H
-3

You can set values from Detail Fragment to Master Fragment like this

model.selected.setValue(item)
Harms answered 1/6, 2017 at 9:50 Comment(3)
yes, as in the example by google I showed in the question :)Capwell
are you speaking about fragment transaction (But Google's example does not show exactly how would I call the detail fragment from master ).Harms
yes. What I would like to know is if there is a way to one fragment call another directly using the new components. But I think it's their purpose to solve this kind of problem.Capwell

© 2022 - 2024 — McMap. All rights reserved.