一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
前言
你是否在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)
// 检测是否开始翻炒,出锅
...
}
}
总结
通过把重复的代码和逻辑封装在自定义的LifecycleObserver
内部,不仅可以给Activity/Fragment
“瘦身”,防止忘记在onStop()/onDestroy()
中收拾,还可以使复用代码更加方便。同时也遵循设计模式,降低了Fragment
与Timer
之间的耦合度,让代码更好维护。