对于架构设计,Google一直想要规范开发者的开发风俗,但是在上层应用开发中又太过于灵活,以是一直没有形成同一的规范,即便如此,Google几乎在1-2年的时间范围内,都会推出一种新的架构设计模式,以此来优化此前的架构模式,从MVP,到MVVM,再到现在的MVI。那么在这篇文章中,我将会根据Google的开发者文档中给出的建议,通过现实的代码实现来深入阐明架构的准则。
1 分层架构
实在分层架构核心的观念就是分离关注点, 在Android最早期的MVC架构中,往往在一个Activity大概Fragment中完成了所有业务逻辑的编写,这种设计现在看来就是一种错误,代码臃肿无法扩展,更不要说随着App巨细不断增大,从而实现业务的可插拔(扩缩)。
因此从MVP架构出现以后,分离关注点开始渐渐被外界认可,数据层专注于数据生产,界面层专注于数据的展示,而Presenter层大概之后的ViewModel层,则是作为两者之间通信的桥梁。那么对于分层架构,Google对于开发者的建议是怎样的呢?
1.1 【强烈建议】利用明确的数据层
数据层,主要用于提供数据,无论是从网络获取,还是从本地数据库获取,都是一个独立的模块,用于公开全部的应用数据,而且会处理绝大部分的业务逻辑。
在现实的项目开发中,建议将数据层存放在data软件包大概data provider模块中,如下
如果将数据层单独拆一个lib_data模块,那么这个模块就是一个公共模块,应用的别的模块均可引用(如果用到数据层的数据),在Google的建议中,即便是只有一个数据源,也要放在单独的软件包大概模块中。
在数据层中,你可以根据业务类型大概数据类型,创建不同的${type}Repository存储类,例如与登录相关的,可以叫做LoginRepository;与支付相关的,叫做PaymentRepository。
记住一点,存储类是唯一可以直接跟数据源打交道的类,其他层不能直接访问数据源,也就意味着外层访问数据层的唯一入口就是存储类。 以是,存储类的主要作用就是:
(1)对外提供应用须要的数据;
(2)集中处理数据的变化;
(3)处理部分业务逻辑;
以是对于存储类来说,须要与数据源绑定在一起,每个数据源类只能负责处理一种数据来源,这个来源可以是网络、本地数据库、本地文件等。 我们拿LoginRepository举例来说:
登录数据源类LoginRemoteDataSource:
- /**
- * 与登录相关的远程数据源,一般指与网络相关的
- */
- class LoginRemoteDataSource {
- suspend fun login(username: String, password: String): Boolean {
- //模拟根据用户名和密码登录
- return withContext(Dispatchers.IO) {
- //延迟2s
- delay(2_000)
- NetUtils.login(username, password)
- }
- }
- }
复制代码 LoginRepository存储类须要持有远程数据源的引用,如果有本地数据源,那么也须要持有本地数据源的引用。
- /**
- * 登录数据存储类
- */
- class LoginRepository(
- private val loginRemoteDataSource: LoginRemoteDataSource
- ) {
- /**
- * 登录
- * @param username 用户名
- * @param password 密码
- */
- suspend fun login(username: String, password: String): Boolean {
- return loginRemoteDataSource.login(username, password)
- }
- }
复制代码 这里讲一下命令规范,对于存储库类以其负责的数据命名。详细命名惯例如下:
数据类型 + Repository。
例如:NewsRepository、MoviesRepository 或 PaymentsRepository。
数据源类以其负责的数据以及利用的来源命名。详细命名惯例如下:
数据类型 + 来源类型 + DataSource。数据来源可以利用Remote大概Local来代表,更易懂。
当我们完成存储库类和数据来源类的设计之后,那么其他层要访问数据层,那么就访问LoginRepository即可。
1.2 【强烈建议】利用明确的界面层
界面层一样寻常指的就是用于渲染界面的Activity大概Fragment,在现实的项目开发中,建议将界面层相关的类放在ui软件包下。
那么界面层的组件,例如Activity、Fragment、ViewModel想要访问数据层的数据时,就会利用到之前的LoginRepository类,通常对于数据访问存储逻辑,是放在ViewModel中实现,也为了保存界面层的数据。
2 界面层
前面讲到了,在界面层中须要获取数据,就要与数据层通信,而为了避免数据层和界面层之间的强耦合,会采用ViewModel作为两者的中心件来处理,那么对于界面层,Google有哪些规范呢?
2.1 【强烈建议】遵循单向数据流原则
什么是单向数据流呢?就是通过ViewModel来公开界面层的状态,并通过方法调用来吸取界面的利用,而且界面层无法更改ViewModel中界面层的状态,以此形成一个单向的数据流,这也是MVI架构的一个核心概念。
- /**
- * 与登录相关的状态
- */
- sealed class LoginUiState {
- object IdleUiState : LoginUiState()
- class LoginSuccessUiState(val username: String) : LoginUiState()
- class LoginErrorUiState(val errorCode: Int, val errorMsg: String) : LoginUiState()
- }
复制代码 LoginUiState是与登录相关的状态,通过ViewModel向界面层公开,界面层在获取这些状态之后,做出相应的利用即可。
- class LoginViewModel(
- private val loginRepository: LoginRepository
- ) : ViewModel() {
- /**
- * 登录逻辑
- */
- fun login(username: String, password: String) {
- viewModelScope.launch {
- loginRepository.login(username, password)
- }
- }
- }
复制代码 LoginViewModel是登录页面持有的ViewModel,此中定义的login方法就是界面层调用,而ViewModel层吸取到界面层的利用之后,开始执行登录的利用。
2.2 【强烈建议】利用生命周期感知型界面状态网络方式
具备生命周期感知的网络方式,主要就是LiveData和Stateflow,而在2.1中,Google建议我们利用ViewModel公开界面层状态,LiveData就不适合这个场景,因此剩下的就是Stateflow,自己Stateflow是不具备生命周期感知的,但是在上篇文章中,我们在先容Kotlin中的热流时,讲过利用repeatOnLifecycleAPI就可以或许使得flow具备生命周期感知能力。
- class LoginViewModel(
- private val loginRepository: LoginRepository
- ) : ViewModel() {
- private val _loginState: MutableStateFlow<LoginUiState> =
- MutableStateFlow(LoginUiState.IdleUiState)
- /**对外暴露登录的状态*/
- val loginState: StateFlow<LoginUiState> = _loginState
- /**
- * 登录逻辑
- */
- fun login(username: String, password: String) {
- viewModelScope.launch {
- val isLoginSuccess = loginRepository.login(username, password)
- if (isLoginSuccess) {
- _loginState.value = LoginUiState.LoginSuccessUiState(username)
- } else {
- _loginState.value = LoginUiState.LoginErrorUiState(-1, "登录失败")
- }
- }
- }
- }
复制代码 我们在LoginViewModel中向界面层暴露了登录的状态loginState,那么在用户点击登录时,便可以拿到这些状态。
- val loginViewModel = LoginViewModel(LoginRepository(LoginRemoteDataSource()))
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- loginViewModel.loginState.collect { state ->
- when (state) {
- is LoginUiState.IdleUiState -> {
- }
- is LoginUiState.LoginSuccessUiState -> {
- Toast.makeText(
- this@MainActivity,
- "${state.username}登录成功",
- Toast.LENGTH_SHORT
- ).show()
- }
- is LoginUiState.LoginErrorUiState -> {
- Toast.makeText(
- this@MainActivity,
- "登录失败",
- Toast.LENGTH_SHORT
- ).show()
- }
- }
- }
- }
- }
- findViewById<Button>(R.id.btn_login).setOnClickListener {
- loginViewModel.login("layz4android", "123456")
- }
复制代码 以是这里还会再提一句,为什么LiveData固然也具备感知生命周期的能力,但是不适用的原因,在状态频繁变化的场景,LiveData会丢失一些状态,但是LiveData一定可以或许把最新的状态返回界面层。
2.3 【强烈建议】请勿未来自 ViewModel 的事件发送到界面
实在这个建议,我并没有看懂,如果按照单向数据流的原则来看,ViewModel是处理事件的那一方,而界面层只是感知状态的变化,而不须要吸取ViewModel的事件。
这里须要注意一点:ViewModel事件应始终会引发界面状态更新。
3 ViewModel
对于ViewModel层的界限,我认为它属于界面层的一部分,但又不完全与界面层强绑定在一起,通过前面的先容,ViewModel就是用来提供界面状态以及对数据层的访问。
3.1 【强烈建议】ViewModel不应该与Android生命周期有关
ViewModel是有自己的生命周期的,其存在的时间是首次哀求ViewModel创建,到宿主完全消散,我们拿Activity举例,当Activity调用onCreate之后,ViewModel就会被创建。
当屏幕发生旋转时,固然Activity执行onDestroy但是会立刻重建,此时ViewModel还是存在内存当中,以是为什么会采用ViewModel存储页面状态,这就是原因之一,比及Activity调用finish完全消散之后,ViewModel才会销毁。
那么即便如此,在利用ViewModel时,不能将Activity、Fragment、Context、Resources等作为依赖传递,如果一定要利用这些参数,那么就不能放到ViewModel中,须要思量放在其他层中。
3.2 【强烈建议】利用协程和数据流
实在如果我们在利用MVI架构之后,数据流将会酿成一个非经常用的工具,而在ViewModel中是会提供viewModelScope协程作用域,用于开启协程。
以是ViewModel在访问数据层时,由于大多数情况下都是进行网络哀求,属于耗时利用,因此数据层中大部分的函数都是挂起函数,以是可以在ViewModel中开启协程,通过异步回调的方式获取服务端的结果,并解析数据分发数据流。
3.3 【强烈建议】在屏幕级别利用ViewModel
什么是屏幕级别,我理解就是我们常见的Activity和Fragment;Google官方克制在一些可重复利用的界面利用ViewModel,如果想要处理可重复利用的界面,须要利用普通的状态容器类(JetPack Compose)。
3.4 【建议】ViewModel公开界面状态
这是Google推出MVI架构之后,出现的一种概念叫UiState,在以往的架构设计中,界面层会拿到服务端哀求的数据,根据拿到的数据判断要展示哪个页面。
像这样业务逻辑,如果少还好,但如果这个界面须要依赖多个接口的数据,那么势必会使整个界面层变得臃肿起来,我个人理解界面层应该只负责展示数据,而不是须要根据数据判断自己该展示什么页面。 以是界面层观察ViewModel层公开的界面状态,根据UiState选择展示某个页面,例如loading、error、success等页面。
但是对于一些开发者来说,这种迁徙可能须要一段时间,因此Google仅仅是建议开发者利用这种头脑,而不是强烈建议,看个人所好了。
像在2.2小节中,我们哀求服务端拿到的是一个Boolean类型的数据状态,因此我们可以自己创建一个数据流MutableStateFlow,并对外提供不可变的StateFlow;但是如果从服务端拿到的是一个数据流,而且是一个冷流,官方建议利用stateIn将其转换为一个StateFlow。但是我理解在ViewModel层网络数据,将状态往界面层抛反而更合适一些
- fun login2(username: String, password: String) {
- viewModelScope.launch {
- loginRepository.login2(username, password).collect{ userInfo->
- if (userInfo == null){
- _loginState.value = LoginUiState.LoginErrorUiState(-1,"登录失败")
- }else{
- _loginState.value = LoginUiState.LoginSuccessUiState(userInfo.username)
- }
- }
- }
- }
复制代码 通过这种方式反而可以或许同一规范。
本篇主要先容了Google对于架构规范给出的一些建议,在现实的项目开发中,盼望可以或许按照今世Android开发规范设计我们的架构,有一些建议显得有些啰嗦,但是真正实验下来之后,项目团体的可扩展性就会变得很高了。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |