}
}
现在我们实现了一个计数器,在这里需要注意,我们不能直接去创建 ViewModel
的实例,而是要通过 ViewModelProvider
来获取 ViewModel
的实例,之所以这样是因为 ViewModel
有独立的生命周期,且其生命周期长于 Activity
的生命周期,如果在 onCreat()
中创建 ViewModel
的实例,那么每次 onCreat()
执行时,ViewModel
都会创建一个实例,这样就无法保存其中的数据了。
ViewModel
对象存在的时间范围是获取 ViewModel
时传递给 ViewModelProvider
的 Lifecycle
。ViewModel
将一直留在内存中,直到限定其存在时间范围的 Lifecycle
永久消失,如下图是 Activity
的生命周期和 VIewModle
的生命周期对应的情况
借助 ViewModelProvider.Factory
,下面我们来实现退出程序后再打开,数据仍然不会消失的效果 先修改 MainViewModel
的代码,如下:
class MainViewModel(counterReserved: Int) : ViewModel(){
var counter = counterReserved //用于计数
}
接着创建 MainViewModelFactory
类,并实现 ViewModelProvider.Factory
接口,如下:
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class): T {
return MainViewModel(countReserved) as T
}
}
我们这里实现了接口要求我们的 create
方法,在方法里面我们创建并返回了一个 MainViewModel
的实例,为什么我们这里就可以创建 MainViewModel
的实例了呢?因为 create()
方法的执行时机和 Activity
的生命周期无关,所以不会产生之前提到的问题。
最后修改 activity 中的代码,如下:
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
lateinit var sp: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sp = getSharedPreferences(“count_reserved”,Context.MODE_PRIVATE)
val countReserved = sp.getInt(“count_reserved”,0)
viewModel = ViewModelProvider(this,MainViewModelFactory(countReserved))
.get(viewModel::class.java)
…
btn_clear.setOnClickListener {
viewModel.counter = 0
refreshCounter()
}
refreshCounter()
}
override fun onPause() {
super.onPause()
val edit = sp.edit()
edit.putInt(“count_reserved”,viewModel.counter)
edit.apply()
}
…
}
========================================================================
顾名思义,Lifecycles
是一个用来感知 Activity
生命周期的组件,下面来学习下简单用法。
新建一个 MyObserver
类,并实现 LifecycleObserver
接口
class MyObserver : LifecycleObserver{
}
LifecycleObserver
这是一个空方法接口,我们可以在 MyObserver
中定义任何方法,如果需要感知 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
、ON_ANY
。前六种分别匹配 Activity
中相应的的生命周期回调,最后一种表示可以匹配 Activity
的任何生命周期回调。
接下来就是需要 LifecycleOwner
去通知 MyObserver
生命周期发生了变化,它可以使用如下的语法结构去通知 MyObserver
lifecycleOwner.lifecycle.addObserver(MyObserver())
这里 LifecycleOwner
调用了 getLifecycle
方法,得到一个 Lifecycle
对象,接着调用 addObserver
来观察 LifecycleOwner
的生命周期,再把 MyObserver
传进去。
LifecycleOwner
是个什么?如何让获取一个 LifecycleOwner
的实例?
大多数情况下,只要 Activity
是继承自 AppCompatActivity
的,或者 Fragment
是继承自 androidx.fragment.app.Fragment
的,那么它们本身就是一个 LifecycleOwner
的实例,所以我们在 Activity
中就可以这样写
class MainActivity : AppCompatActivity() {
…
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
…
lifecycle.addObserver(MyObserver())
}
…
}
现在程序可以感知到 Activity
的生命周期变化,但没法主动获知当前的生命周期状态,解决这个问题,只需要在 MyObserver
的构造函数中将 Lifecycle
对象传进去即可,如下:
class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver{…}
有了 Lifecycle
对象后,就可以在任何地方调用 lifecycle.currntState
来主动获取当前的生命周期状态。
lifecycle.currntState
返回的生命周期状态时一个枚举类型,一共有 5 种状态类型,如下:
INITIALIZED
DESTROYED
CREATED
STARTED
RESUMED
它们与 Activity
的生命周期回调所对应的关系如图:
======================================================================
LiveData
是一种可观察的数据存储器类。与常规的可观察类不同,LiveData
具有生命周期感知能力,意指它遵循其他应用组件(如 Activity
、Fragment
或 Service
)的生命周期。这种感知能力可确保 LiveData
仅更新处于活跃生命周期状态的应用组件观察者。
LiveData
可以包含任何类型的数据,并在数据发生变法的时候通知给观察者
修改 MainViewModel
中的代码,如下:
class MainViewModel(countReserved: Int) : ViewModel() {
var counter = MutableLiveData()
init {
counter.value = countReserved
}
fun plusOne() {
val count = counter.value ?: 0
counter.value = count + 1
}
fun clear() {
counter.value = 0
}
}
这里将 counter
变量修改成了一个 MutableLiveData
对象,这是一种可变的 LiveData
。它主要有三种读写数据的方法,分别是:
-
getvalue()
//用于获取LiveData
中包含的数据 -
setValue()
//用于给LiveData
设置数据,但是只能在主线程中调用 -
postValue()
//用于在非主线程中给LiveData
设置数据
下面来修改 MainActivity 中的代码:
class MainActivity : AppCompatActivity() {
…
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
…
btn_plus.setOnClickListener{
viewModel.plusOne()
}
btn_clear.setOnClickListener {
viewModel.clear()
}
viewModel.counter.observe(this, Observer { count ->
tv_info.text = count.toString()
})
}
override fun onPause() {
super.onPause()
val edit = sp.edit()
edit.putInt(“count_reserved”,viewModel.counter.value ?: 0)
edit.apply()
}
}
这里 counter
变量已经变成了一个 LiveData
对象,任何 LiveData
对象都可以调用它的 observe()
方法来观察数据的变化。observer()
方法接收两个参数:第一个参数是一个 LifecycleOwner
对象,这里也就是 Activity
自己。第二个参数是一个 Observer
接口,当 counter
中包含的数据发生变化时,就会回调到这里。
关于 observe()
方法,Google 官方在专门面向 Kotlin 语言的 API 中提供了很多好用的语法扩展,要使用它需添加依赖:
implementation ‘androidx.lifecycle:lifecycle-livedata-ktv:2.2.0’
之后我们就可以使用如下结构的 observe()
方法了
viewModel.counter.observe(this) { count ->
tv_info.text = count.toString()
}
以上是 LiveData
基本用法,可以正常使用,但仍然不是最规范的用法, 主要问题是我们将 counter
这个可变的 LiveData
暴露给了外部,这样在 ViewModel
外面也是可以给 counter
设置数据,从而破坏了 LiveData
数据的封装性
比较推荐的做法是,永远只暴露不可变的 LiveData
给外部,下面来改造下 MainViewModel
,如下:
class MainViewModel(countReserved: Int) : ViewModel() {
val counter : LiveData
get() =_counter
private val _counter = MutableLiveData()
init {
_counter.value = countReserved
}
fun plusOne() {
val count = _counter.value ?: 0
_counter.value = count + 1
}
fun clear() {
_counter.value = 0
}
}
这里 _counter
变量对于外部便是不可见的,而我们又定义了一个 counter
变量,类型声明为不可变的 LiveData
,并在它的 get()
属性方法中返回 _counter
变量。
这样当外界调用 counter
变量时,实际上获得的是 _counter
的实例,但是无法给 counter
设置数据,从而保证了 LiveData
数据的封装性
LiveData
为了能够应对各种不同需求场景,提供了两种转换方法:map()
和 switchMap()
方法
map()
这个方法的作用是将实际包含数据的 LiveData
和仅用于观察数据的 LiveData
进行转换,实例如下:
比如有一个 User
类,其中包含用户的姓名、年龄,如下:
data class User(var firstName: String,var lastName:String, var age: Int) {}
这里包含了三个数据,但如果我们的 Activity
明确了只需要用户的姓名,不关心年龄时 还将整个 User
暴露出去就不太合适,这时候就可以使用 map()
方法,将 User
类型的 LiveData
自由的转型成任意其他类型的 LiveData
,如下:
class MainViewModel(countReserved: Int) : ViewModel() {
private val userLiveData = MutableLiveData()
val userName : LiveData = Transformations.map(userLiveData){ user ->
“${user.firstName} ${user.lastName}”
}
…
}
map()
方法接收两个参数:
-
第一个:参数是原始的
LiveData
对象; -
第二个:参数是一个转换函数
我们就只需要在转换函数里写具体的逻辑即可
另外,我们将 userLiveData
声明成了 private
,以保证数据的封装性,外部只需要观察 userName
就可以了,当 userLiveData
数据发生变化时,map()
方法会监听到变化并执行转换函数中的逻辑,然后将转换之后的数据通知给 userName
的观察者
switchMap()
我们通过一个实例来学习。根据传入的 userId
参数去服务器请求或者到是数据库中查找相应的 User
对象,但是这里只是模拟示例,因此每次传入的 userId
当作用户姓名来创建一个新的 User
对象即可
代码如下,先创建一个 Repository
单例类,模拟获取用户数据的功能:
object Repository {
fun getUser(userId: String) : LiveData{
val liveData = MutableLiveData()
liveData.value = User(userId,userId,0)
return liveData
}
}
下面获取 Repository
的 LiveData
对象,
class MainViewModel(countReserved: Int) : Int{
…
fun getUser(userId: String) : LiveData {
return Repository.getUser(userId)
}
}
在 Activity
中观察 LiveData
viewModel.getUser(userId).observe(this) { ->
}
以上的这种呢做法完全错误,因为每次调用 getUser()
方法返回的都是一个新的 LiveData
实例,而上述写法会一直观察老的 LiveData
实例,这种情况下,LiveData
是不可能观察的
以下是正确做法,
借助 switchMap
,它的使用场景比较固定:如果 VIewModel
中的某个 LiveData
对象是调用另外的方法获取的,那么就可以借助 switchMap()
方法,将这里 LiveData
对象转换成另外一个可观察的 LiveData
对象。修改 MainViewModel
中的代码,如下:
class MainViewModel(countReserved: Int) : ViewModel() {
…
private val userIdLiveData = MutableLiveData()
val user: LiveData = Transformations.switchMap(userIdLiveData) { userId ->
Repository.getUser(userId)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}
}
switchMap()
方法接收两个参数:
-
第一个:传入新增的
userIdLiveData
,switchMap()
方法会对它进行观察 -
第二个:是一个转换函数,我们必须在这个转换函数中返回一个
LiveData
对象,因为switchMap()
方法的工作原理就是要将转换函数中返回的LiveData
对象转换成另一个可观察的LiveData
对象。
现在 user
对象就是一个可观察的 LiveData
对象了
修改 Activity
中的代码,如下:
class MainActivity : AppCompatActivity() {
…
override fun onCreate(savedInstanceState: Bundle?) {
…
btn_getUser.setOnClickListener {
val userId = (0…1000).random().toString()
viewModel.getUser(userId)
}
viewModel.user.observe(this,{
tv_info.text = it.firstName
})
}
}
现在已经实现了功能并且可以正常运行了。
当我们的 ViewModel
中某个获取数据的方法有可能是没有参数的,这个时候该怎么办呢?
这里我们先创建一个空的 LiveData
对象:
class MyViewModel : ViewModel(){
private val refreshLiveData = MutableLiveData<Any?>()
val refreshResult = Transformatinos.switchMap(refreshLiveData) {
Repository.refresh()
}
fun refresh() {
refreshLiveData.value = refreshLiveData.value
}
}
在 refresh()
方法中,只是将 refreshLiveData
原有的数据取出来(默认是空),再重新设置到 refreshResult
当中,这样就能触发一次数据变化。
在 LiveData
内部不会判断即将设置的数据和原有数据是否相同,只要调用了 setValue()
或 postValue()
方法,就一定会触发数据变化事件,然后我们只需要在 Activity
中观察 refreshResult
这个 LiveData
对象即可
==================================================================
Room
是 Google 官方推出的一个 ORM
(Object Relational Mapping
对象关系映射)框架,并将它加入了 Jetpack
中。
Room
的整体结构主要由 Entity
、Dao
、Database
这三个部分组成,每个部分都有自己明确的职责:
-
Entity
:用于定义封装实际数据的实体类,每个实体类都会在数据库中都有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。 -
Dao
:Dao
是数据访问的意思,同长会在这里对数据库的各项操作进行封装。在实际编程中,逻辑层就不用和底层数据打交道了,直接和Dao
层进行交互就可。 -
Database
:用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao
层的访问实例。
添加依赖:
…
apply plugin: ‘kotlin-kapt’
dependencies{
…
implementation ‘androidx.room:room-runtime:2.2.5’
kapt ‘androidx.room:room-compiler:2.2.5’
}
kapt
只能在 Kotlin
项目中使用,如果是 Java
项目的话,使用 amotationProcessor
即可
接下来按照刚才介绍的 Room
的三个部分来一一进行实现
定义 Entity:
我们直接就使用 上文定义的 User 类来改造
@Entity
data class User(var firstName: String,var lastName:String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0 //给每个实体类都添加一个字段,并设为主键
}
@Entity
注解:将 User
声明成了一个实体类
@PrimaryKey
注解:将 id
字段设为主键,将 autoGenerate
设置为 true
,使得主键的值自动生成
定义 Dao:
这一部分比较关键,因为所有访问数据库的操作都是在这里封装
新建一个 UserDao 接口,如下:
@Dao
interface UserDao {
@Insert
fun insetUser(user: User) : Long
@Update
fun updateUser(newUser: User)
@Query(“select * from User”)
fun loadAllUser() : List
@Query(“select * from User where age > :age”)
fun loadUsersOlderThan(age: Int) : List
@Delete
fun deleteUser(user: User)
@Query(“delete from User where lastName = :lastName”)
fun deleteUserByLastName(lastName: String) : Int
}
@Dao
注解:让 Room
能够识别到 UserDao
是一个 Dao
@Insert
注解:表示将参数传入 User
对象插入数据库中,插入完成后还会返回自动生成的主键 id
值
@Update
注解:表示会将参数中传入的 User
对象更新到数据库当中
@Delete
注解:表示会将参数传入的 User
对象从数据库中删除
以上几种数据库操作都直接使用注解表示即可,不用编写 SQL
语句。如果想要从数据库中查询数据,或者使用非实体类参数来增删改查数据,就必须编写 SQL
语句,比如刚刚定义的 loadAllUsers()
方法,用于从数据库中查询所有用户。
@Query
注解:该注解中必须编写 SQL
语句,可以将方法中传入的参数指定到 SQL
语句当中,比如 loadUserOlderThan()
方法就可以查询所有年龄大于指定参数的用户。
如果是使用非实体类参数来增删改查数据,也要编写 SQL
语句才行,而且只能使用 @Query
注解,比如 deleteUserByLastName()
方法
定义 Database
这部分一般只需要定义三个部分:数据库版本号、包含哪些实体类、提供 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()
方法。之后在 companion object
结构体中编写了一个单例模式,原则上全局应该只存在一个 Appdatabase
实例。
databaseBuilder()
方法接收三个参数:
-
第一个:参数一定要使用
applicationContext
,而不能使用普通context()
,否则容易出现内存泄漏的情况。 -
第二个:参数是
AppDatabase
的Class
类型。 -
第三个:参数是数据库名
修改 MainActivity
中的代码:
class MainActivity : AppCompatActivity() {
…
override fun onCreate(savedInstanceState: Bundle?) {
…
val userDao = AppDatabase.getDatabase(this).userDao()
val user1 = User(“Tony”,“ll”,11)
val user2 = User(“Tom”,“SS”,22)
btn_addData.setOnClickListener {
thread {
user1.id = userDao.insetUser(user1)
user2.id = userDao.insetUser(user2)
}
}
btn_updateData.setOnClickListener {
thread {
user1.age = 33
userDao.updateUser(user1)
}
}
btn_deleteData.setOnClickListener {
thread {
userDao.deleteUserByLastName(“SS”)
}
}
btn_queryData.setOnClickListener {
thread {
for (user in userDao.loadAllUser()) {
Log.d(“MainActivity”,user.toString())
}
}
}
}
}
在 AddData
的点击事件中将 insertUser()
方法返回的主键 id
值赋值给了原来的 User
对象。之所以这样是因为使用 @Update
、@Delete
注解去更新和删除数据时都是基于这个 id
值来操作的。
由于数据库操作属于耗时操作,Room
默认不允许在主线程中进行数据库操作,因此上述的增删改查操作都放在了子线程中,不过为了方便调试,Room
还提供了一个更加简单的方法:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, “app_database”)
.allowMainThreadQueries()
.build()
这样 Room
就允许在主线程进行数据库操作了,不过建议只在测试环境使用
Room 的数据库升级
Room
在数据库升级方面设计得比价繁琐,并没有比原生的 SQLiteDatabase
简单到哪儿去。如果你目前还只是在开发测试阶段,不想编写那么繁琐的数据库升级逻辑,Room
提供了一个简单粗暴的方法:
Room.databaseBuilder(aontext.applicationContext, AppDatabase::class.java. “app_database”)
.fallbackToDestructiveMigration()
.build()
构建 AppDatabase
实例时加入了一个 fallbackToDestructiveMigration()
方法,这样只要数据库进行了升级,Room
就会将当前的数据库销毁,然后再重新创建,但这样做的问题也就显而易见,之前数据库中的数据也会全部丢失。
下面是正规用法
我们先新建一个 Book 的实体类:
@Entity
data class Book(var nane:String, var price: Int) {
@PrimaryKey(autoGenerate = true)
var id : Long = 0
}
然后创建一个 BookDao 接口,并在其中随意定义一些 API
@Dao
interface BookDao {
@Insert
fun insert(book: Book) : Long
@Query(“select * from Book”)
fun loadAllBooks() : List
}
修改 AppDatabase 中的代码:
@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao() : UserDao
面试宝典
面试必问知识点、BATJ历年历年面试真题+解析
学习经验总结
(一)调整好心态
心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。
(二)时间挤一挤,制定好计划
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。
(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
下面是正规用法
我们先新建一个 Book 的实体类:
@Entity
data class Book(var nane:String, var price: Int) {
@PrimaryKey(autoGenerate = true)
var id : Long = 0
}
然后创建一个 BookDao 接口,并在其中随意定义一些 API
@Dao
interface BookDao {
@Insert
fun insert(book: Book) : Long
@Query(“select * from Book”)
fun loadAllBooks() : List
}
修改 AppDatabase 中的代码:
@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao() : UserDao
面试宝典
面试必问知识点、BATJ历年历年面试真题+解析
[外链图片转存中…(img-pYZNpvg0-1715184496866)]
学习经验总结
(一)调整好心态
心态是一个人能否成功的关键,如果不调整好自己的心态,是很难静下心来学习的,尤其是现在这么浮躁的社会,大部分的程序员的现状就是三点一线,感觉很累,一些大龄的程序员更多的会感到焦虑,而且随着年龄的增长,这种焦虑感会越来越强烈,那么唯一的解决办法就是调整好自己的心态,要做到自信、年轻、勤奋。这样的调整,一方面对自己学习有帮助,另一方面让自己应对面试更从容,更顺利。
(二)时间挤一挤,制定好计划
一旦下定决心要提升自己,那么再忙的情况下也要每天挤一挤时间,切记不可“两天打渔三天晒网”。另外,制定好学习计划也是很有必要的,有逻辑有条理的复习,先查漏补缺,然后再系统复习,这样才能够做到事半功倍,效果才会立竿见影。
(三)不断学习技术知识,更新自己的知识储备
对于一名程序员来说,技术知识方面是非常重要的,可以说是重中之重。要面试大厂,自己的知识储备一定要非常丰富,若缺胳膊少腿,别说在实际工作当中,光是面试这一关就过不了。对于技术方面,首先基础知识一定要扎实,包括自己方向的语言基础、计算机基础、算法以及编程等等。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!