本文为现代化 Android
开发系列文章第三篇。
完好目录为:
- 现代化 Android 开发:根底架构
- 现代化 Android 开发:数据类
- 现代化 Android 开发:逻辑层(本文)
- 现代化 Android 开发:组件化与模块化的抉择
- 现代化 Android 开发:多 Activity 多 Page 的 UI 架构
- 现代化 Android 开发:Jetpack Compose 最佳实践
- 现代化 Android 开发:功能监控
写事务时一件繁琐的工作,会触及产品、后端等多端联调,并且多是一些 CRUD 的工作,所以也多被认为是没什么技术含量。但真的去写时,又是 bug 满天飞。所以写 CRUD 没啥难度,可是写好 CRUD 就没那么简单了。假如连个 CRUD 都写不好,那还能谈什么写组件?谈什么写框架?
同是写 CRUD,前后端的侧重点就彻底不一样。后端整个逻辑链路简略些,可是检测高并发、检测大数据量。前端的并发度不高,可是整个链路更杂乱,触及的场景更杂乱。但不管怎样,假如链路走通了,不同事务的代码其实都是迥然不同。所以,能否对自家 App 的事务逻辑进行抽象,也是对大家事务才能的检测。
咱们需求考虑哪些?
咱们不评论只要本地数据的状况,这个没有多少评论价值。
首要咱们要考虑的网络数据获取与本地存储:
- 是否需求本地存储?本地存储有许多优点,例如可以无网络状况下也能运用。
- 网络数据是全量同步仍是增量同步?假如对错推荐类和非实时性的数据,每次都向后端恳求全量数据,那是糟蹋用户流量,并且增加了后端处理的数据量,所以用增量同步是比较好的方式,可是客户端与服务端的逻辑处理就会变得更为杂乱。
而 App
本身事务逻辑就更为杂乱:
- 数据源有网络,有本地数据库,咱们的加载数据的逻辑是怎样的?
-
UI
该怎样感知加载状况? - 反常该怎样处理?怎样处理从头加载?
- 假如是列表数据,或许存在下拉改写和加载更多,该怎样封装?
除此之外,还有许多边际场景,例如:
- 频频进出某个界面,怎样做恳求复用?
- 下拉改写与加载更多怎样阻止频频触发数据恳求?
咱们每写一个事务逻辑,都需求考虑这些问题,假如写一点考虑一点,发现一点问题再解决一点问题,那就会特别苦楚,假如写之前就考虑了一切场景,那代码写起来或许就行云流水。而假如从框架层面加以封装,那就再完美不过了。
单一数据源
运用单一数据源,应该是最佳实践的常识了,其主要的点便是 UI
层数据的来源应该只要一个,假如是只要网络恳求或只要本地数据,那好办,而假如数据来源既有网络也有本地数据库,那咱们 UI
层数据应该只来源于本地数据库。所以简略流程如下图:
数据驱动
现在应该基本上都是数据驱动的方式去更新 UI
了吧。Room
可以直接让返回一个 Flow
或者 LiveData
的数据结构,便是为了方便咱们监听数据库的改变。可是用它的问题是有状况信息传递到 UI
,所以往往还需求别的搞一个状况的 StateFlow
,写起来并不爽,所以我也现在也不用它的这一套,(LiveData
我也不用,毕竟是 Java
时代的产物,对可空处理非常不友好)。
所以我仍是封装自己的完成:
fun <T> logic(
scope: CoroutineScope,
dbAction: suspend () -> T?,
syncAction: suspend () -> RespResult<SyncRet>
) = flow {
// LogicResult 在前文已经提及过
// 首要发送 loading 状况
emit(logicResultLoading())
// 记录下数据成果,网络反常或者网络数据没改变,可以复用数据
var ret: T? = null
// 然后异步敞开一个协程去查询本地数据
// 由于本地数据一般比网络数据快,所以先查询一次,交给 UI 烘托,可以减少用户等候
val local = scope.async {
dbAction()
}
// 敞开另一个协程,查询网络
val sync = scope.async {
syncAction()
}
try {
// 等候本地数据成果
ret = local.await()
// 发送本地数据成果,status 声明为 Local
emit(LogicResult(LogicStatus.Local, ret))
} catch (e: Throwable) {
// 发送反常
emit(LogicResult(status, ret, LOCAL_CODE_ERROR_CATCH, e.message))
}
try {
// 等候网络数据成果
val syncRet = sync.await()
if (syncRet.isOk()) {
// 同步数据成功,那就从头从 DB 读取一次数据, 状况更新为网络
// 其实假如数据没有改变,可以复用前一次的数据成果
emit(LogicResult(LogicStatus.Network, dbAction()))
} else {
// 发送服务端错误
emit(LogicResult(LogicStatus.Network, ret, syncRet.code, syncRet.msg))
}
} catch (e: Throwable) {
// 发送反常
emit(LogicResult(LogicStatus.Network, ret, LOCAL_CODE_ERROR_CATCH, e.message))
}
}.flowOn(Dispatchers.IO)
上述代码,咱们选用 Flow
去构建数据流,正常流程,UI
端就可以收到 loading
、local
、network
状况与数据。假如有反常,也可以经过 status
判断反常来自于哪个环节。经过协程的 async
与 await
,可以让整个流程看上去是串行的。`
当然,咱们实际运用,会有更多的场景,例如:
- 下拉改写时,或者静默改写时,咱们不需求
loading
状况,也不需求先读一次本地数据。 - 假如本地的操作更新了数据库,咱们需求改写数据,那咱们也不需求再次同步网络数据。由于或许需求确认是否是本次恳求的最终态,一切在状况恳求我添加了
LocalButFinal
态,告诉UI
层不会有网络数据了
具体做法便是添一个 mode
参数来控制具体要履行哪些操作:
// 不要加载态
const val LOGIC_FLAG_NOT_LOADING = 1
// 不先读取一次本地数据
const val LOGIC_FLAG_NOT_LOCAL = 1 shl 1
// 不读取网络
const val LOGIC_FLAG_NOT_SYNC = 1 shl 2
// 快捷函数生成 mode
fun logicMode(needLoading: Boolean, needLocal: Boolean, needSync: Boolean): Int {
var logic = 0
if (!needLoading) {
logic = logic or LOGIC_FLAG_NOT_LOADING
}
if (!needLocal) {
logic = logic or LOGIC_FLAG_NOT_LOCAL
}
if (!needSync) {
logic = logic or LOGIC_FLAG_NOT_SYNC
}
return logic
}
fun Int.logicNeedLoading() = (this and LOGIC_FLAG_NOT_LOADING) == 0
fun Int.logicNeedLocal() = (this and LOGIC_FLAG_NOT_LOCAL) == 0
fun Int.logicNeedSync() = (this and LOGIC_FLAG_NOT_SYNC) == 0
经过用 bit
位去检查需求哪些操作,事务运用起来就很便利了。
恳求复用
恳求复用主要是网络层面的,由于是同步到数据库中,大多数状况也不需求去撤销这个恳求,因而运用 emo
的 ConcurrencyShare
就足以解决这个,咱们回到榜首篇文章的比如:
fun logicBookInfo(bookId: Int, mode: Int = 0) = logic(
scope = authSession.coroutineScope, // 运用用户 session 协程 scope,由于有恳求复用,所以退出界面,再进入,会复用之前的网络恳求
mode = mode,
dbAction = {
db.bookDao().bookInfo(bookId)
},
syncAction = {
// 假如已有恳求,那么就等候前一个恳求就好了
concurrencyShare.joinPreviousOrRun("syncBookInfo-$bookId") {
bookApi.bookInfo(bookId).syncThen { _, data ->
db.runInTransaction {
db.userDao().insert(data.author)
db.bookDao().insert(data.info)
}
SyncRet.Full
}
}
}
)
经过 ConcurrencyShare
, 咱们也避免了同一个恳求的并发问题,例如多次发送同一个恳求,由于是增量更新,假如后一个恳求比前一个恳求先回来,然后存了 DB, 那或许数据就错乱了。所以假如同一时间一定该办法的恳求只要一个,那不仅节省了流量,也避免了许多并发导致的数据错乱问题。
列表加载更多
一般的 CRUD 事务逻辑,前面的封装基本就够了,可是对于列表而言,往往要分页加载,Jetpac Compose
提供了 Paging
的组件,可是也就要求数据库要返回 Flow
之类的了,并且易用性也不是很强。大多数场景也没有杂乱到要运用它的状况,所以它的运用也不是很普及。
但咱们许多开发习气的加载更多,很习气的写法便是列表运用 MutableList
, 然后加载更多后往里面 append
数据,咱们前一章有提过 Mutable
数据类型很简单出翔,那这也是一个典型的场景,特别是有时分你想要从头改写列表的时分,或许会呈现下面的履行顺序:
- 触发加载更多
- 触发列表改写
- 列表改写数据先回来了,清空
MutableList
,添补新的数据 - 旧的加载更多的数据回来,
append
进MutableList
,整个列表的数据便是乱序的了,甚至有或许呈现重复数据。
假如清醒一点的同学,还可以在改写列表时撤销下正在履行的加载更多,更多人或许很难发现这个问题,并且由于偶现,想修正也无从下手。
所以列表加载更多虽然和上文的逻辑层关联不大,但我也在这儿稍微提一下,写事务要谨防这种异步问题,写组件更要关注这种异步问题。
正确的做法便是封装成 Immutable
,做法和 PersistentList
类似, 每次加载更多、改写都是发生新的 ListWithLoadMore
data class ListWithLoadMore<T>(
val list: PersistentList<T>,
val hasMore: Boolean,
private val doLoadMore: suspend (current: ListWithLoadMore<T>, count: Int) -> List<T>
) : EmptyChecker {
suspend fun loadMore(count: Int): ListWithLoadMore<T> {
if (list.isEmpty() || !hasMore) {
return this
}
v val more = withContext(Dispatchers.IO) {
doLoadMore(this@ListWithLoadMore, count)
}
return if (more.size < count) {
if (more.isEmpty()) {
ListWithLoadMore(list, false, doLoadMore)
} else {
ListWithLoadMore((list + more).toPersistentList(), false, doLoadMore)
}
} else {
ListWithLoadMore((list + more).toPersistentList(), true, doLoadMore)
}
}
fun prepend(item: T): ListWithLoadMore<T> {
return ListWithLoadMore(list.add(0, item), hasMore, doLoadMore)
}
fun replace(origin: T, item: T): ListWithLoadMore<T> {
val index = list.indexOf(origin)
if (index < 0) {
return this
}
return ListWithLoadMore(list.set(index, item), hasMore, doLoadMore)
}
fun update(index: Int, item: T): ListWithLoadMore<T> {
return ListWithLoadMore(list.set(index, item), hasMore, doLoadMore)
}
fun del(index: Int): ListWithLoadMore<T> {
return ListWithLoadMore(list.removeAt(index), hasMore, doLoadMore)
}
override fun isEmpty(): Boolean {
return list.isEmpty()
}
}
在 Compose
的运用也很简略:
@Composable
function BookListPage(data: ListWithLoadMore<Book>){ //这儿的数据有界面改写获得
// 传入的参数作为 key,假如外层数据,那也直接更新 usedData,
// 前一次的加载更多由于 LaunchedEffect 参数改变而自动撤销
val usedData by remember(data) {
mutableStateOf(data)
}
LazyColumn {
items(usedData.list){
//...
}
item {
// LoadMore 烘托就触发加载更多
LaunchedEffect(usedData){
// 当然实际状况要处理加载出错的状况
usedData = usedData.loadMore()
}
LoadMoreItemUI()
}
}
}
总结
写逻辑和写 UI 都是一堆屁事,细节多,但写好逻辑也不是那么一件简单的事,仍是要多考虑多总结。这也是锻炼自己了解运用各种数据结构的时机。假如十年开发,仍是用榜首年的写法去写事务逻辑,那走底层、写框架有何意义?
所以,今日提到的各个小点,你平时有考虑到多少呢?
我是古哥E下,前微信读书客户端程序猿 / 自学 5 年中医,现为岐黄小筑 App 的负责人。
关注我可得:ChatGPT
开发玩法 | 程序员学习经验 | 组件库新变化 | 中医健康调度。