• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

Android MVVM架构 这一篇就够

互联网 diligentman 2周前 (05-02) 11次浏览

AndroidMVVM架构

  • Android MVVM架构,这一篇就够
    • 相关技术
  • Activity模式的准备工作
    • 一些控件的绑定
      • 1.TextView
      • 2. 绑定点击事件
      • 3. 绑定其他控件,就拿ImageView举例(任何控件都可以如此绑定)
      • 4. 绑定列表等有适配器的控件,ListView为例
  • Fragment模式的准备工作
      • 1.给MainActivity加上VM
      • 2.Fragment的vm我们还种方式绑定
  • 针对适配器实体类的绑定补充

Android MVVM架构,这一篇就够

这篇文章主要是分享我学习安卓前沿技术架构Jetpack的MVVM 主要分为两个部分,一是Activity中使用AndroidViewModel名另外一个是Fragment里使用ViewModel。
MVVM优越性不许多说,内容不周之处欢迎指正。

本文所用到的技术仓库在此:
https://gitee.com/jimonik/my-news/

相关技术

如果您对此了解可以跳过阅读

  1. DataBinding ,数据绑定,主要是把XML布局实例化成为一个bind类,在源码编写的时候就生成.class用以和另外一个类实例(ViewModel)进行绑定,绑定操作在Activity里;
  2. XML写法变动主要是把工具包引用放入标签,其内增加标签对和布局,布局可以直接使用data中引入的class,ViewModel中也可以通过@BindAdapter对布局中的控件进行绑定注解达到初始化的目的;
  3. VM也就是ViewModel,里面放双向绑定的数据(我更喜欢用MuteableLiveData)、静态绑定事件等,老师说的MVC的M貌似可以和它完全没影响的一起工作(我的猜测);

Activity模式的准备工作

  1. Android Studio创建一个空模板,大概是这个鬼样子:
    Android MVVM架构 这一篇就够2. 创建一个MainVM类,继承AndroidViewModel,大概是这样
    Android MVVM架构 这一篇就够
    3.在App的build.gradle里写dataBinding.enabled=true打开数据绑定,并同步。
    Android MVVM架构 这一篇就够4.布局改称这样就完成:
    Android MVVM架构 这一篇就够其中vm就是起的类名字。

  2. 绑定布局和VM的操作放在MainActivity里面:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //获取布局绑定实例
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
       //获取VM实例
        MainVM vm = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(MainVM.class);
        //把他们邦在一起
        binding.setVm(vm);
        //设置VM所使用的生命周期
        binding.setLifecycleOwner(this);
        /**
         * 以下是我的习惯:
         * 
         * 因为在VM中我们可能要用到MainActivity弹Toast什么的,因此需要传一个this
         * 因为若涉及到适配器列表等控件、以及数据绑定则需要在VM中用到binding
         * 因此VM中的setBinding,我把他当作初始化函数使用更方便!
         */
        vm.setBinding(binding,this);
    }
}

6此时的VM:

public class MainVM extends AndroidViewModel {
    private static ActivityMainBinding binding;
    @SuppressLint("StaticFieldLeak")
    private static MainActivity mainActivity;
    public MainVM(@NonNull Application application) { super(application); }
    public void setBinding(ActivityMainBinding binding, MainActivity mainActivity) {
        //把binding和mainActivity都赋值给MainVM作为静态变量备用,因为很多绑定的控件都只能用静态方法
        MainVM.binding =binding;
        MainVM.mainActivity =mainActivity;
    }
}

一些控件的绑定

1.TextView

在MainVM中使用MutableLiveData 其中MutableLiveData的泛型是要监听绑定数据的类型。

public class MainVM extends AndroidViewModel {
    private static ActivityMainBinding binding;
    @SuppressLint("StaticFieldLeak")
    private static MainActivity mainActivity;
    public static MutableLiveData<String> text=new MutableLiveData<>();
    public MainVM(@NonNull Application application) {
        super(application);
        text.setValue("这是初始值");
    }
    public void setBinding(ActivityMainBinding binding, MainActivity mainActivity) {
        //把binding和mainActivity都赋值给MainVM作为静态变量备用,因为很多绑定的控件都只能用静态方法
        MainVM.binding =binding;
        MainVM.mainActivity =mainActivity;
    }
}

同时在XML中

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable name="vm" type="com.demo.MainVM"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.text}"/>
    </LinearLayout>
</layout>

这样,每当text.setValue()就会实时更新界面上的数据。
若是EditText,则将 @{vm.text} 改成 @={vm.text}就可以实现修改编辑框,在vm中的text.getValue()时可以实施获取数据,即数据双向绑定。

2. 绑定点击事件

一般都是在onClick中:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="vm" type="com.demo.MainVM"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:layout_width="wrap_content"
            android:onClick="@{vm::click}"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</layout>

