Android 架构实战MVI进阶

MVI架构的原理和流程

MVI架构是一种基于响应式编程的架构模式,它将应用程序分为四个核心组件:模型(Model)、视图(View)、意图(Intent)和状态(State)。 原理:

  • 模型(Model):负责处理数据的状态和逻辑。
  • 视图(View):负责展示数据和用户界面。
  • 意图(Intent):代表用户的操作,如按钮点击、输入等。
  • 状态(State):反映应用程序的当前状态。

流程:

  1. 用户通过视图(View)发起意图(Intent)。
  2. 意图(Intent)被传递给模型(Model)。
  3. 模型(Model)根据意图(Intent)进行状态(State)的更新。
  4. 状态(State)的变化被传递给视图(View),视图(View)进行相应的界面更新。

优点:

  • 单向数据流:通过单向的数据流动,可确保状态的一致性和可预测性。
  • 响应式特性:MVI利用响应式编程的思想,实现了对状态变化的高效处理。
  • 易于测试:由于数据流的清晰性,测试模型的行为变得更加容易。

缺点:

  • 学习曲线较陡:相对于传统的MVC或MVP,MVI架构需要开发者熟悉响应式编程的概念和工具。
  • 增加了一些复杂性:引入状态管理和数据流管理,可能会增加一定的复杂性。

单向数据流

用户操作以Intent的形式通知Model => Model基于Intent更新State => View接收到State变化刷新UI。数据永远在一个环形结构中单向流动,不能反向流动:

一个Sample快速搭建一个MVI架构的项目

代码示例

代码结构如下:

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

将得到结果:

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<User>
}

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<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {

    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<User>) {
        users.addAll(list)
    }

}

item_layout.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:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="60dp">

    <ImageView
        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" />

    <androidx.appcompat.widget.AppCompatTextView
        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"/>

    <androidx.appcompat.widget.AppCompatTextView
        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" />

</androidx.constraintlayout.widget.ConstraintLayout>

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<User>) : 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<MainIntent>(Channel.UNLIMITED)
    private val _state = MutableStateFlow<MainState>(MainState.Idle)
    val state: StateFlow<MainState>
        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 <T : ViewModel?> create(modelClass: Class<T>): 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<User>) {
        recyclerView.visibility = View.VISIBLE
        users.let { listOfUsers -> listOfUsers.let { adapter.addData(it) } }
        adapter.notifyDataSetChanged()
    }
}

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

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.main.view.MainActivity">

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

    <ProgressBar
        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" />

    <Button
        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" />

</androidx.constraintlayout.widget.ConstraintLayout>

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

实战讲解和代码示例

为了更好地理解MVI架构,让我们通过一个例子进行实战演示。我们将创建一个天气预报应用,展示当前天气和未来几天的天气预报信息。 在代码示例中,我们会用到以下库:

  • RxJava:用于处理响应式数据流。
  • LiveData:用于将数据流连接到视图。
 首先,我们定义模型(Model)的状态(State)类,包含天气预报的相关信息,例如温度、湿度和天气状况等。
data class WeatherState(
    val temperature: Float,
    val humidity: Float,
    val condition: String
)

接下来,我们创建视图(View)界面,展示天气信息,并提供一个按钮用于刷新数据。


class WeatherActivity : AppCompatActivity() {

    // 初始化ViewModel
    private val viewModel: WeatherViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_weather)

        // 监听状态变化,更新UI
        viewModel.weatherState.observe(this, Observer { state ->
            // 更新温度、湿度和天气状况的显示
            temperatureTextView.text = state.temperature.toString()
            humidityTextView.text = state.humidity.toString()
            conditionTextView.text = state.condition
        })

        // 刷新按钮点击事件
        refreshButton.setOnClickListener {
            // 发送刷新数据的意图
            viewModel.processIntent(RefreshIntent)
        }
    }
}

然后,我们创建意图(Intent)类,代表用户操作的动作。在这个例子中,我们只有一个刷新数据的意图。


object RefreshIntent : WeatherIntent

接下来,我们实现模型(Model)部分,包括状态管理和数据流的处理。


class WeatherViewModel : ViewModel() {

    // 状态管理
    private val _weatherState = MutableLiveData<WeatherState>()
    val weatherState: LiveData<WeatherState> = _weatherState

