反向学习,从MVI架构中学习Kotlin的密封类 & 密封类的其他使用场景

Kotlin中密封类的使用

前言

之前看到很多文章讲到Kotlin的密封类的介绍,说实话我本来有点明白的,看了之后我反倒迷糊了。

看我的文章会不会给你一些不一样的理解,下面带着这些疑问看看:

为什么需要密封类这个个东西,class、data class 、object class 这三种类都在 Java 中能实现,为什么 KT 要有一个密封类?

密封类是一个类,也是一个特殊的类,和普通的类 class、data class 、object class 有什么区别?

密封类在哪些场景下推荐使用?

之前我们都是先学习密封类,然后再学习怎么使用,以典型的 MVI 架构为例,本期我们反过来推理,MVI为什么要用密封类,从而理清密封类的特性,最后我们在温习在Android的开发中有哪些场景可以用到密封类。

好了,话不多说,我们带着问题接着往下看吧。

一、MVI的密封类为什么要这么使用

我们以 MVI 的状态,意图,效果这三个密封类说起:

如果你对 MVI 的架构不太了解,可以看看我之前的文章:

遍历全网Android-MVI架构,从简单到复杂学习总结一波

2024年Android开发架构推荐

直接上代码:

kotlin 复制代码
//Effect
sealed class ProfileEffect : IUIEffect {
    data class ToastMessage(val msg: String?) : ProfileEffect()
}

//Intent
sealed class ProfileIntent : IUiIntent {
    object FetchArticle : ProfileIntent()
    object FetchBanner : ProfileIntent()
    object PutKV : ProfileIntent()
    object GetKV : ProfileIntent()
    object RemoveKV : ProfileIntent()
}

//State
data class ProfileState(val bannerUiState: BannerUiState, val articleUiState: ArticleUiState) : IUiState

sealed class BannerUiState {
    object INIT : BannerUiState()
    data class SUCCESS(val banner: List<Banner>) : BannerUiState()
}

sealed class ArticleUiState {
    object INIT : ArticleUiState()
    data class SUCCESS(val article: List<TopArticleBean>) : ArticleUiState()
}

我们以 Intent 为例,为什么要用 object 单例类来设置,为什么还能设置 data class ,使用的是这样用:

scss 复制代码
  when (intent) {
            ProfileIntent.FetchBanner -> fetchBanner()
            ProfileIntent.FetchArticle -> fetchArticle()
            ProfileIntent.GetKV -> getPreference()
            ProfileIntent.PutKV -> putPreference()
            ProfileIntent.RemoveKV -> removePreference()
        }

咦,这不就是枚举吗?

这么理解没毛病,你完全可以理解为密封类就是加强版的枚举,只不过密封类的主要成员是类,而 Kotlin 的原生枚举不支持类。

密封类中的各种类可以携带参数吗?

当然可以,可以使用数据类或者普通类都可以实现参数的携带。

kotlin 复制代码
sealed class AuthIntent : IUiIntent {
    data class TestGson(val gson: Gson) : AuthIntent()
    object FetchArticle : AuthIntent()
    object PutKV : AuthIntent()
    class GetKV(val key :String) : AuthIntent()

    var name: String? = null
}

可以看到虽然我们一样可以在密封类中定义变量,定义普通类,和数据类,单例类,但是我们常用的还是数据类和单例类。

在数据类和普通类中我们就可以传递参数的校验:

kotlin 复制代码
    override fun handleIntent(intent: AuthIntent) {
        when (intent) {
            is AuthIntent.FetchArticle -> fetchArticle()
            is AuthIntent.PutKV -> putPreference()
            is AuthIntent.GetKV -> getPreference(intent.key)
            is AuthIntent.TestGson -> testGson(intent.gson)
        }
    }

而发送 Intent 的时候我们就需要初始化对应的数据类和普通类,而他们两者其实区别是不大的

less 复制代码
    mViewModel.sendUiIntent(AuthIntent.TestGson(activityGson))
    mViewModel.sendUiIntent(AuthIntent.GetKV("123"))

只是数据类主要用于承载相关的数据,它们自动实现了equals()、hashCode()和toString()等方法,便于数据处理和传递和相应的copy修改。也就是说两者都可以只是数据类专注与数据我们用的比较多而已。

而单例类则表示不需要携带额外数据的唯一实例,我们常用于表达特定的状态或事件。

比如一个默认的事件发起请求:

scss 复制代码
 mViewModel.sendUiIntent(AuthIntent.FetchArticle)

MVI 中的 State 到底用密封类还是数据类?

两种定义方式如下:

kotlin 复制代码
sealed class AuthUiState : IUiState {
    object INIT : AuthUiState()
    data class SUCCESS(val article: List<TopArticleBean>) : AuthUiState()
}

