页面里面业务场景太多看花眼了?要不先分个层试试

1,608 阅读17分钟

什么是业务场景

这个词在我们开发工作当中应该经常会被提起,那到底什么是业务场景呢?我的理解是业务场景就是一种状态对应着页面的一种展现方式,比如某些app上你如果是未登录状态,页面上就会展示一个提示你去登录的提示语,然后再加上一个登录按钮,又比如安全验证方面,有的app会根据用户的风险等级去决定是去密码验证还是人脸验证,又或者是同时验证,在这些大大小小的业务场景里面,有的实现起来简单,可能只要两三句代码就完成了,有的可能实现起来会比较复杂,需要三方sdk配置再加上与自己服务端交互才能实现,而当我们一个独立的页面也牵扯进来了多个业务场景,我们通常会如何处理呢?

保守派

这种做法是给每一种业务场景都独立创建个页面,然后基本所有页面元素都差不多,视图逻辑都差不多,唯独在特定场景下实现方式不同,然后在入口的地方根据某种判断条件来决定跳哪一个页面

image.png

优点

这种做法的优点很明显,在每一个Activity里面只需要管理维护各自的对象就好,MilkActivity里面只会有牛奶相关的内容,不会有咖啡的内容出现,如果出来个酒类的需求,我只需要改WineActivity里面的代码就好了

缺点

如果此时来了一个新需求,需要在所有饮品页面中加入一些用户购买数据,那么我就要在三个页面里面都要加上购买数据的请求接口,这无疑是做了重复工作,又或者到了后期视觉验收环节,由于三个页面的页面元素基本相似,所以一个ui问题可能要在三个页面里面都要修改,开发效率大大降低

懒汉派

这种做法不会去建立各种页面,而是会在一个页面里面通过传进来的类型,将不同的代码逻辑套在一个个if-else里面,基本每到一处有差异的地方,你就会看到如下代码

image.png

每一个分支里面的代码量有多有少,甚至我见过TabLayout底下只有一个页面,然后使用index来区分不同的页面逻辑,那代码看起来简直是太一言难尽了

优点

这种做法的优点就是一些共有逻辑只需要实现一遍就好,业务不复杂的情况下,代码还是可以保留一些可读性的

缺点

不足的地方就是当业务量日趋庞大之后,每个if-else里面的业务代码将变得越来越难维护,耦合度也越来越高,当查看某一处业务逻辑的时候,必须将代码滑到最上面看看这部分代码属于哪一种业务类型的,然后滑下来继续看代码,十分不方便,另外如果又进来个新的需求,比如多了一个果汁类型的业务,那么每一个业务逻辑部分都需要另加一个if-else分支,只要有一处漏加就会产生问题,风险就比较大了。所以对于这种多业务场景的页面,我们究竟应该如何来做才能既降低开发与维护成本,也能让功能可以稳定运行呢?

思考如何设计

透过现象看本质,无论是上面说的保守派还是懒汉派,产生那样做法的始作俑者无非就是遇到了两个问题,一个是页面元素产生了差异,一个是业务逻辑产生了差异,我们又想保持老逻辑可以正常运行,又想让代码可以兼容新的逻辑,既然知道了问题的根本,那么我们能不能这样做,将一个页面的视图元素与逻辑代码拆分开来,逻辑代码里面也将视图逻辑与数据逻辑也拆分开来,每一部分都提供一个公共层,我们在公共层代码的基础上,按照业务上的差异,增量的在公共层上添加差异代码

image.png

如上图所示,上半部分的分支就是公共层,黄色板块为基础的视图绑定类与逻辑类,往左边橙色部分为具体实现,再往左灰色板块作为一个业务分层的管理类,负责提供该业务分层的视图绑定类与逻辑类,而下半部分的分支则是包含了一些除了公共层以外其他差异部分的实现,这些都体现在了浅红色板块里面,最左侧绿色板块里面就作为一个代理类,根据传入的不同类型来创建不同的业务分层管理类,这样就能保证在一个页面里面根据执行场景的不同来实现不同的逻辑,以上就是整体设计思路,下面是具体实现过程

