Jetpack项目实践

Jetpack

Jetpack 是一个丰富的组件库,它的组件库按类别分为 4 类,分别是架构(Architecture)、界面(UI)、行为(behavior)和基础(foundation)。每个组件都可以单独使用,也可以配合在一起使用。每个组件都给用户提供了一个标准,能够帮助开发者遵循最佳做法,减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码,让开发者能够集中精力编写重要的业务代码。


65b543566120285bbae37fcb221e9dfa.png

Jetpack 是各种组件库的统称,AndroidX 是这些组件的统一包名。
AndroidX 对原始 Android Support Library 进行了重大改进,后者现在已不再维护。androidx 软件包完全取代了 support 包,不仅提供同等的功能,而且提供了新的库。Jetpack 组件中也是完全使用 androidx 开头的包名。
与 Support Library 一样,androidx 命名空间中的库与 Android 平台分开提供,并向后兼容各个 Android 版本。

LiveData

LiveData是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
LiveData的优点有很多,如不会造成内存泄漏,另外还有一个最重要的优势是数据驱动UI,也是MVVM模型的核心设计,所以LiveData通常会配合ViewModel来使用,ViewModel负责触发数据的更新,更新会通知到LiveData,然后LiveData再通知活跃状态的观察者。此外还有与页面的生命周期绑定数据自动更新等等。

创建 LiveData 对象

在ViewModel中创建LiveData数据对象

private val userList: MutableLiveData> by lazy {
     MutableLiveData>()
}
fun getUserListLiveData() = userList
观察 LiveData 对象

在UI中初始化承载userList对象的ViewModel。

private val mainViewModel: MainViewModel
mainViewModel.getUserListLiveData().observe(this, Observer {
     text.text = it.toString()
})
更新 LiveData 对象

直接在获取数据之后调用setValue或者postValue即可,一般情况下在ViewModel中进行数据的请求和和LiveData的更新。

userList.value = it

另外LiveData还可以和Room协程以及与Flow流进行相互转化。
https://developer.android.google.cn/topic/libraries/architecture/livedata

ViewModel

ViewModel类是被设计用来以可感知生命周期的方式存储和管理 UI 相关数据,ViewModel中数据会一直存活即使 activity configuration发生变化,比如横竖屏切换的时候。ViewModel是Google的MVVM中的重要一环VM。

ViewModel的定义

ViewModel需要集成系统API提供的ViewModel类,然后就是如何操作LiveData了。

class MainViewModel : ViewModel() {
    private val userList = MutableLiveData>()
    
    @ExperimentalCoroutinesApi
    fun getAllUser() {
        GlobalScope.launch(Dispatchers.Main) {
            getAllUserFlow()
                .flowOn(Dispatchers.IO)
                .onStart {
                    Log.e("MainActivity", "getAllUser onStart")
                }
                .onCompletion {
                    Log.e("MainActivity", "getAllUser onCompletion")
                }
                .onEach {
                    userList.value = it
                }
                .catch {
                    Log.e("MainActivity", "getAllUser catch it=${it.message}")
                }
                .collect()
        }
    }
}
ViewModel的使用

在UI(Activity或者Fragment)中实例化ViewModel,最基本的方式:

val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

还可以依赖activity-ktx,它利用kotlin的属性委托property delegate进行初始化:

implementation "androidx.activity:activity-ktx:1.2.2"

val viewModel: MainViewModel by viewModels()

这是在Activity中使用ViewModel,若在Fragment中使用可以依赖fragment-ktx:

implementation "androidx.fragment:fragment-ktx:1.3.2"

val viewModel: MainViewModel by activityViewModels()

另外还有一种,使用Koin一款依赖注入框架进行ViewModel初始化:

val viewModel: MainViewModel by inject()
ViewModel的生命周期

ViewModel对象存在的时间范围是获取 ViewModel ViewModelProvider 的 Lifecycle。ViewModel将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。


viewmodel-lifecycle.png

如何判断ViewModel的生命周期与哪个UI绑定呢,看ViewModelProvider的参数。

在 Fragment 之间共享数据