data class AuthUiState(
    val article: List<TopArticleBean>
) : IUiState

打个比方,我们现在知道了我们的密封类就是一个加强版的枚举,如果我们的 State 中是带各种状态的,并且相对复杂就用密封类,如果只是相对简单的数据那么直接用数据类也是可以的。

详细一点来说,如果你的UI状态具有多种不同的状态,每种状态可能需要不同的数据,或者你想在编译时强制处理所有可能的状态(为了类型安全和可维护性),则应选择使用密封类更好。

如果你的状态比较简单,使用一个数据类就能够覆盖所有需要的状态信息,那么使用data class可能会更简单和直接。

甚至我们还能嵌套使用,在数据类中以密封类为构造参数。

kotlin 复制代码
data class ProfileState(val bannerUiState: BannerUiState, val articleUiState: ArticleUiState) : IUiState

sealed class BannerUiState {
    object INIT : BannerUiState()
    data class SUCCESS(val banner: List<Banner>) : BannerUiState()
}

sealed class ArticleUiState {
    object INIT : ArticleUiState()
    data class SUCCESS(val article: List<TopArticleBean>) : ArticleUiState()
}

只是在 ViewModel 中更新状态的时候,不同的类需要不同的更新方式。

scss 复制代码
    sendUiState(AuthUiState.SUCCESS(articleResult.data))

    updateUiState {
        copy(article = articleResult.data)
    }

数据类中可以用密封类作为构造参数,同时密封类中又能使用数据类当成员,他们的关系有点绕需要理清楚。

虽然两种方法都能用在 MVI 的 State 中,那么哪一种更好呢?相对而言,个人还是喜欢以 data class 为 MVI 的 State 格式,可以充分利用 data class 的 copy 更新从而实现局部监听。

二、密封类的概念

我们通过上面示例实际体验过之后我们在回过头看看密封类的概念。

密封类是一种特殊的类,它用于表示受限的类继承结构:当一个值可以有类型的几种子类型之一,但不能有任何其他类型时。这是通过将类的所有直接子类声明在与密封类相同的文件中来实现的。这种限制的主要好处是在使用 when 表达式时,可以保证覆盖所有的情况,不再需要 else 子句。

密封类不能直接实例化,并且其构造函数默认是 private 的。你可以在密封类内部定义抽象成员,密封类的直接子类可以是数据类(data class)、对象声明(object)或者普通类,但是子类的嵌套层次不限制于一层。

我们以枚举来举例来区别他们,加深密封类的概念。

为什么要用密封类,使用枚举不行吗?

在MVI(Model-View-Intent)架构中,选择使用密封类(sealed classes)而非枚举(enums)来表示状态和意图(Intents),主要是因为密封类提供了枚举无法匹敌的灵活性和表达力。下面是几个关键的考虑因素:

  1. 多样性和扩展性

密封类:可以有多个子类,每个子类可以有不同的属性和方法。这意味着你可以为不同的状态或意图携带不同类型和数量的数据,从而使得状态管理更加灵活和详细。

枚举:所有的枚举实例都必须有相同的属性集合,这在需要为不同的状态携带不同数据的场景中变得不够灵活。

  1. 类型安全

密封类:由于密封类的子类类型是固定的,Kotlin 编译器可以在编译时检查when表达式是否覆盖了所有的可能性,从而减少运行时错误。

枚举:虽然枚举也可以在when表达式中使用,并且可以检查是否覆盖了所有的枚举值,但它们无法携带额外的状态信息,除非你为每个枚举值添加固定的属性集。

3.表达能力

密封类:可以通过创建不同的子类来精确定义每个状态或意图所包含的信息,这为状态的细粒度管理提供了可能。

枚举:更适合于表示一组固定的、简单的值集合。虽然可以通过添加属性和方法来扩展枚举的功能,但这种方式的表达能力仍然不如密封类。

  1. 维护性

密封类:随着应用的发展,你可能需要添加更多的状态或意图。密封类使得添加新的子类变得非常容易,而且可以保持类型安全和清晰的结构。

枚举:添加新的枚举值是简单的,但如果新的状态需要额外的信息或行为,则可能需要重构枚举结构或者选择其他方式来扩展功能。

总结来说,密封类在MVI架构中的使用之所以受到推崇,是因为它们为状态和意图的多样性、扩展性和类型安全提供了优异的支持。密封类允许更精细的控制和更丰富的表达,这在管理复杂的UI状态和应用逻辑时非常有价值。而枚举虽然在某些场景下仍然有用,但在需要表达复杂状态时,它们的功能和灵活性可能受到限制。

密封类与枚举有什么区别?