创建视图绑定类的基类

abstract class BaseBindView {
    abstract fun bindView(activity: Activity?)
}

这个基类主要提供的服务就是去绑定视图,所以只需要一个bindView函数即可

创建数据逻辑类的基类

abstract class BaseViewModel(application: Application) : AndroidViewModel(application)

创建视图逻辑类的基类

abstract class BaseProxyView {
    abstract fun setActivity(activity:Activity)
    abstract fun setViewModel(viewModel:AndroidViewModel)
}

这个基类提供了逻辑类proxyview所要支持的服务,目前有一个设置页面上下文的函数,还有一个设置该页面对应的viewmodel的函数,其他有需要的可以往里面加,比如一些Activity的回调方法,可以通过在BaseProxyView添加函数的方式将具体实现放在逻辑类里面

创建公共视图绑定类

open class DrinkBindView(baseProxyView: BaseProxyView) : BaseBindView() {
    var drinkProxyView: DrinkProxyView

    init {
        drinkProxyView = baseProxyView as DrinkProxyView
    }

    override fun bindView(activity: Activity?) {
        drinkProxyView.initProxyView()
    }
}

公共的视图绑定类继承了BaseBindView,重写了bindView函数,在bindView里面针对页面的元素进行初始化,比如findViewbyId,或者Viewbinding,不过要使用ViewBinding的话还要支持配置不同的binding类,我们这里就使用findViewbyId,这边可能有人还注意到了,在绑定类的构造函数中,还传入了BaseProxyView,这样做的目的是一个为了从视图逻辑类那边提供视图来初始化,毕竟视图本身的声明还是属于视图逻辑那一部分,另一个则是所有的逻辑的开始必须是在视图元素初始化完以后才进行,所以我们也看到了在bindView函数的最后一步,添加了真正开始运行逻辑代码的入口initProxyView,这个时候,我们可以先写个简单的layout文件,当作我们公共层的默认布局,代码如下

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/drink_text"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="20dp"
        android:layout_marginStart="20dp"
        android:textColor="@color/black"
        android:textSize="18sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
        
    <Button
        android:id="@+id/drink_button"
        app:layout_constraintStart_toStartOf="@+id/drink_text"
        app:layout_constraintTop_toBottomOf="@+id/drink_text"
        android:layout_marginTop="10dp"
        android:text="开始喝"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</androidx.constraintlayout.widget.ConstraintLayout>

这个公共的页面有一个按钮,还有一个用来显示文案的TextView,这个时候我们就可以把这个文案与按钮控件的初始化工作添加到DrinkBindView里面

open class DrinkBindView(baseProxyView: BaseProxyView) : BaseBindView() {
    var drinkProxyView: DrinkProxyView

    init {
        drinkProxyView = baseProxyView as DrinkProxyView
    }

    override fun bindView(activity: Activity?) {
        drinkProxyView.apply {
            drinkText = activity?.findViewById(R.id.drink_text)
            drinkBtn = activity?.findViewById(R.id.drink_button)
            initProxyView()
        }
    }
}

创建公共视图逻辑类

open class DrinkProxyView : BaseProxyView() {
    var context:Activity?=null
    var drinkText: TextView? = null
    var drinkBtn:Button?=null
    var mViewModel:DrinkViewModel?=null

    open fun initProxyView() {
        initDrinkText()
        initDrinkButton()
    }
    override fun setActivity(activity: Activity) {
        context = activity
    }

    override fun setViewModel(viewModel: AndroidViewModel) {
        this.mViewModel = viewModel as DrinkViewModel
    }

    /**
     * 初始化文案
     */
    open fun initDrinkText() {


    }

