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

75

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

文档中,Google 展示了这个组件的一个可能用途:

在一个 Activity 中,两个或多个 Fragment 经常需要相互通信。这从来都不是一件简单的事情,因为两个 Fragment 都需要定义一些接口描述,并且拥有者 Activity 必须将它们绑定在一起。此外,两个 Fragment 必须处理对方尚未创建或不可见的情况。

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

我对无需使用片段进行活动间通信的接口可能性感到非常兴奋。

但是,谷歌的示例未能准确展示如何从主片段调用详细信息片段。

我仍然需要使用一个由活动实现的接口(链接),该接口将调用fragmentManager.replace(...),或者使用新架构有其他方法可以做到这一点吗?


6
我并没有那样理解它。 我的理解是,通过共享的“ViewModel”,一个片段(细节)可以从另一个片段(主控)中找出数据更改的情况,而不是直接通信(“从主控调用细节片段”)。 你特别不想做直接通信,原因在引用中已经说明(“两个片段必须处理另一个片段尚未创建或不可见的情况”)。 - CommonsWare
嗯...我以为ViewModel会让我们只使用ViewModel进行通信来解决那段落中所述的问题,而不是像视频中所说的那样使用Activity。但我想你是对的,我仍然需要使用Activity来调用它。 - alexpfx
2
如果您在项目中使用导航架构组件,那么在片段之间共享数据非常容易。在导航组件中,您可以使用导航图作用域初始化ViewModel。这意味着同一导航图中的所有片段及其父Activity共享相同的ViewModel。 - Aminul Haque Aome
是的,在导航组件发布后,它变得更加容易了。 - alexpfx
8个回答

67

2017年6月12日更新:

Android官方提供了一个简单、精确的示例,用于演示Master-Detail模板下ViewModel的工作原理。您应该先查看一下此示例

正如@CommonWare、@Quang Nguyen所提到的,Yigit的目的不是从主控制器调用细节控制器,而是更好地使用中介者模式。但如果您想进行一些片段事务处理,则应在活动中完成。此时,ViewModel类应该作为活动中的静态类,并可能包含一些丑陋的回调来回调活动并使片段事务处理。

我尝试实现了这个功能,并做了一个简单的项目。您可以查看一下。大部分代码都是引用自Google IO 2017,也是结构的一部分。 https://github.com/charlesng/SampleAppArch

我没有使用Master Detail Fragment来实现组件,而是使用旧的(在ViewPager中的片段之间通信)。逻辑应该是相同的。

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

  1. 您希望在中介者中发送和接收的内容应仅在ViewModel中发送和接收。
  2. 片段类中的修改似乎不太多。因为它只是将实现从“接口回调”更改为“监听并响应ViewModel”
  3. View Model的初始化似乎很重要,可能会在活动中被调用。
  4. 使用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.分页代理视图模型(它应该有一个比这更好的名称)

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.空白片段A

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.空白片段B

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

}

15
LifecycleFragment 已被弃用。 - Codelaby
有没有关于带参数的ViewModel的解决方案,可以在构造函数中注入?我想让我的Activity通过工厂提供一组参数来创建ViewModel的实例。然后,我想获取此ViewModel的相同实例,而无需从Activity传递相同的参数到Fragments。这是否可能? - Євген Гарастович
@ЄвгенГарастович 1. 您应该像这样实现ViewModelProvider.Factory https://medium.com/@dpreussler/add-the-new-viewmodel-to-your-mvvm-36bfea86b159,以便您可以创建自己的ViewModelProvider来创建viewmodel实例。 - Long Ranger
1
获取相同的ViewModel实例,只需将getActivity()放入ViewModelProvider中,它将从getactivity中获取实例(如果已创建)。 - Long Ranger
1
@Long Ranger。这意味着我需要将工厂的实例传递给我的片段,以获得相同的ViewModel实例,这基本上与传递参数是相同的。这种做法感觉不对,所以我想知道是否有一种更好的方法来解决这个问题。 - Євген Гарастович
显示剩余3条评论

