android程序结构改变了,基于Android的MVI架构:从双向绑定到单向数据流

5838bf47b911a9eeb7882752261d4635.png

现在从事Android开发多少都要懂点架构知识,从MVC、MVP再到MVVM,想必大家对于其各自的优缺点早已如数家珍。今天介绍的MVI与MVVM非常接近,可以针对性地弥补MVVM中的一些缺陷

何为MVI?

988ba737e534872783fda646aef1cf28.pngMVI即Model-View-Intent,它受Cycle.js前端框架的启发,提倡一种单向数据流的设计思想,非常适合数据驱动型的UI展示项目:

Model: 与其他MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。当前界面展示的内容无非就是UI状态的一个快照:例如数据加载过程、控件位置等都是一种UI状态

View: 与其他MVX中的View一致,可能是一个Activity、Fragment或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新(不是Activity的Intent、后面介绍)

Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model进行数据请求

单向数据流

用户操作以Intent的形式通知Model => Model基于Intent更新State => View接收到State变化刷新UI。

数据永远在一个环形结构中单向流动,不能反向流动:

d8c4b0572b30e949c7cae56459059cf0.png

这种单向数据流结构的MVI有什么优缺点呢?

优点

UI的所有变化来自State,所以只需聚焦State,架构更简单、易于调试

数据单向流动,很容易对状态变化进行跟踪和回溯

state实例都是不可变的,确保线程安全

UI只是反应State的变化,没有额外逻辑,可以被轻松替换或复用

缺点

所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

state是不变的,每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销

有些事件类的UI变化不适合用state描述,例如弹出一个toast或者snackbar

talk is cheap, show me the code。

我们通过一个Sample看一下如何快速搭建一个MVI架构的项目。

代码示例

代码结构如下:

c352ccb5a25376a5b2b4e226f79291c6.png

Sample中的依赖库

// Added Dependencies

implementation "androidx.recyclerview:recyclerview:1.1.0"

implementation 'android.arch.lifecycle:extensions:1.1.1'

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'

implementation 'com.github.bumptech.glide:glide:4.11.0'

//retrofit

implementation 'com.squareup.retrofit2:retrofit:2.8.1'

implementation "com.squareup.retrofit2:converter-moshi:2.6.2"

//Coroutine

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"

复制代码

代码中使用以下API进行请求

https://reqres.in/api/users

复制代码

将得到结果:

87fc458d8b99b7eb4e0f3adc9145f4e3.png

1. 数据层

1.1 User

定义User的data class

package com.my.mvi.data.model

data class User(

@Json(name = "id")

val id: Int = 0,

@Json(name = "first_name")

val name: String = "",

@Json(name = "email")

val email: String = "",

@Json(name = "avator")

val avator: String = ""

)

复制代码

1.2 ApiService

定义ApiService,getUsers方法进行数据请求

package com.my.mvi.data.api

interface ApiService{

@GET("users")

suspend fun getUsers(): List

}

复制代码

1.3 Retrofit

创建Retrofit实例

object RetrofitBuilder {

private const val BASE_URL = "https://reqres.in/api/user/1"

private fun getRetrofit() = Retrofit.Builder()

.baseUrl(BASE_URL)

.addConverterFactory(MoshiConverterFactory.create())

.build()

val apiService: ApiService = getRetrofit().create(ApiService::class.java)

}

复制代码

1.4 Repository

定义Repository,封装API请求的具体实现

package com.my.mvi.data.repository

class MainRepository(private val apiService: ApiService) {

suspend fun getUsers() = apiService.getUsers()

}

复制代码

2. UI层

Model定义完毕后,开始定义UI层,包括View、ViewModel以及Intent的定义

2.1 RecyclerView.Adapter

首先,需要一个RecyclerView来呈现列表结果,定义MainAdapter如下:

package com.my.mvi.ui.main.adapter

class MainAdapter(

private val users: ArrayList

) : RecyclerView.Adapter() {

class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

fun bind(user: User) {

itemView.textViewUserName.text = user.name

itemView.textViewUserEmail.text = user.email

Glide.with(itemView.imageViewAvatar.context)

.load(user.avatar)

.into(itemView.imageViewAvatar)

}

}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

DataViewHolder(

LayoutInflater.from(parent.context).inflate(

R.layout.item_layout, parent,

false

)

)

override fun getItemCount(): Int = users.size

override fun onBindViewHolder(holder: DataViewHolder, position: Int) =

holder.bind(users[position])

fun addData(list: List) {

users.addAll(list)

}

}

复制代码

item_layout.xml

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:id="@+id/container"

android:layout_width="match_parent"

android:layout_height="60dp">

android:id="@+id/imageViewAvatar"

android:layout_width="60dp"

android:layout_height="0dp"

android:padding="4dp"

app:layout_constraintBottom_toBottomOf="parent"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/textViewUserName"

style="@style/TextAppearance.AppCompat.Large"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_marginStart="8dp"

android:layout_marginTop="4dp"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"

