Android实用小技巧之利用Lifecycle写出更好维护的代码
作者:厨师小p 发布时间:2021-09-07 14:00:09
前言
你是否在onStart()启动过某项任务却忘记在onStop()中取消呢?人不是机器,难免会有错漏。就算老手不会犯错,也不能保证新人不会。学会下面的小技巧,让这种粗心成为不可能。
关于Lifecycle的源码,已经有很多大佬分析过。这篇文章的主旨是让读者对Lifecycle的使用场景有更多的体会,这样也能更好地理解源码。先来看一个场景,然后一步一步优化。
场景
假设我们有一个界面,模拟一个厨房。里面有灶台和餐桌。要求每秒钟翻炒一下,总共10秒。一种常规的实现如下:
class KitchenFragment : Fragment() {
private var timer: CountDownTimer? = null
override fun onResume() {
...
timer = object : CountDownTimer(COOKING_TIME_IN_MILLIS, SECOND_IN_MILLIS) {
override fun onTick(millisUntilFinished: Long) {
// 翻炒
}
override fun onFinish() {
// 出锅
}
}
timer.start()
}
override fun onPause() {
timer?.cancel()
...
}
compaion object {
private const val COOKING_TIME_IN_MILLIS = 10000L
}
}
潜在问题:
在别的地方实现类似的功能需要把很多重复代码复制过去
忘记cancel()可能会造成一系列的麻烦
当产品经理突然提出要同时颠勺5秒以及擦桌子20秒,代码会变得很长
优化版本1
先解决第一个问题,把CountDownTimer放到一个单独的class。
class KitchenFragment : Fragment() {
private val timer: CountDownTimer? = null
override fun onResume() {
...
timer = MyCountDownTimer(onTickAction = { 翻炒 },onFinishAction = { 出锅 })
timer.start()
}
override fun onPause() {
timer?.cancel()
...
}
}
// MyCountDownTimer.kt
class MyCountDownTimer@JvmOverloads constuctor(
millisUntilFinished: Long = DEFAULT_DURATION_IN_MILLIS,
countDownInterval: LONG = SECOND_IN_MILLIS,
private val onTickAction: () -> Unit,
private val onFinishAction: () -> Unit = {}
) : CountDownTimer(millisUntilFinished, countDownInterval) {
override fun onTick(millisUntilFinished: Long) {
onTickAction.invoke()
}
override fun onFinish() {
onFinishAction.invoke()
}
compaion object {
private const val DEFAULT_DURATION_IN_MILLIS = 10000L
}
}
需要复用时,只需传入需要改动的参数/方法:
// NeighbourKitchenFragment.kt
class NeighbourKitchenFragment : Fragment() {
private val timer: CountDownTimer? = null
override fun onResume() {
...
timer = MyCountDownTimer(onTickAction = { 翻炒 },onFinishAction = { 甩锅 })
timer.start()
}
override fun onPause() {
timer?.cancel()
...
}
}
复用起来好像方便了一点,但是当上面提到过的的问题3出现时,代码会变成:
class KitchenFragment : Fragment() {
private val cookTimer1: CountDownTimer? = null
private val cookTimer2: CountDownTimer? = null
private val sweepTableTimer: CountDownTimer? = null
override fun onResume() {
...
cookTimer1 = MyCountDownTimer(onTickAction = { 翻炒 },onFinishAction = { 出锅 })
cookTimer1.start()
cookTimer2 = MyCountDownTimer(millisUntilFinished = BRITAIN_SPOON_DURATION_IN_MILLIS,onTickAction = { 颠勺 })
cookTimer2.start()
sweepTableTimer = MyCountDownTimer(millisUntilFinished = SWEEP_TABLE_DURATION_IN_MILLIS,onTickAction = { 擦桌子 })
sweepTableTimer.start()
}
override fun onPause() {
cookTimer1?.cancel()
cookTimer2?.cancel()
sweepTableTimer?.cancel()
...
}
compaion object {
private const val BRITAIN_SPOON_DURATION_IN_MILLIS = 5000L
private const val SWEEP_TABLE_DURATION_IN_MILLIS = 20000L
}
}
随着需求增加,Fragment变得越来越长,也更难维护。同时,当在onResume中添加timer时被同事打断,之后就有可能会忘记在onPause中cancel()。有没有办法解决这些问题呢?
接下来切入正题,让我们看看Lifecycle能做什么。
优化版本2
首先让MyCountDownTimer实现DefaultLifecycleObserver,这样它就是lifecycle-aware的了。这有什么用呢?有了这个,MyCountDownTimer就能在fragment/activity生命周期发生变化的时候得到通知并在内部处理cancel()等操作。
// MyCountDownTimer.kt
// Lifecycle-aware CountDownTimer
class MyCountDownTimer@JvmOverloads constuctor(
millisUntilFinished: Long = DEFAULT_DURATION_IN_MILLIS,
countDownInterval: LONG = SECOND_IN_MILLIS,
private val onTickAction: () -> Unit,
private val onFinishAction: () -> Unit = {}
) : CountDownTimer(millisUntilFinished, countDownInterval), DefaultLifecycleObserver {
override fun onTick(millisUntilFinished: Long) {
onTickAction.invoke()
}
override fun onFinish() {
onFinishAction.invoke()
}
// onResume时自动开始
override fun onResume(owner: LifecycleOwner) {
start()
}
// onPause时自动取消
override fun onPause(owner: LifecycleOwner) {
cancel()
}
// onDestroy时停止观察
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
}
compaion object {
private const val DEFAULT_DURATION_IN_MILLIS = 10000L
}
}
上面例子中的KitchenFragment将会变成这样:
class KitchenFragment : Fragment() {
override fun onCreate() {
...
initTimer()
}
private fun initTimer() {
// 翻炒任务
val timer1 = MyCountDownTimer(onTickAction = { 翻炒 },onFinishAction = { 出锅 })
// 颠勺任务
val timer2 = MyCountDownTimer(millisUntilFinished = BRITAIN_SPOON_DURATION_IN_MILLIS,onTickAction = { 颠勺 })
// 擦桌任务
val timer3 = MyCountDownTimer(millisUntilFinished = SWEEP_TABLE_DURATION_IN_MILLIS,onTickAction = { 擦桌子 })
viewLifecycleOwner.lifecycle.apply {
addObserver(timer1)
addObserver(timer2)
addObserver(timer3)
}
}
compaion object {
private const val BRITAIN_SPOON_DURATION_IN_MILLIS = 5000L
private const val SWEEP_TABLE_DURATION_IN_MILLIS = 20000L
}
}
在Fragment中只需要专注于添加需要的功能,不用操心取消任务与停止观察。既清爽又不容易犯错。
单元测试
因为逻辑代码都封装在MyCountDownTimer,主要测试这个class就可以了。不需要给每一个使用MyCountDownTimer的Fragment都写详细的测试。
只需要mock一个LifecycleOwner就足够,也不需要启动一个mock Fragment。
class MyCountDownTimerTest {
private lateinit var timer: MyCountDownTimer
private lateinit var lifeCycle: LifecycleRegistry
@Before
fun setUp() {
val lifeCycleOwner: LifecycleOwner = mock(LifecycleOwner::class.java)
lifeCycle = LifecycleRegistry(lifeCycleOwner)
timer = MyCountDownTimer(onTickAction = { 翻炒 },onFinishAction = { 出锅 })
lifeCycle.addObserver(timer)
lifeCycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
@Test
fun timerActionExecuted() {
lifeCycle.markState(Lifecycle.State.RESUMED)
// 检测是否开始翻炒,出锅
...
}
}
来源:https://juejin.cn/post/7088171300162076680


猜你喜欢
- 前言:如果让大家说出一款国内比较热门的社交软件,那无疑就是QQ和微信了,说到微信,无不例外的会想到微信公众号和小程序,所以现在它们已经是很多
- 第1部分 List概括List的框架图List 是一个接口,它继承于Collection的接口。它代表着有序的队列。AbstractList
- 本文实例为大家分享了android计算器实现加减乘除的具体代码,供大家参考,具体内容如 * :以下计算器只注重实现功能,不考虑其他BUG,只有
- 尽管在实际开发过程中,我们一般使用ORM框架来代替传统的JDBC,例如Hibernate或者iBatis,但JDBC是Java用来实现数据访
- 本文实例讲述了java GUI编程之paint绘制操作。分享给大家供大家参考,具体如下:import java.awt.*;public c
- 目录构造方法方法介绍reduce归约构造方法// 1.无参数构造方法new ConcurrentHashMap();// 2.指定初始容量n
- 前段时间学习JDBC,要连接mysql获取数据。按照老师的样例数据,要存一些名字之类的信息,用的都是英文名,我当时就不太想用英文,就把我室友
- @RequestBody的作用@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的),所以只能发送
- 分享一个小技巧:在日常开发中有时候需要切换到另外的一个分支,但在某些条件下当前的分支上存在一些文件尚未提交,这时候就需要使用到idea自带的
- 1.底层网络接口采用apache的httpclient连接池框架; 2.图片缓存采用基于LRU的算法; 3.网络接口采用监听者模式; 4.包
- 本文实例讲述了C#自定义签名章实现方法。分享给大家供大家参考。具体实现方法如下:using System;using System.Coll
- Java中的字符串常量池Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid"
- springboot多模块项目添加一新模块选择Maven Module,填写模块名若空白,catalog目录可选择internal更改包名完
- 八皇后问题(N皇后问题)的回溯法求解一、问题描述在一个国际象棋棋盘上放置八个皇后,使得任何两个皇后之间不相互攻击,求出所有的布棋方法,并推广
- 本文给大家介绍的是监听Fragment的触摸事件实现。如果大家有更好的机制,可以留言交流,下面来看看详细的介绍:大家都知道,我们的activ
- SpringBoot @ComponentScan的使用SpringBoot的启动类中有一个@ComponentScan,之前项目由于这个注
- 有个小伙伴遇到了这样一个问题,就是AutoCompleteTextView实现自动填充的功能。同时要具备手机格式化的功能。下拉列表最后一行是
- android的WebView组件可以说是相当的强大,现将项目中经常用到的几个功能总结如下:一、背景设置WebView.setBackgro
- TimeSpan 结构 表示一个时间间隔。命名空间:System 程序集:mscorlib(在 mscorlib.dll 中)说
- C#关于颜色值的表示:常用的颜色值表示方式有两种,一种是10进制的RGB值表示,如(0,113,255),三个值分别表示(红,绿,蓝);一种