Android
开发经过 10 多年的发展,技术在不断更迭,软件复杂度也在不断提升。到目前为止,虽然核心需求越来越少,但是对开发速度的要求越来越高。高可用、流畅的 UI、完善的监控体系等都是现在的必备要求了。国内卷的方向又还包括了跨平台、动态化、模块化。
目前的整体感觉就是,移动开发基本是奄奄一息了。不过也不用过于悲观:一是依旧有很多存量的 App 堪称屎山,是需要有维护人员的,就跟现在很多人去卷 framework
层一样,千万行代码中找 bug
。 二是 AI
日益成熟,那么应用层的创新也会出现,在没有更简洁的设备出现前,手机还是主要载体,总归是需要移动开发去接入的,如果硬件层越来越好,模型直接跑在手机上也不是不可能,所以对跨平台技术也会是新一层的考验,有可能直接去跨平台化了。毕竟去中台化也成了历史的选择。
因而,在这个存量市场,虽然竞争压力很大,但是如果技术过硬,还是能寻求一席之地的。因而我决定用几篇文章来介绍下,当前我认为的现代化 Android
开发是怎样的。其目录为:
- 现代化 Android 开发:基础架构(本文)
- 现代化 Android 开发:数据类
- 现代化 Android 开发:逻辑层
- 现代化 Android 开发:组件化与模块化的抉择
- 现代化 Android 开发:多 Activity 多 Page 的 UI 架构
- 现代化 Android 开发:Jetpack Compose 最佳实践
- 现代化 Android 开发:性能监控
Scope
提到 Android
基础架构,大家可能首先想到的是 MVC
、MVP
、MVVM
、MVI
等分层架构。但针对现代化的 Android
开发,我们首要有的是 scope
的概念。其可以分两个方面:
- 结构化并发之
CoroutineScope
:目前协程基本已经是最推荐的并发工具了,CoroutineScope
的就是对并发任务的管理,例如viewModelScope
启动的任务的生命周期就小于viewModel
的存活周期。 - 依赖注入之
KoinScope
:虽然官方推荐的是hilt
,但其实它并没有koin
好用与简洁,所以我还是推荐koin
。KoinScope
是对实例对象的管理,如果scope
结束, 那 - 么
scope
管理的所有实例都被销毁。
一般应用总会有登录,所以大体的 scope
管理流程图是这样的:
- 1.我们启动
app
, 创建AppScope
,对于koin
而言就是用于存放单例,对于协程来说就是全局任务 - 2.当我们登录后,创建
AuthSessionScope
, 对于koin
而言,就是存放用户相关的单例,对于协程而言就是用户执行相关的任务。当退出登录时,销毁当前的AuthSessionScope
,那么其对应的对象实例、任务全部都会被销毁。用户再次登录,就再次重新创建AuthSessionScope
。目前很多App
对于用户域内的实例,基本上还是用单例来实现,退出登录时,没得办法,就只能杀死整个进程再重启, 所以会有黑屏现象,实现不算优雅。而用scope
管理后,就是一件很自然而实现的事情了。所以尽量用依赖注入,而不要用单例模式 - 3.当我们进入界面后,一般都是从逻辑层获取数据进行渲染,所以依赖注入没多大用了。而协程的
lifecycleScope
、viewModelScope
就比较有用,管理界面相关的异步任务。
所以我们在做架构、做某些业务时,首要考虑 scope
的问题。我们可以把 CoroutineScope
也作为实例存放到 KoinScope
里,也可以把 KoinScope
作为 Context
存放到 CorutineScope
里。
岐黄小筑是将 CoroutineScope
放到 koin
里去以便依赖查找
val sessionCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + coroutineLogExceptionHandler(TAG)) val sessionKoinScope = GlobalContext.get().createScope(...) sessionKoinScope.declare(sessionCoroutineScope)
其实我们也完全可以用
CoroutineScope
的Context
来做实例管理,而移除koin
的使用。但是Context
的使用并没有那么便捷,或许以后它可以进化为完全取代koin
。
架构分层
随着软件复杂度的提升,MVC
、MVP
、MVVM
、MVI
等先后被提出,但我觉得目前所有的开发,都大体遵循某一模式而又不完全遵循,很容易因为业务的节奏,很容易打破,变成怎么方便怎么来。所以使用简单的分层 + 足够优秀的组件化,才是保证开发模式不被打破的最佳实践。下图是岐黄小筑的整体架构图:
整体架构不算复杂,其实重点是在于组件库,emo
已经有 20 个子库了,然后岐黄小筑有一些对于通用逻辑的抽象与封装,使得逻辑层虽然都集中在 logic
层,但整体都是写模板式的代码,可以面向 copy-paste
编程。
以 BookLogic
为例:
// 通过依赖注入传参, 拿到 db 层、网络层、以及用户态信息的应用 class BookLogic( val authSession: AuthSession, val kv: EmoKV, val db: AccountDataBase, private val bookApi: BookApi ) { // 并发请求复用管理 private val concurrencyShare = ConcurrencyShare(successResultKeepTime = 10 * 1000L) // 加载书籍信息,使用封装好的通用请求组件 fun logicBookInfo(bookId: Int, mode: Int = 0) = logic( scope = authSession.coroutineScope, // 使用用户 session 协程 scope,因为有请求复用,所以退出界面,再进入,会复用之前的网络请求 mode = mode, dbAction = { // 从 db 读取本地数据 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 } } } ) // 类似的模板代码 suspend fun logicBookClassicContent(bookId: Int, mode: Int = 0) = logic(...) suspend fun logicBookExpoundContent(bookId: Int, mode: Int = 0) = logic(...) ... } //将其注册到 `module` 中去,目前好像也可以通过注解的方式来做,不过我还没采用那种方式: scopedOf(::BookLogic)
ViewModel
层浮层从 Logic
层读取数据,并可以进行特殊化处理:
class BookInfoViewModel(navBackStackEntry: NavBackStackEntry) : ViewModel() { val bookId = navBackStackEntry.arguments?.getInt(SchemeConst.ARG_BOOK_ID) ?: throw RuntimeException("book_id is required!.") val bookInfoFlow = MutableStateFlow(logicResultLoading<BookInfoPojo>()) init { viewModelScope.launch { runInBookLogic { logicBookInfo(bookId, mode).collectLatest { bookInfoFlow.emit(it) } } } } }
Compose
界面再使用 ViewModel
:
@ComposeScheme( action = SchemeConst.ACTION_BOOK_INFO, alternativeHosts = [BookActivity::class] ) @SchemeIntArg(name = SchemeConst.ARG_BOOK_ID) @Composable fun BookInfoPage(navBackStackEntry: NavBackStackEntry) { LogicPage(navBackStackEntry = navBackStackEntry) { val infoVm = schemeActivityViewModel<BookInfoViewModel>(navBackStackEntry) val detailVm = schemeViewModel<BookDetailViewModel>(navBackStackEntry) val bookInfo by infoVm.bookInfoFlow.collectAsStateWithLifecycle() //... } }
这样整个数据流从网络加载、到存储到数据库、到传递给 UI
进行渲染的整个流程就结束了。
对于其中更多的细节,例如逻辑层具体是怎么封装的?UI
层具体是怎么使用多 Activity
多 Page
?可以期待下之后的文章。