web-dev-qa-db-fra.com

Partage de données entre fragments à l'aide du nouveau composant d'architecture ViewModel

Sur Google dernier IO, Google a publié un aperçu de certains nouveaux composants Arch, dont l’un, ViewModel.

Dans le docs , Google indique l’une des utilisations possibles de ce composant:

Il est très courant que deux ou plusieurs fragments d'une activité doivent communiquer l'un avec l'autre. Cela n’est jamais trivial, car les deux fragments doivent définir une description de l’interface et l’activité du propriétaire doit lier les deux. De plus, les deux fragments doivent gérer le cas où l'autre fragment n'est pas encore créé ou n'est pas visible.

Ce problème récurrent peut être résolu à l'aide d'objets ViewModel. Imaginons un cas courant de fragments maître-détail, dans lequel nous avons un fragment dans lequel l'utilisateur sélectionne un élément dans une liste et un autre fragment qui affiche le contenu de l'élément sélectionné.

Ces fragments peuvent partager un ViewModel en utilisant leur étendue d'activité pour gérer cette communication.

Et montre un exemple d'implémentation:

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

public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onActivityCreated() {
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends LifecycleFragment {
    public void onActivityCreated() {
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // update UI
        });
    }
}

J'étais très enthousiasmé par la possibilité de ne pas avoir besoin des interfaces utilisées pour les fragments afin de communiquer tout au long de l'activité.

Mais l'exemple de Google ne montre pas exactement comment j'appellerais le fragment de détail de master.

Je devrais toujours utiliser ne interface qui sera implémentée par l'activité, qui appellera fragmentManager.replace (...), ou existe-t-il un autre moyen de le faire en utilisant la nouvelle architecture?

42
alexpfx

Mis à jour le 6/12/2017,

Android Official fournit un exemple simple et précis pour illustrer le fonctionnement du ViewModel sur le modèle Master-Detail. Commencez par l'examiner . Partager des données entre des fragments

Comme @CommonWare, @Quang Nguyen l'a dit, le but de Yigit n'est pas de faire l'appel du maître au détail, mais plutôt d'utiliser le motif Homme du milieu. Mais si vous voulez faire une transaction de fragment, cela devrait être fait dans l'activité. À ce moment, la classe ViewModel doit être une classe statique dans Activity et peut contenir des rappels laids pour rappeler l'activité afin d'effectuer la transaction de fragment.

J'ai essayé d'implémenter ceci et de faire un projet simple à ce sujet. Vous pouvez y jeter un coup d'oeil. La plupart du code est référencé dans Google IO 2017, ainsi que dans la structure. https://github.com/charlesng/SampleAppArch

Je n'utilise pas le fragment de détail principal pour implémenter le composant, mais l'ancien (communication entre fragments dans ViewPager). La logique doit être la même.

Mais j'ai trouvé quelque chose d'important avec ces composants

  1. Ce que vous voulez envoyer et recevoir dans l’agent intermédiaire doit être envoyé et reçu dans le modèle de vue uniquement.
  2. La modification ne semble pas trop dans la classe fragment. Comme il ne fait que changer l'implémentation de "Rappel d'interface" à "Écouter et répondre ViewModel"
  3. Voir Le modèle initialize semble important et susceptible d’être appelé dans l’activité.
  4. Utilisation de MutableLiveData pour rendre la source synchronisée en activité uniquement.

Activité 1.Pager

public class PagerActivity extends LifecycleActivity {
    /**
     * 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 = ViewModelProviders.of(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 (Il méritait un meilleur nom plutôt que cela)

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

    public void init()
    {
        messageContainerA = new MutableLiveData<>();
        messageContainerA.setValue("Default Message");
        messageContainerB = new MutableLiveData<>();
        messageContainerB.setValue("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 LifecycleFragment {

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

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //setup the listener for the fragment A
        ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).getMessageContainerA().observe(this, 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);
        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) {
                ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).sendMessageToB("Hello B");
            }
        });
        return view;
    }

}

4.BlankFragmentB

public class BlankFragmentB extends LifecycleFragment {

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

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //setup the listener for the fragment B
        ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).getMessageContainerB().observe(this, 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);
        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) {
                ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).sendMessageToA("Hello A");

            }
        });
        return view;
    }

}
39
Long Ranger

J'ai trouvé une solution similaire à d'autres selon Google Codelabs exemple . J'ai deux fragments où l'un attend un changement d'objet dans l'autre et continue son processus avec l'objet mis à jour.

pour cette approche, vous aurez besoin d'une classe ViewModel comme ci-dessous:

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

}

et le fragment auditeur devrait ressembler à ceci:

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

enfin, le fragment updater peut ressembler à ceci:

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

Il est bon de mentionner que le fragment de programme de mise à jour peut être toute forme de fragments (pas uniquement DialogFragment) et que, pour utiliser ces composants d'architecture, ces lignes de codes devraient figurer dans le fichier build.gradle de votre application. source

dependencies {
  def lifecycle_version = "1.1.1"
  implementation "Android.Arch.lifecycle:extensions:$lifecycle_version"
}
9
Amir jodat

J'ai implémenté quelque chose de similaire à ce que vous voulez, mon modèle de vue contient l'objet LiveData qui contient l'état Enum, et lorsque vous souhaitez modifier le fragment de maître en détails (ou inversement), vous appelez des fonctions ViewModel qui modifient la valeur de vécu, et l'activité est connue. change le fragment car il observe un objet livesata.

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

TestActivité:

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

DétailFragment:

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();
    }
}
6
Alex

Avant que vous utilisiez un rappel attaché à Activity qui est considéré comme un conteneur.
Ce rappel est un intermédiaire entre deux fragments. Les inconvénients de cette solution précédente sont les suivants:

  • Activity doit porter le rappel, cela signifie beaucoup de travail pour Activity.
  • Deux fragments étant étroitement liés, il est difficile de mettre à jour ou de changer de logique ultérieurement.

Avec le nouveau ViewModel (avec support de LiveData), vous avez une solution élégante. Il joue maintenant un rôle d'intermédiaire auquel vous pouvez associer son cycle de vie à Activity.

  • La logique et les données entre deux fragments sont maintenant présentées dans ViewModel.
  • Two Fragment obtient les données/états de ViewModel, ils n'ont donc pas besoin de se connaître.
  • En outre, avec la puissance de LiveData, vous pouvez modifier le fragment de détail en fonction des modifications du fragment de maître en approche réactive au lieu du rappel précédent.

Vous vous débarrassez complètement du rappel qui lie étroitement l'activité et les fragments associés.
Je vous recommande vivement d'utiliser laboratoire de code de Google . À l'étape 5, vous pouvez trouver un exemple intéressant à ce sujet.

5
Quang Nguyen

Je finis par utiliser le propre ViewModel pour contenir le programme d'écoute qui déclenchera la méthode Activity. Semblable à la ancienne manière mais comme je l'ai dit, en passant l'auditeur à ViewModel au lieu du fragment. Donc, mon ViewModel ressemblait à ceci:

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

}

dans StepMasterActivity, je récupère le ViewModel et le définit comme écouteur:

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

...

Dans le fragment, je viens de récupérer le ViewModel

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

et appelez:

stepViewModel.select(step);

Je l'ai testé superficiellement et cela a fonctionné. En mettant en œuvre les autres fonctionnalités liées à cela, je serai au courant de tout problème pouvant survenir.

2
alexpfx