Android

Java安卓学习总结(二十四)

第二十章 数据绑定与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;
}
}

发表评论