app:layout_constraintTop_toTopOf="parent"/>

android:id="@+id/textViewUserEmail"

android:layout_width="0dp"

android:layout_height="wrap_content"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintStart_toStartOf="@+id/textViewUserName"

app:layout_constraintTop_toBottomOf="@+id/textViewUserName" />

复制代码

2.2 Intent

定义Intent用来包装用户Action

package com.my.mvi.ui.main.intent

sealed class MainIntent{

object FetchUser : MainIntent()

}

复制代码

2.3 State

定义UI层的State结构体

sealed class MainState{

object Idle : MainState()

object Loading : MainState()

data class Users(val user: List) : MainState()

data class Error(val error: String?) : MainState()

}

复制代码

2.4 ViewModel

ViewModel是MVI的核心,存放和管理State,同时接受Intent并进行数据请求

package com.my.mvi.ui.main.viewmodel

class MainViewModel(

private val repository: MainRepository

) : ViewModel() {

val userIntent = Channel(Channel.UNLIMITED)

private val _state = MutableStateFlow(MainState.Idle)

val state: StateFlow

get() = _state

init {

handleIntent()

}

private fun handleIntent() {

viewModelScope.launch {

userIntent.consumeAsFlow().collect {

when (it) {

is MainIntent.FetchUser -> fetchUser()

}

}

}

}

private fun fetchUser() {

viewModelScope.launch {

_state.value = MainState.Loading

_state.value = try {

MainState.Users(repository.getUsers())

} catch (e: Exception) {

MainState.Error(e.localizedMessage)

}

}

}

}

复制代码

我们在handleIntent中订阅userIntent并根据Action类型执行相应操作。本case中当出现FetchUser的Action时,调用fetchUser方法请求用户数据。用户数据返回后,会更新State,MainActivity订阅此State并刷新界面。

2.5 ViewModelFactory

构造ViewModel需要Repository,所以通过ViewModelFactory注入必要的依赖

class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {

override fun create(modelClass: Class): T {

if (modelClass.isAssignableFrom(MainViewModel::class.java)) {

return MainViewModel(MainRepository(apiService)) as T

}

throw IllegalArgumentException("Unknown class name")

}

}

复制代码

2.6 定义MainActivity

package com.my.mvi.ui.main.view

class MainActivity : AppCompatActivity() {

private lateinit var mainViewModel: MainViewModel

private var adapter = MainAdapter(arrayListOf())

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main)

setupUI()

setupViewModel()

observeViewModel()

setupClicks()

}

private fun setupClicks() {

buttonFetchUser.setOnClickListener {

lifecycleScope.launch {

mainViewModel.userIntent.send(MainIntent.FetchUser)

}

}

}

private fun setupUI() {

recyclerView.layoutManager = LinearLayoutManager(this)

recyclerView.run {

addItemDecoration(

DividerItemDecoration(

recyclerView.context,

(recyclerView.layoutManager as LinearLayoutManager).orientation

)

)

}

recyclerView.adapter = adapter

}

private fun setupViewModel() {

mainViewModel = ViewModelProviders.of(

this,

ViewModelFactory(

ApiHelperImpl(

RetrofitBuilder.apiService

)

)

).get(MainViewModel::class.java)

}

private fun observeViewModel() {

lifecycleScope.launch {

mainViewModel.state.collect {

when (it) {

is MainState.Idle -> {

}

is MainState.Loading -> {

buttonFetchUser.visibility = View.GONE

progressBar.visibility = View.VISIBLE

}

is MainState.Users -> {

progressBar.visibility = View.GONE

buttonFetchUser.visibility = View.GONE

renderList(it.user)

}

is MainState.Error -> {

progressBar.visibility = View.GONE

buttonFetchUser.visibility = View.VISIBLE

Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()

}

}

}

}

}

private fun renderList(users: List) {

recyclerView.visibility = View.VISIBLE

users.let { listOfUsers -> listOfUsers.let { adapter.addData(it) } }

adapter.notifyDataSetChanged()

}

}

复制代码

MainActivity中订阅mainViewModel.state,根据State处理各种UI显示和刷新。

activity_main.xml:

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".ui.main.view.MainActivity">

android:id="@+id/recyclerView"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:visibility="gone" />

android:id="@+id/progressBar"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

app:layout_constraintBottom_toBottomOf="parent"

app:layout_constraintEnd_toEndOf="parent"

android:visibility="gone"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toTopOf="parent" />

android:id="@+id/buttonFetchUser"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="@string/fetch_user"

app:layout_constraintBottom_toBottomOf="parent"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toTopOf="parent" />

复制代码

如上,一个完整的MVI项目就完成了。

最后

MVI在MVVM的基础上,规定了数据的单向流动和状态的不可变性,这类似于前端的Redux思想,非常适合UI展示类的场景。MVVM也好,MVI也好都不是架构的最终形态,世界上没有完美的架构,要根据项目情况选择适合的架构进行开发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值