第二十章 数据绑定与MVVM
有关MVVM框架
MVVM是将原先的MVC框架下的C(控制器)进行功能拆分,变成独立的小类来进行协同工作,这样可以将控制器中臃肿的代码抽到布局文件里(如寻找并绑定一个组件、设置这个组件内容等等),也将部分动态控制器代码放到ViewModel(避免破坏框架的责任单一性原则)中去,方便开发和验证。
有关数据绑定
启用数据绑定
在app/build.gradle中添加:
dataBinding
{
enabled=true;
}
不过这个写法已经过时了,可以改写成:
buildFeatures{
dataBinding = true
// for view binding :
// viewBinding = true
}
这一步是允许项目长生数据绑定类(自动生成)。如果布局需要使用数据绑定,还需要改造成数据绑定布局,级把整个布局定义放入到<layout>标签中:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"> </androidx.recyclerview.widget.RecyclerView> </layout>
此时该布局文件就已经有了自己的绑定类,在实例化视图层级结构时,不需要再使用LayoutInflater,而是实例化这个绑定类。实例化完成之后,就可以获取并配置其中的组件了。
@Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { FragmentBeatBoxBinding binding= DataBindingUtil.inflate(inflater,R.layout.fragment_beat_box,container,false); binding.recyclerView.setLayoutManager(new GridLayoutManager(getActivity(),3)); binding.recyclerView.setAdapter(new SoundAdapter(mBeatBox.getSounds())); return binding.getRoot(); }
private class SoundHolder extends RecyclerView.ViewHolder
{
private ListItemSoundBinding mBinding;
private SoundHolder(ListItemSoundBinding binding)
{
super(binding.getRoot());
mBinding=binding;
mBinding.setViewModel(new SoundViewModel(mBeatBox));
}
}
private class SoundAdapter extends RecyclerView.Adapter<SoundHolder>
{
@NonNull
@Override
public SoundHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater=LayoutInflater.from(getActivity());
ListItemSoundBinding binding=DataBindingUtil.inflate(inflater,R.layout.list_item_sound,viewGroup,false);
return new SoundHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull SoundHolder soundHolder, int i) {
}
@Override
public int getItemCount() {
return 0;
}
}
可以对比一下先前的方式:
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle saveInstanceState) { View view=inflater.inflate(R.layout.fragment_crime_list,container,false); mCrimeRecyclerView=(RecyclerView)view.findViewById(R.id.crime_recycler_view); mCrimeRecyclerView.setLayoutManager(new WrapContentLinearLayoutManager(getActivity())); ...... return view; }
private class CrimeHolder extends Holder implements View.OnClickListener
{
public CrimeHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater.inflate(R.layout.list_item_crime,parent,false));
itemView.setOnClickListener(this);
mTitleTextVIew=(TextView) itemView.findViewById(R.id.crime_title);
mDateTextVIew=(TextView)itemView.findViewById(R.id.crime_date);
}
}
private class CrimeAdapter extends RecyclerView.Adapter<Holder> implements ItemHelper
{
...
@Override
public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater=LayoutInflater.from(getActivity());
return new CrimeHolder(layoutInflater,parent);
}
@Override
public void onBindViewHolder(Holder holder, int position) {
...
}
@Override
public int getItemCount() {
return mCrimes.size();
}
...
}
目前的变化是不需要用findViewById来获取视图了。
绑定数据
要绑定数据,还需要在文件中用<data>标签声明数据对象,再用绑定操作符@{}就可以在布局文件中直接使用数据对象的值。
为了保持架构的责任的单一划分,不能将模型层直接参与到视图的显示过程中,因此引入视图模型(ViewModel)对象来配合数据绑定使用以显示视图。
首先需要创建一个视图模型:
public class SoundViewModel extends BaseObservable {
private Sound mSound;
private BeatBox mBeatBox;
public SoundViewModel(BeatBox beatBox)
{
mBeatBox=beatBox;
}
public String getTitle()
{
return mSound.getName();
}
@Bindable
public Sound getSound()
{
return mSound;
}
public void setSound(Sound sound)
{
mSound=sound;
notifyChange();
}
}
然后是按照数据绑定的方法将数据绑定至模型层,第一步是在布局文件中声明属性:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="viewModel" type="com.shopkeeper.beatboxchallenge.SoundViewModel" /> </data> ... </layout>
原先产生的绑定类上,现在又定义了一个叫viewModel的属性,其使用是就是先前定义的SoundViewModel类,同时还产生了viewModel的getter和setter方法。
第二步是绑定数据到按钮名称
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.shopkeeper.beatboxchallenge.SoundViewModel" />
</data>
<Button
android:layout_width="match_parent"
android:layout_height="120dp"
android:text="@{viewModel.title}"
tools:text="Sound name" />
</layout>
之后就是关联到使用视图模型,即是对Adapter和ViewHolder的操作,可以结合先前的方式对比着看。
现在:
private class SoundHolder extends RecyclerView.ViewHolder
{
private ListItemSoundBinding mBinding;
private SoundHolder(ListItemSoundBinding binding)
{
super(binding.getRoot());
mBinding=binding;
mBinding.setViewModel(new SoundViewModel(mBeatBox));
}
public void bind(Sound sound)
{
mBinding.getViewModel().setSound(sound);
mBinding.executePendingBindings();
}
}
private class SoundAdapter extends RecyclerView.Adapter<SoundHolder>
{
private List<Sound> mSounds;
public SoundAdapter(List<Sound> sounds)
{
mSounds=sounds;
}
@NonNull
@Override
public SoundHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater=LayoutInflater.from(getActivity());
ListItemSoundBinding binding=DataBindingUtil.inflate(inflater,R.layout.list_item_sound,viewGroup,false);
return new SoundHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull SoundHolder soundHolder, int i) {
Sound sound=mSounds.get(i);
soundHolder.bind(sound);
}
@Override
public int getItemCount() {
return mSounds.size();
}
}
之前:
private class CrimePoliceHolder extends Holder implements View.OnClickListener { private Button mButton; private TextView mTitleTextVIew; private TextView mDateTextVIew; private Crime mCrime; public CrimePoliceHolder(LayoutInflater inflater, ViewGroup parent) { super(inflater.inflate(R.layout.list_item_police,parent,false)); itemView.setOnClickListener(this); mTitleTextVIew=(TextView) itemView.findViewById(R.id.crime_title); mDateTextVIew=(TextView)itemView.findViewById(R.id.crime_date); ... } public void bind(Crime crime) { mCrime=crime; mTitleTextVIew.setText(mCrime.getTitle()); SimpleDateFormat dateFormat=new SimpleDateFormat("E, MMM dd, yyyy"); mDateTextVIew.setText(dateFormat.format(crime.getDate())); } ... } private class CrimeAdapter extends RecyclerView.Adapter<Holder> implements ItemHelper { private List<Crime> mCrimes; public CrimeAdapter(List<Crime> crimes) { mCrimes=crimes; } @Override public Holder onCreateViewHolder(ViewGroup parent, int viewType) { LayoutInflater layoutInflater=LayoutInflater.from(getActivity()); return new CrimeHolder(layoutInflater,parent); } @Override public void onBindViewHolder(Holder holder, int position) { Crime crime=mCrimes.get(position); holder.bind(crime); } ... } }
在绑定数据中,两种方式的主要区别体现在bind()方法中,原先需要对每一个组件进行分别的设置,现在将所有这些要修改的组件的操作全都由绑定类的实例来完成。
有关assets
assets一方面无需配置管理,另一方面由于没有配置管理,其无法在布局或其它资源中无法调用它们。因此如果只是单纯的想要调用某些文件,就可以将其用assets处理。
导入assets
由于其和系统资源(resource)是并列的关系,所以需要创建assets目录,再将文件放入这个目录或其子目录就可以了。
处理assets
创建一个资源管理类,在其中使用AssetsManager类实例来访问assets。
public class BeatBox { private static final String TAG="BeatBox"; private static final String SOUND_FOLDER="sample_sounds"; private AssetManager mAssets; private List<Sound> mSounds=new ArrayList<>(); public BeatBox(Context context) { mAssets=context.getAssets(); loadSounds(); } ... }
使用assets
创建一个管理类来保存assets中的文件的信息(路径、名称):
public class Sound {
private String mAssetPath;
private String mName;
public Sound(String assetPath)
{
mAssetPath=assetPath;
String[] components=assetPath.split("/");
String filename= components[components.length-1];
mName=filename.replace(".wav","");
}
public String getAssetPath() {
return mAssetPath;
}
public String getName() {
return mName;
}
}
之后就可以遍历AssetManager中每一个文件,然后将每一个文件存储到一个List中备用了:
public class BeatBox {
private static final String TAG="BeatBox";
private static final String SOUND_FOLDER="sample_sounds";
private AssetManager mAssets;
private List<Sound> mSounds=new ArrayList<>();
public BeatBox(Context context)
{
mAssets=context.getAssets();
loadSounds();
}
private void loadSounds()
{
String[] soundNames;
try {
soundNames=mAssets.list(SOUND_FOLDER);
Log.i(TAG,"Found "+soundNames.length+" sounds");
} catch (IOException ioe) {
Log.e(TAG,"Could not list assets", ioe);
return;
}
for (String filename:soundNames)
{
String assetPath=SOUND_FOLDER+"/"+filename;
Sound sound=new Sound((assetPath));
mSounds.add(sound);
}
}
public List<Sound> getSounds()
{
return mSounds;
}
}