注册

首页弹框太多?Flow帮你“链”起来

很多App一打开,首页都会有各种各样的交互,比如权限授权,版本更新,阅读协议,活动介绍,用户权限变更等,这些交互大多数都是以弹框为主,也会有少数几个是以页面或者别的形式出现,但是无论是弹框还是页面,这些只是表现形式,这种交互难点在于

  1. 如何去判断它们什么时候出来

  2. 它们出来的先后次序是什么

  3. 中途需求如果增加或者删除一个弹框或者页面,我们应该改动哪些逻辑

常见的做法

可能这种需求刚开始由于弹框少,交互还简单,所以大多数的做法就是直接在首页用if-else去完成了

if(条件1){
  //弹框1
}else if(条件2){
   //弹框2
}

但是当需求慢慢迭代下去,首页弹框越来越多,判断的逻辑也越来越复杂,判断条件之间还存在依赖关系的时候,我们的代码就变得很可怕了

if(条件1 && 条件2 && 条件3){
  //弹框1
}else if(条件1 && (条件2 || 条件3)){
   //弹框2
}else if(条件2 && 条件3){
   //弹框3
}else if(....){
  ....
}

这种情况下,这些代码就变的越来越难维护,长久下来,造成的问题也越来越多,比如

  1. 代码可读性变差,不是熟悉业务的人无法去理解这些逻辑代码

  2. 增加或者减少弹框或者条件需要更改中间的逻辑,容易产生bug

  3. 每个分支的弹框结束后,需要重新从第一个if再执行一遍判断下一个弹框是哪一个,如果条件里面牵扯到io操作,也会产生一定的性能问题

设计思路

能否让每个弹框作为一个单独的任务,生成一条任务链,链上的节点为单个任务,节点维护任务执行的条件以及任务本身逻辑,节点之间无任何依赖关系,具体执行由任务链去管理,这样的话如果增加或者删除某一个任务,我们只需要插拔任务节点就可以

e68acea988e74acf8cc9c5a410cd6fc1~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

定义任务

首先我们先简单定一个任务,以及需要执行的操作

interface SingleJob {
   fun handle(): Boolean
   fun launch(context: Context, callback: () -> Unit)
}
  • handle():判断任务是否应该执行的条件

  • launch():执行任务,并在任务结束后通过callback通知任务链执行下一条任务

实现任务

定义一个TaskJobOne,让它去实现SingleJob

class TaskJobOne : SingleJob {
   override fun handle(): Boolean {
       println("start handle job one")
       return true
  }
   override fun launch(context: Context, callback: () -> Unit) {
       println("start launch job one")
       AlertDialog.Builder(context).setMessage("这是第一个弹框")
          .setPositiveButton("ok") {x,y->
               callback()
          }.show()
  }
}

这个任务里面,我们先默认handle的执行条件是true,一定会执行,实际开发过程中可以根据需要来定义条件,比如判断登录态等,lanuch里面我们简单的弹一个框,然后在弹框的关闭的时候,callback给任务链,为了调试的时候看的清楚一些,在这两个函数入口分别打印了日志,同样的任务我们再创建一个TaskJobTwo,TaskJobThree,具体实现差不多,就不贴代码了

任务链

首先思考下如何存放任务,由于任务之间需要体现出优先级关系,所以这里决定使用一个LinkedHashMap,K表示优先级,V表示任务

object JobTaskManager {
   val jobMap = linkedMapOf(
       1 to TaskJobOne(),
       2 to TaskJobTwo(),
       3 to TaskJobThree()
  )
}

接着就是思考如何设计整条任务链的执行任务,因为这个是对jobMap里面的任务逐个拿出来执行的过程,所以我们很容易就想到可以用Flow去控制这些任务,但是有两个问题需要去考虑下

  1. 如果直接将jobMap转成Flow去执行,那么出现的问题就是所有任务全部都一次性执行完,显然不符合设计初衷

  2. 我们都知道Flow是由上游发送数据,下游接收并处理数据的一个自上而下的过程,但是这里我们需要一个job执行完以后,通过callback通知任务链去执行下一个任务,任务的发送是由前一个任务控制的,所以必须设计出一个环形的过程

