做网站建设平台,wordpress文章中标签,弥勒建设局网站,建设网站issMVI框架搭建与使用前言正文一、创建项目① 配置AndroidManifest.xml② 配置app的build.gradle二、网络请求① 生成数据类② 接口类③ 网络请求工具类三、意图与状态① 创建意图② 创建状态四、ViewModel① 创建存储库② 创建ViewModel③ 创建ViewModel工厂五、UI① 列表适配器②…
MVI框架搭建与使用前言正文一、创建项目① 配置AndroidManifest.xml② 配置app的build.gradle二、网络请求① 生成数据类② 接口类③ 网络请求工具类三、意图与状态① 创建意图② 创建状态四、ViewModel① 创建存储库② 创建ViewModel③ 创建ViewModel工厂五、UI① 列表适配器② 数据渲染六、源码前言 有一段时间没有去写过框架了最近新的框架MVI其实出来有一段时间了只不过大部分项目还没有切换过去对于公司的老项目来说之前的MVC、MVP也能用没有替换的必要而对于新建的项目来说还是可以替换成功MVVM、MVI等框架的。本文完成后的效果图 正文 每当一个新的框架出来都会解决掉上一个框架所存在的问题但同时也会产生新的问题瑕不掩瑜可以在实际开发中解决掉产生的问题就能够更好的使用框架那么MVI解决了MVVM的什么问题呢 MVI同样是基于观察者模式只不过数据通信方面是单向的解决了MVVM双向通信所带来的问题实际上MVVM也能做成单向通讯但是这样就不是纯粹的MVVM当然了仁者见仁智者见智。MVI框架适用于UI变化很多的项目通过数据去驱动UIMVI就是Model、View、Intent。
Model 这里的Model有所不同里面还包含UI的状态。View 还是视图例如Activity、Fragment等。Intent 意图这个和Activity的意图要区分开我觉得说成是行为可能更妥当表示去做什么。
多说无益我们还是进入实操环节吧。
一、创建项目
首先创建一个名为MviDemo的项目 项目创建好了下面我们需要先进行项目的基本配置。
① 配置AndroidManifest.xml 文章中会通过一个网络API接口拿到数据来进行MVI框架的搭建与使用接口地址如下
http://service.picasso.adesk.com/v1/vertical/vertical?limit30skip180adultfalsefirst0orderhot通过浏览器打开可以得到很多数据如图所示 这些数据都是JSON格式的后面我们还会用到这些数据。因为接口使用的是http而不是https所以在xml文件夹下新建一个network_security_config.xml代码如下
?xml version1.0 encodingutf-8?
network-security-configbase-config cleartextTrafficPermittedtrue /
/network-security-config然后在AndroidManifest.xml中的application标签中配置它如图所示 从Android 9.0起默认使用https进行网络访问如果要进行http访问则需要添加这个配置。还需要添加一个网络访问静态权限
uses-permission android:nameandroid.permission.INTERNET/添加位置如下图所示 项目正常搭建还需要一些依赖库和其他的一些设置下面我们配置app模块下的build.gradle。
② 配置app的build.gradle 请注意这里是配置app的build.gradle而不是项目的build.gradle很多人会配置错误所以我再次强调一下将你的项目切换到Android模式如下图所示 这里我标注了一下你看到有两个build.gradle文件两个文件的后面有灰色的文字说明就很清楚的知道这两个build.gradle分别是项目和模块的。下面打开app模块下的build.gradle在里面找到dependencies{}闭包闭包中添加如下依赖 // lifecycleimplementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1implementation androidx.lifecycle:lifecycle-runtime-ktx:2.4.1//glideimplementation com.github.bumptech.glide:glide:4.14.2//retrofitimplementation com.squareup.retrofit2:retrofit:2.9.0//retrofit moshiimplementation com.squareup.retrofit2:converter-moshi:2.6.2//moshi used KotlinJsonAdapterFactoryimplementation com.squareup.moshi:moshi-kotlin:1.9.3//Coroutineimplementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1implementation org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1添加位置如下图所示 然后再打开viewBinding在android{}闭包下添加如下代码 buildFeatures {viewBinding true}添加位置如下图所示 添加之后你会看到右上角有一个Sync Now点击它进行依赖的载入配置配置好之后进入下一步为了确保你的项目没有问题你可以现在运行一下看看。
二、网络请求 当我们使用Kotlin时网络访问就变得更简单了只需要Retrofit和协程即可首先我们在com.llw.mvidemo包下新建一个data包然后在data包下新建一个model包model包下我们可以通过刚才使用网页访问API拿到的JSON数据来生成一个数据类。
① 生成数据类
生成数据类这里我们可以使用一个插件搜索JSON To Kotlin Class如下图所示 下载安装之后如果需要重启你就重启AS重启之后右键点击model → New → Kotlin data class File from JSON如图所示 在出现的弹窗中复制通过网页请求得到的JSON数据字符串如图所示 这里如果觉得看起来不舒服点击 Format 进行JSON数据格式化然后我们需要设置数据类的名称这里输入Wallpaper因为我们需要使用Moshi将JSON数据直接转成数据类所以这里我们点击Advanced如图所示 这里默认是None选择MoShi(Reflect)其他的不用更改点击OK此弹窗关闭回到之前的弹窗然后点击 Generate 生成数据类你会发现有三个数据类分别是Wallpaper、Res和Vertical我们看一下Wallpaper的代码
package com.llw.mvidemo.data.modelimport com.squareup.moshi.Jsondata class Wallpaper(Json(name code)val code: Int,Json(name msg)val msg: String,Json(name res)val res: Res
)这里每一个字段上都有一个Json注解这里是MoShi依赖库的注解主要检查一下导包的问题这里还有一个小故事Google 的Gson库算是推出比较早的从事Gson库的开发人员后面离职去了Square也就是OkHttp、Retrofit的开发者。Retrofit一开始是支持Gson转换的后面增加了MoShi的转换Moshi拥有出色的Kotlin支持以及编译时代码生成功能可以使应用程序更快更小。这个故事我也是听说的你可以自己去求证下面继续。
② 接口类 现在数据类有了那么我们就需要根据这个数据类来写一个接口类在com.llw.mvidemo包下新建一个network包network包下创建一个接口类ApiService代码如下所示
interface ApiService {/*** 获取壁纸*/GET(v1/vertical/vertical?limit30skip180adultfalsefirst0orderhot)suspend fun getWallPaper(): Wallpaper
}这里属于Retrofit的使用方式增加了协程的使用而已就取代了RxJava的线程调度。
③ 网络请求工具类
现在有接口下面我们来做网络请求在network包下新建一个NetworkUtils类代码如下
package com.llw.mvidemo.networkimport com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory/*** 网络工具类*/
object NetworkUtils {private const val BASE_URL http://service.picasso.adesk.com//*** 通过Moshi 将JSON转为为 Kotlin 的Data class*/private val moshi: Moshi Moshi.Builder().add(KotlinJsonAdapterFactory()).build()/*** 构建Retrofit*/private fun getRetrofit() Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(MoshiConverterFactory.create(moshi)).build()/*** 创建Api网络请求服务*/val apiService: ApiService getRetrofit().create(ApiService::class.java)
}由于担心你看的时候导错包现在贴代码我会将导包的信息也贴出来这样你总不会再导错包了吧。下面简单说明一下这个类首先我定义了一个常量BASE_URL。作为网络接口请求的地址头然后构建了MoShi通过MoShi去进行JSON转Kotlin数据类的处理之后就是构建Retrofit将MoShi设置进去最后就是通过Retrofit创建一个网络请求服务。
三、意图与状态 之前我们说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.stateimport 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.repositoryimport 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.viewmodelimport 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 ChannelMainIntent(Channel.UNLIMITED)//可变状态数据流private val _state MutableStateFlowMainState(MainState.Idle)//可观察状态数据流val state: StateFlowMainState get() _stateinit {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表示加载中然后进行一个网络请求结果就是成功或者失败如果成功则赋值WallpapersView中收集到这个状态后就可以进行页面数据的渲染了请求失败也要更改状态。
③ 创建ViewModel工厂
在viewmodel包下新建一个ViewModelFactory类代码如下
package com.llw.mvidemo.ui.viewmodelimport 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: ClassT): 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 version1.0 encodingutf-8?
com.google.android.material.imageview.ShapeableImageView xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoandroid:idid/iv_wall_paperandroid:layout_widthmatch_parentandroid:layout_height300dpandroid:layout_margin4dpandroid:scaleTypecenterCropapp:shapeAppearanceOverlaystyle/roundedImageStyle /这里使用了ShapeableImageView这个控件的优势就在于可以自己设置圆角在themes.xml中添加如下代码 !-- 圆角图片 --style nameroundedImageStyleitem namecornerFamilyrounded/itemitem namecornerSize24dp/item/style添加位置如下图所示 下面进行我们在ui包下新建一个adapter包adapter包下新建一个WallpaperAdapter类里面的代码如下所示
package com.llw.mvidemo.ui.adapterimport 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: ArrayListVertical) :RecyclerView.AdapterWallpaperAdapter.ViewHolder() {fun addData(data: ListVertical) {verticals.addAll(data)}class ViewHolder(itemWallPaperRvBinding: ItemWallpaperRvBinding) :RecyclerView.ViewHolder(itemWallPaperRvBinding.root) {var binding: ItemWallpaperRvBindinginit {binding itemWallPaperRvBinding}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) ViewHolder(ItemWallpaperRvBinding.inflate(LayoutInflater.from(parent.context), parent, false))override fun getItemCount() verticals.sizeoverride fun onBindViewHolder(holder: ViewHolder, position: Int) {//加载图片verticals[position].img.let {Glide.with(holder.itemView.context).load(it).into(holder.binding.ivWallPaper)}}
}这里的代码相对比较简单就不做说明了属于适配器的基本操作了。
② 数据渲染
适配器写好之后我们需要修改一下activity_main.xml中的内容修改后代码如下所示
?xml version1.0 encodingutf-8?
androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoxmlns:toolshttp://schemas.android.com/toolsandroid:layout_widthmatch_parentandroid:layout_heightmatch_parenttools:context.ui.MainActivityandroidx.recyclerview.widget.RecyclerViewandroid:idid/rv_wallpaperandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:paddingStart2dpandroid:paddingEnd2dpandroid:visibilitygone /ProgressBarandroid:idid/pb_loadingandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:visibilitygoneapp:layout_constraintBottom_toBottomOfparentapp:layout_constraintEnd_toEndOfparentapp:layout_constraintStart_toStartOfparentapp:layout_constraintTop_toTopOfparent /Buttonandroid:idid/btn_get_wallpaperandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:text获取壁纸app:layout_constraintBottom_toBottomOfparentapp:layout_constraintEnd_toEndOfparentapp:layout_constraintStart_toStartOfparentapp:layout_constraintTop_toTopOfparent //androidx.constraintlayout.widget.ConstraintLayout下面我们进入MainActivity修改里面的代码如下所示
package com.llw.mvidemo.uiimport 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.launchclass MainActivity : AppCompatActivity() {private lateinit var binding: ActivityMainBindingprivate lateinit var mainViewModel: MainViewModelprivate var wallPaperAdapter WallpaperAdapter(arrayListOf())override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)//使用ViewBindingbinding ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)//绑定ViewModelmainViewModel ViewModelProvider(this, ViewModelFactory(NetworkUtils.apiService))[MainViewModel::class.java]//初始化initView()//观察ViewModelobserveViewModel()}/*** 观察ViewModel*/private fun observeViewModel() {lifecycleScope.launch {//状态收集mainViewModel.state.collect {when(it) {is MainState.Idle - {}is MainState.Loading - {binding.btnGetWallpaper.visibility View.GONEbinding.pbLoading.visibility View.VISIBLE}is MainState.Wallpapers - { //数据返回binding.btnGetWallpaper.visibility View.GONEbinding.pbLoading.visibility View.GONEbinding.rvWallpaper.visibility View.VISIBLEit.wallpaper.let { paper -wallPaperAdapter.addData(paper.res.vertical)}wallPaperAdapter.notifyDataSetChanged()}is MainState.Error - {binding.pbLoading.visibility View.GONEbinding.btnGetWallpaper.visibility View.VISIBLELog.d(TAG, observeViewModel: $it.error)Toast.makeText(thisMainActivity, it.error, Toast.LENGTH_LONG).show()}}}}}/*** 初始化*/private fun initView() {//RV配置binding.rvWallpaper.apply {layoutManager GridLayoutManager(thisMainActivity, 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