然后在VM中加个普通点击事件(必须是静态)

public class MainVM extends AndroidViewModel {
    private static ActivityMainBinding binding;
    @SuppressLint("StaticFieldLeak")
    private static MainActivity mainActivity;
    public MainVM(@NonNull Application application) { super(application); }
    public void setBinding(ActivityMainBinding binding, MainActivity mainActivity) {
        //把binding和mainActivity都赋值给MainVM作为静态变量备用,因为很多绑定的控件都只能用静态方法
        MainVM.binding =binding;
        MainVM.mainActivity =mainActivity;
    }
    public static void click(View view){
        Toast.makeText(mainActivity, "你点击了按钮", Toast.LENGTH_SHORT).show();
    }
}

3. 绑定其他控件,就拿ImageView举例(任何控件都可以如此绑定)

   <ImageView
            android:layout_width="100dp"
            app:bindImage="@{vm.imgdir}"
            android:layout_height="100dp"
            tools:ignore="ContentDescription" />

这一句

app:bindImage="@{vm.imgdir}"

的bindImage是随便起的一个名字,而

vm.imgdir

则是在VM中定义的

public static MutableLiveData<String> imgdir=new MutableLiveData<>();

注意,如果app报错则需在根节点加入引用:

xmlns:app="http://schemas.android.com/apk/res-auto"

而在VM中则

  public static MutableLiveData<String> imgdir=new MutableLiveData<>();
    @BindingAdapter("bindImage")
    public static void bindImage(ImageView imageView,MutableLiveData<String> imgdir){
        if (!imgdir.getValue().equals("")){
            imageView.setImageBitmap(BitmapFactory.decodeFile(imgdir.getValue()));
        }
    }

其中imgdir需要在构造器中初始化, @BindingAdapter(“bindImage”)的bindImage正是xml中我起的名字的,这个方法名字可以随意起,我这里和BindingAdapter里面的保持一样(习惯)。

参数一:就是要绑定的控件实例,可以用来初始化,设置点击事件什么的。
参数二:是xml中传入的数据,也就是在VM中定义的imgdir。

工作顺序是一旦imgdir.setValue()或者imgdir.postValue()更新了数据,那么bindImage方法便会执行,就实现了数据的监听,在bindImage方法中设置布局内容就相当于更新后的数据同步布局,达到高度解耦效果。

4. 绑定列表等有适配器的控件,ListView为例

首先先准备适配器需要的几个文件

1.Adapter

public class Adapter extends BaseAdapter {
    Context context;
    public List<Bean> data;
    public Adapter(Context context, List<Bean> objects){
        super();
        this.context=context;
        data=objects;
    }
    @Override
    public int getCount() {
        return Objects.requireNonNull(data).size();
    }
    @Override
    public Object getItem(int position) {
        return Objects.requireNonNull(data).get(position);
    }
    @Override
    public long getItemId(int position) {
        return position;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent){
        @SuppressLint("ViewHolder") ViewDataBinding binding= DataBindingUtil.inflate(LayoutInflater.from(context),R.layout.item, parent, false);
        binding.setVariable(BR.bean, Objects.requireNonNull(data).get(position));
        return binding.getRoot();
    }
}

可见,getView中的
binding.setVariable(BR.bean, Objects.requireNonNull(data).get(position));
正是使实体类bean(相当于Activity的VM)和item绑定在一起的方法,经此一句,再不复写其他代码。

  1. Bean实体类
public class Bean {
    public String text;
    public Bean(String text){
        this.text=text;
    }
}

建议写成public,不要写任何setter和getter

  1. item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="bean" type="com.demo.Bean" />
    </data>
    <LinearLayout
        android:padding="10dp"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:text="@{bean.text}"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"/>
    </LinearLayout>
</layout>

细品,和activity的布局、vm一毛一样,vm和bean,就换了名字

  1. 在VM设置adapter和列表点击长按事件,(在setBinding中)
public class MainVM extends AndroidViewModel {
    @SuppressLint("StaticFieldLeak")
    private static ActivityMainBinding binding;
    @SuppressLint("StaticFieldLeak")
    private static MainActivity mainActivity;
    //初始化好
    private final List<Bean> data=new ArrayList<>();

    public MainVM(@NonNull Application application) { super(application); }
    public void setBinding(ActivityMainBinding binding, MainActivity mainActivity) {
        //把binding和mainActivity都赋值给MainVM作为静态变量备用,因为很多绑定的控件都只能用静态方法
        MainVM.binding =binding;
        MainVM.mainActivity =mainActivity;
        
        //设置适配器方式和以往不同
        binding.setAdp(new Adapter(mainActivity,data));
        //通过binding来设置点击长按事件
        binding.list.setOnItemClickListener(null);
        binding.list.setOnItemLongClickListener(null);
        //往列表里添加数据
        data.add(new Bean("emmmmm"));
        //更新列表
        binding.getAdp().notifyDataSetChanged();
        //不在主线陈更新
        mainActivity.runOnUiThread(() -> binding.getAdp().notifyDataSetChanged());
    }
}