41
根据官方Google教程,现在您可以通过by activityViewModels()获得共享视图模型。
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()

17
针对 Kotlin 的一个迫切需要的解决方案是,在使用 viewModels() 而非 activityViewModels() 的情况下,数据不会被共享。请注意不要改变原文意思,使翻译更加通俗易懂。 - secretshardul
2
文档中没有说明,但我们是否需要先在容器活动中初始化ViewModel?仅在两个片段中添加activityViewModels()的SharedViewModel并不能为我创建ViewModel。 - Ali Akber

20

根据谷歌代码实验室的示例,我发现了与其他人类似的解决方案。我有两个片段,其中一个等待另一个对象的更改,并使用更新后的对象继续其过程。

为此方法,您需要一个如下所示的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);
   }
}

值得一提的是,更新片段(updater fragment)可以是任何形式的片段(不仅限于DialogFragment),为了使用这些架构组件,您应该在应用程序的build.gradle文件中添加以下代码行。 来源

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

model.item.observe(getActivity(), new Observer<YourObjectModel>(){ } 使用 getActivity 是正确的选择。我之前使用了 viewLifecycleOwner。 - Muhammad Irfan

7
我实现了类似于您想要的功能,我的ViewModel包含一个LiveData对象,其中包含枚举状态。当您想要从主页面更改到详细信息页面(或反之亦然)时,您调用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;
        }
    }
}

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

详情碎片:

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

1
在onCreateView中初始化ViewModel会导致当Fragment的方向改变时出现NPE。 - Prakash
另一种类似于枚举状态的方法是在ViewModel中添加一个名为navigate()的方法,该方法将发出任何值,在Activity中找出顶部的Fragment并根据其导航到下一个Fragment(或任何Fragment事务)。 - sat

6

在使用一个回调函数之前,它会附加到被认为是容器的Activity中。
这个回调函数是两个Fragment之间的中间人。 这种先前解决方案的问题如下:

  • Activity必须承载回调函数,这意味着Activity需要做很多工作。
  • 两个Fragment紧密耦合,以后更新或更改逻辑很困难。

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

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

现在您完全摆脱了与Activity和相关Fragment紧密耦合的回调函数。
我强烈建议您通过Google的代码实验室。在第5步中,您可以找到一个很好的示例。


2

我最终使用自己的ViewModel来保存监听器,以触发Activity方法。类似于旧的方式,但是我将监听器传递给了ViewModel而不是Fragment。因此,我的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);

我进行了表面测试,它可以正常工作。在我实现与此相关的其他功能时,我将注意可能出现的任何问题。


这里提出的大多数解决方案,包括这个,存在一个问题,即如果有两个以上共享的Fragment,则很有可能显示错误的数据,因为LiveData始终会显示最新发布的数据。 - Otieno Rowland

1

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

  • 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)
              }
          }
      }
    
上述方法是一个好的实践,因为它可以避免由于空指针异常而导致的崩溃。
编辑:正如btraas所补充的那样:activity被编译成了被标记为@Nullable的getActivity()。 activity和getActivity()都是可访问且等效的。

activity被编译为getActivity(),在android SDK中标记为@NullableactivitygetActivity()都是可访问且等效的。 - btraas
是的,我可能表达不清楚。你可以使用getActivity(),但这不是Kotlin的方式。很好的评论,btraas。 - Pedro Henrique

-3
您可以像这样从详细片段设置主片段的值。
model.selected.setValue(item)

是的,就像我在问题中展示的谷歌示例一样,您只需返回翻译后的文本即可 :) - alexpfx
你是在谈论片段事务吗? (但是Google的示例并没有完全展示我如何从主要视图调用详细片段)。 - santhosh
我想知道是否有一种方法可以使用新组件直接调用另一个片段。但我认为它们的目的就是解决这类问题。 - alexpfx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接