开启生长之旅!这是我参与「日新计划 2 月更文应战」的第 1 天,点击检查活动详情

很多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操作,也会发生必定的功用问题

规划思路

能否让每个弹框作为一个单独的使命,生成一条使命链,链上的节点为单个使命,节点保护使命履行的条件以及使命自身逻辑,节点之间无任何依靠联系,详细履行由使命链去办理,这样的话假如添加或许删去某一个使命,咱们只需求插拔使命节点就能够

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

界说使命

首要咱们先简略定一个使命,以及需求履行的操作

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

咱们的使命链就完结了,看下效果

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

经过日志咱们能够看到,的确是每次封闭一个弹框,才开始履行下一条使命,这样一来,假如某个使命的条件不满意,或许不想让它履行了,只需求改动对应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()
    }
}

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

能够看到经过第一个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!!)
                    }
                }
            }
    }
}

运转一下,效果如下

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

咱们看到发动后第一个使命并没有马上履行,而是等了一会才去履行,那是由于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,然后三个接口现已从同步又变成异步操作了,运转一下看看效果

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

总结

大致的一个效果算是完结了,这仅仅一个demo,实践需求傍边或许更复杂,弹框,页面,小气泡来回交互的状况都有或许,这里也仅仅想给一些正在优化项目的的同学提供一个思路,或许接手新需求的时分,鼓励多思考一下有没有更好的规划计划