Fragment模式的准备工作

以官方自带的boot navigation activity为例,他创建好三这样的:
Android MVVM架构 这一篇就够他把一个activity分成了三个fragment,每一个fragment分配了一个ViewModel,其中一个是:

public class DashboardFragment extends Fragment {
    private DashboardViewModel dashboardViewModel;
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        dashboardViewModel = new ViewModelProvider(this).get(DashboardViewModel.class);
        View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
        final TextView textView = root.findViewById(R.id.text_dashboard);
        dashboardViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {
                textView.setText(s);
            }
        });
        return root;
    }
}

不难发现,fragment的VM实例写好后,通过observe观察数据的方式在fragment中监听数据,大部分代码还是写在了fragment中,不符合最优解耦方式,这里我们稍微 改变一下:

1.给MainActivity加上VM

在build.gradle中:dataBinding.enabled=true

MainVM.java

public class MainVM extends AndroidViewModel {
    @SuppressLint("StaticFieldLeak")
    private static MainActivity mainActivity;
    private static ActivityMainBinding binding;

    public MainVM(@NonNull Application application) {
        super(application);
    }
    @BindingAdapter("bindNav")
    public static void bindNav(BottomNavigationView bottomNavigationView,MainVM mainVM){
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications).build();
        NavController navController = Navigation.findNavController(mainActivity, R.id.nav_host_fragment);
        NavigationUI.setupActionBarWithNavController(mainActivity, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(bottomNavigationView, navController);
    }
    public void setBinding(ActivityMainBinding binding, MainActivity mainActivity) {
        MainVM.binding =binding;
        MainVM.mainActivity =mainActivity;
    }
}

把底部的导航栏通过绑定的方法监听其点击。

MainActivity.java

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        MainVM vm = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(MainVM.class);
        binding.setVm(vm);
        binding.setLifecycleOwner(this);
        vm.setBinding(binding,this);
    }
}

这是常规的绑定方法。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="vm"
            type="com.demo.MainVM" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.google.android.material.bottomnavigation.BottomNavigationView
            app:bindNav="@{vm}"
            android:id="@+id/nav_view"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="0dp"
            android:layout_marginEnd="0dp"
            android:background="?android:attr/windowBackground"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:menu="@menu/bottom_nav_menu" />
        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/nav_view"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:navGraph="@navigation/mobile_navigation" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

也就增加了个 app:bindNav="@{vm}"

2.Fragment的vm我们还种方式绑定

以dashboard为例

DashboardFragment.java改成

public class DashboardFragment extends Fragment {
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
        FragmentDashboardBinding binding = DataBindingUtil.bind(root);
        DashboardViewModel vm= new ViewModelProvider(this).get(DashboardViewModel.class);
        binding.setVm(vm);
        binding.setLifecycleOwner(this);
        vm.setBinding(binding,requireActivity());
        return root;
    }
}

DashboardViewModel.java改成

public class DashboardViewModel extends ViewModel {
    public static MutableLiveData<String> text=new MutableLiveData<>();
    public DashboardViewModel() {
        text.setValue("初始直");
    }
    public void setBinding(FragmentDashboardBinding binding, FragmentActivity requireActivity) {

    }
}

fragment_dashboard.xml改成

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="vm"
            type="com.demo.ui.dashboard.DashboardViewModel" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:text="@{vm.text}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</layout>

就这样,fragment的数据绑定形式改成了和Activity一样的方式,绑定的方式也和Activity一样。

针对适配器实体类的绑定补充

有时,在列表实体类中可能需要写@BindAdapter方法进行耗时操作,
我们同样可以传入activity让他回到主线程

例如

//这是个Bean实体类
public class CollectionBean {
    public String newsId,userId,title,id;
    public CollectionBean(String id,String newsId, String userId){
        this.id=id;
        this.newsId=newsId;
        this.userId=userId;
    }
    @BindingAdapter(value = {"bindTitle","activity"}, requireAll = false)
    public static void getTitle(TextView textView, String newsId, Activity activity){
        BmobApi.get("https://api2.bmob.cn/1/classes/news/"+newsId, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                activity.runOnUiThread(() -> textView.setText("未知标题"));
            }
            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) {
                activity.runOnUiThread(() -> {
             //这里是主线程
                });
            }
        });
    }
}

总结完毕


程序员灯塔
转载请注明原文链接:Android MVVM架构 这一篇就够
喜欢 (0)