Обмен данными между фрагментами с использованием нового компонента архитектуры ViewModel

В последнем Google IO Google выпустил предварительный просмотр некоторых новых компонентов дуги, один из которых, ViewModel.

В документах google показывает одно из возможных вариантов использования этого компонента:

Очень часто бывает, что два или более фрагмента в действии должны взаимодействовать друг с другом. Это никогда не бывает тривиальным, поскольку оба фрагмента должны определять описание интерфейса, а активность владельца должна связывать их вместе. Более того, оба фрагмента должны обрабатывать случай, когда другой фрагмент еще не создан или невидим.

Эта общая точка боли может быть устранена с помощью объектов ViewModel. Представьте общий пример фрагментов мастер-детали, где у нас есть фрагмент, в котором пользователь выбирает элемент из списка и другой фрагмент, который отображает содержимое выбранного элемента.

Эти фрагменты могут совместно использовать ViewModel, используя их область действия для обработки этого сообщения.

И показывает пример реализации:

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

Я был очень взволнован возможностью не нуждаться в тех интерфейсах, которые использовались для фрагментов для связи через активность.

Но пример Google точно не показывает, как я могу назвать фрагмент детали из мастера.

Я все равно должен использовать интерфейс, который будет реализован в результате активности, которая вызовет функцию fragmentManager.replace(...), или есть другой способ сделать это с использованием новой архитектуры?

Ответ 1

Обновлено 6/12/2017,

Официальные лица Android предоставляют простой, точный пример, например, как работает ViewModel в шаблоне Master-Detail, вы должны сначала взглянуть на него. Обмен данными между фрагментами

Как @CommonWare, @Quang Nguyen метионизировал, Yigit не предназначен для того, чтобы сделать звонок от мастера до детали, но лучше использовать шаблон среднего человека. Но если вы хотите сделать некоторую транзакцию фрагмента, это должно быть сделано в действии. В этот момент класс ViewModel должен быть как статический класс в Activity и может содержать некоторый Ugly Callback для вызова активности для совершения транзакции фрагмента.

Я попытался реализовать это и сделать простой проект об этом. Вы можете взглянуть на него. В основном код ссылается на Google IO 2017, а также на структуру. https://github.com/charlesng/SampleAppArch

Я не использую мастер-фрагмент для реализации компонента, но старый (связь между фрагментом в ViewPager.) Логика должна быть одинаковой.

Но я нашел что-то важное, используя эти компоненты

  1. Что вы хотите отправить и получить в Среднем человеке, их следует отправлять и получать только в режиме просмотра модели
  2. Модификация в классе фрагментов не слишком велика. Поскольку это только изменяет реализацию с "Обратного вызова интерфейса" на "Прослушивание и просмотр ViewModel",
  3. View Model initialize кажется важным и, вероятно, будет вызван в действие.
  4. Использование MutableLiveData, чтобы источник синхронизировался только в действии.

Деятельность 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 (он заслуживает лучшего имени, а не этого)

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

}

Ответ 2

Прежде чем использовать обратный вызов, который присоединяется к Activity, который рассматривается как контейнер.
Этот обратный вызов является средним человеком между двумя фрагментами. Плохие вещи об этом предыдущем решении:

  • Активность должна нести обратный вызов, это означает большую работу для Activity.
  • Два фрагмента тесно связаны, их можно обновить или изменить позже.

С новым ViewModel (с поддержкой LiveData) у вас есть элегантное решение. Теперь он играет роль среднего человека, к которому вы можете привязать свой жизненный цикл к Activity.

  • Логика и данные между двумя фрагментами теперь выложены в ViewModel.
  • Два фрагмента получают данные/состояние из ViewModel, поэтому им не нужно знать друг друга.
  • Кроме того, с мощью LiveData вы можете изменить фрагмент детали, основанный на изменения мастер-фрагмента в реактивном подходе, а не на предыдущем обратном пути.

Теперь вы полностью избавляетесь от обратного вызова, который тесно связан с действиями и связанными с ними фрагментами.
Я настоятельно рекомендую вам через лабораторию Google. На шаге 5 вы можете найти хороший пример.

Ответ 3

Я реализовал что-то похожее на то, что вы хотите, моя модель просмотра содержит объект LiveData, который содержит состояние Enum, и когда вы хотите изменить фрагмент с основного на детали (или наоборот), вы вызываете функции ViewModel, которые изменяют значение прожиточного значения, и активность знает измените фрагмент, потому что он наблюдает объект livingata.

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

Перечни:

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

Ответ 4

Я в конечном итоге использую собственный ViewModel, чтобы удерживать слушателя, который вызовет метод Activity. Как и по- старому, но, как я сказал, передача слушателя в ViewModel вместо фрагмента. Итак, моя ViewModel выглядела так:

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

}

в StepMasterActivity Я получаю ViewModel и устанавливаю его как слушателя:

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

...

В фрагменте я просто извлекаю ViewModel

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

и звоните:

stepViewModel.select(step);

Я тестировал его поверхностно, и это сработало. Когда я приступаю к реализации других функций, связанных с этим, я буду знать о любых проблемах, которые могут возникнуть.

Ответ 5

Я нашел аналогичное решение, как и другие, в соответствии с примером google codelabs. У меня есть два фрагмента, где один из них ждет изменения объекта в другом и продолжает процесс с обновленным объектом.

для этого подхода вам понадобится класс ViewModel, как показано ниже:

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

}

и фрагмент слушателя должен выглядеть так:

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

наконец, фрагмент обновления может быть таким:

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

Хорошо отметить, что фрагмент обновления может быть любой формой фрагментов (а не только для DialogFragment), и для использования этих компонентов архитектуры вы должны иметь эти строки кодов в файле build.gradle приложения. источник

dependencies {
  def lifecycle_version = "1.1.1"
  implementation "android.arch.lifecycle:extensions:$lifecycle_version"
}

Ответ 6

Вы можете установить значения из Фрагмента детали в Мастер-фрагмент следующим образом:

model.selected.setValue(item)