传统的Fragment共享数据是比较困难的事情,首先Fragment之间是隔离的,不能通信。其次如果使用他们的Activity则有一个类型强转的问题,Fragment的设计初衷就不是为单一Activity服务的,也不可取。最后一般都得借助另外一个类来保存多个Fragment都要使用的数据,这样做会让整体架构设计变得很糟糕。现在使用ViewModel让此事变得非常轻松合理。就是每个Fragment都可以直接订阅ViewModel的observe,因为ViewModel的数据驱动UI的本质还是观察者模式,每个Fragment都可以持有一个观察者。
https://developer.android.google.cn/topic/libraries/architecture/viewmodel

Room

Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
如需在应用中使用 Room,请将以下依赖项添加到应用的 build.gradle 文件。

implementation 'androidx.room:room-runtime:2.2.6'
kapt 'androidx.room:room-compiler:2.2.6'
implementation 'androidx.room:room-ktx:2.2.6'
testImplementation 'androidx.room:room-testing:2.2.6'
基本使用

Room 包含 3 个主要组件:Database、Entity、Dao。
Database:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。包含:@Database、继承扩展 RoomDatabase 的抽象类、在注释中添加与数据库关联的实体列表、包含具有 0 个参数且返回使用@Dao注释的类的抽象方法。

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Entity:

@Entity(tableName="user")
data class User(

    @PrimaryKey
    val userId: UserId,

    @ColumnInfo(name = "name")
    val name: Name,

    @ColumnInfo(name = "age")
    val age: Age
)

typealias UserId = Int

typealias Name = String

typealias Age = Int

Dao:

@Dao
interface UserDao {

    @Query("SELECT * FROM user")
    fun getAllUser(): Flow>

    @Query("SELECT * FROM user WHERE userId = :id")
    fun findUserById(id: UserId): Flow

    @Insert
    fun insertUser(user: User)

    @Delete
    fun deleteUser(user: User)
}

初始化Database:

Room.databaseBuilder(
    applicationContext,
    AppDatabase::class.java, "database-name"
)
  .addCallback(AppDatabaseCallback())
  .addMigrations(AppDatabaseMigration())
  .build()

其中的AppDatabaseCallback是管理Database的初始化每个阶段的回调

class AppDatabaseCallback : RoomDatabase.Callback() {

    /**
     * Called when the database is created for the first time. This is called after all the
     * tables are created.
     *
     * @param db The database.
     */
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
    }

    /**
     * Called when the database has been opened.
     *
     * @param db The database.
     */
    override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
    }

    /**
     * Called after the database was destructively migrated
     *
     * @param db The database.
     */
    override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
        super.onDestructiveMigration(db)
    }
}

其中AppDatabaseMigration是升级库升级管理器

class AppDatabaseMigration : Migration(1, 2) {
    /**
     * Should run the necessary migrations.
     *
     *
     * This class cannot access any generated Dao in this method.
     *
     *
     * This method is already called inside a transaction and that transaction might actually be a
     * composite transaction of all necessary `Migration`s.
     *
     * @param database The database instance
     */
    override fun migrate(database: SupportSQLiteDatabase) {
        TODO("Not yet implemented")
    }
}

现在看起来Room并没有特别的优势能让我们忍不住马上把代码从SQLite切换过来,但是本地数据库的访问毕竟访问的是磁盘,所以最好的写法一定是异步访问的,如果用Flow和协程配合把Room包装起来也会让代码阅读起来非常的流畅,并且Room和Flow流配合也是数据响应的。
https://developer.android.google.cn/training/data-storage/room

Flow

Flow其实和RxJava解决的事情是一样的。链式调用,完美解决回调函数的使用出现的无限代码缩进;异步,和协程配合可以解决异步调用;Flow可以其他技术兼容,比方说Room、ViewModel并且可以与LiveData相互转化,就是说Flow可以串起来整个架构的数据流。

创建数据流

1.flow

flow {
   emit(1)
}

2.flowof 其中也是调用了flow()去初始化一个Flow

flowOf(1)
修改数据流

修改数据流的方法有很多,基本上RxJava有的基本上都可以找到对应的方法。这里只说几个重要的:

flowOf(1)
  .flowOn(Dispatchers.IO)
  .onStart {
    Log.e("MainActivity", "onStart")
  }
  .onCompletion {
    Log.e("MainActivity", "onCompletion")
  }
  .onEach {
    liveData.value = it
  }
  .catch {
    Log.e("MainActivity", "catch it=$it")
  }

