Android MVI框架搭建与使用(上)https://developer.aliyun.com/article/1407583
三、意图与状态
之前我们说MVI的I 是Intent,表示意图或行为,和ViewModel一样,我们在使用Intent的时候,也是一个Intent对应一个Activity/Fragment。
① 创建意图
在data
包下创建一个intent
包,intent
包下新建一个MainIntent
类,代码如下所示:
package com.llw.mvidemo.data.intent /** * 页面意图 */ sealed class MainIntent { /** * 获取壁纸 */ object GetWallpaper : MainIntent() }
这里只有一个GetWallpaper,表示获取壁纸的动作,你还可以添加其他的,例如保存图片、下载图片等,现在意图有了,下面来创建状态,一个意图有用多个状态。
② 创建状态
在data
包下创建一个state
包,state
包下新建一个MainState
类,代码如下:
package com.llw.mvidemo.data.state import com.llw.mvidemo.data.model.Wallpaper /** * 页面状态 */ sealed class MainState { /** * 空闲 */ object Idle : MainState() /** * 加载 */ object Loading : MainState() /** * 获取壁纸 */ data class Wallpapers(val wallpaper: Wallpaper) : MainState() /** * 错误信息 */ data class Error(val error: String) : MainState() }
这里可以看到四个状态,获取壁纸属于其中的一个状态,通过状态可以去更改页面中的UI,后面我们会看到这一点,这里的状态你还可以再进行细分,例如每一个网络请求你可以增加一个请求中、请求成功、请求失败。
四、ViewModel
在MVI模式中,ViewModel的重要性又提高了,不过我们同样要添加Repository,作为数据存储库。
① 创建存储库
在data
包下创建一个repository
包,repository
包下新建一个MainRepository
类,代码如下:
package com.llw.mvidemo.data.repository import com.llw.mvidemo.network.ApiService /** * 数据存储库 */ class MainRepository(private val apiService: ApiService) { /** * 获取壁纸 */ suspend fun getWallPaper() = apiService.getWallPaper() }
这里的代码就没什么好说的,下面我们写ViewModel,和MVVM模式中没什么两样的。
② 创建ViewModel
下面在com.llw.mvidemo
包下新建一个ui
包,ui
包下新建一个adapter
包,adapter
包下新建一个MainViewModel
类,代码如下:
package com.llw.mvidemo.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.llw.mvidemo.data.repository.MainRepository import com.llw.mvidemo.data.intent.MainIntent import com.llw.mvidemo.data.state.MainState import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch /** * @link MainActivity */ class MainViewModel(private val repository: MainRepository) : ViewModel() { //创建意图管道,容量无限大 val mainIntentChannel = Channel<MainIntent>(Channel.UNLIMITED) //可变状态数据流 private val _state = MutableStateFlow<MainState>(MainState.Idle) //可观察状态数据流 val state: StateFlow<MainState> get() = _state init { viewModelScope.launch { //收集意图 mainIntentChannel.consumeAsFlow().collect { when (it) { //发现意图为获取壁纸 is MainIntent.GetWallpaper -> getWallpaper() } } } } /** * 获取壁纸 */ private fun getWallpaper() { viewModelScope.launch { //修改状态为加载中 _state.value = MainState.Loading //网络请求状态 _state.value = try { //请求成功 MainState.Wallpapers(repository.getWallPaper()) } catch (e: Exception) { //请求失败 MainState.Error(e.localizedMessage ?: "UnKnown Error") } } } }
这里首先创建一个意图管道,然后是一个可变的状态数据流和一个不可变观察状态数据流,观察者模式。在初始化的时候就进行意图的收集,你可以理解为监听,当收集到目标意图MainIntent.GetWallpaper
时就进行相应的意图处理,调用getWallpaper()
函数,这里面修改可变的状态_state
,而当_state
发生变化,state
就观察到了,就会进行相应的动作,这个通过是在View中进行,也就是Activity/Fragment中进行。这里对_state
首先赋值为Loading
,表示加载中,然后进行一个网络请求,结果就是成功或者失败,如果成功,则赋值Wallpapers
,View中收集到这个状态后就可以进行页面数据的渲染了,请求失败,也要更改状态。
③ 创建ViewModel工厂
在viewmodel包下新建一个ViewModelFactory类,代码如下:
package com.llw.mvidemo.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.llw.mvidemo.network.ApiService import com.llw.mvidemo.data.repository.MainRepository /** * ViewModel工厂 */ class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { // 判断 MainViewModel 是不是 modelClass 的父类或接口 if (modelClass.isAssignableFrom(MainViewModel::class.java)) { return MainViewModel(MainRepository(apiService)) as T } throw IllegalArgumentException("UnKnown class") } }
五、UI
前面我们写好基本的框架内容,下面来进行使用,简单来说,请求数据然后渲染出来,因为这里请求的是壁纸数据,所以我需要写一个适配器。
① 列表适配器
在创建适配器之前首先我们需要创建一个适配器所对应的item布局,在layout下新建一个item_wallpaper_rv.xml
,代码如下图所示:
<?xml version="1.0" encoding="utf-8"?> <com.google.android.material.imageview.ShapeableImageView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/iv_wall_paper" android:layout_width="match_parent" android:layout_height="300dp" android:layout_margin="4dp" android:scaleType="centerCrop" app:shapeAppearanceOverlay="@style/roundedImageStyle" />
这里使用了ShapeableImageView,这个控件的优势就在于可以自己设置圆角,在themes.xml中添加如下代码:
<!-- 圆角图片 --> <style name="roundedImageStyle"> <item name="cornerFamily">rounded</item> <item name="cornerSize">24dp</item> </style>
添加位置如下图所示:
下面进行我们在ui包下新建一个adapter
包,adapter
包下新建一个WallpaperAdapter
类,里面的代码如下所示:
package com.llw.mvidemo.ui.adapter import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.llw.mvidemo.data.model.Vertical import com.llw.mvidemo.databinding.ItemWallpaperRvBinding /** * 壁纸适配器 */ class WallpaperAdapter(private val verticals: ArrayList<Vertical>) : RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() { fun addData(data: List<Vertical>) { verticals.addAll(data) } class ViewHolder(itemWallPaperRvBinding: ItemWallpaperRvBinding) : RecyclerView.ViewHolder(itemWallPaperRvBinding.root) { var binding: ItemWallpaperRvBinding init { binding = itemWallPaperRvBinding } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(ItemWallpaperRvBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun getItemCount() = verticals.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { //加载图片 verticals[position].preview.let { Glide.with(holder.itemView.context).load(it).into(holder.binding.ivWallPaper) } } }
这里的代码相对比较简单,就不做说明了,属于适配器的基本操作了。
② 数据渲染
适配器写好之后,我们需要修改一下activity_main.xml
中的内容,修改后代码如下所示:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" 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.MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_wallpaper" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingStart="2dp" android:paddingEnd="2dp" android:visibility="gone" /> <ProgressBar android:id="@+id/pb_loading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btn_get_wallpaper" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="获取壁纸" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
下面我们进入MainActivity
,修改里面的代码如下所示:
package com.llw.mvidemo.ui import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.view.View import android.widget.Toast import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.llw.mvidemo.network.NetworkUtils import com.llw.mvidemo.databinding.ActivityMainBinding import com.llw.mvidemo.data.intent.MainIntent import com.llw.mvidemo.data.state.MainState import com.llw.mvidemo.ui.adapter.WallpaperAdapter import com.llw.mvidemo.ui.viewmodel.MainViewModel import com.llw.mvidemo.ui.viewmodel.ViewModelFactory import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var mainViewModel: MainViewModel private var wallPaperAdapter = WallpaperAdapter(arrayListOf()) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //使用ViewBinding binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) //绑定ViewModel mainViewModel = ViewModelProvider(this, ViewModelFactory(NetworkUtils.apiService))[MainViewModel::class.java] //初始化 initView() //观察ViewModel observeViewModel() } /** * 观察ViewModel */ private fun observeViewModel() { lifecycleScope.launch { //状态收集 mainViewModel.state.collect { when(it) { is MainState.Idle -> { } is MainState.Loading -> { binding.btnGetWallpaper.visibility = View.GONE binding.pbLoading.visibility = View.VISIBLE } is MainState.Wallpapers -> { //数据返回 binding.btnGetWallpaper.visibility = View.GONE binding.pbLoading.visibility = View.GONE binding.rvWallpaper.visibility = View.VISIBLE it.wallpaper.let { paper -> wallPaperAdapter.addData(paper.res.vertical) } wallPaperAdapter.notifyDataSetChanged() } is MainState.Error -> { binding.pbLoading.visibility = View.GONE binding.btnGetWallpaper.visibility = View.VISIBLE Log.d("TAG", "observeViewModel: $it.error") Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show() } } } } } /** * 初始化 */ private fun initView() { //RV配置 binding.rvWallpaper.apply { layoutManager = GridLayoutManager(this@MainActivity, 2) adapter = wallPaperAdapter } //按钮点击 binding.btnGetWallpaper.setOnClickListener { lifecycleScope.launch{ //发送意图 mainViewModel.mainIntentChannel.send(MainIntent.GetWallpaper) } } } }
说明一下,首先声明变量并在onCreate()
中进行初始化,这里绑定ViewModel
采用的是ViewModelProvider()
,而不是ViewModelProviders.of
,这是因为这个API已经被移除了,在之前的版本中是过时弃用,在最新的版本中你都找不到这个API了,所以使用ViewModelProvider()
,然后通过ViewModelFactory
去创建对应的MainViewModel
。
initView()
函数中是控件的一些配置,比如给RecyclerView添加布局管理器和设置适配器,给按钮添加点击事件,在点击的时候发送意图,发送的意图被MainViewModel中mainIntentChannel
收集到,然后执行网络请求操作,此时意图的状态为Loading
。
observeViewModel()
函数中是对状态的收集,在状态为Loading
,隐藏按钮,显示加载条,然后网络请求会有结果,如果是成功,则在UI上隐藏按钮和加载条,显示列表控件,并添加数据到适配器中,然后刷新适配器,数据就会渲染出来;如果是失败则显示按钮,隐藏加载条,打印错误信息并提示一下。这样就完成了通过状态更新UI的环节,MVI的框架就是这样设计的。
页面UI(点击事件发送意图) → ViewModel收集意图(确定内容) → ViewModel更新状态(修改_state) → 页面观察ViewModel状态(收集state,执行相关的UI)
这是一个环,从UI页面出发,最终回到UI页面中进行数据渲染,我们看看效果。
六、源码
欢迎Star 或 Fork,山高水长,后会有期~
源码地址:MviDemo