AndroidMVVM架构
Android MVVM架构,这一篇就够
这篇文章主要是分享我学习安卓前沿技术架构Jetpack的MVVM 主要分为两个部分,一是Activity中使用AndroidViewModel名另外一个是Fragment里使用ViewModel。
MVVM优越性不许多说,内容不周之处欢迎指正。
本文所用到的技术仓库在此:
https://gitee.com/jimonik/my-news/
相关技术
如果您对此了解可以跳过阅读
- DataBinding ,数据绑定,主要是把XML布局实例化成为一个bind类,在源码编写的时候就生成.class用以和另外一个类实例(ViewModel)进行绑定,绑定操作在Activity里;
- XML写法变动主要是把工具包引用放入标签,其内增加标签对和布局,布局可以直接使用data中引入的class,ViewModel中也可以通过@BindAdapter对布局中的控件进行绑定注解达到初始化的目的;
- VM也就是ViewModel,里面放双向绑定的数据(我更喜欢用MuteableLiveData)、静态绑定事件等,老师说的MVC的M貌似可以和它完全没影响的一起工作(我的猜测);
Activity模式的准备工作
-
Android Studio创建一个空模板,大概是这个鬼样子:
2. 创建一个MainVM类,继承AndroidViewModel,大概是这样
3.在App的build.gradle里写dataBinding.enabled=true打开数据绑定,并同步。
4.布局改称这样就完成:
其中vm就是起的类名字。 -
绑定布局和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绑定在一起的方法,经此一句,再不复写其他代码。
- Bean实体类
public class Bean {
public String text;
public Bean(String text){
this.text=text;
}
}
建议写成public,不要写任何setter和getter
- 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,就换了名字
- 在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为例,他创建好三这样的:
他把一个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(() -> {
//这里是主线程
});
}
});
}
}
总结完毕