学习Android之探究Jetpack
高级程序开发组件——Jetpack
Jetpack是一个开发组件工具集,它的主要目的是帮助我们编写出更加简洁的代码,并简化我们的开发过程。Jetpack中的组件有一个特点,它们大部分不依赖于任何Android系统版本,这意味着这些组件通常是定义在AndroidX库当中的,并且拥有非常好的向下兼容性。
Jetpack主要由基础、架构、行为、界面这4部分组成。其实里面有很多东西我们都是已经学过了的,比如通知、权限、Fragment等等。
其中的许多架构组件就是专门为MVVM架构量身打造的。
1 ViewModel
ViewModel算是Jetpack中最重要的组件之一了。在传统开发模式下,Activity的任务很重,既要负责逻辑处理,还要控制UI展示,甚至还要处理网络回调等。一旦在大项目中用这种方式,项目就会表的臃肿且难以维护,这就体现出了MVP、MVVM架构的重要性。
而ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。可以在一定程度上减少Activity中的逻辑。
此外,ViewModel还有一个重要特性,当手机发生横竖屏旋转时,Activity会重新创建,里面的数据会丢失,而ViewModel不会被重新创建,只有当Activity退出时才会销毁。因此,与界面相关的变量存放在ViewModel中,不用担心发生旋转时数据丢失。
我们来看一下ViewModel的生命周期:
1.1 ViewModel的基本用法
通过实现一个简单的计数器来学习ViewModel的基本用法。
Jetpack的组件通常是以AndroidX库的形式发布的,所以一些常用的Jetpack组件会在项目创建时自动包含进去。
不过要用ViewModel组件还是需要添加依赖:
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
一个好的编程规范是给每一个Activity和Fragment都创建一个对应的ViewModel,因此这里需要为MainActivity创建一个对应的MainViewModel类,继承自ViewModel,我们要实现的是一个计数器的功能,所以在这里面定义一个counter变量计数:
class MainViewModel : ViewModel() { var counter = 0 }
我们可以给界面添加一个按钮,点击一次计数器就+1,并把最新的计数显示出来,修改activity_main.xml中的代码:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/infoText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:textSize="30sp"/> <Button android:id="@+id/plusOneBtn" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Plus One" /> </LinearLayout>
接着开始实现计数的逻辑,回到MainActivity中:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) binding.plusOneBtn.setOnClickListener { viewModel.counter++ refreshCounter() }
refreshCounter() } private fun refreshCounter() { binding.infoText.text = viewModel.counter.toString() } }
注意:不能直接去创建ViewModel的实例,一定要通过ViewModelProvider来获取ViewModel的实例。语法如下:
ViewModelProvider(<Activity或Fragment实例>).get(<ViewModel>::class.java)
这是因为ViewModel有着独立的生命周期,并且长于Activity,如果在onCreate()方法中创建ViewModel的实例,那么每次执行onCreate()方法的时候,ViewModel都会创建一个新的实例,这样就当旋转时就无法保留数据了。
1.2 向ViewModel传递参数
我们发现,上面创建的MainViewModel的构造函数中没有任何参数,但是如果我们需要通过构造函数来传递一些参数,该怎么做?由于所有ViewModel的实例都是用过ViewModelProvider来获取的,因此没有任何地方可以向ViewModel的构造函数中传递参数。
不过我们只需要借助ViewModelProvider.Factory就可以实现了。
现在在屏幕旋转的时候不会丢失数据,但是退出程序再进来数据就会被清零,这里就实现一下保存数据功能。在退出程序时保存计数,然后打开时读取保存的计数,并传递给ViewModel,修改MainViewModel中的代码:
class MainViewModel(countReserved: Int) : ViewModel() { var counter = countReserved }
这里很好理解,接下来就是如何向MainViewModel的构造函数中传递数据了。新建一个MainViewModelFactory类,继承ViewModelProvider.Factory接口:
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory{ override fun <T : ViewModel> create(modelClass: Class<T>): T { return MainViewModel(countReserved) as T } }
MainViewModelFactory的构造函数中也接收了一个countReserved参数,然后实现create()方法,在此方法中,创建了MainViewModel的实例,并将countReserved参数传了进入。
为什么这里可以直接创建MainViewModel的实例呢?因为create()方法的执行时机和Activity的生命周期无关,所以不会产生问题。
另外,我们还可以添加一个清零按钮,id为clearBtn。
最后回到MainActivity中:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding lateinit var viewModel: MainViewModel lateinit var sp : SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) sp = getPreferences(Context.MODE_PRIVATE) val countReserved = sp.getInt("count_reserved", 0) viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)) .get(MainViewModel::class.java) ... binding.clearBtn.setOnClickListener { viewModel.counter = 0 refreshCounter() } refreshCounter() } override fun onPause() { super.onPause() sp.edit { putInt("count_reserved", viewModel.counter) } } ... }
首先获取SharedPreferences的实例,读取之前保存的数据。接着在ViewModelProvider中,额外传入了一个MainViewModelFactory参数,将读取的数据传给了MainViewModelFactory的构造函数。只有这种写法才能将数据最终传递给MainViewModel的构造函数。
至此,传参功能完成。
2 Lifecycles
我们可能经常会遇到感知Activity生命周期的情况,比如,某界面中发起了一条网络请求,但是当请求得到回应的时候,界面可能已经关闭了,此时就不应该继续对响应结果进行处理。因此,我们需要能时刻感知到Activity的生命周期,以便在适当的时候进行逻辑控制。
在一个Activity中去感知它的生命周期非常简单,而如果要在一个非Activity的类中去感知Activity的生命周期,应该怎么办呢?
可以通过在Activity中嵌入一个隐藏的Fragment来进行感知,或者通过手写监听器的方式来进行感知,等等。
手写监听器的方式
通过手写监听器的方式来对Activity的生命周期进行感知:
class MyObserver { fun activityStart() { } fun activityStop() { } } class MainActivity : AppCompatActivity() { lateinit var observer: MyObserver override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) observer = MyObserver() } override fun onStart() { super.onStart() observer.activityStart() } override fun onStop() { super.onStop() observer.activityStop() } }
为了能让MyObserver能够感知到Activity的生命周期,需要专门在MainActivity中重写响应的生命周期方法,然后再通知给MyObserver。这种方式虽然能够正常工作,但是需要在Activity中编写太多额外的逻辑。
而Lifecycles组件就是为了解决这一问题而出现的,它可以让任何一个类都能轻松感知到Activity的生命周期,同时也不需要大量编写逻辑。
使用它,先新建一个MyObserver类,实现LifecycleObserver接口:
class MyObserver : LifecycleObserver { }
LifecycleObserver是一个空方法接口,不需要重写任何方法。
接下来可以在里面定义方法了,想要感知Activity的生命周期,还得借助额外的注解功能才能,如下:
class MyObserver : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) fun activityStart() { Log.d("MyObserver", "activityStart: ") } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun activityStop() { Log.d("MyObserver", "activityStop: ") } }
在方法上使用了@OnLifecycleEvent注解,并传入了一种生命周期事件。
生命周期事件的类型有7种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP和ON_DESTROY分别匹配Activity中相应的生命周期回调;还有一种ON_ANY表示可以匹配Activity的任何生命周期回调。
所以,上述代码中的activityStart()和activityStop()方法就应该分别在Activity的onStart()和onStop()触发的时候执行。
但是目前还是不能正常工作的,因为当Activity的生命周期发生变化时并没有去通过MyObserver,这个时候就要借助LifecycleOwner,它使用如下语法结构让MyObserver得到通知:
lifecycleOwner.lifecycle.addObserver(MyObserver())
首先调用LifecycleOwner的getLifecycle()方法,得到一个Lifecycle对象,然后调用它的addObserver()方法观察LifecycleOwner的生命周期,再把MyObserver的实例传进去就可以了。
接下来的问题就是,怎样获取LifecycleOwner的实例?
实际上,如果Acitvity是继承自AppCompatActivity的,或者Fragment是继承自androidx.fragment.app.Fragment的,那么它们本身就是一个LifecycleOwner的实例。这样我们就可以在MainActivity中这样写:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(MyObserver()) } }
现在,MyObserver能够自动感知Activity的生命周期了,上述内容在Fragment也是通用的。
不过目前只是感知,我们还可以主动获取当前的生命周期状态。只需要在MyObserver的构造函数中讲Lifecycle对象传进来即可:
class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver { ... }
有了Lifecycle对象,就可以在任何地方调用lifecycle.currentState来主动获取当前的生命周期状态。
lifecycle.currentState返回的生命周期状态是一个枚举类型,一共有INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED这5种状态类型。它们与Activity的生命周期回调所对应的关系如图所示。
当获取的生命周期状态是CREATED的时候,说明onCreate()方法已经执行了,但是onStart()方法还没有执行。当获取的生命周期状态是STARTED的时候,说明onStart()方法已经执行了,但是onResume()方法还没有执行,以此类推。
3 LiveData
LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。LiveData特别适合与ViewModel结合在一起使用。
3.1 LiveData的基本用法
回顾上面编写的计数器其实是存在问题的,当每次点击+1按钮的时候,都会先给ViewModel中的计数+1,然后立即获取最新的计数。这种方式在单线程中可以正常工作,但是如果在ViewModel的内部开启了线程去执行一些耗时逻辑,那么在点击+1按钮之后会立刻去获取最新的数据,得到的肯定还是之前的数据。
我们一直使用的都是在Activity中手动获取ViewModel中的数据这种交互方式,但是ViewModel却无法将数据的变化主动通知给Activity。不要想着把Activity的实例传给ViewModel来实现主动通知,这是错误的做法,是很有可能造成内存泄漏的。
解决方案就是使用LiveData,如果我们用LiveData来包装计数器的计数,然后在Activity中观察它,就可以主动将数据变化通知给Activity了。
具体实现如下,修改MainViewModel中的代码:
class MainViewModel(countReserved: Int) : ViewModel() { var counter = MutableLiveData<Int>() init { counter.value = countReserved } fun plusOne() { val count = counter.value ?: 0 counter.value = count + 1 } fun clear() { counter.value = 0 } }
这里的counter变量成为了一个MutableLiveData对象,泛型为Int。MutableLiveData是一种可变的LiveData,它有3中读写数据的方法,分别是getValue()、setValue()、postValue()方法。
getValue()方法用于获取LiveData中包含的数据;
setValue()方法用于给LiveData设置数据,但是只能在主线程中调用;
postValue()方法用于在非主线程中给LiveData设置数据。
plusOne()方法中取到的数据可能为空,所以使用了一个?:操作符,当获取到空数据时,用0来作为默认计数。MainViewModel修改完了。
接下来修改MainActivity:
class MainActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { ... binding.plusOneBtn.setOnClickListener { viewModel.plusOne() } binding.clearBtn.setOnClickListener { viewModel.clear() } viewModel.counter.observe(this, Observer { count -> binding.infoText.text = count.toString() }) } override fun onPause() { super.onPause() sp.edit { putInt("count_reserved", viewModel.counter.value ?: 0) } } }
在点击事件中我们调用的是MainViewModel中的相应方法。最关键的一步是,调用了viewModel.counter的observe()方法来观察数据的变化。现在counter变量是一个LiveData对象,任何LiveData对象都可以调用它的observe()方法来观察数据的变化。
observe()方法接收两个参数:
第一个是一个LifecycleOwner对象,而Activity本身就是一个LifecycleOwner对象。
第二个是一个Observer接口,当counter中包含的数据发生变化时,就会回调到这里,因此在这里将计数更新。
这个时候就不用担心ViewModel内部会不会开启线程执行耗时逻辑了。如果需要在子线程中给LiveData设置数据,一定要使用postValue()方法。
这里思考一个问题?
LiveData的observe()方法是一个Java方法,观察Observer接口,会发现这是一个单抽象方法接口,只有一个待实现的onChanged()方法。既然是单抽象方法接口,为什么调用observe()方法时却没有使用之前学过的Java函数式API的写法呢?
这种情况比较特殊,因为observe()方法接收的另一个参数LifecycleOwner也是一个单抽象方法接口。当一个Java方法同时接收两个单抽象方法接口参数时,要么同时使用函数式API的写法,要么都不使用函数式API的写法。因为我们传入的第一个参数是this,所以第二个参数就不能使用函数式API的写法了。
不过,有一个专门为Kotlin语言设计的库——lifecycle-livedata-ktx,这个库在2.2.0版本加入了对observe()方法的语法扩展,只需要添加一下依赖:
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
然后就可以使用如下语法结构的observe()方法:
viewModel.counter.observe(this) { count -> binding.infoText.text = count.toString() }
现在是能正常工作的,但是并不规范,主要问题就是将counter这个可变的LiveData暴露给了外部,破坏了ViewModel数据的封装性,还有一定风险。
推荐做法是:永远只暴露不可变的LiveData给外部。这样在非ViewModel中只能观察到LiveData的数据变化,而不能给LiveData设置数据。修改MainViewModel来实现这样的功能:
class MainViewModel(countReserved: Int) : ViewModel() { val counter: LiveData<Int> get() = _counter private val _counter = MutableLiveData<Int>() init { _counter.value = countReserved } fun plusOne() { val count = _counter.value ?: 0 _counter.value = count + 1 } fun clear() { _counter.value = 0 } }
这里先将原来的counter变量改名为_counter变量, 并加上private修饰符,这样_counter变量对于外部就是不可见的了。然后又新定义了一个counter变量,将它的类型声明为不可变的LiveData,并在它的get()属性方法中返回_counter变量。
这样,当外部调用counter变量时,实际上获取到的是_counter的实例,但是无法给counter设置数据,从而保证了ViewModel的数据封装性。
目前这种写法是非常规范的,也是官方比较推荐的。
3.2 map和switchMap
LiveData提供了两种转换方法:map()和switchMap()方法。
map()方法的作用是将实际包含函数的LivaData和仅用于观察数据的LiveData进行转换。比如说有一个User类,其中包含用户的姓名和年龄,我们可以在ViewModel中创建一个相应的LiveData来包含User类型的数据,如下:
class MainViewModel(countReserved: Int) : ViewModel() { val userLiveData = MutableLiveData<User>() ... }
此时如果明确MainActivity中只会显示用户的姓名,而不在意年龄。那么这个时候还将整个User类型的LiveData暴露给外部就不合适了。
而map()方法就是专门用来解决这种问题的。它可以将User类型的LiveData自由地转型成任意其他类型的LiveData,用法如下:
class MainViewModel(countReserved: Int) : ViewModel() { private val userLiveData = MutableLiveData<User>() val userName: LiveData<String> = Transformations.map(userLiveData) { user -> "${user.firstName} ${user.lastName}" } ... }
这里调用了Transformations的map()方法来对LiveData的数据类型进行转换。
map()接收两个参数:
第一个是原始的LiveData对象;
第二个是一个转换函数,在里面编写具体的转换逻辑即可。这里就是将User对象转换成了一个只包含用户姓名的字符串。
现在userLiveData声明成了private,保证了数据的封装性,外部使用的时候就观察userName这个LiveData即可。
当userLiveData的数据发生变化时,map()方法会监听到变化并执行转换函数中的逻辑,然后再将转换之后的数据通知给userName的观察者。
而switchMap()方法使用场景非常固定,但可能比map()更常用。
我们之前所有的LiveData对象的实例都是在ViewModel中创建的。在实际的项目中,很可能ViewModel中的某个LiveData对象是调用另外的方法获取的。
比如以下情况,新建一个Repository单例类,如下所示:
object Repository { fun getUser(userId: String): LiveData<User> { val liveData = MutableLiveData<User>() liveData.value = User(userId, userId, 0) return liveData } }
这里将每次传入的userId当作用户姓名来创建一个新的User对象。
每次调用getUser()方法都会返回一个新的LiveData实例。
然后我们在MainViewModel中也定义一个getUser()方法,并调用Repository的getUser()方法来获取LiveData对象:
class MainViewModel(countReserved: Int) : ViewModel() { ... fun getUser(userId: String): LiveData<User> { return Repository.getUser(userId) } }
接下来就是解决如何在Activity中观察LiveData的数据变化。
上面提到过 “每次调用getUser()方法都会返回一个新的LiveData实例。”,所以如果使用一下写法是错误的:
viewModel.getUser(userId).observe(this) { user -> }
因为每次得到的都是一个新的LiveData实例,无法观察到数据的变化。
此时就出现了switchMap()方法,它的使用场景很固定:如果ViewModel中的某个LiveData对象是调用另外的方法获取的,那么我们就可以借助switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。
回到MainViewModel中:
class MainViewModel(countReserved: Int) : ViewModel() { ... private val userIdLiveData = MutableLiveData<String>() val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId -> Repository.getUser(userId) } fun getUser(userId: String){ userIdLiveData.value = userId } }
这里定义了一个新的userIdLiveData对象,来观察userId的数据变化,然后调用Transformations的switchMap()方法,用来对另一个可观察的LiveData对象进行转换。
swiychMap()方法接收两个参数:
第一个是新增的userIdLiveData,switchMap()方法会对它进行观察;
第二个是一个转换函数。还必须在这个转换函数中返回一个LiveData对象,因为switchMap()方法的工作原理就是要将转换函数中返回的LiveData对象转换成另一个可观察的LiveData对象。所以我们只需要在转换函数中调用Repository的getUser()方法来得到LiveData对象并返回。
来梳理一遍switchMap()的工作流程:
首先,当外部调用MainViewModel的getUser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的userId值设置到userIdLiveData当中。一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap()方法就会执行,并且调用我们编写的转换函数。然后在转换函数中调用Repository.getUser()方法获取真正的用户数据。同时,switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象,对于Activity而言,只要去观察这个LiveData对象就可以了。
接下来我们来了解一下LiveData、ViewModel和Lifecyclers组件之间的关系。
LiveData之所以能够成为Activity和ViewModel之间通信的桥梁,并且还不会有内存泄漏的风险,靠的就是Lifecyclers组件。LiveData在内部使用了Lifecyclers组件来自我感知生命周期的变化,从而可以在Activity销毁的时候及时释放引用,避免产生内存泄漏的问题。
另外,由于要减少性能消耗,当Activity处于不可见的状态的时候(比如手机息屏,或者被其他的Activity遮挡),如果LiveData中的数据发生了变化,是不会通知给观察者的。只有当Activity重新恢复可见状态时,才会将数据通知给观察者,而LiveData之所以能够实现这种细节的优化,依靠的还是Lifecyclers组件。
如果在Activity处于不可见状态的时候,LiveData发生了多次数据变化,当Activity恢复可见状态时,只有最新的那份数据才会通知给观察者,前面的数据在这种情况下相当于已经过期了,会被直接丢弃。
4 Room
市面上有许多专门为Android数据库设计的ORM框架。ORM(Object Relational Mapping)也叫对象关系映射。我们用的编程语言是面向对象语言,而用的数据库是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了。
它给我们带来的好处就是可以使用面向对象的思维来和数据库交互。而Android官方推出的一个ORM框架就是Room。
4.1 使用Room增删改查
Room的整体结构主要由Entity、Dao和Database三部分组成。
-
Entity:用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
-
Dao:Dao是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。
-
Database:用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例。
使用Room还需要添加依赖:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' dependencies { ... implementation 'androidx.room:room-runtime:2.4.2' kapt 'androidx.room:room-compiler:2.4.2' }
这里新增了一个kotlin-kapt插件,同时在dependencies闭包中添加了两个Room的依赖库。由于Room会根据我们在项目中声明的注解来动态生成代码,因此这里一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能则一定要先添加kotlin-kapt插件。注意,kapt只能在kotlin项目中使用,如果是Java项目,就要使用annotationProcessor。
首先来定义实体类,可以用之前定义的User类,但是还需要进行修改,如下:
@Entity data class User(var firstName: String, var lastName: String, var age: Int) { @PrimaryKey(autoGenerate = true) var id: Long = 0 }
一个良好的数据库编程习惯是给每个实体类都加上一个id字段,并设置为主键。
接下来是定义Dao,这是Room中最关键的地方,所有访问数据库的操作都在这儿。新建一个UserDao接口:
@Dao interface UserDao { @Insert fun insertUser(user: User): Long @Update fun updateUser(newUser: User) @Query("select * from User") fun loadAllUsers(): List<User> @Query("select * from User where age > :age") fun loadUsersOlderThan(age: Int): List<User> @Delete fun deleteUser(user: User) @Query("delete from User where lastName = :lastName") fun deleteUserByLastName(lastName: String): Int }
接口上面要使用@Dao注解,这样Room才能将它识别成一个Dao。Room提供了@Insert、@Delete、@Update和@Query这4种相应的注解。@Insert注解插入数据后会返回主键id值。
如果想要从数据库中查询数据或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。就比如loadUsersOlderThan()方法和deleteUserByLastName()方法,需要在@Query注解中编写SQL语句进行增删改。
而且Room是支持在编译时动态检查SQL语句的。
最后定义Database,它只有3个部分的内容:数据库版本号、包含的实体类、Dao层的访问实例。新建AppDatabase.kt文件:
@Database(version = 1, entities = [User::class]) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao companion object { private var instance: AppDatabase? = null @Synchronized fun getDatabase(context: Context): AppDatabase { instance?.let { return it } return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database") .build().apply { instance = this } } } }
@Database注解中多个实体类之间用逗号隔开,AppDatabase类必须继承自RoomDatabase类,并使用abstract关键字将它声明成抽象类,然后提供相应的抽象方法,用于获取之前编写的Dao的实例,比如这里提供的userDao()方法。不过我们只需要进行方法声明就可以了,具体的方法实现是由Room在底层自动完成的。
然后在companion object结构体中编写了一个单例模式,因为原则上全局应该只存在一份AppDatabase的实例。然后在getDatabase()方法中判断:如果instance变量不为空就直接返回,否则就调用Room.databaseBuilder()方法来构建一个AppDatabase的实例。
databaseBuilder()方法接收3个参数,注意第一个参数一定要使用applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况。第二个参数是AppDatabase的Class类型,第三个参数是数据库名。最后调用build()方法完成构建,并将创建出来的实例赋值给instance变量,然后返回当前实例即可。
在Activity中使用方法如下:
val userDao = AppDatabase.getDatabase(this).userDao() val user1 = User("john","man", 25) binding.addDataBtn.setOnClickListener { thread { user1.id = userDao.insertUser(user1) } } binding.queryDataBtn.setOnClickListener { thread { for (user in userDao.loadAllUsers()) { Log.d("MainActivity", user.toString()) } } }
很容易理解,先获取UserDao的实例,再调用相应方法即可。由于数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作的,所以开启了子线程。
不过为了方便测试,可以在构建AppDatabase实例的时候,接入一个allowMainThreadQueries()方法,这样Room就允许在主线程中进行数据库操作了,不过只建议在测试环境下使用。如下:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database") .allowMainThreadQueries() .build()
4.2 Room的数据库升级
Room数据库的升级还是不太简便,如果项目还在开发测试阶段,那么可以使用如下方法:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database") .fallbackToDestructiveMigration() .build()
这个方法会将当前数据销毁后重建,副作用就是里面的数据都会消失。
已经发布的产品就不适合用这种方式了,标准方式如下:
- 如果需要在数据库中添加一张表Book,首先就是创建Book的实体类,类中包含了主键id、书名、页数字段;
- 然后创建一个BookDao接口,在里面定义一些API;
- 最后修改AppDatabase中的代码。编程数据库升级逻辑,如下:
@Database(version = 2, entities = [User::class, Book::class]) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun bookDao(): BookDao companion object { val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("create table book (id integer primary key autoincrement not null, " + "name text not null, pages integer not null)") } } private var instance: AppDatabase? = null @Synchronized fun getDatabase(context: Context): AppDatabase { instance?.let { return it } return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database") .addMigrations(MIGRATION_1_2) .build().apply { instance = this } } } }
首先在第一行注解中升级版本号,并加入Book实体类,接着提供bookDao()方法获取实例。
关键点在于:实现了一个匿名类Migration,它的实例变量命名为MIGRATION_1_2,传入1和2,表示数据库版本从1升级到2的时候就执行匿名类中的升级逻辑。之后在里面编写相应的SQL语句。
最后构建AppDatabase实例的时候,加入addMigrations方法,并传入MIGRATION_1_2即可。
如果是数据库升级是需要向表中新增列的话,就用alter语句修改表结构即可,比如现在我们往Book表中新增一个作者字段,首先修改Book实体类:
@Entity data class Book(var name: String, var pages: Int, var author: String) { @PrimaryKey(autoGenerate = true) var id: Long = 0 }
然后修改AppDatabase:
@Database(version = 3, entities = [User::class, Book::class]) abstract class AppDatabase : RoomDatabase() { ... companion object { ... val MIGRATION_2_3 = object : Migration(2,3) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("alter table Book add column author text not null default 'unknown'") } } ...return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build().apply { instance = this } } } }
5 WorkManager
Android 8.0系统开始禁用了Service的后台功能,只允许使用前台Service。
WorkManager很适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层的实现方法,降低我们的使用成本,它还支持周期性任务、链式任务处理等。
但是WorkManager和Service没有直接的联系。Service是四大组件之一,在没有被销毁的情况下一直在后台运行,而WorkManager是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务依然能得到执行,因此WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性同步数据等。
5.1 WorkManager的基本用法
添加依赖:
implementation 'androidx.work:work-runtime:2.7.1'
它的基本用法分为3步:
- 定义一个后台任务,实现具体逻辑;
- 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
- 将该后台任务请求传入WorkManager的enqueue()方法中,系统会在适合的时候运行。
第一步,定义一个后台任务,创建一个SimpleWorker类:
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) { override fun doWork(): Result {
// 编写具体后台任务逻辑 Log.d("SimpleWorker", "doWork: ") return Result.success() } }
继承自Worker类,并调用它唯一的构造函数,然后重写它的doWork()方法。
doWork()方法不会运行在主线程中,可以在里面执行耗时操作。返回的Resule对象表示任务运行的结果。
第二步,进行最基本的配置:
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
OneTimeWorkRequest.Builder是WorkRequest.Builder的子类,用于构建单次运行的后台任务请求。它还有一个子类PeriodicWorkRequest.Builder,可用于构建周期性运行的后台任务请求,但是为了降低设备性能消耗,PeriodicWorkRequest.Builder构造函数中传入的运行周期间隔不能短于15分钟,如下:
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build()
最后一步,将构建出的后台任务请求传入WorkManager的enqueue()方法中:
WorkManager.getInstance(context).enqueue(request)
5.2 使用WorkManager处理复杂任务
我们可以让后台任务在指定的延迟时间后运行,借助setIntitalDelay()方法:
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java) .setInitialDelay(5, TimeUnit.MINUTES) .build()
这里是5分钟后运行,可以自行指定时间单位。
还可以给后台任务添加标签:
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java) .setInitialDelay(5, TimeUnit.MINUTES) .addTag("simple") .build()
添加标签的好处就是可以通过标签来取消后台任务请求:
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
也可以使用id取消后台任务请求:
WorkManager.getInstance(this).cancelWorkById(request.id)
只不过,使用id只能取消单个后台任务请求,而使用标签就可以取消所有用此标签的后台任务请求。
一次性取消所有后台任务请求代码:
WorkManager.getInstance(this).cancelAllWork()
如果后台任务的doWork()方法返回了Result.retry(),那么是可以结合setBackoffCriteria()方法来重新执行任务:
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java) ...
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) .build()
setBackoffCriteria()方法接收3个参数:
第一个参数用于指定如果任务再次执行失败,下次重试的时间以什么样的形式延迟,有两个值可选,LINEAR表达以线性的方式延迟,EXPONENTIAL表示以指数的方式延迟。
第二和第三个参数就很好理解了,重新执行任务的时间不能少于10秒。
这就是Result.retry()的作用,而Result.success()和Result.failure()的作用就是通知任务运行结果的,我们可以对运行结果监听:
WorkManager.getInstance(this) .getWorkInfoByIdLiveData(request.id) .observe(this) { workInfo -> if (workInfo.state == WorkInfo.State.SUCCEEDED) { Log.d("MainActivity", "suceeded ") } else if (workInfo.state == WorkInfo.State.FAILED) { Log.d("MainActivity", "failed ") } }
调用getWorkInfoByIdLiveData()方法,并传入后台任务请求的id,会返回一个LiveData对象。然后就可以调用LiveData对象的observe()方法来观察数据变化了,以此监听后台任务的运行结果。
链式任务
比如定义了3个独立的后台任务:同步数据、压缩数据和上传数据。现在我们想要实现先同步、再压缩、最后上传的功能,就可以借助链式任务来实现,代码如下:
val sync = ... val compress = ... val upload = ... WorkManager.getInstance(this) .beginWith(sync) .then(compress) .then(upload) .enqueue()
beginWith()方法用于开启一个链式任务,后面要接上什么样的后台任务,只需要使用then()方法来连接即可。另外WorkManager还要求,必须在前一个后台任务运行成功之后,下一个后台任务才会运行。也就是说,如果某个后台任务运行失败,或者被取消了,那么接下来的后台任务就都得不到运行了。
不要依赖WorkManager去实现核心功能,因为在国产手机上可能不稳定,大多数国产手机厂商在进行Android系统定制的时候增加了一个一键关闭的功能,会杀死所有非白名单的应用程序,被杀死的应用程序及无法接受广播,也无法执行WorkManager的后台任务。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)