    // 处理意图
    fun processIntent(intent: WeatherIntent) {
        when (intent) {
            RefreshIntent -> fetchWeatherData()
        }
    }

    // 获取天气数据
    private fun fetchWeatherData() {
        // 发起网络请求或其他数据获取逻辑
        // 更新状态
        val weatherData = // 获取的天气数据
        val newState = WeatherState(
            temperature = weatherData.temperature,
            humidity = weatherData.humidity,
            condition = weatherData.condition
        )
        _weatherState.value = newState
    }
}

全文对Android中MVI的架构讲解,其中包括原理、项目演示以及实战演练。有关更多的Android架构学习进阶可以参考《Android核心技术手册》文档,点击可以查看详细的内容板块。

总结

MVI架构通过响应式数据流和单向数据流的特性,提供了一种可维护、可测试且具备响应式特性的架构模式。尽管学习曲线较陡,但在大型复杂应用开发中,MVI架构能够更好地管理状态和响应用户操作。通过合理设计状态模型和注意副作用管理,我们可以充分发挥MVI架构的优势,提升应用的可维护性和用户体验。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/216105.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

⭐ Unity + ARKIT 介绍 以及 平面检测的实现

在AR插件中&#xff0c;ARKIT是比较特殊的一个&#xff0c;首先他在很多追踪上的效果要比其他的AR插件要好&#xff0c;但是只能在IOS系统设备上运行。 1.首先ARKIT在最新版Unity已经集成在AR Foundation中&#xff0c;那我们就需要ARSession 和ARSessionOrigin这两个重要组件…

京东数据平台(京东商家数据):2023年10月京东饮料行业品牌店铺销量销额排行榜

鲸参谋监测的京东平台10月份饮料市场销售数据已出炉&#xff01; 10月份&#xff0c;饮料市场整体销售上涨。根据鲸参谋平台的数据显示&#xff0c;今年10月份&#xff0c;京东平台饮料市场的销量为670万&#xff0c;同比增长约2%&#xff1b;销售额为3.8亿&#xff0c;同比增长…

盛元广通智慧水务实验室管理系统

盛元广通智慧水务实验室管理系统通过分析基础业务系统流程&#xff0c;对业务系统流程从项目管理、水样管理、易耗品管理、仪器设备管理、异常报警、数据分析方面、旨在提高水质监测工作的效率、准确性和数据管理能力。通过自动化系统的建设解决了自动化操控问题&#xff0c;实…

揭秘MySQL索引世界:概念、分类、应用场景一网打尽

一、索引概念 MySQL索引是一种用于提高数据库查询性能的数据结构。它允许数据库系统更有效地检索数据行&#xff0c;减少了在大型数据集中搜索特定数据的时间。索引的作用类似于书籍的目录&#xff0c;通过提供关键字与实际数据位置之间的映射&#xff0c;加速对数据库表中数据…

idea利用SpringMVC框架整合ThymeLeaf

简洁一些&#xff1a;两个重要文件 1.controller指定html文件:我们访问http://localhost:8080/test package com.example.appledemo.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import o…

ubuntu20.04使用LIO-SAM对热室空间进行重建

