现代化 Android 开发:数据类

简介: 本文为现代化 Android 开发系列文章第二篇。

命名


随着软件复杂度的逐步提升,正确命名数据类已经是确保代码可读性和可维护性的重要一环了。好的命名,可以清晰的传递其类型和用途,让我们在使用和查找时都会特别方便。当然,谈到命名,我们往往会强调要遵循命名规范、使用有意义的名字、避免缩写和简写。不过这只是基础中的基础。我们还需要在架构的各个环节做好统一命名规范和约束。


我们这里以 Article 为例来阐述在各个环节应该怎样命名。


最基础的应该是我们往数据库存取 ORM 对象,例如:

@Entity
data class Article(
    @PrimaryKey
    val id: Int,
    val title: String,
    val author_id: Int,
    val content: String, 
)
@Entity
data class User(
    @PrimaryKey
    val id: Int,
    val name: String
)

这种数据结构,一般后端也会有一套,如果前后端协议定义得好,那基本上大多数都是通用的,只是语言不同而已,直接用 ChatGPT 转一下就好了。在开发岐黄小筑时,我就需要写 rustkotlinTypeScript 三个端的数据结构,所以代码翻译我用的还是挺多的,由于涉及多个编辑器写代码,所以我还是使用 emo-ai代码翻译来得方便。

room 是个很好的库,可惜它诞生于 kotlin-serialization 之前,否则我觉得依赖 kotlin-serialization 来实现是更加完美的,可以复用 kotlin-serialization@SerialName 之类的注解,目前每次类似情况都要打两个注解,着实有点麻烦。

一般而言,我们的数据是来源于网络,它一般是一个复杂的结构体,那该怎么表示呢?


我的处理方式是加上 Resp 后缀。 例如 Article 的网络结果可以表示为:

@Serializable
data class ArticleResp(
    val article: Article,
    val user: User,
    ... // other fields.
)

通过 -Resp 我就可以知道这是一个网络结果的承载类。 这里的后台返回结果,我是可以直接复用数据库结构,这是依赖于前后端协议定得比较好,而更多情况,后端返回的数据可能是:

{
    "article": {
        "id": 0,
        "title": "xx",
        "author": {
            // User 字段...
        }
        // 其它字段...
    }
}

}

这种数据结构形式,就是可读性高一点,网页端因为不需要存储起来做离线,那使用起来就很方便,App 则需要存储到数据库方便离线使用或者增量同步数据,那就需要额外的数据结构来表示,再解析为基础数据结构。这个额外的数据结构我一般加 TO (Transfer Object)后缀来表示:

@Serializable
data class ArticleTO(
    val id: Int,
    val author: User,
    ...
)

.)

如果我们往服务端 post 数据的话,一般也会不同于基础数据结构,这个时候加 Payload 后缀表示:

@Serializable
data class ArticlePayload(
    val title: String,
    val content: String
)

当我们需要从数据库读取数据时,我们需要从几个表中读取数据,这个 Room 也建议使用 POJO 的后缀来组合数据:

data class ArticlePOJO(
    @Embedded
    val ariticle: Article,
    @Relation(...)
    val author: User
)

可能很多人还是喜欢数据层返回 ArticleTO 这样的数据结构,但如果使用 Room,还是强制使用 POJO 这种组合形式比较好。


到了 UI 层,可能还需要进一步封装成 UI 舒适的数据结构,那么一般加 VOView Object) 后缀。


总结一下,我们在不同场景使用不同的后缀来区分数据类:


  • 使用 Resp 后缀表示网络结果类
  • 使用 TO 后缀表示传输数据类
  • 使用 Payload 后缀也是表示传输数据类,主要用 post 请求的 body 部分,作为请求的一部分
  • 使用 POJO 后缀表示为查询 room 并组合数据
  • 使用 VO 后缀表示为 UI 而使用的数据类


如此,当我们看到某个类,就知道其用于什么场景,我们需要改数据结构,也可以快速搜索定位了。

immutable


在以前,写一个实体类,首要的就是写一堆 setget 方法,但目前推荐的是用 data class 来定义 immutable 的对象。其除了是为了限制使用者随意修改数据外,更多的还是多线程环境下,mutable 对象太不稳定了。很多偶现的问题,多是因为 mutable 的问题,可以称之为程序员的噩梦。目前 KMM 也有大火的趋势,而它多线程场景只能使用 immutable 对象。所以,早点习惯使用 immutable 的编程方式,早日逃离多线程偶现的噩梦。


Jetpack ComposeUI 体系下,Immutable 的重要性就更大了,很多人写出的界面卡顿无比,可能就是用了 Mutable 的数据结构,导致很多时候 recomposition 没办法 skip 掉。


一个简单的例子就是 List 的使用:

@Composable
function ArticleList(list: List<Article>){
    for(...){
        Item(ariticle)
    }
}
@Composable
function ArticlePage(){
    var counter by remember {
        mutableStateOf(0)
    }
    Button(modifier = Modifier.throttleClick {
        counter++
    })
    ArticleList(...)
}

在上面的例子中,ArticlePage 自身有一个计数器,然后使用了 ArticleList 组件,每次计数器增加,触发 ArticlePage 重组,当执行 ArticleList 函数时,它会检查参数是否变更,如果没有变更,那就可以 skip 掉,否则就会触发 ArticleList 函数的重新执行,因为 List 是不稳定的,所以它就会触发 ArticleList 的重组,就会重新跑 for 循环,如果 for 循环的每个 Item 也不是稳定的,那每个 Item 就继续重组,所以整个界面的每个操作都表现得特别卡。


所以还不习惯 immutable 开发的同学,写 Jetpack Compose 就会遇到很多坑。


所以我们得使用 kotlinx-collections-immutable 中的数据结构来替代 ListMap 之类的数据结构,我比较好奇的是 Compose 内部是引入了这个库的,但是并没有对外开放,我们还得自己引入一次,不知官方的考量是什么。


如果你的数据是 mutable, 除了性能问题,那么最主要的问题还是子线程修改可能导致的 crash,一个经典的场景就是数组越界:

class A {
    var list = mutableListOf(1, 2, 3, 4, 5)
}
val handled = AtomicBoolean(false)
@Composable
fun Page(a: A) {
    LaunchEffect(Unit){
        withContext(Dispatchers.IO){
            delay(100)
            // 这里在背景线程移除
            list.removeLast()
            handled.set(true)
        }
    }
    LazyColumn {
        // 这里通过 `list.size` 确定 有多少个 `item`
        items(a.list.size){
            // 这里是为了必现而写的死等待操作
            while(!handled.get()){
            }
            // 实际执行到这里时,由于 `list.size` 已经变化,所以最后一个 `item` 时就数组越界了
            Text(text = "${a.list[it]")
        }
    }
}

在实际开发中,数据更新与渲染时机都充满不确定性,所以开发毫无破绽,上线后就是飙升的 crash 率。


所以 rust 的所有权系统真的是伟大,从开发阶段就阻止了程序员犯错,能编译通过,这种问题就是不可能存在的。

使用 Result 包装


我们从后端获取结果,或者将数据流交给 UI 层,都会涉及到成功、失败等各种状态,所以用 Result 来包装下数据结果,然后丢给下一层处理,也是一个主流了。

例如网络层结果,我有提供 RespResult 类:

// 加载状态:加载中、本地读取、网络读取、内存 cache 等。
enum class LogicStatus {
    Loading,
    Local,
    LocalButFinal,
    Network,
    Cache
}
data class LogicResult<T>(
    val status: LogicStatus,
    val data: T?,
    val code: Int = RESP_CODE_OK,
    val msg: String? = null
) {
    inline fun <U> then(block: (T) -> U?): LogicResult<U> {
        return LogicResult(status, data?.let(block), code, msg)
    }
}

这个 then 方法的命名,我是抄 rust 的,就是如果结果是成功的,则转换为另一个 RespResult, 否则的话就把失败信息透传到下一层。相当于 map 操作。