将密封类视作一种"高级的枚举"是一个合理的比喻,特别是在它们用于表达限定的类型集合时。密封类和枚举类(enum class)都可以用于在Kotlin中表示一个有限的、闭合的值集合,但它们在使用上和功能上有一些关键的差异。

相同点:

限定性:两者都用于表示一组有限的值或类型。

类型安全:使用密封类或枚举可以让Kotlin编译器通过类型检查来保证代码的安全性。

不同点:

表达能力:

枚举类主要用于表示一组固定的常量,每个枚举常量都是其枚举类型的一个唯一实例。枚举常量可以携带属性(比如值或名称),但它们的类型是固定的,不能有子类型。 密封类允许更复杂的类型层次结构,每个子类可以有不同的类型和构造函数参数,能够携带各自的状态和数据。这提供了更大的灵活性来表达复杂的类型关系和数据结构。

实例化方式:

枚举类的每个枚举常量在定义时就已经是一个单一实例,不能在运行时创建更多实例。 密封类的子类可以是数据类、对象声明或普通类,允许在运行时创建多个实例(除了通过object声明的单例之外)。

用途和适用场景:

枚举类适用于表示没有或只有少量附加数据和逻辑的固定集合,如状态、选项、配置项。

密封类更适合表示复杂的状态或分支逻辑,其中每个状态或分支可能有不同的属性和行为,尤其是在需要用类型来区分和处理多种复杂情况时。

综上所述,密封类和枚举类各有特点,根据需要表达的复杂性和场景选择使用。枚举类在表示简单且固定的值集合时很有用,而密封类则在需要表示复杂的、有层次的类型关系时更加强大和灵活。

三、其他的使用场景

在Android开发中,除了以上讲到的UI事件处理、网络状态表示和状态管理,密封类还可以用于以下场景:

导航管理

在管理应用中的导航时,你可以使用密封类来表示所有可能的导航目标(如不同的屏幕或视图)。这样做可以确保你在处理导航逻辑时考虑了所有可能的情况,从而避免运行时错误。

kotlin 复制代码
sealed class NavigationTarget {
    object HomeScreen : NavigationTarget()
    object DetailScreen : NavigationTarget()
    class UserProfileScreen(val userId: String) : NavigationTarget()
}

fun navigateTo(target: NavigationTarget) {
    when (target) {
        NavigationTarget.HomeScreen -> { /* 导航到主页 */ }
        NavigationTarget.DetailScreen -> { /* 导航到详情页 */ }
        is NavigationTarget.UserProfileScreen -> { /* 使用target.userId导航到用户资料页 */ }
    }
}

多视图类型的列表

在构建一个包含多种视图类型的RecyclerView时,你可以使用密封类来表示不同的列表项类型。这可以让你更容易地管理不同类型的视图并确保类型安全。

kotlin 复制代码
sealed class ListItem {
    data class Header(val title: String) : ListItem()
    data class Content(val text: String) : ListItem()
    object Footer : ListItem()
}

class MyAdapter(private val items: List<ListItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun getItemViewType(position: Int): Int {
        return when (items[position]) {
            is ListItem.Header -> TYPE_HEADER
            is ListItem.Content -> TYPE_CONTENT
            is ListItem.Footer -> TYPE_FOOTER
        }
    }

    // 实现其他方法,如onCreateViewHolder和onBindViewHolder,处理不同的视图类型
}

处理异步任务结果

处理异步任务的结果时,如从数据库或网络获取数据,可以使用密封类来表示成功、失败或其他状态的结果。这使得结果处理更加清晰和类型安全。

kotlin 复制代码
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

fun fetchData(): Result<String> {
    return try {
        // 模拟获取数据
        Result.Success("Data")
    } catch (e: Exception) {
        Result.Error(e)
    }
}

fun handleResult(result: Result<String>) {
    when (result) {
        is Result.Success -> println(result.data)
        is Result.Error -> println("Error: ${result.exception.message}")
    }
}

除此之外,用于类型判断用枚举无法解决的都可以用密封类来实现,特别是带参数的一些类型判断更是合适。

总结

本文先从 MVI 的架构中的密封类使用开始,怎么使用密封类,为什么要使用密封类,密封类和枚举的区别和异同以及Android开发中的其他应用场景。

我们从另一种角度理解为密封类其实就是一种特殊的,高级的枚举,那么从这个角度出发我相信就会有不一样的视角,也会更容易理解了。

OK,本文主要是思路的讲解,相关代码文章中已经全部贴出。

最后惯例,如果有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码/注释有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。

相关推荐
Eastsea.Chen1 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年9 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX9 小时前
kotlin
开发语言·kotlin
建群新人小猿11 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神12 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛12 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法13 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter14 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快15 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl16 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5