    /**
     * 初始化按钮
     */
    open fun initDrinkButton(){

    }
}

这个视图逻辑类是一些公共的实现,可供其他逻辑类来继承使用或者定制自己有差异的逻辑,在这个视图逻辑类里面,做的工作主要是声明视图,初始化上下文,初始化viewmodel,以及针对每个视图开发具体业务逻辑,这里建议每一个视图的业务逻辑都可以维护在一个独立的函数中,并且函数是open的,这样做的目的是当有差异性的业务需求来的时候,只需要重写有差异的视图对应的函数即可,不用将其他无关代码逻辑复制后再去实现一遍。

定义代理服务

我们现在已经实现了绑定类与逻辑类的代码,那么接下来就是我们的代理类,去代理已经被拆分好的绑定类,视图逻辑类以及数据逻辑类供页面使用,代码如下

interface IDrink {
    fun getBindView(proxyView: BaseProxyView?): BaseBindView?

    fun getProxyView(): BaseProxyView?

    fun getViewModel(): BaseViewModel?
}

创建代理类

class Drink(mDrink:IDrink):IDrink {
    private var drink:IDrink? = null
    init {
        drink = mDrink
    }

    override fun getBindView(proxyView: BaseProxyView?): BaseBindView? {
        return drink?.getBindView(proxyView)
    }

    override fun getProxyView(): BaseProxyView? {
        return drink?.getProxyView()
    }

    override fun getViewModel(): BaseViewModel? {
        return drink?.getViewModel()
    }
}

这个代理类的作用就是根据不同的业务传参来生成对应的代理Drink类,现在先创建一个公共代理实现类

class BaseDrinkImpl : IDrink {
    override fun getBindView(proxyView: BaseProxyView?): BaseBindView? {
        return DrinkBindView(proxyView!!)
    }

    override fun getProxyView(): BaseProxyView? {
        return DrinkProxyView()
    }

    override fun getViewModel(): BaseViewModel? {
        return ViewModelProvider.AndroidViewModelFactory(MainApplication.application!!)
            .create(DrinkViewModel::class.java)
    }
}

当没有新的业务到来之前,我们的页面都是默认走的这个实现类,最后在Activity里面的代码配置如下

class DrinkActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_drink)
        val drink = Drink(BaseDrinkImpl())
        val proxyView: BaseProxyView = drink.getProxyView()!!
        proxyView.setActivity(this)
        proxyView.setViewModel(drink.getViewModel()!!)
        with( DrinkBindView(proxyView)){
            bindView(this@DrinkActivity)
        }
    }
}

这里先以BaseDrinkImpl作为参数来获取我们的代理类Drink,然后通过Drink获取页面所需要的绑定类与逻辑类,最后通过bindView函数完成视图初始化以及开启业务逻辑代码,这样我们的一个整体架构就已经出来了,现在就能尝试下用这个架构来开发需求了

实践一下

开发公共业务逻辑

我们公共层必须要有一套完整的业务实现,比如这里的业务逻辑就是点击按钮后,开始计时,第一秒文案是喝了1杯饮料,第二秒文案是喝了2杯饮料以此类推,逻辑很简单,首先就是在viewmodel里面写上这个定时器的代码

open class DrinkViewModel(application: Application) : BaseViewModel(application) {
    val _stateFlow by lazy { MutableStateFlow("") }
    val stateFlow = _stateFlow.asStateFlow()

    open fun beginTodrink(){
        var cup = 0
       viewModelScope.launch {
           flow {
               while (true){
                   delay(1000)
                   emit(cup++)
               }
           }.collect{
               _stateFlow.value = "喝了${it}杯饮料"
           }
       }
    }
}

然后在我们的视图逻辑类DrinkProxyView里面写上视图相关的代码,这里文案与按钮分别都做了事情,文案是显示从数据逻辑类那边传来的数据,而按钮是点击开启这个定时操作,所以我们需要将这两件事情分别写在对应的initDrinkTextinitDrinkButton函数里面,代码如下

