资讯详情

一文看懂现代 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 正常运作的基础正是因为 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 即可

@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() 

标签: 转向传感器组件dsl传感器传感器pwd12

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

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