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

特点:

  1. 针对整个应用程序的监听,与Activity数量无关;
  2. Lifecycle.Event.ON_CREATE只会被调用一次,Lifecycle.Event.ON_DESTROY永远不会被调用。

Lifecycle的好处

  1. 帮助开发者建立可感知生命周期的组件;
  2. 组件在其内部管理自己的生命周期,从而降低模块耦合度;
  3. 降低内存泄漏发生的可能性;
  4. Activity、Fragment、Service、Application均有Lifecycle支持。

ViewModel

ViewModel的诞生

  • 瞬态数据的丢失
  • 异步调用的内存泄漏
  • 类膨胀提高维护难度和测试难度

ViewModel的作用

  • 它是介于View(视图)和Model(数据模型)之间的桥梁
  • 使视图和数据能够分离,也能保持通信

AndroidViewModel

  • 不要向ViewModel中传入Context,会导致内存泄漏
  • 如果要使用Context,请使用AndroidViewModel中的Application

ViewModel的生命周期

viewmodel-lifecycle

LiveData

LiveData和ViewModel的关系

在ViewModel中的数据发生变化时通知页面

LiveData和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)
    }
}

优势

  1. 确保界面符合数据状态
  2. 不会发生内存泄漏
  3. 不会因Activity停止而导致崩溃
  4. 不再需要手动处理生命周期
  5. 数据始终保持最新状态
  6. 适当的配置更改
  7. 共享资源

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

  1. pageSize:每页显示的数据的大小;
  2. prefetchDistance:预刷新的距离,距离最后一个item多远时加载数据,默认为pageSize;
  3. 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列表上,而是获取网络分页数据并更新到数据库中。

一般步骤:

  1. 判断LoadType。
  2. 无网络加载本地数据。
  3. 请求网络分页数据。
  4. 插入数据库。

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架构

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 显示的设置初始化顺序

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")
  }
}

添加新评论