/**
 * 初始化文案
 */
open fun initDrinkText() {
    MainScope().launch {
        mViewModel?.stateFlow?.collect{
            drinkText?.text = it
        }
    }

}

/**
 * 初始化按钮
 */
open fun initDrinkButton(){
    drinkBtn?.apply {
        setOnClickListener {
            mViewModel?.beginTodrink()
        }
    }
}

这样我们这个需求就已经完成了,运行后的效果就是下面这样

aa1.gif

扩展业务--视图逻辑与数据逻辑存在差异

现在比如我们又来了一个需求,有一个页面上有两个按钮,分别是全部,牛奶,需求要求点击全部按钮,跳转到我们原来的DrinkActivity实现老逻辑,点击牛奶按钮,跳转到另一个页面,与DrinkActivity比较相似,但是按钮文案变成了开始喝牛奶,文案也是一秒中显示一次,但是变成了喝了xx杯牛奶,并且文案颜色变成了红色,该如何做呢,是新建一个MilkActivity还是在DrinkActivity里面加上if-else的逻辑判断,都不是,还记得之前那个关系图吗,当有新的差异需求来的时候,我们只需要针对逻辑上的差异创建对应的代理类与逻辑类就好了,由于这里我们视图上的元素没有变化,依然还是按钮与文案,所以绑定类暂时不用另外创建,还是用的公共层的视图绑定类,至于逻辑类,首先我们要考虑一点,点击牛奶按钮显示的页面中,显示的文案数据是跟公共层有差异的,公共层里面,DrinkViewModel里面发送的文案是固定的喝了${it}杯饮料,但是在牛奶业务的页面里面需求要求显示的文案是喝了${it}杯牛奶,那么我们可以在新建的MilkViewModel里面,重写beginTodrink函数,然后将DrinkViewModel里面的代码复制过去,更改下发送的数据就好了,像这样

class MilkViewModel(application: Application) : DrinkViewModel(application) {
    override fun beginTodrink() {
        var cup = 0
        viewModelScope.launch {
            flow {
                while (true){
                    delay(1000)
                    emit(cup++)
                }
            }.collect{
                _stateFlow.value = "喝了${it}杯牛奶"
            }
        }
    }
}

虽然逻辑上没什么问题,但是还可以更简化一点,因为我们这边的差异仅仅是体现在发送的数据上,对于整个beginTodrink函数的功能来说,并没有什么不同,所以我们应该把定制的范围缩小一点,将发送的数据单独拿出来对外提供一个函数,先更改下公共层的数据逻辑类的代码

open class DrinkViewModel(application: Application) : BaseViewModel(application) {
    val _stateFlow by lazy { MutableStateFlow("") }
    val stateFlow = _stateFlow.asStateFlow()

    open fun beginTodrink(){
        var cup = 0
       viewModelScope.launch {
           flow {
               while (true){
                   delay(1000)
                   emit(cup++)
               }
           }.collect{
               _stateFlow.value = drinkData(it)
           }
       }
    }

    open fun drinkData(num:Int):String{
        return "喝了${num}杯饮料"
    }
}

新增了一个函数drinkData,这个函数专门用来定义发送的数据内容,这样的话我们在MilkViewModel里面只需要重写drinkData函数,更改一下它的返回值就好了,其他逻辑依然还是由公共的DrinkViewModel来实现

class MilkViewModel(application: Application) : DrinkViewModel(application) {
    override fun drinkData(num: Int): String {
        return "喝了${num}杯牛奶"
    }
}