首先我们需要一个变量去保存当前需要执行的任务优先级,我们定义它为curLevel,并设置初始值为1,表示第一个执行的是优先级为1的任务

var curLevel = 1

这个变量将会在任务执行完以后,通过callback回调以后再自增,至于自增之后如何再去执行下一条任务,这个通知的事情我们交给StateFlow

val stateFlow = MutableStateFlow(curLevel)
fun doJob(context: Context, job: SingleJob) {
   if (job.handle()) {
       job.launch(context) {
           curLevel++
           if (curLevel <= jobMap.size)
               stateFlow.value = curLevel
      }
  } else {
       curLevel++
       if (curLevel <= jobMap.size)
           stateFlow.value = curLevel
  }
}

stateFlow初始值是curlevel,当上层开始订阅的时候,不给stateFlow设置value,那么stateFlow初始值1就会发送出去,开始执行优先级为1的任务,在doJob里面,当任务的执行条件不满足或者任务已经执行完成,就自增curLevel,再给stateFlow赋值,从而执行下一个任务,这样一个环形过程就有了,下面是在上层如何执行任务链

MainScope().launch {
   JobTaskManager.apply {
       stateFlow.collect {
           flow {
               emit(jobMap[it])
          }.collect {
               doJob(this@MainActivity, it!!)
          }
      }
  }
}

我们的任务链就完成了,看下效果

bd8f3ae3fb4f4f05bf40a72e7c705f70~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

通过日志我们可以看到,的确是每次关闭一个弹框,才开始执行下一条任务,这样一来,如果某个任务的条件不满足,或者不想让它执行了,只需要改变对应job的handle条件就可以,比如现在把TaskJobOne的handel设置为false,在看下效果

class TaskJobOne : SingleJob {
   override fun handle(): Boolean {
       println("start handle job one")
       return false
  }
   override fun launch(context: Context, callback: () -> Unit) {
       println("start launch job one")
       AlertDialog.Builder(context).setMessage("这是第一个弹框")
          .setPositiveButton("ok") {x,y->
               callback()
          }.show()
  }
}

c9865484deb24752a15db64563a578a6~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

可以看到经过第一个task的时候,由于已经把handle条件设置成false了,所以直接跳过,执行下一个任务了

依赖于外界因素

上面只是简单的模拟了一个任务链的工作流程,实际开发过程中,我们有的任务会依赖于其他因素,最常见的就是必须等到某个接口返回数据以后才去执行,所以这个时候,执行你的任务需要判断的东西就更多了

  • 是否优先级已经轮到它了

  • 是否依赖于某个接口

  • 这个接口是否已经成功返回数据了

  • 接口数据是否需要传递给这个任务 鉴于这些,我们就要重新设计我们的任务与任务链,首先要定义几个状态值,分别代表任务的不同状态

const val JOB_NOT_AVAILABLE = 100
const val JOB_AVAILABLE = 101
const val JOB_COMBINED_BY_NOTHING = 102
const val JOB_CANCELED = 103
  • JOB_NOT_AVAILABLE:该任务还没有达到执行条件

  • JOB_AVAILABLE:该任务达到了执行任务的条件

  • JOB_COMBINED_BY_NOTHING:该任务不关联任务条件,可直接执行

  • JOB_CANCELED:该任务不能执行

接着需要去扩展一下SingleJob的功能,让它可以设置状态,获取状态,并且可以传入数据

interface SingleJob {
  ......
   /**
    * 获取执行状态
    */
   fun status():Int

   /**
    * 设置执行状态
    */
   fun setStatus(level:Int)

