高级组件开发,Jetpack的学习
Jetpack简介
Jetpack的家族还是十分强大的,主要由基础,架构,行为,界面这四个部分组成,像通知,权限,Fragment都属于Jetpack
目前Android官方最为推荐的项目架构就是MVVM,因而Jetpack中的许多组件就是专门为MVVM架构量身打造的
ViewModel
ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据.也就是说,只要是界面上能看到的数据,他的相关变量都应该存放在ViewModel中,而不是Activity中,这样可以在一定程度上减少Activity中的逻辑.
另外,ViewModel中还有一个非常重要的特性,当手机发生横竖屏旋转的时候,Activity会被重建,同时存放在Activity中的数据也会丢失.而VIewModel的生命周期和Activity不同,他可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁.
ViewModel的基本用法
先加入如下的依赖:
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
通常来说,比较好的编程规范是给每一个Activity和Fragment都创建一个对应的ViewModel,因此这里我们就为MainActivity创建一个MainViewModel类,并且让他继承ViewModel,如下:
public class MainViewModel extends ViewModel {
// 可以添加成员变量和方法
}
所有与界面有关的数据都应该放在ViewModel中,那么我们要实现一个计数器的功能,就可以在ViewModel中加入一个counter变量用于计数,如下:
public class MainViewModel extends ViewModel {
private int counter = 0;
public int getCounter() {
return counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
}
现在需要在界面上添加一个按钮,每点击一次按钮就让计数器加一,并且把最新的计数器显示在界面上
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/infoText"
android:layout_gravity="center_horizontal"
android:textSize="32sp" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/plusOneBtn"
android:layout_gravity="center_horizontal"
android:text="Plus One" />
</LinearLayout>
现在开始实现计数器的逻辑,修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
private MainViewModel viewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
Button plusOneBtn = findViewById(R.id.plusOneBtn);
plusOneBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
viewModel.setCounter(viewModel.getCounter() + 1);
refreshCounter();
}
});
refreshCounter();
}
private void refreshCounter() {
TextView infoText = findViewById(R.id.infoText);
infoText.setText(String.valueOf(viewModel.getCounter()));
}
}
需要注意的是,我们绝对不可以直接去创建ViewModel的实例,而是一定要通过ViewModelProvider来获取ViewModel的实例.
这是因为ViewModel有独立的生命周期,并且其生命周期要长于Activity.如果我们在onCreate()方法中创建ViewModel的实例,那么每次onCreate()方法执行的时候,ViewModel都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据.
此时就可以开始运行了.
向ViewModel传递参数
由于所有ViewModel的实例都是通过ViewModelProvider来获取的,因此我们没有任何地方可以向VIewModel的构造函数中传递参数.
当然,这个问题也不难解决,只需要借助ViewModelProvider.Factory就可以实现了.
现在的计数器虽然在屏幕旋转的时候不会丢失数据,但是如果退出程序之后再重新打开,那么之前的计数就会被清零了.现在就要保证即使在退出程序后又重新打开的情况下,数据仍然不会丢失.
实现这个功能就需要在 退出程序的时候对当前的计数进行保存,然后在重新打开程序的时候读取之前保存的计数,并传递给MainViewModel,因此,这里修改MainViewModel中的代码,如下
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
private int counter;
public MainViewModel(int countReserved) {
this.counter = countReserved;
}
public int getCounter() {
return counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
}
接下来的问题是如何向MainViewModel的构造函数传递数据了,就需要使用ViewModelProvider.Factory,下面看看如何实现.
新建一个MainViewModelFactory类,并让他实现ViewModelProvider.Factory接口
public class MainViewModelFactory implements ViewModelProvider.Factory {
private int countReserved;
public MainViewModelFactory(int countReserved) {
this.countReserved = countReserved;
}
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
return (T) new MainViewModel(countReserved);
}
}
在布局中添加一个清零按钮
<Button
android:id="@+id/clear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="清0"/>
然后修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {
private MainViewModel viewModel;
private SharedPreferences sp;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sp = getPreferences(Context.MODE_PRIVATE);
int countReserved = sp.getInt("count_reserved", 0);
viewModel = new ViewModelProvider(this, new MainViewModelFactory(countReserved))
.get(MainViewModel.class);
Button plusOneBtn = findViewById(R.id.plusone);
plusOneBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
viewModel.setCounter(viewModel.getCounter() + 1);
refreshCounter();
}
});
Button clearBtn = findViewById(R.id.clear);
clearBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
viewModel.setCounter(0);
refreshCounter();
}
});
refreshCounter();
}
@Override
protected void onPause() {
super.onPause();
SharedPreferences.Editor editor = sp.edit();
editor.putInt("count_reserved", viewModel.getCounter());
editor.apply();
}
private void refreshCounter() {
TextView infoText = findViewById(R.id.textview);
infoText.setText(String.valueOf(viewModel.getCounter()));
}
}
sp 是之前创建的 SharedPreferences 对象的引用,用于访问应用程序的私有存储空间。
getInt(“count_reserved”, 0) 是一个方法调用,用于从 SharedPreferences 中获取名为 “count_reserved” 的键对应的整数值。这个方法接受两参数:
“count_reserved”:作为键的名称,用于检索相应的值。
0:默认值,如果 “count_reserved” 键不存在,就会返回这个默认值。
如果 “count_reserved” 键存在于 SharedPreferences 中,那么 getInt() 方法将返回存储的整数值。如果 “count_reserved” 键不存在,它将返回默认值 0。
这段代码通常用于初始化应用程序的变量或状态,以确保在第一次运行应用程序或用户之前没有指定值时,能够有一个合理的默认值。在这种情况下,countReserved 变量将存储从 SharedPreferences 中读取的整数值,用于后续的操作,例如创建 MainViewModel 实例。
@Override
protected void onPause() {
super.onPause();
SharedPreferences.Editor editor = sp.edit();
editor.putInt(“count_reserved”, viewModel.getCounter());
editor.apply();
}
super.onPause();:首先,调用了父类的 onPause() 方法,以确保执行了父类的默认操作。
SharedPreferences.Editor editor = sp.edit();:创建了一个 SharedPreferences.Editor 对象,并将其赋值给名为 editor 的变量。SharedPreferences.Editor 用于编辑 SharedPreferences 对象中的数据,允许你添加、修改或删除键值对。
editor.putInt(“count_reserved”, viewModel.getCounter());:使用 editor 对象,将名为 “count_reserved” 的键和 viewModel 中的 counter 值存入 SharedPreferences。具体来说,它将 viewModel 中的 counter 值写入 SharedPreferences 的 “count_reserved” 键中,以便在应用程序的下一次运行时使用。
editor.apply();:最后,调用 apply() 方法来提交对 SharedPreferences 的编辑操作。这确保了数据的修改被立即应用并保存到 SharedPreferences 中。
总结:这段代码的目的是在用户离开应用程序(暂停或停止活动)时将 viewModel 中的 counter 值保存到应用程序的 SharedPreferences 中,以便下次应用程序运行时可以恢复这个值。这通常用于保持应用程序状态,以确保用户在应用程序暂停或重新启动时不会丢失之前的数据。
Lifecycles
Lifecycles组件就是为了可以让任何一个类都能轻松的感知到Activity的生命周期,同时又不需要在Activity中编写大量的逻辑处理.
新建一个MyObserver类,并让它实现LifecycleObserver接口
public class MyObserver implements LifecycleObserver {
private Lifecycle lifecycle;
public MyObserver() {
this.lifecycle = lifecycle;
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void activityStart() {
Log.d("MyObserver", "activityStart");
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void activityStop() {
Log.d("MyObserver", "activityStop");
}
}
我们在方法上使用了@OnLifecycleEvent注解,并传入了一种生命周期事件.生命周期的事件类型一共有7种:ON_CREATE,ON_START,ON_PAUSE,ON_STOP和ON_DESTORY分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY类型,表示可以匹配Activity的任何周期回调
但是到这里还是无法正常工作的,因为当Activity的生命周期发生变化的时候并没有人去通知MyOberserver,这个时候就得借助LifecycleOwner了.
LifecycleOwner在MainActivity中的声明,需添加:
MyObserver myObserver = new MyObserver();
lifecycle.addObserver(myObserver);
不过当前MyObserver虽然能够感知到Activity的生命周期发生了变化,却没有办法主动获知当前的生命周期状态.此时需要在MyObserver的构造函数中将Lifecycle传进来即可.所以,此时的MyObersver中的内容
public class MyObserver implements LifecycleObserver {
private Lifecycle lifecycle;
public MyObserver() {
this.lifecycle = lifecycle;
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void activityStart() {
Log.d("MyObserver", "activityStart");
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void activityStop() {
Log.d("MyObserver", "activityStop");
}
}
有了Lifecycle对象之后,我们就可以在任何地方调用lifecycle.currentState来主动获知当前的生命周期状态.lifecycle.currentState返回的生命周期是一个枚举类型,一共有INITALIZED,DESTROYED,CREATED,STARTED,RESUMED这五种状态
LiveData
LiveData是Jetpack提供的一种响应式编程软件,他可以包含任何类型的数据,并在数据发生变化的时候通知给观察者.LiveData特别适合与ViewModel结合在一起使用.
LiveData的基本用法
我们一直使用的都是在Activity中手动获取ViewModel中的数据这种交互方式,但是ViewModel却无法将数据的变化主动通知给Activity.
或许可能这样想,把Activity的实例传给ViewModel,这样ViewModel就可以主动对Activity进行通知了?千万不可以这样做.ViewModel的生命周期是长于Activity的,如果把Activity的实例传给ViewModel,就很有可能因为Activity无法释放内存而造成内存泄漏,这是非常错误的写法.
而解决这个问题就需要用到LiveData.LiveData可以包含任何类型的数据,并在数据发生变化的时候通知给观察者.也就是说,我们将计数使用LiveData来包装,然后再Activity中去观察它,就可以主动将数据变化通知给Activity了
修改MainViewModel中的代码,如下:
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.MutableLiveData;
public class MainViewModel extends ViewModel {
private MutableLiveData<Integer> counter = new MutableLiveData<>();
public MainViewModel(int countReserved) {
counter.setValue(countReserved);
}
public MutableLiveData<Integer> getCounter() {
return counter;
}
public void plusOne() {
Integer count = counter.getValue();
if (count == null) {
count = 0;
}
counter.setValue(count + 1);
}
public void clear() {
counter.setValue(0);
}
}
我们将counter变量修改成了一个MutableLiveData对象,并指定它的泛型为Int,表示它包含的是整型数据.MutableLiveData是一种可变的LiveDta,他的用法非常简单,主要有3种读写数据的方法,分别是getValue(),setValue()和postValue()方法.getValue()用于获取LiveData中包含的数据,setValue()用于给LiveData设置数据,但是只能在主线程中调用;postValue()方法用于在非主线程中给LiveData设置数据
在init结构体中给counter设置数据,这样之前保存的计数值就可以在初始化的时候得到恢复.新增的plusOne()和clear()这两个方法,分别用于给计数加1以及将计数清零.plusOne()方法中的逻辑是先获取counter中包含的数据,然后给它加一,在重新设置到counter当中
修改MainActivity中的代码:
public class MainViewModel extends ViewModel {
private MutableLiveData<Integer> counter = new MutableLiveData<>();
public MainViewModel(int countReserved) {
counter.setValue(countReserved);
}
public MutableLiveData<Integer> getCounter() {
return counter;
}
public void plusOne() {
Integer count = counter.getValue();
if (count == null) {
count = 0;
}
counter.setValue(count + 1);
}
public void clear() {
counter.setValue(0);
}
}
在"Plus One"按钮的点击事件中我们去调用MainViewModel的plusOne()方法,而在"Clear"按钮的点击事件中应该去调用MainViewModel的clear()方法.
接下来,最为关键,调用了viewModel.counter的observer()方法来观察数据的变化.经过对MainViewModel的改造,现在counter变量已经变成了一个LiveData对象,任何LiveData对象都可以调用它的observe()方法来观察数据的变化.observe()方法本身接收两个参数:第一个参数是一个LifecycleOwner对象,Activity本身就是一个LifecycleOwner对象,因此直接传this就好;第二个参数是一个Observer接口,当counter中包含的数据发生变化时,就会回调到这里,因此我们将最新的技术回调到这里即可.
添加依赖:
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
修改MainViewMoel中的代码:
public class MainViewModel extends ViewModel {
private MutableLiveData<Integer> _counter = new MutableLiveData<>();
public MainViewModel(int countReserved) {
_counter.setValue(countReserved);
}
public MutableLiveData<Integer> getCounter() {
return _counter;
}
public void plusOne() {
Integer count = _counter.getValue();
_counter.setValue(count != null ? count + 1 : 1);
}
public void clear() {
_counter.setValue(0);
}
}
其中,可以看到,将原来的counter变量改名为_counter变量,并给它加上private修饰符,这样_counter变量对于外部就是不可见的.然后我们又重新定义了一个counter变量,将他的类型声明为不可变的LiveData,并在它的get()属性方法中返回_counter变量.
map和switchMap
map()方法:
这个方法的基本作用就是将实际包含数据的LiveData和仅用于观察的LiveData进行转换.
有一个例子,具体如下:
public class MainViewModel extends ViewModel {
private MutableLiveData<User> userLiveData = new MutableLiveData<>();
public LiveData<String> userName = Transformations.map(userLiveData, user -> user.getFirstName() + " " + user.getLastName());
public MainViewModel(int countReserved) {
// 构造函数的逻辑,如果需要的话,可以在这里初始化数据
}
}
这里可以看到,调用了Transformations的map()方法对LiveData的数据类型进行转换.map()方法接收两个参数:第一个参数是原始的LivaData对象;第二个参数是一个转换函数,我们在转换函数里编写具体的逻辑即可.这里的逻辑也很简单,就是将User对象转换成一个只包含用户姓名的字符串.
另外还将userLiveData声明成了private,以保证数据的封装性.外部使用的时候只要观察userName这个LivaData就可以了.当userLivaData的数据发生变化时,map()方法会监听到变化并执行转换函数中的逻辑,然后再将转换之后的数据通知给userName的观察者
switchMap()方法
前面我们所学的内容都有一个前提:LiveData对象的实例都是在ViewModel中创建的.然而在实际的项目中,不可能一直是这种理想情况,很有可能ViewModel中的某个LiveData对象是调用另外的方法获取的
新建一个Repository单例类
public class Repository {
public LiveData<User> getUser(String userId) {
MutableLiveData<User> liveData = new MutableLiveData<>();
liveData.setValue(new User(userId, userId, 0));
return liveData;
}
}
这里在Repository类中添加了一个getUser()方法,这个方法接收一个userId参数.按照正常的逻辑,我们应该根据传入的userId参数去服务器请求或者到数据库中查找相应的User对象,但是这里只是示例,因此每次将传入的userId当做用户姓名来创建一个新的User对象即可.
需要注意的是,getUser()方法返回的是一个包含User数据的LiveData对象,而且每次调用getUser()方法都会返回一个新的LiveData实例.
然后我们在MainViewModel中也定义一个getUser()方法,并且让它调用Repository的getUser()方法来获取LiveData对象:
public LiveData<Repository.User> getUser(String userId) {
return Repository.getUser(userId);
}
接下来的问题就是,在Activity中如何观察LiveData的数据变化呢?既然getUser()方法返回的是一个LiveData对象,那么可不可以直接在Activity中使用如下呢?
viewModel.getUser(userId).observe(this, user -> {
// 在这里处理观察到的用户数据
});
这么做是完全错误的.因为每次调用getUser()方法返回的都是一个新的LiveData实例,而上述写法会一直观察老的LiveData实例,从而根本无法观察到数据的变化.你会发现,这种情况下的LiveData是不可观察的.
这个时候,switchMap()方法就可以派上用场了.它的使用场景非常固定:如果ViewModel中的某个LiveData对象是调用另外的方法获取的,那么我们就可以借助switchMap()方法,将这个LiveData对象转成另外一个可观察的LiveData对象.
修改MainViewModel中的代码:
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
private MutableLiveData<String> userIdLiveData = new MutableLiveData<>();
public LiveData<User> user = Transformations.switchMap(userIdLiveData, userId -> Repository.getUser(userId));
public void getUser(String userId) {
userIdLiveData.setValue(userId);
}
}
这里新定义了一个新的userIdLiveData对象,用来观察userId的数据变化,然后调用了Transformations的switchMap()方法,用来对另一个可观察的LiveData对象进行转换.
switchMap()方法同样接收两个参数:第一个参数传入我们新增的userIdLiveData,switchMap()会对它进行观察;第二个参数是一个转换函数,注意,我们必须在这个转换函数中返回一个LiveData对象,因为switchMap()方法的工作原理就是要将转换函数中返回的LiveData对象