像这样实现,不管是从代码量上,还是逻辑上我们都可以清晰的看到,牛奶业务里面在数据上的差异体现在了发送的数据上,其他的不变,这样的做法放在我们平时的开发里面,就可以使用在像接口请求上,比如有个新的页面,基本与现有的某个页面相同,但是在调用某个接口上,新的页面需要传的参数或者对于返回值的处理不一样,那么我们就可以通过这种方式,仅仅将传参的过程或者处理返回值的过程封装起来并暴露出去,以最小的工作量和代码量来完成这个需求,既不用重复造轮子,后期改问题也可以快速定位到具体业务代码里面,整体结构也清晰了不少。我们言归正传继续开发这个需求,在这个需求里除了发送数据上的差异,在视图上也有两处差异,分别是按钮文案不同以及展示文案的色值不同,既然是视图逻辑上的差异,那么我们就需要再创建一个MilkProxyView,并且重写按钮与文案的逻辑函数

class MilkProxyView: DrinkProxyView() {
    override fun initDrinkText() {
        super.initDrinkText()
        drinkText?.setTextColor(Color.RED)
    }

    override fun initDrinkButton() {
        super.initDrinkButton()
        drinkBtn?.text = "开始喝牛奶"
    }
}

我们看到无论是initDrinkText还是initDrinkButton,都分别先调用了父类函数方法,然后才是各自实现对应的逻辑,这个表示保留默认实现,然后在这个基础上再做开发,如果我们的牛奶业务里面比如点击按钮已经不是开启倒计时了,而是去请求个接口,拿下发的数据展示在文案上,那么可以不需要调用super.initDrinkButton()这一行代码,完全在MilkProxyView里面重新对按钮实现新的逻辑,这样也就达到了同样的页面元素,在不同的业务场景实现不同的逻辑的效果,现在数据逻辑以及视图逻辑都开发完成,就差使用代理类将它们放到页面中去了,这里再创建一个MilkImpl的类,实现IDrink接口,在对应重写函数中创建对应的逻辑类实例

class MilkImpl: IDrink {
    override fun getBindView(proxyView: BaseProxyView?): BaseBindView? {
        return DrinkBindView(proxyView!!)
    }

    override fun getProxyView(): BaseProxyView? {
        return MilkProxyView()
    }

    override fun getViewModel(): BaseViewModel? {
        return ViewModelProvider.AndroidViewModelFactory(MainApplication.application!!)
            .create(MilkViewModel::class.java)
    }
}

最后一步在Activity中通过intent传进来的参数,判断该选择使用哪一个业务代理类,代码实现如下

class DrinkActivity : AppCompatActivity() {
    lateinit var drink: Drink

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_drink)
        val type = intent.getStringExtra("type")
        drink = when(type){
            "milk"->{
                Drink(MilkImpl())
            }
            else ->{
                Drink(BaseDrinkImpl())
            }
        }
        val proxyView: BaseProxyView = drink.getProxyView()!!
        proxyView.setActivity(this)
        proxyView.setViewModel(drink.getViewModel()!!)
        with( DrinkBindView(proxyView)){
            bindView(this@DrinkActivity)
        }

    }
}

入口的代码就不展示了,无非就是不同按钮点击后传不同的参数到Activity的过程,我们直接看下运行效果

aa2.gif

现在我们已经实现了在一个页面里面,业务逻辑零耦合的实现了两种不一样的业务需求,但这个时候可能有些小伙伴要来吐槽了,这个是不是太理想化了,我们哪次需求变动,界面上不得多点元素少点元素啊,那么我们再新增个需求,试试看在页面里面如果多个视图元素又该怎么做?

扩展业务--视图元素差异与定制差异元素逻辑

我们再新增一个咖啡业务需求,要求现在有一组图片,需要每秒钟展示一张,图片下方需要展示一段文字,描述当前是第几张图片,然后再加一个按钮用来开启这个任务,整体分析下来,按钮与显示文案的控件都可以使用公共层的视图,但是这里又多出来一个ImageView,这个是公共层没有的,需要在咖啡业务里面定制,那么我们的布局文件也不能用公共层的了,得先创建个咖啡业务的布局文件

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <ImageView
        android:id="@+id/coffee_image"
        android:src="@mipmap/coffee1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginTop="20dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/coffee_text"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/coffee_image"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/coffee_text"
        android:layout_marginTop="20dp"
        android:text="更换图片开始"
        android:id="@+id/coffee_button"
        android:layout_width="match_parent"
        android:layout_height="50dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

