Android MVI架构

这个架构意思是单向数据流,View → Intent → ViewModel → Model → View , 界面由State 状态对象决定显示成什么样,状态是只读的,只能新建,不能修改。每次更新列表,需要给新的数据,数据对象的引用变了触发页面刷新。 写个显示武侠人物的demo测试下。

libs.versions.toml:

bash 复制代码
[versions]
agp = "9.0.1"
coreKtx = "1.18.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.10.0"
activityCompose = "1.13.0"
kotlin = "2.0.21"
composeBom = "2024.09.00"

# MVI 依赖
lifecycle = "2.10.0"
coroutines = "1.7.3"
recyclerview = "1.3.2"
swiperefreshlayout = "1.1.0"

appcompat = "1.7.0"
material = "1.12.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }

# MVI 依赖
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }

androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-material = { group = "com.google.android.material", name = "material", version.ref = "material" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

app/build.gradle.kts:

Groovy 复制代码
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.testmvi"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.testmvi"
        minSdk = 29
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    buildFeatures {
        viewBinding = true
    }
}

dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.androidx.material)

    // Compose 可以保留
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.compose.ui)
    implementation(libs.androidx.compose.ui.graphics)
    implementation(libs.androidx.compose.ui.tooling.preview)
    implementation(libs.androidx.compose.material3)

    // MVI核心依赖
    implementation(libs.androidx.lifecycle.viewmodel.ktx)
    implementation(libs.kotlinx.coroutines.android)
    implementation(libs.androidx.recyclerview)
    implementation(libs.androidx.swiperefreshlayout)

    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

武侠人物Hero:

Kotlin 复制代码
package com.example.testmvi

data class Hero(
    val id: Int,
    val name: String,
    val skill: String,
    val sect: String
)

意图HeroIntent:

Kotlin 复制代码
package com.example.testmvi

/**
 * 意图。用户想干什么,全部定义成意图
 */
sealed class HeroIntent {
    object LoadHeroes : HeroIntent()
    object RefreshHeroes : HeroIntent()
}

状态HeroState:

Kotlin 复制代码
package com.example.testmvi

/**
 * 意图。用户想干什么,全部定义成意图
 */
sealed class HeroIntent {
    object LoadHeroes : HeroIntent()
    object RefreshHeroes : HeroIntent()
}

HeroViewModel:

Kotlin 复制代码
package com.example.testmvi

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

/**
 * 处理逻辑, 即根据意图 → 更新状态
 */
class HeroViewModel : ViewModel() {
    private val _state = MutableStateFlow(HeroState())
    val state: StateFlow<HeroState> = _state

    /**
     * 接收 Intent → 处理逻辑 → 更新 State
     */
    fun handleIntent(intent: HeroIntent) {
        when (intent) {
            HeroIntent.LoadHeroes -> loadHeroes()
            HeroIntent.RefreshHeroes -> loadHeroes()
        }
    }

    private fun loadHeroes() { // 不访问网络,就在主线程执行了。
        _state.update { it.copy(isLoading = true) } // 什么语法?

        viewModelScope.launch {
            delay(1000)

            val heroes = listOf(
                Hero(1, "郭靖", "降龙十八掌", "丐帮"),
                Hero(2, "杨过", "黯然销魂掌", "古墓派"),
                Hero(3, "张无忌", "乾坤大挪移", "明教"),
                Hero(4, "乔峰", "降龙十八掌", "丐帮"),
                Hero(5, "令狐冲", "独孤九剑", "华山派"),
                Hero(6, "灭绝师太", "峨眉剑法", "峨眉")
            )

            _state.update {
                it.copy(
                    isLoading = false,
                    heroes = heroes,
                    error = null
                )
            }
        }
    }
}

HeroAdapter:

Kotlin 复制代码
package com.example.testmvi

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.testmvi.databinding.ItemHeroBinding

class HeroAdapter : ListAdapter<Hero, HeroAdapter.HeroViewHolder>(HeroDiffCallback()) {

    inner class HeroViewHolder(private val binding: ItemHeroBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(hero: Hero) {
            binding.tvName.text = hero.name
            binding.tvSkill.text = "武功:${hero.skill}"
            binding.tvSect.text = "门派:${hero.sect}"
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeroViewHolder {
        val binding = ItemHeroBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return HeroViewHolder(binding)
    }

    override fun onBindViewHolder(holder: HeroViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class HeroDiffCallback : DiffUtil.ItemCallback<Hero>() {
        override fun areItemsTheSame(oldItem: Hero, newItem: Hero): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Hero, newItem: Hero): Boolean {
            return oldItem == newItem
        }
    }
}

MainActivity:

Kotlin 复制代码
package com.example.testmvi

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.testmvi.databinding.ActivityMainBinding
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: HeroViewModel
    private lateinit var adapter: HeroAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initList()
        viewModel = HeroViewModel()

        observeState()

        viewModel.handleIntent(HeroIntent.LoadHeroes)

        binding.refreshLayout.setOnRefreshListener {
            viewModel.handleIntent(HeroIntent.RefreshHeroes)
        }
    }

    private fun initList() {
        adapter = HeroAdapter()
        binding.rvHeroes.layoutManager = LinearLayoutManager(this)
        binding.rvHeroes.adapter = adapter
    }

    private fun observeState() {
        lifecycleScope.launch {
            viewModel.state.collect { state ->
                binding.refreshLayout.isRefreshing = state.isLoading
                state.error?.let { Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show() }
                adapter.submitList(state.heroes)
            }
        }
    }
}

activity_main.xml:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/refreshLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvHeroes"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

item_hero.xml:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/tvName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:textStyle="bold"/>

    <TextView
        android:id="@+id/tvSkill"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:layout_marginTop="4dp"/>

    <TextView
        android:id="@+id/tvSect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textColor="#666"/>

</LinearLayout>

执行:

下拉刷新:

ok.

相关推荐
Zhu7585 小时前
在k8s集群部署MySQL单实例,支持多个主流发行版
android·mysql·kubernetes
AFinalStone8 小时前
Android 7系统异常问题排查(四)Framework层(上)—System Server Watchdog
android·系统异常
宝杰X79 小时前
Android Room3 多平台数据库
android·数据库
huibin14785236912 小时前
热缓解学习记录
android·学习
小田的博客15 小时前
SAP MM 供应商银行主数据更新报错!message R1228!
android·java·服务器
一笑的小酒馆16 小时前
Android智能猫砂盆视频加载慢卡顿问题分析
android
只会cv的小前端18 小时前
七巧低代码服务端脚本使用方法
android·低代码·rxjava
18 小时前
Android 自定义 View 实战:从零打造工业级全向摇杆控件(OmniJoystickView)
android·kotlin·自定义view·摇杆控件
雨白18 小时前
深入理解 Kotlin 协程 (十一):以逸待劳,探秘 select 多路复用与并发安全策略
android·kotlin
hunterandroid20 小时前
[Android 从零到一] Android 深度链接与 App Links:从 URI Scheme 到可验证的应用跳转
android