flowOn()作用是指定被观察者在哪类线程中执行,很像RxJava,接受一个Dispatchers参数,Dispatchers.IO IO线程,一般处理网络请求;Dispatchers.Main 主线程,一般处理UI展示;Dispatchers.Default 默认线程由系统分配,主要处理CPU密集型操作,比方说一个庞大的遍历。flowOn()和RxJava的observeOn()一样都是作用于调用链它以上的部分。但是仅仅这样写还不会实现异步的目的,Flow必须配合协程完成异步请求。
onStart()是在Flow流成功执行之前最先触发的方法,这里可以做一些准备工作。
onCompletion()是在Flow流成功执行完成之后最后一个方法,表示完成。都可以在RxJava中找到影子。
onEach()这个方法的传入的高阶函数有个参数传回表示Flow最后的对象,就是整个Flow流的目的。相当于RxJava方法的onNext()。
catch()是调用链抛出异常之后会触发此方法的执行,参数是Exception和RxJava一样,它的作用域也是在调用链它以上的部分,所以一般写在整个流的最后,确保异常不会外流导致崩溃。
此外还有很多二级方法可供使用:https://www.jianshu.com/p/0d0ee5fd4931

Flow转化LiveData

最简单的使用

flowOf(1).asLiveData()

https://developer.android.google.cn/kotlin/flow

协程

上面说了Flow流,但是单独Flow流是没法进行工作的,它只是一种设计结构,这里说说如果配合协程进行异步任务开发。
协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。

特点

轻量:您可以在单个线程上运行多个协程,因为协程支持挂起suspend,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
如需在 Android 项目中使用协程,请将以下依赖项添加到应用的 build.gradle 文件中:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
}

第一个依赖的协程自身的功能包,第二个依赖提供了viewModelScope扩展属性可以直接调用launch(默认参数为Dispatchers.Main)

挂起函数

被suspend关键字修饰的函数为可挂起函数

private suspend fun getAllUser()

挂起函数可以调用普通函数,普通函数不可以直接调用挂起函数,挂起函数只能在协程中或其他挂起函数中调用。挂起函数就是将函数的执行暂停,然后省下来资源去做别的事情,挂起函数挂起协程时,不会阻塞协程所在的线程。挂起函数是协程实现异步任务的主要手段。

协程的创建
GlobalScope.launch(Dispatchers.Main) 

launch()是CoroutineScope的扩展函数,而GlobalScope又继承CoroutineScope,核心的东西都在CoroutineScope中,CoroutineScope可以看作协程自身,Dispatchers.Main指定协程执行在主线程中但不会阻塞线程。launch()方法有一个返回值Job,类似于RxJava的Disposable,可以用全局变量持有,它可以取消并且有简单生命周期。
runBlocking {}:是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main函数和测试设计的。
withContext {}:不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。
async {}:可以实现与 launch()一样的效果,在后台创建一个新协程,唯一的区别是它有返回值,因为CoroutineScope.async {}返回的是 Deferred 类型。Deferred是Job的子类,区别在于完成之后又返回值可以根据返回值做一些处理。

协程与Flow配合实现异步
GlobalScope.launch(Dispatchers.Main) {
  getAllUserFlow()
    .flatMapConcat {
      deleteUserFlow(it)
    }
    .flowOn(Dispatchers.IO)
    .onStart {
      Log.e("MainActivity", "onStart")
    }
    .onCompletion {
      Log.e("MainActivity", "onCompletion")
    }
    .catch {
      Log.e("MainActivity", "catch it=${it.message}")
    }
    .collect()
}

Dispatchers.Main:表示协程在主线程上创建。
getAllUserFlow():调用网络服务接口,返回Flow携带数据。
.flowOn(Dispatchers.IO):表示被观察者就是getAllUserFlow()的操作在IO线程上执行。
.collect():相当于RxJava的subscribe(),不过它可以不携带参数,也可以携带,建议是用.OnEach()处理返回的数据,.collect()只做订阅,因为Flow属于冷链必须订阅才能激活。因为之前讲过.catch()方法只能捕获到它前面的流操作,所以使用携带参数的.collect(),在处理函数中若抛出异常是直接导致程序崩溃的。
这样协程的基本使用的可以运用到项目中了。https://developer.android.google.cn/kotlin/coroutines

koin

https://zhuanlan.zhihu.com/p/188485918

Retrofit扩展

整体架构

final-architecture.png

你可能感兴趣的:(Jetpack项目实践)