这个布局文件里面与之前的布局文件存在着两个差异,一个是id都不一样了,一个是多了个ImageView,由于我们这里的ButtonTextView做的事情同公共层里面的相同,所以可以直接将coffee_textcoffee_button这两个id绑定至公共层页面里面的drinkBtndrinkText,这样我们就可以使用公共页面里面按钮与文字的功能了,再新建个CoffeeBindView,在里面做绑定视图的事情,代码如下所示

class CoffeeBindView(baseProxyView: BaseProxyView) : DrinkBindView(baseProxyView) {
    var coffeeProxyView:CoffeeProxyView
    init {
        coffeeProxyView = baseProxyView as CoffeeProxyView
    }

    override fun bindView(activity: Activity?) {
        drinkProxyView.drinkText = activity?.findViewById(R.id.coffee_text)
        drinkProxyView.drinkBtn = activity?.findViewById(R.id.coffee_button)
        coffeeProxyView.imageView = activity?.findViewById(R.id.coffee_image)
        coffeeProxyView.initProxyView()
    }
}

我们看到按钮与文案直接使用的是DrinkProxyView里面的变量,只有图片用的是CoffeeProxyView里面的imageView,这里面就体现出了咖啡业务里面新增的视图只有一个imageview,差异逻辑也只是在imageview上做,但是除此之外,我们刚刚是不是新建了一个layout文件,但是在公共层的DrinkActivity里面,貌似setcontentView里面是固定使用的默认layout文件,所以这部分我们也需要让它可配置的,如何配呢?就在BaseProxyView里面新增一个设置布局文件的函数

abstract class BaseProxyView {
    abstract fun setActivity(activity:Activity)
    abstract fun setViewModel(viewModel:AndroidViewModel)

    /**
     * 设置不同业务场景的布局文件
     */
    abstract fun layoutId():Int
}

新增了一个layoutId函数用来设置布局文件的id,然后在公共层的DrinkProxyView里面通过重写这个函数,将默认的布局文件设置进去

open class DrinkProxyView : BaseProxyView() {
    var context:Activity?=null
    var drinkText: TextView? = null
    var drinkBtn:Button?=null
    var mViewModel:DrinkViewModel?=null

    open fun initProxyView() {
        initDrinkText()
        initDrinkButton()
    }
    override fun setActivity(activity: Activity) {
        context = activity
    }

    override fun setViewModel(viewModel: AndroidViewModel) {
        this.mViewModel = viewModel as DrinkViewModel
    }

    override fun layoutId(): Int {
        return R.layout.activity_drink
    }
}

这样设置以后,如果其他业务的布局结构同公共层的布局结构相似,那么不需要在对应的视图逻辑类里面重写layoutId函数,就会使用DrinkProxyView默认设置的布局文件,但是比如像我们这个咖啡业务里面布局结构存在差异了,就可以把新建的布局文件设置到咖啡业务的视图逻辑类里面

class CoffeeProxyView : DrinkProxyView() {
    var imageView: ImageView? = null

    override fun layoutId(): Int {
        return R.layout.activity_coffee
    }
 }

最后再将layoutId配置到DrinkActivity里面就完成了配置不同业务层的布局文件的功能

val proxyView: BaseProxyView = drink.getProxyView()!!
setContentView(proxyView.layoutId())

现在来看下如何实现公共层没有的切换图片的功能,首先从网上随便下几张咖啡相关的图片

image.png

然后在CoffeeProxyView加上切换图片的逻辑代码

