资讯详情

一文看懂现代 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 提前处理 Nullablefun  <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 正常运作的基础正是因为 mListnewList 保持了 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 的主要特点和优势如下:

  • :UI State 在 ViewModel 集中管理,降低了多数据源之间的同步成本
  • :UI 的更新来 VM 的状态变化,UI 自身不持有状态、不耦合业务逻辑
  • :UI 发送 event 给 VM 对状态集中修改,状态变化可回溯、利于单测

项目中凡是涉及 UI 的业务场景都是基于 UDF 打造的。以 HomePage 为例,其中包括 BannerListContentList 两组数据展示,所有的数据集中管理在 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 即可

@AndroidEntryPointclass 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) {
                 

标签: 如何将传感器封装成一个库

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台