学习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()

  这个方法会将当前数据销毁后重建,副作用就是里面的数据都会消失。

  已经发布的产品就不适合用这种方式了,标准方式如下:

  1.   如果需要在数据库中添加一张表Book,首先就是创建Book的实体类,类中包含了主键id、书名、页数字段;
  2.   然后创建一个BookDao接口,在里面定义一些API;
  3.   最后修改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步:

  1. 定义一个后台任务,实现具体逻辑;
  2. 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
  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的后台任务。

 

posted @   PeacefulGemini  阅读(226)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
回顶部
点击右上角即可分享
微信分享提示