2023年Android现代开发
Posted Calvin880828
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2023年Android现代开发相关的知识,希望对你有一定的参考价值。
2023年现代android开发
下面与大家分享如何构建具有2023年最新趋势的Android应用程序。
Android是什么?
Android 是一种基于 Linux 内核并由 Google 开发的开源操作系统。它用于各种设备,包括智能手机、平板电脑、电视和智能手表。
目前,Android 是世界上移动设备使用最多的操作系统;根据statcounter的一份最近 12 个月的样本报告,Android 的市场份额为71.96%。
接下来,我将提及我认为对于在 Android 上构建现代应用程序很重要的工具、库、体系结构、指南和其他实用程序的列表。
Kotlin
Kotlin 是由JetBrains开发的一种编程语言。由谷歌推荐,谷歌于 2017 年 5 月正式宣布Kotlin作为Android开发语言。它是一种与 Java 兼容并可以运行在 JVM 上的现代编程语言,这使得它在 Android 应用程序开发中的采用速度非常快。
不管你是不是Android新手,你都应该把Kotlin作为你的首选,不要逆水行舟🏊🏻😎,Google在Google I/O 2019上宣布了这种做法。有了Kotlin,你将可以使用所有现代语言的特性,包括协程的强大功能和使用为 Android 生态系统开发的现代库。
Kotlin官网
https://kotlinlang.org/docs/home.html
Jetpack Compose
Jetpack Compose 是 Android 推荐的用于构建原生 UI 的现代工具包。它简化并加速了 Android 上的 UI 开发。
Jetpack Compose 文档
https://developer.android.com/jetpack/compose
Jetpack Compose 是 Android Jetpack 库的一部分,使用 Kotlin 编程语言轻松创建原生用户界面。此外,它还与其他 Android Jetpack 库(例如 LiveData 和 ViewModel)集成,以便更轻松地构建响应式和可维护的 Android 应用程序。
Jetpack Compose 的一些主要功能包括:
- 声明式用户界面。
- 可定制的小部件。
- 易于与现有代码集成。
- 实时预览。
- 改进的性能。
资源
-
官方文档
https://developer.android.com/jetpack/compose -
Jetpack Compose 路线图
https://developer.android.com/jetpack/androidx/compose-roadmap -
课程
https://developer.android.com/courses/jetpack-compose/course -
Compose 到 Kotlin 兼容性映射
https://developer.android.com/jetpack/androidx/releases/compose-kotlin
Android Jetpack
Jetpack 是一套库,可帮助开发人员遵循最佳实践、减少样板代码并编写可跨 Android 版本和设备一致工作的代码,以便开发人员可以专注于他们关心的代码。
Android Jetpack 文档
https://developer.android.com/jetpack
它的一些最常用的工具是:
-
ViewModel
https://developer.android.com/topic/libraries/architecture/viewmodel -
Room
https://developer.android.com/training/data-storage/room -
DataStore
https://developer.android.com/topic/libraries/architecture/datastore -
WorkManager
https://developer.android.com/topic/libraries/architecture/workmanager -
Navigation
https://developer.android.com/guide/navigation -
CameraX
https://developer.android.com/training/camerax -
Compose
https://devjorgecastro.medium.com/modern-android-app-development-in-2023-ff445d3652b4#af4e
Material Design
Material Design 是一个适应性强的指南、组件和工具系统,支持用户界面设计的最佳实践。在开源代码的支持下,Material Design 简化了设计人员和开发人员之间的协作,并帮助团队快速构建精美的产品。
官网
https://m3.material.io/
Material Design 得到了 Google 的设计师和开发人员的支持,这将使我们有一个指南来处理 Android、Flutter 和 Web 的 UI/UX。
目前,Material Design 的最新版本是 3。
Clean Arch
它基于通过将软件划分为层来实现职责分离。
特征
- 独立于框架。
- 可测试。
- 独立于用户界面。
- 独立于数据库。
- 独立于任何外部机构。
- 依赖规则
作者在他的文章
The Clean Code Blog中很好地描述了依赖规则
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
使该体系结构起作用的最重要规则是依赖规则。这条规则说源代码依赖只能指向内部。内圈中的任何人都无法对外圈中的事物一无所知。特别是,在外圈中声明的名称不得在内圈中的代码中提及。这包括函数、类。变量,或任何其他命名的软件实体。
Clean Arch Blog
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Android 中的Clean Arch
- 表示:活动、片段、视图模型、其他视图组件。
- 域:用例、实体、存储库和其他域组件。
- 数据:存储库实现、映射器、DTO 等。
Presentation Layer的架构模式
架构模式是一种更高级别的策略,旨在帮助设计软件架构,其特点是在可重用框架内为常见架构问题提供解决方案。架构模式类似于设计模式,但它们的规模更大,并且解决更多的全局问题,例如系统的整体结构、组件之间的关系以及数据的管理方式。
在表示层中,我们有一些架构模式,其中我想强调以下几点:
- MVVM
- MVI
Android 架构指南
https://developer.android.com/topic/architecture
依赖注入
依赖注入是一种软件设计模式,它允许客户端从外部源获取其依赖项,而不是自己创建它们。它是一种用于在对象及其依赖项之间实现控制反转 (IoC) 的技术。
- Hilt ❤️
https://developer.android.com/training/dependency-injection/hilt-android - Dagger
https://dagger.dev/ - Koin
https://insert-koin.io/
模块化
模块化是一种软件设计技术,它允许您将应用程序划分为独立的模块,每个模块都有自己的功能和责任。
模块化的好处
可重用性:通过拥有独立的模块,它们可以在应用程序的不同部分甚至其他应用程序中重用。
严格的可见性控制:模块使您能够轻松控制向代码库的其他部分公开的内容。
可定制的交付:Play Feature Delivery使用应用程序包的高级功能,允许您有条件地或按需交付应用程序的某些功能。
可扩展性:通过拥有独立的模块,可以在不影响应用程序其他部分的情况下添加或删除功能。
易于维护:通过将应用程序划分为独立的模块,每个模块都有自己的功能和职责,代码更容易理解和维护。
易于测试:通过拥有独立的模块,它们可以被隔离测试,这使得检测和修复错误变得容易。
架构改进:模块化有助于改进应用程序的架构,从而更好地组织和构建代码。
改善协作:通过拥有独立的模块,开发人员可以同时处理应用程序的不同部分,而不会受到干扰。
构建时间:一些 Gradle 功能,例如增量构建、构建缓存或并行构建,可以利用模块化来提高构建性能。
更多信息参考官方文档
https://developer.android.com/topic/modularization
网络
- OkHttp
https://square.github.io/okhttp/ - Retrofit
https://square.github.io/retrofit/
序列化
在本节中,我想提一下我认为的两个重要工具:Moshi与 Retrofit 和Kotlin Serialization一起广泛使用,这是 Jetbrain 的 Kotlin 团队的赌注。
-
Moshi
https://github.com/square/moshi -
Kotlin Serialization
https://github.com/Kotlin/kotlinx.serialization
Moshi和Kotlin Serialization是 Kotlin 和 Java 的两个序列化/反序列化库,允许您将对象转换为 JSON 或其他序列化格式,反之亦然。两者都提供了针对移动和桌面应用程序优化的用户友好界面。Moshi 主要关注 JSON 序列化,而 Kotlin 序列化支持各种序列化格式,包括 JSON。
图片加载
要从 Internet 加载图像,可以使用多个第三方库来帮助您处理该过程。图像加载库为您做了很多繁重的工作;它们处理缓存(因此您不会多次下载图像)和网络逻辑以下载图像并将其显示在屏幕上。
-
Android官方文档
https://developer.android.com/jetpack/compose/graphics/images/loading -
Coil
https://github.com/coil-kt/coil#jetpack-compose -
Glide
https://bumptech.github.io/glide/int/compose.html
react及线程管理
当我们谈论反应式编程和异步过程时,我们的第一个选择是Kotlin Coroutines;多亏了Suspension Functions,Flow我们可以满足所有这些需求。但是,我相信在本节中值得强调RxJava甚至在 Android 应用程序开发中的重要性。对于我们这些已经使用 Android 多年的人来说,我们知道 RxJava 是一个非常强大的工具,具有大量用于处理数据流的功能。我仍然认为 RxJava 是今天值得考虑的一个有趣的替代方案。
- Kotlin Coroutines(suspend 及Flow API)
https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md#using-in-your-projects - RxJava
https://github.com/ReactiveX/RxJava
本地存储
LocalStore
构建移动应用程序的一个重点是能够在本地持久保存数据,例如一些会话数据或缓存数据等。根据您的应用需求选择正确的存储选项非常重要。我们可以存储键值对等非结构化数据或数据库等结构化数据。请记住,这一点并未提及我们可用的所有类型的本地存储(例如文件存储),仅提及允许我们保存数据的工具。
- DataStore
https://developer.android.com/topic/libraries/architecture/datastore - EncryptedSharedPreferences
https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences
测试
Testing
- JUnit 5
https://junit.org/junit5/ - Mockk
https://mockk.io/ANDROID.html - Espresso
https://developer.android.com/training/testing/espresso - Robolectric
https://robolectric.org/
R8 optimizations
R8 是默认编译器,可将项目的 Java 字节码转换为可在 Android 平台上运行的 DEX 格式。它是一种工具,通过缩短类及其属性的名称,消除项目中未使用的代码和资源,帮助我们混淆和减少应用程序的代码。要了解更多信息,请查看有关收缩、混淆和优化您的应用程序的Android 文档。
https://developer.android.com/studio/build/shrink-code
- Code shrinking
- Resource shrinking
- Obfuscation
- Optimization
Play Feature Delivery
Google Play 的应用服务模型称为 Dynamic Delivery,它使用 Android App Bundle 为每个用户的设备配置生成和提供优化的 APK,因此用户仅下载运行您的应用所需的代码和资源。
Android文档
https://developer.android.com/guide/playcore/feature-delivery
自适应布局
Adaptive layouts
随着具有不同形状因素的移动设备使用的增长,我们需要有工具使我们能够使用适应不同类型屏幕的 Android 应用程序。这就是 Android 为我们提供Window Size Classes的原因,简单来说,它是三大屏幕格式组,它们标记了我们开发设计的关键点。有了这个,我们避免了考虑许多屏幕设计的复杂性,将我们的可能性减少到 3 组,即:Compat、Medium和Expanded 。
Windows 尺寸等级
支持不同的屏幕尺寸
https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes
我们拥有的另一个重要资源是Canonical Layouts,它们是预定义的屏幕设计,可用于我们 Android 应用程序中的大多数场景,并向我们展示了如何使其适应大屏幕的指南。
Canonical Layouts
https://m3.material.io/foundations/adaptive-design/canonical-layouts
其他一些资源
3 things to know about Form Factors at Google I/O 2022
https://android-developers.googleblog.com/2022/05/form-factors-google-io-22.html
Playlist: Form Factors at Google I/O
https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_seN7mrwgU7mDYKA0hq_ib
Form-Factor Training
https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-5
Form Factors at Google I/O 2022 (2)
https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_seN7mrwgU7mDYKA0hq_ib
性能表现
当我们为 Android 开发应用程序时,我们必须确保更好的用户体验,不仅在应用程序开始时,而且在整个执行过程中。出于这个原因,拥有使我们能够对可能影响应用程序性能的情况进行预防性分析和持续监控的工具非常重要,因此这里列出了可以帮助您实现此目的的工具:
- Benchmark
https://developer.android.com/topic/performance/benchmarking/benchmarking-overview - Baseline Profiles
https://developer.android.com/topic/performance/baselineprofiles/overview - App Startup
https://developer.android.com/topic/libraries/app-startup - Firebase Performance Monitoring
https://firebase.google.com/docs/perf-mon - JankStats library
https://developer.android.com/topic/performance/jankstats
应用内更新
当您的用户让您的应用程序在他们的设备上保持最新时,他们可以尝试新功能,并从性能改进和错误修复中受益。尽管某些用户在其设备连接到不按流量计费的连接时启用后台更新,但可能需要提醒其他用户安装更新。应用内更新是一项 Google Play 核心库功能,可提示活跃用户更新您的应用。
运行 Android 5.0(API 级别 21)或更高版本的设备支持应用内更新功能。此外,仅 Android 移动设备、Android 平板电脑和 Chrome 操作系统设备支持应用内更新。
_应用内更新文档
https://developer.android.com/guide/playcore/in-app-updates
应用内评论
Google Play 应用内评论 API 可让您提示用户提交 Play 商店评分和评论,而无需离开您的应用或游戏。
通常,应用内评论流程可以在应用的整个用户旅程中随时触发。在此流程中,用户可以使用 1 到 5 星系统对您的应用进行评分并添加可选评论。提交后,评论将发送到 Play 商店并最终显示。
为保护用户隐私并避免 API 滥用,您的应用应遵循有关何时请求应用内评论和评论提示设计的严格指南。
应用内评论文档
https://developer.android.com/guide/playcore/in-app-review
辅助功能
辅助功能是软件设计和构造中的一个重要功能,除了改善用户体验外,它还为需要辅助功能的人提供使用应用程序的能力。该概念旨在改善的一些残障人士包括:有视力问题、色盲、听力问题、灵活性问题和认知障碍等的人。
注意事项:
- 增加文本可见性(颜色对比度、可调整大小的文本)
- 使用大而简单的控件
- 描述每个 UI 元素
辅助功能官方文档
https://developer.android.com/guide/topics/ui/accessibility
安全
安全性即使不是最重要的方面,也是我们在开发保护设备完整性、数据安全性和用户信任的应用程序时必须考虑的因素,这就是为什么我在下面列出一系列帮助您实现此目的的提示。
- 加密敏感数据和文件:使用
EncryptedSharedPreferences
和EncryptedFile
。 - 应用基于签名的权限:
- 在您可以控制的应用程序之间共享数据时,使用基于签名的权限。
< manifest xmlns:android = "http://schemas.android.com/apk/res/android"
package = "com.example.myapp" >
< permission android:name = "my_custom_permission_name"
android:protectionLevel = "signature" / >
- 不要将应用程序配置所需的密钥、令牌或敏感数据直接放在项目存储库中的文件或类中。请改用
local.properties
。
Version Catalogs
Gradle 提供了一种集中管理项目依赖项的标准方式,称为版本目录;7.0版本试验引入,7.4版本正式发布。
优势:
- 对于每个目录,Gradle 都会生成类型安全的访问器,以便您可以在 IDE 中轻松添加具有自动完成功能的依赖项。
- 每个目录对构建的所有项目都是可见的。它是声明依赖项版本并确保对该版本的更改适用于每个子项目的中心位置。
- 目录可以声明依赖包,它们是通常一起使用的“依赖组”。
- 目录可以将依赖项的组和名称与其实际版本分开,并使用版本引用代替,从而可以在多个依赖项之间共享版本声明。
Catalogs更多信息
https://docs.gradle.org/current/userguide/platforms.html
Logger
logger是一种软件工具,用于记录有关程序执行的信息;重要事件、错误、调试消息和其他可能有助于诊断问题或了解程序工作方式的信息。记录器可以配置为将消息写入不同的位置,例如日志文件、控制台、数据库,或者通过将消息发送到日志服务器。
Timber
https://github.com/JakeWharton/timber
Linter
Linter 是一种编程工具,用于分析程序源代码以发现代码中潜在的问题或错误。这些问题可能是句法、不合适的代码风格、缺乏文档、安全问题等等,它们会对代码的质量和可维护性产生影响。
- Android Lint
https://medium.com/swlh/what-is-android-lint-17fa0d87abb2 - Detekt
https://detekt.dev/ - Ktlint
https://pinterest.github.io/ktlint/
一文看懂现代 Android 开发最佳实践
What is MAD?
MAD 的全称是 Modern Android Development , 它是一系列技术栈和工具链的集合,涵盖了从编程语言到开发框架等各个环节。
Android 自 08 年诞生之后的多年间 SDK 变化一直不大,开发方式较为固定。13 年起技术更新逐渐加速,特别是 17年之后, 随着 Kotlin 及 Jetpack 等新技术的出现 Android 开发方式发生了很大变化,去年推出的 Jetpack Compose 更是将这种变化推向了新阶段。Goolge 将这些新技术下的开发方式命名为 MAD ,以此区别于旧有的低效的开发方式。
https://developer.android.com/series/mad-skills
MAD 可以指导开发者更高效地开发出优秀的移动应用,它的优势这主要体现在以下几点:
- 稳定可靠:相对于众多三方库,Google 的类库会长期维护,值得信赖
- 接入友好:提供大量示例代码和参考文档,可以帮助你快速上手
- 灵活适配:框架种类丰富多样,适用于不同阶段不同规模的项目
- 体验一致:不同设备不同版本系统下具备一致的开发体验
MAD 助力应用出海
在 MAD 的指导下项目的代码架构也更加合理、更具可维护性。下图是项目中 MAD 的整体应用情况
接下来,本文将分享一些我们在对 MAD 实践过程中的心得和案例
1. Kotlin
Kotlin 是 Andorid 认可的首选开发语言,我们的项目中,所有代码都使用 Kotlin 开发。Kotlin 的语法十分简洁,相对于 Java 同等功能的代码规模可以减少 25%。此外 Kotlin 还具有很多 Java 所不具备的优秀特性:
1.1 Safety
Kotlin 在安全性方面有很多优秀的设计,比如空安全以及数据的不可变性。
Null Safety
Kotlin 的空安全特性让很多运行时 NPE 提前到编译期暴露和发现,有效降低线上崩溃的发生。我们在代码中重视对 Nullable 类型的判断和处理,我们在数据结构定义时都力求避免出现可空类型,最大限度降低判空成本;
interface ISelectedStateController<DATA>
fun getStateOrNull(data: DATA): SelectedState?
fun selectAndGetState(data: DATA): SelectedState
fun cancelAndGetState(data: DATA): SelectedState
fun clearSelectState()
// 使用 Elvis 提前处理 Nullable
fun <DATA> ISelectedStateController<DATA>.getSelectState(data: DATA): SelectedState
return getStateOrNull(data) ?: SelectedState.NON_SELECTED
Java 时代我们只能通过 getStateOrNull
这类的命名规范来提醒返回值的可空,Kotlin 通过 ?
让我们可以更好地感知 Nullable 的风险;我们还可以使用 Elvis 操作符 ?:
将 Nullable
转成 NonNull
便于后续使用;Kotlin 的 !!
让我们更容易发现 NPE 的潜在风险并可以诉诸静态检查给予警告。
Kotlin 的默认参数值特性也有助于防止 NPE 的出现。像下面这样的结构体定义,在反序列化等场景中不必担心 Null
的出现。
data class BannerResponse(
@SerializedName("data") val data: BannerData = BannerData(),
@SerializedName("message") val message: String = "",
@SerializedName("status_code") val statusCode: Int = 0
)
我们在全面拥抱 Kotlin 之后,NPE 方面的崩溃率只有 0.3‰,而通常 Java 项目的 NPE 会超过 1‰
Immutable
Kotlin 的安全性还体现在数据不会被随意修改。我们在代码中大量使用 data class 并且要求属性使用 val
而非 var
定义,这有利于单向数据流范式在项目中的推广,在架构层面实现数据的读写分离。
data class HomeUiState(
val bannerList: Result<BannerItemModel> = Result.Success(emptyList()),
val contentList: Result<ContentViewModel> = Result.Success(emptyList()),
)
sealed class Result<T>
data class Success<T>(val list: List<T> = emptyList()) : Result<T>()
data class Error<T>(val message: String) : Result<T>()
如上,我们使用 data class 定义 UiState
用在 ViewModel
中。 val
声明属性保证了 State 的不可变性。使用密封类定义 Result
有利于对各种请求结果进行枚举,简化逻辑。
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
_uiState.value =
_uiState.value.copy(bannerList = Result.Success(it))
需要更新 State 时,借助 data class 的 copy
方法可以快捷地拷贝构造一个新实例。
Immutable 还体现在集合类的类型上。我们在项目中提倡非必要不使用 MutableList
这样的 Mutable 类型,可以减少 ConcurrentModificationException
等多线程问题的发生,同时更重要的是避免了因为 Item 篡改带来的数据一致性问题:
viewModel.uiState.collect
when (it)
Result.Success -> bannerAdapter.updateList(it.list)
else ...
fun updateList(newList: List<BannerItemModel>)
val diffResult = DiffUtil.calculateDiff(BannerDiffCallback(mList, newList), true)
diffResult.dispatchUpdatesTo(this)
比如上面例子中 UI 侧接收到 UiState
更新通知后,提交 DiffUtil
刷新列表。DiffUtil
正常运作的基础正是因为 mList
和 newList
保持了 Immutable
类型。
1.2 Functional
函数在 Kotlin 中是一等公民,可以作为参数或返回值的类型组成高阶函数,高阶函数可以在集合操作符等场景下提供更加易用的 API。
Collection operations
val bannerImageList: List<BannerImageItem> =
bannerModelList.sortedBy
it.bType
.filter
!it.isFrozen()
.map
it.image
上面的代码中我们对 BannerModelList
依次完成排序、过滤,并转换成 BannerImageItem
类型的列表,集合操作符的使用让代码一气呵成。
Scope functions
作用域函数是一系列 inline
的高阶函数。它们可以作为代码的粘合剂,减少临时变量等多余代码的出现。
GalleryFragment().apply
setArguments(arguments ?: Bundle().apply
putInt("layoutId", layoutId())
)
.let fragment ->
supportFragmentManager.beginTransaction()
.apply
if (needAdd) add(R.id.fragment_container, fragment, tag)
else replace(R.id.fragment_container, fragment, tag)
.also
it.setCustomAnimations(R.anim.slide_in, R.anim.slide_out)
.commit()
当我们创建并启动一个 Fragment 时,可以基于作用域函数完成各种初始化工作,就像上面例子那样。这个例子同时也提醒我们过度使用这些作用域函数(或集合操作符),也会影响代码的可读性和可调试性,只有“恰到好处”的使用函数式编程才能真正发挥 Kotlin 的优势。
1.3 Corroutine
Kotlin 协程让开发者摆脱了回调地狱的出现,同时结构化并发的特性也有助于对子任务更好地管理,Android 的各种原生库和三方库在处理异步任务时都开始转向 Kotlin 协程。
Suspend function
在项目中,我们倡导使用挂起函数封装异步逻辑。在数据层 Room 或者 Retorfit 使用挂起函数风格的 API 自不必说,一些表现层逻辑也可以基于挂起函数来实现:
suspend fun doShare(
activity: Activity,
contentBuilder: ShareContent.Builder.() -> Unit
): ShareResult = suspendCancellableCoroutine cont ->
val shareModel = ShareContent.Builder()
.setEventCallBack(object : ShareEventCallback.EmptyShareEventCallBack()
override fun onShareResultEvent(result: ShareResult)
super.onShareResultEvent(result)
if (result.errorCode == 0)
cont.resume(result)
else
cont.cancel()
).apply(contentBuilder)
.build()
ShareSdk.showPanel(createPanelContent(activity, shareModel))
上例的 doShare
用挂起函数处理照片的分享逻辑:弹出分享面板供用户选择分享渠道,并将分享结果返回给调用方。调用方启动分享并同步获取分享成功或失败的结果,代码风格更符合直觉。
Flow
项目中使用 Flow 替代 RxJava 处理流式数据,减少包体积的同时,CoroutineScope
可以有效避免数据泄露:
fun CoroutineScope.getBannerList(): Flow<List<BannerItemModel>> =
DatabaseManager.db.bannerDao::getAll.asFlow()
.onCompletion
this@Repository::getRemoteBannerList.asFlow().onEach
launch
DatabaseManager.db.bannerDao.deleteAll()
DatabaseManager.db.bannerDao.insertAll(*(it.toTypedArray()))
.distinctUntilChanged()
上面的例子用于从多个数据源获取 BannerList
。我们增加了磁盘缓存的策略,先请求本地数据库数据,再请求远程数据。Flow 的使用可以很好地满足这类涉及多数据源请求的场景。而另一面在调用侧,只要提供合适的 CoroutineScope
就不必担心泄露的发生。
1.4 KTX
一些原本基于 Java 实现的 Android 库通过 KTX 提供了针对 Kotlin 的扩展 API,让它们在 Kotlin 工程中更容易地被使用。
我们的项目使用 Jetpack Architecture Components 搭建 App 基础架构,KTX 帮助我们大大降低了 Kotlin 项目中的 API 使用成本,举几个最常见的 KTX 的例子:
fragment-ktx
fragment-ktx 提供了一些针对 Fragment 的 Kotlin 扩展方法,比如 ViewModel 的创建:
class HomeFragment : Fragment()
private val homeViewModel : HomeViewModel by viewModels()
...
相对于 Java 代码在 Fragment 中创建 ViewMoel 变得极其简单,其背后的是现实活用了各种 Kotlin 特性,十分巧妙。
inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = this ,
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, ownerProducer().viewModelStore , factoryProducer)
viewModels
是 Fragment 的 inline
扩展方法,通过 reified
关键字在运行时获取泛型类型用来创建具体 ViewModel 实例:
fun <VM : ViewModel> Fragment.createViewModelLazy(
viewModelClass: KClass<VM>,
storeProducer: () -> ViewModelStore,
factoryProducer: (() -> Factory)? = null
): Lazy<VM>
val factoryPromise = factoryProducer ?:
defaultViewModelProviderFactory
return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
createViewModelLazy
返回了一个 Lazy<VM>
实例,这似的我们可以通过 by
关键字创建 ViewModel
,这里借助 Kotlin 的代理特性实现了实例的延迟创建。
viewmodle-ktx
viewModel-ktx 提供了针对 ViewModel 的扩展方法, 例如 viewModelScope
,可以随着 ViewModel 的销毁及时终止过期的异步任务,让 ViewModel 更安全地作为数据层与表现层之间的桥梁使用。
viewModelScope.launch
//监听数据层的数据
repo.getMessage().collect
//向表现层发送消息
_messageFlow.emit(message)
实现原理也非常简单
val ViewModel.viewModelScope: CoroutineScope
get()
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null)
return scope
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
viewModelScope
本质上是 ViewModle 的扩展属性,通过 custom get
创建 CloseableCoroutineScope
的同时,记录到 JOB_KEY
的位置中
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope
override val coroutineContext: CoroutineContext = context
override fun close()
coroutineContext.cancel()
CloseableCoroutineScope
其实是一个 Closeable
,在 ViewModel 的 onClear
时查找 JOB_KEY
并被调用 close
以取消 SupervisorJob
,终止所有子协程。KTX 活用了 Kotlin 的各种特性和语法糖 ,后面 Jetpack 章节会看到更多 KTX 的使用。
2. Android Jetpack
Android 通过 Jetpack 为开发者提供 AOSP 之上的基础能力支持,其范围覆盖了从 UI 到 Data 各个层级,降低了开发者们自造轮子的需求。近期 Jetpack 组件的架构规范又进行了全面升级,帮助我们在开发过程中能更好地贯彻关注点分离这一设计目标。
2.1 Architecture
Android 倡导表现层和数据层分离的架构设计,并使用单向数据流(Unidirectional Data Flow)完成数据通信。Jetpack 通过一系列 Lifecycle-aware 的组件支持了 UDF 在 Android 中的落地。
UDF 的主要特点和优势如下:
- 唯一真实源(SSOT):UI State 在 ViewModel 集中管理,降低了多数据源之间的同步成本
- 数据自上而下流动:UI 的更新来 VM 的状态变化,UI 自身不持有状态、不耦合业务逻辑
- 事件自下而上传递:UI 发送 event 给 VM 对状态集中修改,状态变化可回溯、利于单测
项目中凡是涉及 UI 的业务场景都是基于 UDF 打造的。以 HomePage
为例,其中包括 BannerList
和 ContentList
两组数据展示,所有的数据集中管理在 UiState
中
class HomeViewModel() : ViewModel()
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
fun fetchHomeData()
fetchJob?.cancel()
fetchJob = viewModelScope.launch
with(repo)
//request BannerList
try
getBannerList().collect
_uiState.value =
_uiState.value.copy(bannerList = Result.Success(it))
catch (ioe: IOException)
// Handle the error and notify the UI when appropriate.
_uiState.value =
_uiState.value.copy(
bannerList = Result.Error(getMessagesFromThrowable(ioe))
)
//request ContentList
try
getContentList().collect
_uiState.value =
_uiState.value.copy(contentList = Result.Success(it))
catch (ioe: IOException)
_uiState.value =
_uiState.value.copy(
contentList = Result.Error(getMessagesFromThrowable(ioe))
)
如上代码所示,HomeViewModel
从 Repo 获取数据并更新 UiState
,View 订阅此状态并刷新 UI。viewModelScope.launch
提供的 CoroutineScope
可以随着 ViewModel 的 onClear
结束运行中的协程,避免泄露。
数据层我们使用 Repository Pattern 封装本地数据源和远程数据源的具体实现:
class Repository
fun CoroutineScope.getBannerList(): Flow<List<BannerItemModel>>
return DatabaseManager.db.bannerDao::getAll.asFlow()
.onCompletion
this@Repository::getRemoteBannerList.asFlow().onEach
launch
DatabaseManager.db.bannerDao.deleteAll()
DatabaseManager.db.bannerDao.insertAll(*(it.toTypedArray()))
.distinctUntilChanged()
private suspend fun getRemoteBannerList(): List<BannerItemModel>
TODO("Not yet implemented")
以 getBannerList
为例,先从数据库请求本地数据加速显示,然后再请求远程数据源更新数据,同时进行持久化,便于下次请求。
UI 层的逻辑很简单,订阅 ViewModel 的数据并刷新 UI 即可
@AndroidEntryPoint
class HomeFragment : Fragment()
@Inject
lateinit var viewModel : HomeViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch
repeatOnLifecycle(Lifecycle.State.STARTED)
viewModel.uiState.collect
// Update UI elements
我们使用 Flow 代替 LiveData 对 UiState 进行封装,lifecycleScope
使得 Flow 变身 Lifecycle-aware 组件;repeatOnLifecycle
让 Flow 像 LiveData 一样在 Fragment 前后台切换时自动停止数据流的发射,节省资源开销。
2.2 Navigation
作为“单 Activity 架构”的实践者,我们选择了使用 Jetpack Navigation 作为 App 的导航组件。Navigation 组件实现了导航设计原则,为跨应用切换或应用内页面间的切换提供了一致的用户体验,并且提供了各种优势,包括:
- 处理 Fragment 事务;
- 默认情况下,正确处理往返操作;
- 为动画和转场提供标准化资源;
- 实现和处理深层链接;
- 包括导航界面模式(例如抽屉式导航栏和底部导航),开发者只需完成极少的额外工作;
- 提供 Gradle 插件用以保证在不同页面传递参数时类型安全;
- 提供了导航图范围的 ViewModel,以在同导航图内的页面进行数据共享;
TODO
Navigation 提供了 XML 以及 Kotlin DSL 两种配置方式。我们在项目中发挥 Kotin 的优势,基于类型安全的 DSL 创建导航图,同时通过函数提取为页面统一指定转场动画:
fun NavHostFragment.initGraph() = run
createGraph(nav_graph.id, nav_graph.dest.home)
fragment<HomeFragment>(nav_graph.dest.effect_detail)
action(nav_graph.action.home_to_effect_detail)
destinationId = nav_graph.dest.effect_detail
navOptions
applySlideInOut()
//统一指定转场动画
internal fun NavOptionsBuilder.applySlideInOut()
anim
enter = R.anim.slide_in
exit = R.anim.slide_out
popEnter = R.anim.slide_in_pop
popExit = R.anim.slide_out_pop
在 Activity 中,调用 initGraph()
为 Root Fragment 初始化导航图:
@AndroidEntryPoint
class MainActivity : AppCompatActivity()
private val navHostFragment: NavHostFragment by lazy
supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navHostFragment.navController.apply
graph = navHostFragment.initGraph()
而在 Fragment 中,使用 navigation-fragment-ktx 提供的 findNavController()
可以随时基于当前 Destination
进行正确的页面跳转:
@AndroidEntryPoint
class EffectDetailFragment : Fragment()
/* ... */
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
nextButton.setOnClickListener
findNavController().navigate(nav_graph.action.effect_detail_to_loading))
// Back to previous page
backButton.setOnClickListener
优质资源2023年Android开发源码手册,最新整理
开发三年就是高级开发?Android开发如何备战2023年金三银四,挑战年薪50W