逻辑层到 UI 层,我也有 LogicResult 类:

// 加载状态:加载中、本地读取、网络读取、内存 cache 等。
enum class LogicStatus {
    Loading,
    Local,
    LocalButFinal,
    Network,
    Cache
}
data class LogicResult<T>(
    val status: LogicStatus,
    val data: T?,
    val code: Int = RESP_CODE_OK,
    val msg: String? = null
) {
    inline fun <U> then(block: (T) -> U?): LogicResult<U> {
        return LogicResult(status, data?.let(block), code, msg)
    }
}

其实我并不觉得目前这种封装有多么完美,因为对于加载、失败等状态、data 是没有意义的, 而且因为要兼容这些状态,我还必须的让 data 可空。主要还是因为 kotlinenum 不够强,rustenum 与模式匹配完败 kotlin,如果是 rust, 那这数据结构就可以封装为:

enum LogicResult<T> {
    Loading,
    Local(T),
    Network(T),
    Err(i32, Option<String>)
}

当然,kotlin 也可以封装成类似的,那就是使用 sealed class, 不过因为有泛型,所以对 Loading 等又不能完美的表现。


seal class 很多场景也是很好使用的,例如岐黄小筑的思辨流,或者很多产品都有的动态,可能存在各种 type, 所以很多开发会写出下面的数据类:

data class ThinkVO {
    val id: Int,
    val content: String,
    val type: ThinkType,
    val typRelated1: TypeRelated1? = null,
    val typRelated2: TypeRelated2? = null,
    val typRelated2: TypeRelated2? = null,
}

然后在使用时根据 type 去决定使用哪一个 typRelated 字段,久而久之,就形成了一个巨无霸的类结构。


如果用 sealed class 改造成:

sealed class ThinkVO(val think: Think)
data class TypeRelated1ThinkVO(think: Think, typRelated1: TypeRelated1): ThinkVO(think)
data class TypeRelated2ThinkVO(think: Think, typRelated2: TypeRelated1): ThinkVO(think)
// 使用时通过 when 可以避免分支遗漏
when(think){
    is TypeRelated2ThinkVO ->
    is TypeRelated2ThinkVO ->
}

ated2ThinkVO ->}

如此,可以避免一堆的 null 检查,如果数据有异常,也是底层进行过滤与打日志,通过 kotlin 语法检查保证上层使用不会出问题。这也是 Result 包装的应用。

总结


本文主要聊了一下编写数据类的三个方面:

1.合理的命名

2.使用 immutable

3.使用 Result 包装

好的数据结构封装,是写好业务逻辑的最基础的一环,希望对大家有所帮助,感谢你的阅 读。

目录
相关文章
|
2月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
260 4
|
2月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
2月前
|
缓存 前端开发 Android开发
安卓开发中的自定义视图:从零到英雄
【10月更文挑战第42天】 在安卓的世界里,自定义视图是一块画布,让开发者能够绘制出独一无二的界面体验。本文将带你走进自定义视图的大门,通过深入浅出的方式,让你从零基础到能够独立设计并实现复杂的自定义组件。我们将探索自定义视图的核心概念、实现步骤,以及如何优化你的视图以提高性能和兼容性。准备好了吗?让我们开始这段创造性的旅程吧!
43 1
|
2月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
3天前
|
Dart 前端开发 Android开发
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
1月前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
65 19
|
2月前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
1月前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
70 14
|
1月前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
1月前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
在数字时代,掌握安卓应用开发技能是进入IT行业的关键。本文将引导读者从零基础开始,逐步深入安卓开发的世界,通过实际案例和代码示例,展示如何构建自己的第一个安卓应用。我们将探讨基本概念、开发工具设置、用户界面设计、数据处理以及发布应用的全过程。无论你是编程新手还是有一定基础的开发者,这篇文章都将为你提供宝贵的知识和技能,帮助你在安卓开发的道路上迈出坚实的步伐。
40 5

热门文章

最新文章