   /**
    * 设置数据
    */
   fun setBundle(bundle: Bundle)
}

更改一下任务的实现

class TaskJobOne : SingleJob {
   var flag = JOB_NOT_AVAILABLE
   var data: Bundle? = null
   override fun handle(): Boolean {
       println("start handle job one")
       return  flag != JOB_CANCELED
  }
   override fun launch(context: Context, callback: () -> Unit) {
       println("start launch job one")
       val type = data?.getString("dialog_type")
       AlertDialog.Builder(context).setMessage(if(type != null)"这是第一个${type}弹框" else "这是第一个弹框")
          .setPositiveButton("ok") {x,y->
               callback()
          }.show()
  }
   override fun setStatus(level: Int) {
       if(flag != JOB_COMBINED_BY_NOTHING)
           this.flag = level
  }
   override fun status(): Int = flag

   override fun setBundle(bundle: Bundle) {
       this.data = bundle
  }
}

现在的任务执行条件已经变了,变成了状态不是JOB_CANCELED的任务才可以执行,增加了一个变量flag表示这个任务的当前状态,如果是JOB_COMBINED_BY_NOTHING表示不依赖外界因素,外界也不能改变它的状态,其余状态则通过setStatus函数来改变,增加了setBundle函数允许外界向任务传入数据,并且在launch函数里面接收数据并展示在弹框上,我们在任务链里面增加一个函数,用来给对应优先级的任务设置状态与数据

fun setTaskFlag(level: Int, flag: Int, bundle: Bundle = Bundle()) {
       if (level > jobMap.size) {
           return
      }
       jobMap[level]?.apply {
           setStatus(flag)
           setBundle(bundle)
      }
  }

我们现在可以把任务链同接口一起关联起来了,首先我们先创建个viewmodel,在里面创建三个flow,分别模拟三个不同接口,并且在flow里面向下游发送数据

class MainViewModel : ViewModel(){
   val firstApi = flow {
       kotlinx.coroutines.delay(1000)
       emit("元宵节活动")
  }
   val secondApi = flow {
       kotlinx.coroutines.delay(2000)
       emit("端午节活动")
  }
   val thirdApi = flow {
       kotlinx.coroutines.delay(3000)
       emit("中秋节活动")
  }
}

接着我们如果想要去执行任务链,就必须等到所有接口执行完毕才可以,刚好flow里面的zip操作符就可以满足这一点,它可以让异步任务同步执行,等到都执行完任务之后,才将数据传递给下游,代码实现如下

val mainViewModel: MainViewModel by lazy {
   ViewModelProvider(this)[MainViewModel::class.java]
}

MainScope().launch {
   JobTaskManager.apply {
       mainViewModel.firstApi
           .zip(mainViewModel.secondApi) { a, b ->
               setTaskFlag( 1, JOB_AVAILABLE, Bundle().apply {
                   putString("dialog_type", a)
              })
               setTaskFlag( 2, JOB_AVAILABLE, Bundle().apply {
                   putString("dialog_type", b)
              })
          }.zip(mainViewModel.thirdApi) { _, c ->
               setTaskFlag( 3, JOB_AVAILABLE, Bundle().apply {
                   putString("dialog_type", c)
              })
          }.collect {
               stateFlow.collect {
                   flow {
                       emit(jobMap[it])
                  }.collect {
                       doJob(this@MainActivity, it!!)
                  }
              }
          }
  }
}

运行一下,效果如下

d615a264a0e44eb08ced2686e2e8982f~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

我们看到启动后第一个任务并没有立刻执行,而是等了一会才去执行,那是因为zip操作符是等所有flow里面的同步任务都执行完毕以后才发送给下游,flow里面已经执行完毕的会去等待还没有执行完毕的任务,所以才会出现刚刚页面有一段等待的时间,这样的设计一般情况下已经可以满足需求了,毕竟正常情况一个接口的响应时间都是毫秒级别的,但是难防万一出现一些极端情况,某一个接口响应忽然变慢了,就会出现我们的任务链迟迟得不到执行,产品体验方面就大打折扣了,所以需要想个方案解决一下这个问题

