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

标签:android,lifecycle
0
投稿

猜你喜欢

  • Flutter加载图片流程MultiFrameImageStreamCompleter解析

    2023-07-19 02:45:55
  • 基于String和List<String>间的相互转换方式

    2022-09-25 15:52:21
  • C#委托与冒泡排序实例

    2022-05-03 13:26:21
  • C#中缓存的基本用法总结

    2023-12-06 01:22:45
  • Android应用的多语言支持的实现方法

    2023-07-09 18:26:43
  • 用Java连接sqlserver数据库时候几个jar包的区别分析

    2021-11-07 02:18:01
  • idea创建maven父子工程导致子工程无法导入父工程依赖

    2021-09-17 09:34:08
  • 如何用IDEA调试BUG的几种方法

    2022-08-04 17:12:46
  • Java读取json数据并存入数据库的操作代码

    2023-09-23 06:00:57
  • IDEA2020.1常用配置说明

    2023-01-09 02:11:50
  • Java父线程(或是主线程)等待所有子线程退出的实例

    2022-10-23 16:25:50
  • C#实现Zip压缩目录中所有文件的方法

    2021-05-29 15:41:47
  • 详解Java中的实例初始化块(IIB)

    2023-06-08 08:35:46
  • 如何自动生成Mybatis的Mapper文件详解

    2023-07-28 08:20:54
  • 谷歌被屏蔽后如何搭建安卓环境

    2022-10-23 02:07:36
  • Android中解决EditText放到popupWindow中,原有复制、粘贴、全选、选择功能失效问题

    2021-07-12 10:51:49
  • SpringBoot整合Elasticsearch游标查询的示例代码(scroll)

    2022-02-11 02:02:13
  • 深入浅出重构Mybatis与Spring集成的SqlSessionFactoryBean(上)

    2021-12-01 18:27:49
  • C#模拟window操作鼠标的方法

    2021-07-17 01:50:22
  • Android App实现监听软键盘按键的三种方式

    2021-09-14 10:39:27
  • asp之家 软件编程 m.aspxhome.com