第十三章 高级程序开发组件,探究Jetpack(第三版)
2017年Google推出了一个官方的架构组件库——Architecture Components,旨在帮助开发者编写出更加符合高质量代码规范、更具有架构设计的应用程序。
2018年Google推出了一个全新的开发组件工具及Jetpack,并将Architecture Components纳入其中。
ViewModel
ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放于界面相关的数据的。只要是界面上能看到的数据,它的相关变量就应该存放在ViewModel中,而不是Activity中。
ViewModel只是用来管理UI的数据的,千万不要让它持有View、Activity或者Fragment的引用(小心内存泄露)
当手机发生横竖屏旋转的时候Activity会被重建,同时存放在Activity中的数据与也会丢失。而ViewModel不会被重建,只有当Activity退出的时候才会跟着Activity一同销毁。
ViewModel的基本用法
使用ViewModel组件,需要在app/build.gradle文件中添加如下依赖:
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
比较好的编程规范时给每一个Activity和Fragment都创建一个对应的ViewModel。
创建的常用方式
由于ViewModel的生命周期是由系统维护的,因此不能直接在代码中通过new的方式创建。
-
直接用 ViewModelProvider 获取
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
-
通过实现ViewModelFactory接口创建
-
创建一个类实现ViewModelProvider.Factory接口
public class VMFactory implements ViewModelProvider.Factory { @NonNull @Override public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { return (T) new MainViewModel(); } }
-
在MainActivity例获取实例
viewModel = new ViewModelProvider(this, new VMProvider()).get(MainViewModel.class);
-
这两种方式都是基于ViewModelProvider.Factory来生成ViewModel的实例,只不过第一种方式如果加Factory参数,会使用内部默认的Factory。
我们要实现一个计数器的功能。
给MainActivity创建一个对应的MainViewModel类,让这个类继承于ViewModel,然后在这个类中加入所有与界面相关的数据。
public class MainViewModel extends ViewModel {
public int counter = 0;
}
修改activity_main.xml文件中的代码
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<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>
添加了TextView用来显示计数。添加了一个Button用来使计数加一。
接着实现计数器的逻辑,编写MainActivity中的代码。
public class MainActivity extends AppCompatActivity {
private MainViewModel viewModel;
private Button poBtn;
private TextView infoText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// viewModel = new ViewModelProvider(this).get(MainViewModel.class);
viewModel = new ViewModelProvider(this, new VMProvider()).get(MainViewModel.class);
poBtn = (Button) findViewById(R.id.plusOneBtn);
infoText = (TextView) findViewById(R.id.infoText);
poBtn.setOnClickListener(view -> {
viewModel.counter++;
refreshCounter();//刷新计数器显示的内容
});
}
private void refreshCounter() {
//刷新
String text = viewModel.counter + "";
infoText.setText(text);
}
}
之所以不能在MainActivity中使用new MainViewModel创建实例。因为ViewModel有独立的生命周期,并且它的生命周期应该长于Activity。如果在MainActivity中直接创建ViewModel的实例,当手机屏幕旋转时,MainActivity会重新创建一个新的实例,原来的数据就无法保留。
所以要通过ViewModelProvider来获取实例。
这样就做到了旋转手机屏幕显示不会丢失数据。
向ViewModel传递参数
我们实现了旋转手机屏幕不会丢失数据。但退出程序后这个计数还是会重新开始。
-
修改MainViewModel类的代码
public class MainViewModel extends ViewModel { public int counter = 0; public MainViewModel() { } public MainViewModel(int counterReserved) { this.counter = counterReserved; } }
-
新建一个类实现ViewModel.Factory接口
public class VMProvider implements ViewModelProvider.Factory { private int countReserved = 0; public VMProvider(int countReserved) { this.countReserved = countReserved; } public VMProvider() { } @NonNull @Override public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { return (T)new MainViewModel(countReserved); } }
这两个类都写了两个构造方法,如果调用空参的方法获取ViewModel的实例就是从0开始计数。如果调用含参的方法就是保持之前的结果开始计数。
-
修改activity_main.xml文件中的代码
<LinearLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> ... <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/clearBtn" android:text="Clear" /> </LinearLayout>
添加了一个按钮用来让用户手动清零。
-
修改MainActivity中的代码
public class MainActivity extends AppCompatActivity { ... private Button clearBtn; private SharedPreferences sp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sp = getPreferences(Context.MODE_PRIVATE); int countReserved = sp.getInt("count_reserved", 0);//取出保存的数据,如果没有返回默认值为0 // viewModel = new ViewModelProvider(this).get(MainViewModel.class); viewModel = new ViewModelProvider(this, new VMProvider(countReserved)).get(MainViewModel.class); ... clearBtn = (Button) findViewById(R.id.clearBtn); clearBtn.setOnClickListener(view -> { viewModel.counter = 0; refreshCounter();//刷新计数器显示的内容 }); refreshCounter(); } //从写onPause()方法对计数器当前数据进行保存,在活动与用户停止交互的时候就保存数据,不会丢失数据 @Override protected void onPause() { super.onPause(); SharedPreferences.Editor editor = sp.edit();//获取SharedPreferences.Editor的对象 editor.putInt("count_reserved", viewModel.counter); editor.apply(); } private void refreshCounter() { //刷新 String text = viewModel.counter + ""; infoText.setText(text); } }
这样无论是退出程序还是进入后台,数据都不会丢失。
Lifecycles
手写监听器感知Activity的生命周期
通过手写监听器的方式来对Activity的生命周期进行感知:
Lifecycles的使用
-
创建观察者
-
注册观察者
Lifecycle的作用是在活动和碎片外感知Activity的生命周期。
Lifecycle的应用场景
Android——Lifecycles的学习_android lifecycles_Liquor...的博客-CSDN博客
创建观察者
-
创建类实现LifecycleObserve接口
public class MyObserve implements LifecycleObserver { //LifecycleObserve是一个空接口,只需要实现,而不需要重写任何方法。 //方法的参数可以传LifecycleOwner,也可以不写 //方法要加上注解@OnLifecycleEvent(Lifecycle.Event.XXX),可以让该方法监听到注解生命周期的变化 //这个注解现在已经弃用,官方推荐让观察者类直接实现DefaultLifecycleObserver接口 @OnLifecycleEvent(Lifecycle.Event.ON_START)//在Activity的onStart()回调时执行 private void activityStart(LifecycleOwner owner) { Log.d("TAG", "activityStart: "); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP)//在Activity的onStop()回调时执行 private void activityStop() { Log.d("TAG", "activityStop: "); } }
在方法上使用了@OnLifecycleEvent注解,并传入了一种生命周期时间。生命周期事件的类型一共有七种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP、ON_DESTROY分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY的类型,表示可以匹配Activity的任何生命周期回调。
@OnLifecycleEvent 遭废弃,推荐使用 DefaultLifecycleObserver 替代_fundroid的博客-CSDN博客
注册观察者
AppCompatActivity与Fragment是默认的被观察者,在其内部通过调用getLifecycle().addObserver(观察者)注册定义好的观察者即可。
只要你的Activity是继承自AppCompatActivity的,或者你的Fragment是继承自androidx.fragment.app. Fragment的,那么它们本身就是一个LifecycleOwner的实例,这部分工作已由AndroidX库自动帮我们完成了。
我们只需要在所要监听的活动或者碎片添加如下代码即可。
getLifecycle().addObserver(new MyObserve());//注册观察者
效果:先将应用打开再关闭。
先后出现两句日志。
主动获知当前的生命周期状态
这就是Lifecycle组件最常见的用法了。不过目前MyObserver虽然可以感知到Activity的生命周期发生了变化,却没办法主动获知当前的生命周期状态。
使用Lifecycle对象的getCurrentState()方法可以在活动和碎片的任何地方获知到当前的生命周期状态。
Lifecycle.State state = getLifecycle().getCurrentState();
Log.d("TAG", state.toString() );
想在活动或碎片外的地方感知当前的生命周期状态。可以给MyObserver类添加一个变量,在构造方法中接收并保存一个Lifecycle的对象。
MyObserver类中:
public class MyObserver implements LifecycleObserver {
...
private Lifecycle lifecycle;
public MyObserver(Lifecycle lifecycle) {
this.lifecycle = lifecycle;
}
public Lifecycle getMyLifecycle() {
return lifecycle;
}
}
MainActivity中:
getLifecycle().addObserver(new MyObserver(getLifecycle()));//注册观察者
注册观察者的时候将本活动的Lifecycle对象交给观察者保管。
这样就算活动外,也可以通过MyObserver的对象再调用getCurrentState()方法主动感知当前的生命周期状态。
getCurrentState()方法
这个方法的返回值是一个枚举类型,一共有INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED这5中状态类型,他们与Activity的生命周期回调所对应的关系如图。
LiveData
LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。LiveData特别适合与ViewModel结合在一起使用。
LiveData的基本用法
前面介绍ViewModel时编写的计数器其实存在问题。
当点击按钮,计数器加一,之后会立即获取最新的计数返回到UI上。这种方式在单线程模式下确实可以正常工作,但如果ViewModel的内部开启了线程去执行一些耗时的逻辑。那么获取并返回到UI上的计数可能会是之前的数据。counter耗时线程中还没有改变。
问题是如何让将ViewModel的数据变化主动通知给Activity。
这里一定不能把Activity的实例传给ViewModel。因为Activity的生命周期比ViewModel的生命周期更短,可能导致Activity无法及时释放而造成内存泄漏。
应该使用LiveData解决。
修改MainViewModel中的代码:
public class MainViewModel extends ViewModel {
public MutableLiveData<Integer> counter = new MutableLiveData<Integer>();
//计数器变为MutableLiveData类型的
public MainViewModel() {
}
public MainViewModel(int counterReserved) {
this.counter.setValue(counterReserved);
}
public void plusOne() {
int count = counter.getValue() == null ? 0 : counter.getValue();
//将当前技术其中的数取出来,如果是null就设置默认值为0
counter.setValue(count + 1);
//加一存回去
}
public void clear() {
counter.setValue(0);
//直接设置为0
}
}
LivaData主要有三个方法用于读写数据:getValue()方法yonyuhuoquLiveData中包含的数据;setValue()方法用于给LiveData设置数据,但只能在主线程中调用,在UI线程中调用该方法通知数据变更;postValue()方法用于在非主线程中给LiveData设置数据,在子线程中调用该方法通知数据变更,该方法中切换到UI线程后调用setValue方法。
修改MainActivity中的代码:
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
poBtn = (Button) findViewById(R.id.plusOneBtn);
infoText = (TextView) findViewById(R.id.infoText);
poBtn.setOnClickListener(view -> {
viewModel.plusOne();
});//增加计数的按钮
clearBtn = (Button) findViewById(R.id.clearBtn);
clearBtn.setOnClickListener(view -> {
viewModel.clear();
});//清空计数的按钮
viewModel.counter.observe(this, new Observer<Integer>() {
//第一个参数接受一个LifecycleOwner的对象,Activity本身就是一个LifecycleOwner的对象
//第二个参数是Observer接口
//LivaData的数据发生变化时就会执行onChanged()方法
@Override
public void onChanged(Integer integer) {
refreshCounter();
}
});//观察数据变化的方法
}
//从写onPause()方法对计数器当前数据进行保存,在活动与用户停止交互的时候就保存数据,不会丢失数据
@Override
protected void onPause() {
super.onPause();
SharedPreferences.Editor editor = sp.edit();//获取SharedPreferences.Editor的对象
editor.putInt("count_reserved", viewModel.counter.getValue() == null ? 0 : viewModel.counter.getValue());
editor.apply();
}
private void refreshCounter() {
//刷新
String text = viewModel.counter.getValue() + "";
infoText.setText(text);
}
}
我们把其他地方的refreshConter()方法删掉。让LiveData帮我们监视计数是否发生变化。通过LiveData的observe()方法设置监视。如果发生了变化会执行第二个参数接口中的重写方法onChanged(),在这里面调用刷新方法refreshCounter()。
然后将添加、清空、刷新等操作使用LivaData的三个读写数据的方法修改。
要注意在子线程中修改数据一定要使用postValue()方法。
拓展:
Android推荐写法
为了保证ViewModel书的封装性。官方推荐永远只暴露不可变的LiveData 给外部。这样在非ViewModel 中就只能观察 LiveData 的数据变化,而不能给LiveData 设置数据。
修改MainViewModel中的代码:
public class MainViewModel extends ViewModel {
private MutableLiveData<Integer> _counter = new MutableLiveData<Integer>();
public final LiveData<Integer> counter = _counter;
public MainViewModel() {
}
public MainViewModel(int counterReserved) {
this._counter.setValue(counterReserved);
}
public void plusOne() {
int count = counter.getValue() == null ? 0 : counter.getValue();
_counter.setValue(count + 1);
}
public void clear() {
_counter.setValue(0);
}
}
这里先将原来的counter变量名改为_counter变量,并给它加上private修饰符。这样数据就是对外不可见的了。
然后我们由重新定义了一个counter变量,将它的类型声明为不可变的LiveData。
Kotlin:
包装后的LiveData只能使用getValue()获取数据,而不能使用setValue()和postValue()修改数据。
map和switchMap(陌生)
两种转化方法。
map()方法
map()方法的作用是将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。
应用场景:假设现在有一个User类,类中包含用户的姓名和年龄字段。
如果MainActivity中明确只会显示用户的姓名,而完全不关心用户的年龄。想要避免将整个LiveData的数据暴露出去,而是只暴露姓名。
map()方法可以将User类型的LiveData自由的转型成任意其它类型的LiveData。
public class UserViewModel extends ViewModel {
private final MutableLiveData<User> userLiveData = new MutableLiveData<>();
public LiveData<String> userName = Transformations.map(userLiveData, new Function<User, String>() {
@Override
public String apply(User input) {
return input.getName();
}
});
...
}
//可以看到这里我们调用了Transformations的map()方法来对LivaData的数据类型进行转换。map方法接受两个参数,第一个是原始的LiveData;第二个参数是一个转换函数。我们在转换函数里编写具体的转换逻辑即可。当userLiveData的数据变动时,会自动调用这个方法更新转换
class User {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
switchMap()方法
应用场景:ViewModel中的某个LiveData对象是调用另外的方法获取的,并不是直接在ViewModel中创建的实例。假如现在有一个Repository单例类。
class Repository {
public LiveData<User> getUser() {
MutableLiveData<User> liveData = new MutableLiveData<>();
liveData.setValue(new User());
return liveData;
}
}
这里我们在这个Repository类中添加了一个getUser()方法,按照正常的逻辑,我们应该传入一个useId参数去服务器请求或者到数据库中查找相应的User对象,但是这里只是模拟实例,因此每次将传入的userId当作用户姓名来创建一个新的User对象即可。这里参数userId也省略了,直接创建空参的User对象。
getUser()方法返回的是一个包含User数据的LiveData对象,而且没调用一次都会返回一个新的LiveData实例。
public class UserViewModel extends ViewModel {
...
public LiveData<User> getUser() {
return Repository.getUser();
}
}
我们在UserViewModel中也编写一个getUser()方法,并让他去调用Repository的getUser()方法来获取一个新的LiveData对象。
如果ViewModel中的某个LiveData对象是调用另外的方法获取的,那么我们就可以借助switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。
修改UserViewModel中的代码:
public class UserViewModel extends ViewModel {
...
private final MutableLiveData<String> userIdLivaData = new MutableLiveData<>();
LiveData<User> user = Transformations.switchMap(userIdLivaData, new Function<String, LiveData<User>>() {
@Override
public LiveData<User> apply(String input) {
return Repository.getUser();
}
});
//Transformations.switchMap()
/*
当userIdLiveData的数据变动就会自动调用这个方法更新
第一个参数:传入我们新增的userIdLiveData
第二个参数:是一个转换函数,返回一个可观察的LivaData对象
*/
public void getUser(String userId) {
userIdLivaData.setValue(userId);
}
}
先定义了一个userIdLivaData对象,用来观察userId的数据变化,然后调用了Transformations的switchMap()方法,用来对另一个可观察的LiveData对象进行转换。
userIdLiveData就是我们调用方法从外部获取的LiveData数据,使用switchMap()方法转换成了user可观察的数据。
当外部调用UserViewModel的getuser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会讲传入的userId设置到useridLiveData中。一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap()方法就会执行,并且调用我们编写的转换函数。然后再转换函数中调用Repository.getUser()方法获取到真正的用户数据。同时switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象。在MainActivity中观察这个对象即可。(user)
没看懂。
Room
Android数据库的ORM框架。也叫对象关系映射。就是将面向对象的语言和面向关系的数据库之间建立一种映射关系。
ORM框架可以用面向对象的思维来和数据库进行交互。
Android官房推出了一个ORM框架并加入到 Jetpack 中,就是 Room。
Room是一个持久性数据库。
Room持久性库提供了SQLite的抽象层,以便在充分利用SQLite的同时允许流畅的数据库访问,利用SQlite的全部功能。
使用Room进行增删改查
Room的整体结构
-
Entity。用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
-
Dao。是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候就不需要与底层数据库打交道了,直接和Dao层进行交互即可。
-
Database。用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例。
导包:
android 中Room 的简单使用android room注解sinat_bond的博客-CSDN博客
Entity实体类
首先定义实体类。
@Entity
//注解@Entity将这个各类声明成一个实体类
public class User {
@ColumnInfo(name = "name")
private String name;
//注解@ColumnInfo代表列的数据,name可以给该列命名
@ColumnInfo(name = "age")
private int age;
@PrimaryKey(autoGenerate = true)
//注释@PrimaryKey表示主键 将autoGenerate设置为true表示让主键自增
private long id;
}
Dao数据访问对象
Room用法中最关键的地方,封装访问数据库的操作。
新建一个userDao接口,注意必须使用接口。
@Dao
public interface UserDao {
@Query("select * from User")
//查询 @Query注解
List<User> loadAllUser();
@Query("select * from User where age > :userAge")
//查询age大于参数userAge的用户
List<User> loadUsersOlderThan(int userAge);
@Query("delete from User where name = :userName")
//删除用户名为参数userName的用户
//如果使用非实体类(对象)参数,要用@Query注解找到要操作的对象,在括号里进行相应操作
int deleteByLastName(String userName);
@Delete
//删除
void deleteUser(User user);
@Insert
//插入、添加 加入数据库之后返回主键id值
long insertUser(User user);
@Update
//更新
void updateUser(User user);
}
UserDao接口上面使用了一个@Dao注解,这样Room才能将他识别为一个Dao。Dao内部是对数据库的操作进行封装,数据库常用的操作有增删改查,所以Room也提供了这四种注解。
Database数据库
定义数据库类一定要满足下面条件:
-
该类必须带有@Database注解,该注解包含列出所有与数据库关联的数据实体的entities数组。
-
该类必须是一个抽象类,用于拓展RoomDatabase。
-
对于与数据库关联的每个Dao,数据库必须定义一个具有零参
定义好三个部分的内容:
-
数据库的版本号
-
包含哪些实体类
-
以及提供Dao层的访问实例
新建一个AppDatabase文件,编写如下代码:
@Database(version = 1, entities = {User.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
//定义抽象方法,用于获取Dao类的实例
//只需要进行方法的声明,具体的方法实现是由Room在底层自动完成的。
//有几个Dao层,就应该写几个抽象方法。
private static AppDatabase instance = null;
public static AppDatabase getInstance(Context context) {
if (instance == null) {
Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database");
/*
* Room.databaseBuilder()方法接收三个参数
* 第一个参数是一个applicationContext,不能是context。
* 第二个参数是AppDatabase的class对象
* 第三个参数是数据库名
* 最后调用build()方法创建数据库的实例。
* */
instance = database.build();
}
return instance;
}
}
AppDatabase类的头部使用了@Database注解,并在注解中声明了版本号和包含的实体类,如果有多个实体类可以用逗号隔开。
将AppDatabase定义成抽象类,并继承于RoomDatabase,然后提供相应的抽象方法返回Dao层的实例。
至此一个Database数据库就建立好了。
补充AppDatabase类,将这个类设置成单例模式。因为通常一个数据库在全局只需要一个实例。定义了一个私有变量instance,和一个公开方法getInstance(),如果instance为空,就调用Room.databaseBuilder()方法创建一个数据库的实例,并保存在instance里返回;如果instance不为空则直接返回这个instance。
进行测试
修改activity_main.xml文件中的代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
android:orientation="vertical">
...
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/addDataBtn"
android:text="Add Data"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/updateDataBtn"
android:text="Update Data"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/deleteDataBtn"
android:text="Delete Data"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/queryDataBtn"
android:text="Query Data"/>
</LinearLayout>
添加了4个按钮y哦那个与增删改查4个操作。
然后修改MianActivity中的代码
public class MainActivity extends AppCompatActivity {
...
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
//首先获取了UserDao的实例,并创建两个User对象。
UserDao userDao = AppDatabase.getInstance(this).userDao();//获取到Dao层实例
User user1 = new User("张三", 23);
User user2 = new User("李四", 24);
//然后在“Add Data”按钮中添加点击事件。调用UserDao的insertUser()方法
findViewById(R.id.addDataBtn).setOnClickListener(view -> {
new Thread(new Runnable() {
@Override
public void run() {
user1.id = userDao.insertUser(user1);
user2.id = userDao.insertUser(user2);
}
}).start();
});
findViewById(R.id.updateDataBtn).setOnClickListener(view -> {
new Thread(() -> {
user1.age = 42;
userDao.updateUser(user1);
}).start();
});
findViewById(R.id.deleteDataBtn).setOnClickListener(view -> {
new Thread(new Runnable() {
@Override
public void run() {
userDao.deleteByLastName("张三");
}
}).start();
});
findViewById(R.id.queryDataBtn).setOnClickListener(view -> {
new Thread(new Runnable() {
@Override
public void run() {
for (User user:
userDao.loadAllUser()) {
Log.d("TAG", user.toString());
}
}
}).start();
});
}
...
}
因为数据库操作属于耗时操作,,Room默认是不允许在主线程中进行数据操作的。因此创建了子线程进行数据库操作。
不过为了方便测试,Room还提供了一个更简单的方法。
Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database").allowMainThreadQueries();
instance = database.build();
在原先勾线AppDatabase实例的 Room.databaseBuilder() 方法后面加上allowMainThreadQueries()方法,这样Room就郧西在主线程中进行数据库操作了,这个方法之间以在测试环境下使用。
Room的数据库升级
Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database")
.fallbackToDestructiveMigration();
instance = database.build();
在构建AppDatabase实例的时候,在Room.databaseBuilder()方法后加上fallbackToDestructiveMigration()方法。这样只要数据库进行升级,Room就会将当前的数据库销毁,然后再重新创建。但之前数据库中的数据全部丢失了。
如果程序还在开发阶段,这个方法是可以用的。
正规写法
在数据库中添加一张表Book。
-
entity实体类
@Entity public class Book { @PrimaryKey(autoGenerate = true) public long id; @ColumnInfo(name = "name") public String name; @ColumnInfo(name = "pages") public int pages; }
-
Dao层接口
@Dao public interface BookDao { @Insert long insertBook(Book book); @Query("select * from Book") List<Book> loadAllBooks(); }
-
修改AppDatabase中的代码。
@Database(version = 2, entities = {User.class, Book.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); public abstract BookDao bookDao(); public static final Migration MIGRATION_1_2 = new Migration(1, 2) { //参数表示数据库从版本1升级到2 @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null, pages integer not null)"); } }; private static AppDatabase instance = null; public static AppDatabase getInstance(Context context) { if (instance == null) { Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database") .addMigrations(MIGRATION_1_2); instance = database.build(); } return instance; } }
改动的地方:1.注解@Database的版本号由1变成了2。
2.注解@Database的实体类中添加了Book.class。
3.抽象类AppDatabase中添加了获取BookDao的实例的抽象方法。
4.定义了一个Migration类型的变量,实现了Migration()匿名内部类,在重写方法migrate()中使用了建表语句。
5.在Room.databaseBuilder()方法后加上了addMigration()方法,并传入Migration变量作为参数。
如果数据库升级时想要修改的时表中的一列,而不是整张表。使用alter语句修改表结构就可以了。
Book实体类中:
@Entity
public class Book {
@PrimaryKey(autoGenerate = true)
public long id;
@ColumnInfo(name = "name")
public String name;
@ColumnInfo(name = "pages")
public int pages;
@ColumnInfo(name = "author")
public String author;
}
我们添加了一个作者字段,表示这张表添加一列。
修改AppDatabase中的代码:
@Database(version = 3, entities = {User.class, Book.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract BookDao bookDao();
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null, pages integer not null)");
}
};
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
//参数表示数据库从版本2升级到3
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("alter table Book add colum author not null default 'unknown'");
}
};
private static AppDatabase instance = null;
public static AppDatabase getInstance(Context context) {
if (instance == null) {
Builder<AppDatabase> database = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3);
//将MIGRATION_1_2, MIGRATION_2_3作为参数同时传递
instance = database.build();
}
return instance;
}
}
注解@Database的版本号变成了3;然后编写一个Migration类型的变量,重写方法migrate()中写alter语句添加一列。最后添加到addMigration()方法的参数中。
WorkManager
WorkManager适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlarmManager还是JobScheduler实现,从而降低了我们的使用成本。另外WorkManager还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。
WorkManager和Service并不相同,也没有直接的联系。WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然会得到执行。因此WorkManager很适合用于执行一些定期和服务器进行交互的任务。
WorkManager的基本用法
导依赖包:
implementation 'androidx.work:work-runtime:2.8.1'
WorkManager的用法主要分为以下三步:
-
定义一个后台任务,并实现具体的逻辑;
-
配置该后台任务的运行条件和约束信息,并构建后台任务请求;
-
将该后台任务请求传入WorkManager的enqueue()方法中,系统会在合适的时间运行。
首先定义一个后台任务
新建一个SimpleWorker类:
public class SimpleWorker extends Worker {
public SimpleWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
Log.d("TAG", "doWork: in SimpleWorker");
return Result.success();
}
}
每一个后台任务都必须继承自Worker类,并调用它唯一的构造函数。然后重写父类的doWork()方法,在这个方法中编写具体的后台任务逻辑即可。
doWork()方法不会运行在主线程当中,可以放心的在这里执行耗时逻辑。
doWork()方法会返回一个Result对象,用于表示任务的运行结果,成功就返回Result.success(),失败就返回Result.failure()。还有Result.retry()方法,他也表示失败,可以结合WorkRequest.Builder的setBackoffCriteria()方法来重新执行任务。
配置该后台任务的运行条件和约束信息,并构建后台任务请求
WorkRequest.Builder的子类:
-
OneTimeWorkRequest.Builder 用于构建单次运行的后台任务请求。
OneTimeWorkRequest request1 = new OneTimeWorkRequest.Builder(SimpleWorker.class).build(); //传递的参数是刚才创建的 后台任务的class对象
-
PeriodicWorkRequest.Builder 可用于构建周期性运行的后台任务请求。但是为了降低设备性能消耗,该够早函数中传入的运行周期间隔不能短于15 分钟。
PeriodicWorkRequest request2 = new PeriodicWorkRequest.Builder(SimpleWorker.class, 15, TimeUnit.MINUTES).build(); //参数:1.后台任务类的class对象;2.运行周期间隔时间;3.时间单位
将后台任务请求传入WorkManager的enqueue()方法中
创建出后台任务请求对象之后,传入WorkManager的enqueue()方法中,系统就会在合适的时间去执行了:
WorkManager.getInstance(this).enqueue(request1);
代码示例:
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/doWorkBtn"
android:text="Do Work"/>
//在activity_main.xml文件中添加了一个按钮,点击开始执行任务
...
findViewById(R.id.doWorkBtn).setOnClickListener(view ->{
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
WorkManager.getInstance(this).enqueue(request);
//在MainActivity中的onCreate()方法中设置按钮的监听事件
});
使用WorkManager处理复杂的任务
让后台任务在指定的延迟时间后运行
借助setInitialDelay()方法。
OneTimeWorkRequest request1 = new OneTimeWorkRequest.Builder(SimpleWorker.class)
.setInitialDelay(5, TimeUnit.MINUTES) //5, 单位分钟
.build();
表示让SimpleWorker这个给后台任务在5分钟之后运行。
通过标签来取消后台任务请求
如果没有标签可以通过id来取消后台任务请求:
WorkManager.getInstance(this).cancelWorkById(request.getId());
使用id只能取消单个后台任务请求,而使用标签可以将头一标签的后台任务请求全部取消。
WorkManager.getInstance(this).cancelAllWorkByTag("TAG");
这种取消针对的是将“TAG”设置为标签的后台任务请求:(addTag()方法)
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SimpleWorker.class)
.addTag("Tag")
.setInitialDelay(5, TimeUnit.MINUTES)
.build();
一次清空所有后台任务请求:
WorkManager.getInstance(this).cancelAllWork();
后台任务失败后重新执行
如果后台任务的doWork()方法中失败返回了Result.retry(),此时是可以结合setBackoffCriteria()方法来重新执行任务:
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SimpleWorker.class)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build();
setBackoffCriteria()方法接受三个参数:
第一个参数用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟。有两个可选值:LINEAR代表下次重试的时间已线性的方式延迟;EPONENTIAL表示下次重试的时间以指数的方式延迟。
第二、第三个参数用于指定在多久后第一次重新执行任务,时间最短不能少于10秒。
监听后台任务执行结果(doWork()方法的返回状态)
//对doWork()方法的返回结果(后台任务运行结果)进行监听
WorkManager.getInstance(this)
.getWorkInfoByIdLiveData(request.getId())
.observe(this, new Observer<WorkInfo>() {
@Override
public void onChanged(WorkInfo workInfo) {
if (workInfo.getState() == WorkInfo.State.SUCCEEDED){
//当doWork()方法返回的是Result.success();
Log.d("TAG", "onChanged: 后台任务执行成功");
} else if (workInfo.getState() == WorkInfo.State.FAILED){
//当doWork()方法返回的是Result.failure();
Log.d("TAG", "onChanged: 后台任务执行失败");
}
}
});
调用WorkManager实例的getWorkInfoByIdLiveData()方法,参数传入后台任务请求的id。这个方法会返回一个LiveData对象。可以直接调用LiveData对象的observe()方法来观察结果。
另外也可以调用getWorkInfosByTagLiveData()方法监听同意标签名下所有的后台任务请求的运行结果,参数需要传入一个"Tag"。
链式任务
OneTimeWorkRequest a = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
OneTimeWorkRequest b = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
OneTimeWorkRequest c = new OneTimeWorkRequest.Builder(SimpleWorker.class).build();
WorkManager.getInstance(this)
.beginWith(a)
.then(b)
.then(c)
.enqueue();
现在有a、b、c三个独立的后台任务。要求先执行a、在执行b、最后执行c的功能,就可以借助链式任务来实现。
调用WorkManager的实例方法beginWith()表示开启一个链式任务,之后需要用then()方法连接,最后调用enqueue()方法递交任务请求。
对于链式任务,只有前一个任务执行成功之后,下一个任务才会执行。
WorkManager可以用,但别依赖他去实现什么核心和功能,因为他在国产手机上可能不太稳定。