优化

首先我们需要当应用启动以后就立马执行任务链,判断当前需要执行任务的优先级与curLevel是否一致,另外,该任务的状态是可执行状态

/**
* 应用启动就执行任务链
*/
fun loadTask(context: Context) {
   judgeJob(context, curLevel)
}

/**
* 判断当前需要执行任务的优先级是否与curLevel一致,并且任务可执行
*/
private fun judgeJob(context: Context, cur: Int) {
   val job = jobMap[cur]
   if(curLevel == cur && job?.status() != JOB_NOT_AVAILABLE){
       MainScope().launch {
           doJob(context, cur)
      }
  }
}

我们更改一下doJob函数,让它成为一个挂起函数,并且在里面执行完任务以后,直接去判断它的下一级任务应不应该执行

private suspend fun doJob(context: Context, index: Int) {
   if (index > jobMap.size) return
   val singleJOb = jobMap[index]
   callbackFlow {
       if (singleJOb?.handle() == true) {
           singleJOb.launch(context) {
               trySend(index + 1)
          }
      } else {
           trySend(index + 1)
      }
       awaitClose { }
  }.collect {
       curLevel = it
       judgeJob(context,it)
  }
}

流程到了这里,如果所有任务都不依赖接口,那么这个任务链就能一直执行下去,如果遇到JOB_NOT_AVAILABLE的任务,需要等到接口响应的,那么任务链停止运行,那什么时候重新开始呢?就在我们接口成功回调之后给任务更改状态的时候,也就是setTaskFlag

fun setTaskFlag(context:Context,level: Int, flag: Int, bundle: Bundle = Bundle()) {
   if (level > jobMap.size) {
       return
  }
   jobMap[level]?.apply {
       setStatus(flag)
       setBundle(bundle)
  }
   judgeJob(context,level)
}

这样子,当任务链走到一个JOB_NOT_AVAILABLE的任务的时候,任务链暂停,当这个任务依赖的接口成功回调完成对这个任务状态的设置之后,再重新通过judgeJob继续走这条任务链,而一些优先级比较低的任务依赖的接口先完成了回调,那也只是完成对这个任务的状态更改,并不会执行它,因为curLevel还没到这个任务的优先级,现在可以试一下效果如何,我们把threeApi这个接口响应时间改的长一点

val thirdApi = flow {
   kotlinx.coroutines.delay(5000)
   emit("中秋节活动")
}

上层执行任务链的地方也改一下

MainScope().launch {
   JobTaskManager.apply {
       loadTask(this@MainActivity)
       mainViewModel.firstApi.collect{
           setTaskFlag(this@MainActivity, 1, JOB_AVAILABLE, Bundle().apply {
               putString("dialog_type", it)
          })
      }
       mainViewModel.secondApi.collect{
           setTaskFlag(this@MainActivity, 2, JOB_AVAILABLE, Bundle().apply {
               putString("dialog_type", it)
          })
      }
       mainViewModel.thirdApi.collect{
           setTaskFlag(this@MainActivity, 3, JOB_AVAILABLE, Bundle().apply {
               putString("dialog_type", it)
          })
      }
  }
}

应用启动就loadTask,然后三个接口已经从同步又变成异步操作了,运行一下看看效果

32d6bc1008ce4d9fa4515753b6bfe9f4~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

总结

大致的一个效果算是完成了,这只是一个demo,实际需求当中可能更复杂,弹框,页面,小气泡来回交互的情况都有可能,这里也只是想给一些正在优化项目的的同学提供一个思路,或者接手新需求的时候,鼓励多思考一下有没有更好的设计方案

作者:Coffeeee
来源:juejin.cn/post/7195336320435601467

0 个评论

要回复文章请先登录注册