Jetpack总结
Lifecycle
Lifecycle应用
一般不需要指定依赖,我们只需要依赖implementation 'androidx.appcompat:appcompat:1.4.1'
即可。如果需要指定特定的Lifecycle依赖,可以参考这里:https://developer.android.com/jetpack/androidx/releases/lifecycle。
使用Lifecycle解耦页面组件
可以为方法添加@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
此类注解实现,不过该方式已经被废弃,替代方案:DefaultLifecycleObserver or LifecycleEventObserver
。
使用LifecycleService解耦Service与组件
adb geo设置GPS位置方法:adb -s emulator-5554 emu geo fix 121.4961236714487 31.24010934431376
。
使用ProcessLifecycleOwner监听应用程序生命周期
//依赖
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
//定义CustomObserver
class CustomObserver : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
}
}
//使用
ProcessLifecycleOwner.get().lifecycle.addObserver(CustomObserver())
特点:
- 针对整个应用程序的监听,与Activity数量无关;
- Lifecycle.Event.ON_CREATE只会被调用一次,Lifecycle.Event.ON_DESTROY永远不会被调用。
Lifecycle的好处
- 帮助开发者建立可感知生命周期的组件;
- 组件在其内部管理自己的生命周期,从而降低模块耦合度;
- 降低内存泄漏发生的可能性;
- Activity、Fragment、Service、Application均有Lifecycle支持。
ViewModel
ViewModel的诞生
- 瞬态数据的丢失
- 异步调用的内存泄漏
- 类膨胀提高维护难度和测试难度
ViewModel的作用
- 它是介于View(视图)和Model(数据模型)之间的桥梁
- 使视图和数据能够分离,也能保持通信
AndroidViewModel
- 不要向ViewModel中传入Context,会导致内存泄漏
- 如果要使用Context,请使用AndroidViewModel中的Application
ViewModel的生命周期
LiveData
LiveData和ViewModel的关系
在ViewModel中的数据发生变化时通知页面
示例
//ViewModel
class MyViewModel : ViewModel() {
private lateinit var currentSecond: MutableLiveData<Int>
fun getCurrentSecond() : MutableLiveData<Int> {
if (!::currentSecond.isInitialized) {
currentSecond = MutableLiveData(0)
}
return currentSecond
}
}
//MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testLiveData()
}
private fun testLiveData() {
val textView = findViewById<TextView>(R.id.textView)
viewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application)).get(MyViewModel::class.java)
textView.text = "${viewModel.getCurrentSecond().value}"
viewModel.getCurrentSecond().observe(this) {
textView.text = "$it"
}
startTimer()
}
private fun startTimer() {
Timer().schedule(object : TimerTask() {
override fun run() {
//非UI线程用postValue,UI线程用setValue
viewModel.getCurrentSecond().postValue((viewModel.getCurrentSecond().value ?: 0) + 1)
}
}, 1000, 1000)
}
}
优势
- 确保界面符合数据状态
- 不会发生内存泄漏
- 不会因Activity停止而导致崩溃
- 不再需要手动处理生命周期
- 数据始终保持最新状态
- 适当的配置更改
- 共享资源
DataBinding
意义
让布局文件承担了部分原本属于页面的工作,使页面与布局耦合度进一步降低。
简单示例
//lib的build.gradle
android {
defaultConfig {
dataBinding {
enabled = true
}
}
}
<!-- 打开布局文件,同时选中option+enter(Mac)就会出现是否要转换为dataBinding布局的提示 -->
<!-- sub.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="dataBindingBean2"
type="com.example.myapplication.DataBindingBean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{dataBindingBean2.name}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="dataBindingBean"
type="com.example.myapplication.DataBindingBean" />
<import type="com.example.myapplication.DataBindingBeanUtil" />
<variable
name="eventHandle"
type="com.example.myapplication.EventHandleListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{DataBindingBeanUtil.INSTANCE.transform(dataBindingBean.name)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{eventHandle.buttonOnClick}"
android:text="@{dataBindingBean.name}"
app:layout_constraintTop_toTopOf="parent" />
<include
layout="@layout/sub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dataBindingBean2="@{dataBindingBean}"
android:layout_marginTop="100dp"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
package com.example.myapplication
//
data class DataBindingBean(
val name: String
)
//
object DataBindingBeanUtil {
fun transform(str: String) : String {
return "_${str}_"
}
}
//
class EventHandleListener(private val context: Context) {
fun buttonOnClick(view: View) {
Toast.makeText(context, "like", Toast.LENGTH_SHORT).show()
}
}
//
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.dataBindingBean = DataBindingBean("测试名")
binding.eventHandle = EventHandleListener(this)
}
}
BindingAdapter示例
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="netImage"
type="String" />
<variable
name="localImage"
type="int" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
app:image="@{netImage}"
app:defaultImage="@{localImage}"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.netImage = ""
binding.localImage = R.mipmap.ic_launcher_round
}
}
object TestBindingAdapter {
@JvmStatic
@BindingAdapter(value = ["image", "defaultImage"], requireAll = false)
fun setImage(imageView: ImageView, url: String?, resId: Int) {
if (url.isNullOrEmpty()) {
imageView.setImageResource(resId)
} else {
//加载网络图片
}
}
}
双向绑定
BaseObservable
//
data class User(var name: String)
//
class UserViewModel : BaseObservable() {
private val user: User = User("jack")
@Bindable
fun getUserName(): String {
return user.name
}
fun setUserName(name: String) {
if (name != user.name) {
user.name = name
Log.i("shenbf", "name is $name")
//BR是build之后自动生成的类
notifyPropertyChanged(BR.userName)
}
}
}
//
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_main)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.userViewModel = UserViewModel()
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="userViewModel"
type="com.example.myapplication.UserViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- 注意:这里有个=符号,正是因为有了这个符号才能够实现双向绑定 -->
<EditText
android:text="@={userViewModel.userName}"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ObservableField
和上一节BaseObservable十分相似,只有UserViewModel
不一样,其他文件代码都一样,代码如下:
class UserViewModel {
private val userObservableField: ObservableField<User>
init {
val user = User("jack")
userObservableField = ObservableField()
userObservableField.set(user)
}
fun getUserName(): String? {
return userObservableField.get()?.name
}
fun setUserName(name: String) {
userObservableField.get()?.name = name
Log.i("shenbf", "name is $name")
}
}
Room
Paging3
使用
//项目的build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
implementation 'androidx.paging:paging-runtime:3.0.0-alpha03'
加载数据的流程
PageConfig
pageSize
:每页显示的数据的大小;prefetchDistance
:预刷新的距离,距离最后一个item多远时加载数据,默认为pageSize;initialLoadSize
:初始化加载数量,默认为pageSize * 3。
注意:initialLoadSize官方建议,一般要比pageSize大一些,比如说设置两倍的pageSize,默认为3倍。这里遇到过两个问题。
问题1
当pageSize=8,prefetchDistance=1,initialLoadSize=8时,上拉加载之后再次下拉刷新,然后再想上拉加载,发现无法加载,解决办法是将initialLoadSize设置大一些,比如说设置为8*2。
问题2
当pageSize=15,prefetchDistance=10,initialLoadSize=15时,同时配置ROOM实现私聊功能,由于LimitOffsetPagingSource的getRefreshKey方法的计算逻辑影响,会出现A跟B聊天时,收到C给A发的消息,A当前页面会抖动的问题,解决办法也是将initialLoadSize设置大一些,比如说设置为15*3。
//由于LimitOffsetPagingSource.kt没有提供源码,所以在AndroidStudio中只能将它编译为Java代码查看
package androidx.room.paging;
@RestrictTo({Scope.LIBRARY_GROUP})
public abstract class LimitOffsetPagingSource extends PagingSource {
@Nullable
public Integer getRefreshKey(@NotNull PagingState state) {
Intrinsics.checkNotNullParameter(state, "state");
int initialLoadSize = state.getConfig().initialLoadSize;
Integer var10000;
if (state.getAnchorPosition() == null) {
var10000 = null;
} else {
//AnchorPosition是包括placeholders在内的最近访问的索引值,一般可以理解为屏幕内显示条目的个数
Integer var10001 = state.getAnchorPosition();
Intrinsics.checkNotNull(var10001);
var10000 = Math.max(0, var10001 - initialLoadSize / 2);
}
return var10000;
}
}
PagingSource
- Key:分页标识类型,如页码,则为Int。
- Value:返回列表元素的类型。
abstract class PagingSource<Key : Any, Value: Any> { ... }
RemoteMediator
RemoteMediator和PagingSource相似,都需要覆盖load()方法,但是不同的是RemoteMediator不是加载分页数据到RecyclerView列表上,而是获取网络分页数据并更新到数据库中。
一般步骤:
- 判断LoadType。
- 无网络加载本地数据。
- 请求网络分页数据。
- 插入数据库。
Room支持
如果使用的是Room,从2.3.0-alpha 开始,它将默认为您实现 Paging Source。在定义 Dao 接口的 Query 语句时,返回类型要使用 Paging Source 类型。同时不需要在 Query 里指定页数和每页展示数量,页数由 PagingSource 来控制,每页数量页在 PagingConfig 中定义。
LoadType
LoadType是一个枚举类,里面定义了三个值,如下所示:
- LoadType.Refresh:在初始化刷新的时候使用,首次访问或者调用PagingDataAdapter.refresh()时触发。
- LoadType.Append:在加载更多的时候使用,需要注意的是当LoadType.REFRESH触发了,LoadType.PREPEND也会触发。
- LoadType.Prepend:在当前列表头部添加数据的时候使用。
PagingState
- pages:List<Page Key, Value>>返回的上一页的数据,主要用来获取上一页最后一条数据作为下一页的开始位置。
- config: Pagingconfig 返回的初始化设置的 Paging Config 包含了 pageSize、prefetchDistance、initialLoadSize 等等。
MediatorResult
- 请求出现错误,返回 MediatorResult.Error(e)。
- 请求成功且有数据,返回 MediatorResult.Success(end OfPaginationReached = true)。
- 请求成功但是没有数据,返回 MediatorResult.Success(endOfPaginationReached = false)。
缓存
使用cachedIn函数可以将数据缓存在viewModel中,也可以自定义Scope,但是要记得cancel,防止内存泄漏。
open class ChatRoomBaseViewModel : ViewModel() {
fun queryMessages(): Flow<PagingData<ChatMessageEntity>> {
return Pager(
config = ChatRepo.defaultPagingConfig,
pagingSourceFactory = {
ChatMessageRepo.queryMessages(ChatRepo.currentOnlineConvId)
}).flow.cachedIn(viewModelScope).map { ChatMessageEntityTransform.map(it) }
}
}
Paging3架构
Hilt
使用
//工程的build.gradle
buildscript {
dependencies {
classpath 'com.google.dragger:hilt-android-gradle-plugin:2.28.1-alpha'
}
}
//项目的build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin '
}
def hilt_version = "2.28-alpha"
implementation "com.google.dragger:hilt-android:$hilt_version"
kapt "com.google.dragger:hilt-android-compiler:$hilt_version"
def hilt_vew_version = "1.0.0-alpha01"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_view_version"
kapt "androidx.hilt:hilt-compiler:$hilt_view_version"
定义
负责托管对象与对象之间的注入关系。
注解
- @HiltAndroidApp:触发Hilt的代码生成。
- @AndroidEntryPoint:创建一个依赖容器,该容器遵循Android类的生命周期。
- @Module:告诉 Hit 如何提供不同类型的实例。
- @lnstallln:Install 用来告诉 Hilt 这个模块会被安装到哪个组件上。
- @Provides:告诉Hilt如何获得具体实例。
- @Singleton:单例。
- @ViewModellnject:通过构造函数,给ViewModel注入实例。
App Startup
App Startup 是 Android Jetpack 最新成员,提供了在 App 启动时初始化组件简单、高效的方法,无论是 library 开发人员还是 App 开发人员都可以使用 App Startup 显示的设置初始化顺序
。
implementation 'androidx.startup:startup-runtime:1.1.1'
//AppHelper
object AppHelper {
lateinit var mContext: Context
fun init(context: Context) {
mContext = context
}
}
//AppInitializer
package com.fqxyi.init
class AppInitializer : Initializer<Unit> {
//create方法在Application的onCreate方法之前执行
override fun create(context: Context) {
AppHelper.init(context)
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
<!--AndroidManifest.xml-->
<!--必须保证authorities这个值在整个手机上是唯一的-->
<manifest xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.fqxyi.init.AppInitializer"
android:value="androidx.startup" />
</provider>
</application>
</manifest>
Coil
- 性能优秀
- 体积较小:其包体积与Picasso相当,显著低于Glide 和Fresco,仅仅只有1500个方法,但是在功能上却不输于其他同类库
- 简单易用:配合Kotlin扩展方法等语法优势,APl简单易用
- 技术先进:基于Coroutine、OkHttp、Okio、Androidx等先端技术开发,确保了技术上的先进性
- 丰富功能:缓存管理 (Mem Cache DiskCache)、动态采样 (Dynamic image sampling)、加载中暂停/终止等功能有助于提高图片加载效率
Data Mapper
使用Data Mapper分离数据源的Model和页面显示的Model,不要因为数据源的增加、修改或者删除,导致上层页面也要跟着一起修改。
//Mapper接口定义
interface Mapper<I, O> {
fun map(input: I): O
}
//使用
class Entity2Model : Mapper<Entity, Model> {
override fun map(input: Entity): Model {
TODO("xxxxx")
}
}