class CoffeeProxyView : DrinkProxyView() {
    var imageView: ImageView? = null
    val imageList = listOf(
        R.mipmap.coffee1, R.mipmap.coffee2, R.mipmap.coffee3,
        R.mipmap.coffee4, R.mipmap.coffee5, R.mipmap.coffee6
    )
    var index = 0

    override fun layoutId(): Int {
        return R.layout.activity_coffee
    }

    override fun initProxyView() {
        super.initProxyView()
        initImageView()
    }

    private fun initImageView() {
        MainScope().launch {
            mViewModel?.stateFlow?.collect {
                imageView?.setImageResource(imageList[index])
                index++
                if (index > 5) index = 0
            }
        }
    }
}

这里我们看到了重写了公共层的initProxyView函数,这么做的主要原因是initProxyView是所有视图元素逻辑初始化的入口位置,由于我们咖啡业务里面新增切换图片业务,所以也必须将图片相关初始化逻辑也放在initProxyView中进行,而super.initProxyView()这行代码也是必须存在的,因为只有加了这行代码,才可以使用公共层里面按钮的点击功能,不然的话我们只能在CoffeeProxyView里面新定义个按钮再添加点击事件,这样一来功能就与公共层重复了,不符合设计初衷,另外需求里面还提到,需要有个文案展示当前是展示的第几张图片,但由于公共层里面文案是展示的数据逻辑类里面StateFlow发出来的数据,所以这一部分需要重写initDrinkText函数,重新给drinkText设置文案

override fun initDrinkText() {
    MainScope().launch {
        mViewModel?.stateFlow?.collect {
            drinkText?.text = "当前显示的是第${index+1}张图片"
        }
    }
}

到这里我们咖啡业务的视图逻辑类已经完成了,老规矩我们创建CoffeeImpl用来提供CoffeeBindViewCoffeeProxyView

class CoffeeImpl: IDrink {
    override fun getBindView(proxyView: BaseProxyView?): BaseBindView? {
        return CoffeeBindView(proxyView!!)
    }

    override fun getProxyView(): BaseProxyView? {
        return CoffeeProxyView()
    }

    override fun getViewModel(): BaseViewModel? {
        return ViewModelProvider.AndroidViewModelFactory(MainApplication.application!!)
            .create(DrinkViewModel::class.java)
    }
}

由于在咖啡业务里面数据逻辑没变化,所以这部分依然使用公共层的DrinkViewModel,然后我们去Activity里面将CoffeeImpl也配置进去

class DrinkActivity : AppCompatActivity() {
    lateinit var drink: Drink
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val type = intent.getStringExtra("type")
        drink = when (type) {
            "milk" -> Drink(MilkImpl())
            "coffee" -> Drink(CoffeeImpl())
            else -> Drink(BaseDrinkImpl())
        }
        val proxyView: BaseProxyView = drink.getProxyView()!!
        setContentView(proxyView.layoutId())
        proxyView.setActivity(this)
        proxyView.setViewModel(drink.getViewModel()!!)
        with(when(type){
            "coffee" -> CoffeeBindView(proxyView)
            else -> DrinkBindView(proxyView)
        }){
            bindView(this@DrinkActivity)
        }
    }
}

在Activity里面根据传进来的type来生成不同的代理类Drink,另外由于咖啡业务的布局也不同,对于了不同的绑定类,所以当typecoffee的时候,绑定类使用的是CoffeeBindView,其他情况依然还是使用默认的DrinkBindView,现在来运行下看看咖啡业务逻辑完成的怎么样

aa3.gif

总结

相信如果文章能看到这里的小伙伴一定会对这种拆分业务逻辑的思想有了一定的概念了,这种思想的本质也就是将页面当中繁琐的业务通过分析整理,将这些复杂的逻辑代码分成业务公共层与业务定制层,在应对新业务或者维护老业务的时候,只需要判断业务的本质是处理数据还是视图,是公共逻辑还是定制逻辑,然后去对应的位置做修改即可。