75

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?

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
alexpfx
  • 6,412
  • 12
  • 52
  • 88
  • 6
    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"). – CommonsWare May 30 '17 at 22:36
  • 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: https://youtu.be/bEKNi1JOrNs?t=2005. But I think you're right, I still have call it using activity. – alexpfx May 30 '17 at 23:01
  • 2
    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. – Aminul Haque Aome Nov 19 '19 at 05:24
  • yes, it become much easier after the release of navigation components. – alexpfx Nov 19 '19 at 12:55

8 Answers8

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

}
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
Long Ranger
  • 5,888
  • 8
  • 43
  • 72
  • 15
    LifecycleFragment is deprecated – Codelaby Jan 24 '18 at 13:48
  • 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? – Євген Гарастович May 11 '18 at 07:36
  • @ЄвгенГарастович 1. You should implement ViewModelProvider.Factory like this https://medium.com/@dpreussler/add-the-new-viewmodel-to-your-mvvm-36bfea86b159 so that you can make your own ViewModelProvider to create instance of viewmodel. – Long Ranger May 11 '18 at 10:22
  • 1
    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. – Long Ranger May 11 '18 at 10:23
  • 1
    @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 this – Євген Гарастович May 12 '18 at 18:48
  • @ЄвгенГарастович did you figure out a clean way of passing the factories to the fragment? – Dipo Areoye Nov 06 '19 at 21:56
  • 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 factory – Євген Гарастович Nov 07 '19 at 22:07
  • @ЄвгенГарастович it is, assuming you create the viewModel before the Activity's `super.onCreate`. – EpicPandaForce Jan 13 '21 at 17:27
41

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()
Daniel
  • 2,415
  • 3
  • 24
  • 34
  • 16
    Much needed solution for Kotlin. Data is not shared if `viewModels()` is used instead of `activityViewModels()`. – secretshardul Nov 10 '20 at 13:03
  • 2
    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. – Ali Akber Jun 07 '21 at 04:40
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"
}
starball
  • 20,030
  • 7
  • 43
  • 238
Amir jodat
  • 569
  • 6
  • 13
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();
    }
}
Alex
  • 9,102
  • 3
  • 31
  • 35
  • 1
    Initializing viewmodel inside onCreateView will cause NPE when orientation of fragment changes . – Prakash Apr 21 '18 at 17:38
  • 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) – sat Jun 22 '18 at 09:54
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.

Quang Nguyen
  • 2,600
  • 2
  • 17
  • 24
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.

alexpfx
  • 6,412
  • 12
  • 52
  • 88
  • 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. – Otieno Rowland Jan 06 '19 at 20:22
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.

Pedro Henrique
  • 131
  • 1
  • 4
  • `activity` is compiled into `getActivity()` which is marked as `@Nullable` in the android SDK. `activity` and `getActivity()` are both accessible and equivalent. – btraas Dec 09 '20 at 19:09
  • Yeah, I may have expressed myself badly. you can use getActivity(), but it's not the kotlin way to d it. Nice comment, btraas. – Pedro Henrique Dec 11 '20 at 00:28
-3

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

model.selected.setValue(item)
santhosh
  • 31
  • 7
  • yes, as in the example by google I showed in the question :) – alexpfx Jun 01 '17 at 09:56
  • are you speaking about fragment transaction (But Google's example does not show exactly how would I call the detail fragment from master ). – santhosh Jun 01 '17 at 10:01
  • 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. – alexpfx Jun 01 '17 at 10:09