一、安装LIO-SAM 1.环境配置 默认已经安装过ros sudo apt-get install -y ros-Noetic-navigation sudo apt-get install -y ros-Noetic-robot-localization sudo apt-get install -y ros-Noetic-robot-state-publisher 安装 gtsam(如果是18.04的ubuntu直接按照官网配置&…

RabbitMQ 的七种消息传递形式

文章目录 一、RabbitMQ 架构简介二、准备工作 三、消息收发1. Hello World2. Work queues3. Publish/Subscrite3.1. Direct3.2. Fanout3.3. Topic3.4. Header 4. Routing5. Topics 大部分情况下&#xff0c;我们可能都是在 Spring Boot 或者 Spring Cloud 环境下使用 RabbitMQ&…

HTTPS 之fiddler抓包--jmeter请求

一、浅谈HTTPS 我们都知道HTTP并非是安全传输&#xff0c;在HTTPS基础上使用SSL协议进行加密构成的HTTPS协议是相对安全的。目前越来越多的企业选择使用HTTPS协议与用户进行通信&#xff0c;如百度、谷歌等。HTTPS在传输数据之前需要客户端&#xff08;浏览器&#xff09;与服务…

2023年,社媒营销的「心智王者」到底是谁?

“在未来社会&#xff0c;每个人都可能在15分钟内出名&#xff0c;并有机会出名15分钟。” ——安迪沃霍尔 2023年品牌营销&#xff0c;社交媒体是绝对主战场&#xff1a; 明星加持&#xff0c;玩转粉丝经济&#xff1b; “满天星”式种草&#xff0c;打造爆品&#xff1b; …

Burp suite抓虚拟机的包

参考&#xff1a;物理机burp抓虚拟机包) 打开物理机的Burp&#xff0c;Proxy->Proxy settings->Add->Specific address&#xff0c;挑个自己喜欢的&#xff08;除了 127.0.0.1 和 IPV6 地址&#xff09;。 端口号自己填一个。 打开虚拟机浏览器&#xff0c;Internet选…

代码随想录算法训练营第五十五天【动态规划part15】 | 392.判断子序列、115.不同的子序列

392.判断子序列 题目链接 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 求解思路 也可以用双指针来做。 动规五部曲 1.确定dp数组及其下标含义 以下标i-1为结尾的字符串s&#xff0c;和以下标j-1为结尾的字符串t&#xff0c;相同子序列的长度…

EtherCAT超高速实时运动控制卡XPCIE1032H上位机C#开发(四):板载IO与总线扩展IO的编码器与脉冲配置的应用

XPCIE1032H功能简介 XPCIE1032H是一款基于PCI Express的EtherCAT总线运动控制卡&#xff0c;可选6-64轴运动控制&#xff0c;支持多路高速数字输入输出&#xff0c;可轻松实现多轴同步控制和高速数据传输。 XPCIE1032H集成了强大的运动控制功能&#xff0c;结合MotionRT7运动…

自动化框架错误排查:本地全通过,pipline上大部分报错

现象: 最近经过一次切环境和验证码部分的代码重构,果不其然,我们的自动化框架就出错了 我在本地修改调试,并在堡垒机上全部跑过 但在pipline上则大部分报错 进一步排查 这么多case报错,而且报错log都一模一样,推断是底层出错 我在堡垒机上使用命令行来跑case,发现与…

vue 修改 this.$confirm 的文字样式、自定义样式

通常使用 confirm 确认框时&#xff0c;一般这样写&#xff1a; <template><el-button type"text" click"open">点击打开 Message Box</el-button> </template><script>export default {methods: {open() {this.$confirm(此…

14、pytest像用参数一样使用fixture

官方实例 # content of test_fruit.py import pytestclass Fruit:def __init__(self, name):self.name nameself.cubed Falsedef cube(self):self.cubed Trueclass FruitSalad:def __init__(self, *fruit_bowl):self.fruit fruit_bowlself._cube_fruit()def _cube_fruit(s…

揭秘预付费电表怎么无线收费——方便快捷收费

【摘要】针对目前市场上普遍以Ic卡作为售电介质的预付费售电系统存在的问题&#xff0c;介绍了一种新型的无线预付费售电系统及其构成&#xff0c;并给出了整个系统设计的完整方案。整个系统包括用户终端和电力管理系统端&#xff0c;它们之间通过双工通信可以将用户用电信息和…

科普类软文怎么写才能提高用户接受度?媒介盒子分享

科普类软文以干货为主&#xff0c;可以给用户带来实用价值&#xff0c;但是相应会比较枯燥。如何才能把科普内容讲得专业又有趣&#xff0c;从而提高用户接受度呢&#xff1f;媒介盒子接下来就分享三大技巧&#xff1a; 一、 联系产品选题 科普类软文想要写好就需要做好选题&…

如何使用Node.js快速创建本地HTTP服务器并实现异地远程访问

文章目录 前言1.安装Node.js环境2.创建node.js服务3. 访问node.js 服务4.内网穿透4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口 5.固定公网地址 前言 Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台运行环境。Node.js 由 OpenJS Foundation&#xff0…

吉祥物虚拟人IP:如何持续为品牌年轻化营销赋能

如今&#xff0c;「得年轻人者&#xff0c;得天下」逐渐成为各品牌营销的一个共识&#xff0c;年轻化营销则成为品牌吸引和留存用户的手段。在数字化时代&#xff0c;品牌也不再满足于通过产品带动用户共情&#xff0c;而是突破传统的、单向的吉祥物打造模式&#xff0c;推陈出…