使用新的架构组件 ViewModel 在片段之间共享数据

Posted

技术标签:

【中文标题】使用新的架构组件 ViewModel 在片段之间共享数据【英文标题】:Sharing data between fragments using new architecture component ViewModel 【发布时间】:2017-10-31 13:24:29 【问题描述】:

在 Last Google IO 上,Google 发布了一些新架构组件的预览,其中之一是 ViewModel。

在docs google 中显示了此组件的一种可能用途:

一个活动中的两个或多个片段需要 互相交流。这绝不是微不足道的,因为这两个片段 需要定义一些接口描述,并且所有者活动必须 将两者绑定在一起。而且,两个片段都必须处理这个案子 另一个片段尚未创建或不可见。

这个常见的痛点可以通过使用 ViewModel 对象来解决。 想象一个主从片段的常见情况,我们有一个 用户从列表中选择一个项目和另一个 显示所选项目内容的片段。

这些片段可以使用它们的活动范围共享一个 ViewModel 处理此通信。

并给出一个实现示例:

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

我对不需要那些用于 Fragment 的接口通过 Activity 进行通信的可能性感到非常兴奋。

但 Google 的示例并未准确显示我将如何从 master 调用详细片段。

我仍然必须使用将由 Activity 实现的 an interface,它将调用 fragmentManager.replace(...),或者还有其他方法可以使用新架构?

【问题讨论】:

我没有这样解释。我将其解释为一个片段(详细信息)可以通过共享的ViewModel 了解另一个片段(主)的数据更改,而不是片段将直接通信(“从主控调用详细信息片段”)。由于引用段落中概述的原因,您特别想要进行直接通信(“两个片段必须处理另一个片段尚未创建或不可见的情况”)。 嗯.. 我认为 viewmodel 将允许我们解决该段中解释的问题,使用 viewmodel 进行通信,而不是活动,如本视频所述:youtu.be/bEKNi1JOrNs?t=2005。但我认为你是对的,我仍然使用活动来调用它。 如果您在项目中使用导航架构组件,则在片段之间共享数据非常容易。在 Navigation 组件中,您可以使用导航图范围初始化 ViewModel。这意味着同一个导航图中的所有 Fragment 及其父 Activity 共享同一个 ViewModel。 是的,导航组件发布后变得容易多了。 【参考方案1】:

于 2017 年 6 月 12 日更新,

Android 官方提供了一个简单、精确的例子来说明 ViewModel 如何在 Master-Detail 模板上工作,你应该先看看它。Share data between fragments

正如@CommonWare,@Quang Nguyen 所说,Yigit 的目的不是从大师到细节的调用,而是更好地使用中间人模式。但是如果你想做一些片段事务,应该在活动中完成。此时,ViewModel 类应该是 Activity 中的静态类,并且可能包含一些 Ugly Callback 来回调 Activity 以进行 Fragment 事务。

我已经尝试实现这一点并为此做了一个简单的项目。你可以看看它。大部分代码引用自 Google IO 2017,结构也是如此。 https://github.com/charlesng/SampleAppArch

我没有使用Master Detail Fragment来实现组件,而是旧的(ViewPager中fragment之间的通信)。逻辑应该是一样的。

但我发现使用这些组件很重要

    您想在中间人中发送和接收的内容,应该只在 View Model 中发送和接收 片段类中的修改似乎不是太多。因为它只是将实现从“接口回调”更改为“监听和响应 ViewModel” 视图模型初始化似乎很重要,可能会在活动中被调用。 使用 MutableLiveData 使源仅在活动中同步。

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(它应该有一个更好的名字而不是这个)

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;
    


【讨论】:

LifecycleFragment 已弃用 有参数的​​ ViewModels 的解决方案,注入 cunstructor 吗?我希望我的 Activity 创建一个 ViewModel 实例,通过工厂提供一组参数。然后我想获得这个视图模型的相同实例,而不必将相同的参数从活动传递到片段。这甚至可能吗? @ЄвгенГарастович 1. 你应该像 medium.com/@dpreussler/… 这样实现 ViewModelProvider.Factory 以便你可以创建自己的 ViewModelProvider 来创建 viewmodel 的实例。 2.获取视图模型的相同实例,您可以将 getActivity() 放入 ViewModelProvider 中,然后如果创建了它,它将从 getactivity 中获取实例。 @Long Ranger。这意味着我必须将 Factory 的实例传递给我的 Fragments 以获得相同的 ViewModel 实例,这与传递参数基本相同。只是感觉不对,所以我想知道是否有一种干净的方法来做到这一点【参考方案2】:

正如the official Google tutorial 中所写,现在您可以使用by activityViewModels() 获得共享视图模型

// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()

【讨论】:

Kotlin 急需的解决方案。如果使用viewModels() 而不是activityViewModels(),则不会共享数据。 文档中没有说任何地方,但是我们必须先在容器活动中初始化ViewModel吗?仅在两个片段中通过 activityViewModels() 添加 SharedViewModel 并没有为我创建 ViewModel。【参考方案3】:

根据 google codelabs example,我找到了与其他人类似的解决方案。 我有两个片段,其中一个等待另一个中的对象更改,并使用更新的对象继续其过程。

对于这种方法,您需要一个 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 文件中包含这些代码行。 source

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

【讨论】:

model.item.observe(getActivity(), new Observer() getActivity 是正确的使用方法。我使用的是 viewLifecycleOwner【参考方案4】:

在使用附加到被视为容器的 Activity 的回调之前。 该回调是两个片段之间的中间人。 以前的解决方案的坏处是:

Activity要携带回调,这意味着很多工作 活动。 两个Fragment紧密耦合,以后很难更新或改变逻辑。

有了新的 ViewModel(支持 LiveData),您就有了一个优雅的解决方案。它现在扮演中间人的角色,您可以将其生命周期附加到 Activity。

两个 Fragment 之间的逻辑和数据现在在 ViewModel 中布局。 两个 Fragment 从 ViewModel 获取数据/状态,因此它们不需要相互了解。 此外,借助 LiveData 的强大功能,您可以根据主 Fragment 的变化以响应方式而不是以前的回调方式来更改详细 Fragment。

您现在完全摆脱了与 Activity 和相关 Fragment 紧密耦合的回调。 我强烈推荐您通过Google's code lab。在第 5 步中,您可以找到一个很好的示例。

【讨论】:

【参考方案5】:

我实现了与您想要的类似的东西,我的 viewmodel 包含 LiveData 对象,该对象包含 Enum 状态,当您想要将片段从 master 更改为 details(或反向)时,您调用 ViewModel 函数来更改 livedata 值,并且Activity 知道要更改片段,因为它正在观察 livedata 对象。

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
    

测试活动:

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;
        
    

主片段:

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

【讨论】:

在onCreateView中初始化viewmodel会在片段方向改变时导致NPE。 另一种类似的方法代替枚举状态是在 ViewModel 中有一个名为 navigate() 的方法,该方法将发出任何值并在活动中找出哪个片段位于顶部并导航到下一个片段,具体取决于那。 (或任何片段交易)【参考方案6】:

我最终使用自己的 ViewModel 来支持将触发 Activity 方法的侦听器。类似于old way,但正如我所说,将侦听器传递给 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);

我对它进行了表面测试,它确实有效。当我着手实现与此相关的其他功能时,我会意识到可能发生的任何问题。

【讨论】:

这里建议的大多数解决方案(包括这个)的问题是,如果有两个以上的共享片段,则很有可能显示错误的数据,因为 LiveData 将始终显示最新的发布数据。【参考方案7】:

对于那些使用 Kotlin 的人,请尝试以下方法:

将 androidx ViewModel and LiveData 库添加到您的 gradle 文件中

像这样在片段中调用你的视图模型:

  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)
          
      
  

上述方法是一个很好的做法,因为它可以避免由于空指针异常而导致的崩溃

编辑:作为btraas 补充:活动被编译成getActivity(),在android SDK 中被标记为@Nullable。 activity 和 getActivity() 都是可访问且等效的。

【讨论】:

activity 编译成getActivity(),在android SDK 中标记为@NullableactivitygetActivity() 都是可访问且等效的。 是的,我可能表达得很糟糕。你可以使用 getActivity(),但这不是 kotlin 的方式。不错的评论,btraas。【参考方案8】:

您可以像这样设置从 Detail Fragment 到 Master Fragment 的值

model.selected.setValue(item)

【讨论】:

是的,就像我在问题中展示的谷歌示例一样:) 你是在谈论片段交易吗(但 Google 的示例并未准确显示我将如何从 master 调用详细片段)。 是的。我想知道的是,是否有一种方法可以让一个片段直接使用新组件调用另一个片段。但我认为解决这类问题是他们的目的。

以上是关于使用新的架构组件 ViewModel 在片段之间共享数据的主要内容,如果未能解决你的问题,请参考以下文章

在 ViewModel 之间共享数据

在 SwiftUI 组件之间共享 ViewModel

如何使用新的导航架构组件从扩展 BroadcastReceiver 的类导航到片段

在 FAB 单击时导航到片段(导航架构组件)

MVVM 架构,ViewModel和LiveData

导航架构组件 - 对话框片段