前言
首先看一段简易的代码:
public class MainActivity extends AppCompatActivity {
//1.定义组件
private TextView textView;
private Button button;
//2.定义变量
private int num;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//3.初始化组件
textView = findViewById(R.id.textView);
button = findViewById(R.id.button);
//4.设置事件等
textView.setText(String.valueOf(num)));
button.setOnClickListener(v -> {
num++;
textView.setText(String.valueOf(num)));
});
}
}
类似这种结构的Activity经常出现在我的代码中,尤其是注释中标注序号的那些样板化代码。并且随着项目愈发庞大后,这样的样板化代码也占据了Activity类的半壁江山,拉低开发效率的同时让人感觉代码十分臃肿。而ViewModel等架构组件的出现就是为了解决此类问题,正如Google官方所言:Jetpack 是一个由多个库组成的套件,可帮助开发者遵循最佳做法,减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码,让开发者精力集中编写重要的代码。
ViewModel
ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。
1、新建一个类,继承自ViewModel类,并定义界面相关的属性和方法。
public class MainViewModel extends ViewModel {
private int num = 0;
public int getNum() {
return num;
}
public void add() {
num++;
}
}
2、在Activity中实例化对应的ViewModel。
public class MainActivity extends AppCompatActivity {
private MainViewModel mainViewModel;
private TextView textView;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
textView = findViewById(R.id.textView);
button = findViewById(R.id.button);
textView.setText(String.valueOf(mainViewModel.getNum()));
button.setOnClickListener(v -> {
mainViewModel.add();
textView.setText(String.valueOf(mainViewModel.getNum()));
});
}
}
相比于最初的例子,使用ViewModel帮助我们消除了定义变量这部分样板化代码。不仅如此,我们知道,当应用发生配置变化,如屏幕旋转、语言改变等情况时,若想保存临时数据,则需调用onSaveInstanceState
和onRestoreInstanceState
方法保存和恢复数据,而使用ViewModel则不需要考虑这一点,具体原因可以参考ViewModel的生命周期。
数据可在发生屏幕旋转等配置更改后继续留存,如结果所示。
LiveData
LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
LiveData一般配合ViewModel使用,只需对之前的VIewModel类进行改写即可:
public class ViewModelWithLiveData extends ViewModel {
private MutableLiveData<Integer> num;
public MutableLiveData<Integer> getNum() {
if (num == null) {
num = new MutableLiveData<>();
num.setValue(0);
}
return num;
}
public void add() {
num.setValue(num.getValue() + 1);
}
}
LiveData类型的数据可以注册观察者,使数据在发生改变时通知观察者做出相应动作。相较于ViewModel,增加了注册观察者的代码。
public class LiveDataActivity extends AppCompatActivity {
private ViewModelWithLiveData viewModelWithLiveData;
private TextView textView;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live_data);
textView = findViewById(R.id.textView);
button = findViewById(R.id.button);
viewModelWithLiveData = new ViewModelProvider(this).get(ViewModelWithLiveData.class);
viewModelWithLiveData.getNum().observe(this, new Observer<Integer>() {
@Override
public void onChanged(Integer integer) {
textView.setText(String.valueOf(integer));
}
});
button.setOnClickListener(v -> viewModelWithLiveData.add());
}
}
LiveData通过使用观察者模式,当数据发生改变时即通知UI更新,同样能减少部分样板化代码,具体到此例中即避免了更新文本代码textView.setText(String.valueOf(num))
的重复编写。
DataBinding
借助DataBinding,我们可以使用声明性格式将布局中的界面组件与应用中的数据源绑定。如需启用数据绑定,需要在模块的 build.gradle 文件中将 dataBinding 构建选项设置为 true,如下所示:
android {
...
buildFeatures {
dataBinding true
}
}
无需再修改ViewModel类,但需要对布局文件进行调整。启用数据绑定后,在容器上使用快捷键Alt+Enter将出现以下选项:
点击Convert to data binding layout,布局文件将自动修改,只需要在<data></data>之间定义数据名称和注入数据源,然后为组件绑定对应属性即可。此处数据名为data,数据源为DataBindingViewModel,实际与之前的ViewModelWithLiveData内容一致。
绑定数据的赋值表达式使用 @{}
语法,对应布局文件中Button的onClick属性android:onClick="@{()->data.add()}"
和TextView的text属性android:text="@{String.valueOf(data.num)}"
<?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="data"
type="com.month.viewmodeldemo.DataBindingViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".DataBindingActivity">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="252dp"
android:text="Button"
android:onClick="@{()->data.add()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(data.num)}"
android:textSize="36sp"
app:layout_constraintBottom_toTopOf="@+id/button1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.517" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
然后修改Activity,可以明显地发现,相比于最初的例子,大部分样板化代码已经被清除,代码变得十分简洁清晰。
public class DataBindingActivity extends AppCompatActivity {
private DataBindingViewModel dataBindingViewModel;
private ActivityDataBindingBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
dataBindingViewModel = new ViewModelProvider(this).get(DataBindingViewModel.class);
binding.setData(dataBindingViewModel);
binding.setLifecycleOwner(this);
}
}
这里有几个需要注意的点:
- Binding的类型,它根据Activity的类名生成,此处为ActivityDataBindingBinding;
- setContentView方法有所改变,改为DataBindingUtil.setContentView(Activity activity, int layoutId);
- setData方法,该方法对应布局文件中定义的数据名,也就是name属性,如果name=“bean”,则方法名为setBean。
此外,如果仍需在Activity中调用组件的相关方法,可以直接通过组件id获取到这个组件,像这样:
binding.textView1.setVisibility(View.INVISIBLE);
binding.button1.setEnabled(false);
More…
ViewModel实例化问题
刚开始学习和使用ViewModel时遇到的一个问题,实例化ViewModel的代码报错,不能正确编译。但写此文章时该问题突然消失,暂时不知是何原因。
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
如果遇到此问题,可以通过以下代码进行实例化:
mainViewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(MainViewModel.class);
以下是ViewModelProvider的部分源码,可以看出默认情况下构造方法中也会添加此Factory。
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
: NewInstanceFactory.getInstance());
}
SavedStateHandle
ViewModel解决了系统配置发生变化时数据的留存问题,但还有一种情况没有考虑:即因资源不足,应用被系统杀死的情况,当这种情况发生时,临时数据仍会丢失。下图中,通过设置使应用进入后台即被杀死,以模拟这种情况。
要解决这个问题,一种方法就是调用onSaveInstanceState
和onRestoreInstanceState
方法保存和恢复数据,另一种方法就是使用SavedStateHandle,下面介绍如何使用:
首先引入依赖。
dependencies {
...
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
}
SaveStateHandle类似于Bundle,可以通过set/get方法存取各种类型的值。将这个类引入ViewModel中,并对代码进行调整,注意在构造方法中需要添加SavedStateHandle这一参数。
public class DataBindingViewModel extends ViewModel {
private MutableLiveData<Integer> num;
private SavedStateHandle handle;
public DataBindingViewModel(SavedStateHandle savedStateHandle) {
handle = savedStateHandle;
num = new MutableLiveData<>();
if (!handle.contains("num")) {
num.setValue(0);
handle.set("num", num.getValue());
}
num.setValue(handle.get("num"));
}
public MutableLiveData<Integer> getNum() {
return num;
}
public void add() {
num.setValue(num.getValue() + 1);
handle.set("num", num.getValue());
}
}
当然,实例化ViewModel的代码也需要修改:
dataBindingViewModel = new ViewModelProvider(this, new SavedStateViewModelFactory(getApplication(), this)).get(DataBindingViewModel.class);
这样一来,即使应用被系统杀死,临时数据也不会丢失了。
AndroidViewModel
如果希望VIewModel拥有访问全局资源的能力,那么要用到AndroidViewModel类。通过继承AndroidViewModel,ViewModel将可以调用getApplication()方法,这样一来,也就可以通过SharedPreferences对重要数据进行持久化存储。
与SavedStateHandle的使用类似,需要在构造方法中添加Application参数,并在ViewModelProvider的构造方法中提供对应的Factory。
public class MainViewModel extends AndroidViewModel {
public MainViewModel(@NonNull Application application) {
super(application);
}
//省略部分代码...
}
mainViewModel = new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(MainViewModel.class);
如果与SavedStateHandle配合使用,则无需此操作,因为SavedStateViewModelFactory的构造方法中带有Application参数。
public class DataBindingViewModel extends AndroidViewModel {
private SavedStateHandle handle;
public DataBindingViewModel(Application application, SavedStateHandle savedStateHandle) {
super(application);
handle = savedStateHandle;
}
//省略部分代码...
}
dataBindingViewModel = new ViewModelProvider(this, new SavedStateViewModelFactory(getApplication(), this)).get(DataBindingViewModel.class);