Android MVVM的实现

article/2025/9/30 8:34:05

Android MVVM的实现

在这里插入图片描述

前言:

在我们写一些项目的时候,通常会对一些常用的一些常用功能进行抽象封装,简单例子:比如BaseActivity,BaseFragment等等…一般这些Base会去承载一些比如标题栏,主题之类的工作,方便Activity的一些风格的统一,也是预留一些函数方便进行HOOK进而实现一些功能。除此之外,一个网络请求也会根据项目采用的技术进行一些封装,比如OkHttp的全局的单例呀,网络请求的成功与失败的回调呀,把相应的状态进行上抛给View,这些都是我们在新建一个项目,采用不同技术方案时需要考虑的问题。

下面我就分享一下比较常用的一些技术方案去实现的一个MVVM的一个基础架构组件。

Find View

在Android项目中,因为传统的View布局的方式采用的是通过xml进行控件的布局,那么不可避免的我们需要在代码中进行view的操作,那么我们就需要findViewById,这个方法可能是每一个Android的开发人员都非常熟悉的一个方法,它的作用那我们就不必说,就是发现一个view并获取对应的实例,那么当我们布局文件越来越复杂的时候,我们需要每一个view都find一遍的话,那么明显重复代码冗长且易出错,但是不写又不行。当时你可以说用compose呀,抛掉XML,可是技术的普及总是需要一定的时间,在这之前传统的xml也是不能抛弃的。

在Android中用来代替的findViewById的方法有很多,但是大多已经过时了,或者说已经不推荐了,方案主要有以下几种。

方案状态优缺点
Butter Knife停止更新,库作者已不推荐-
kotlin-android-extensions谷歌已不推荐-
Data Binding可用优点:可以直接实现双向绑定,在XML中支持表达式
缺点:1.BUG比较难定位 2.根标签必须是layout 3.对构建速度和性能有部分影响
View Binding可用优点:避免findViewById大量重复代码同时精剪了部分功能,避免了Data Binding存在的问题
缺点:不支持双向绑定

一般情况下,推荐采用View Binding,View Binding一般使用方法如下:

val binding = ActivityLoginBinding.inflate(layoutInflater)
// val binding = ActivityLoginBinding.inflate(layoutInflater, parent, false)
// val binding = ActivityLoginBinding.bind(view)
binding.tv.text = "Hello Android!"

可能你会觉得如果每个使用这个布局的地方都要inflate一遍,那么我们可以借助kotlin的委托和反射,进一步简化代码。
最终呈现的效果,如下面代码所示:

class TestActivity : AppCompatActivity() {private val binding: ActivityLoginBinding by binding()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding.edName.setText("hello")}}

上述代码通过就通过委托和反射的方式,减少inflate和setContentView的代码。
具体实现可以参考:
https://github.com/DylanCaiCoding/ViewBindingKTX

Base类

在这部分,我觉得每个项目在Base中封装是不一样的,所以在本文章中,Base这一块反而是比较简单的一块,简单的有个继承关系即可

open class BaseActivity : AppCompatActivity()open class BaseApplication : Application() open class BaseViewModel : ViewModel()

有一点需要说明的是,在Base Activity中我们需要尽量不去修改activity的生命周期,或者说添加而外的生命周期,如果是在多人合作的项目中,开发人员错误地理解部分代码,那么就有可能出现问题。所以我们在Base中尽量不要修改对应的生命周期,减少学习成本。

依赖注入

依赖注入或许在Java开发中很常见,其实也是可以应用在Android中用来减少一些重复代码,也可以方便开发人员对代码进行一些简单的测试,只需要替换对应的实现类即可。

那么Android常见的依赖注入方案有

方案优缺点
Dagger优点:功能强大
缺点:学习成本高,在Android上应用需要一定的熟练程度
Hilt谷歌根据Android平台的特点,在 Dagger 的基础上构建而成,减少了一定的学习成本
koin根据委托实现,学习成本低

这里比较推荐Hilt,那么Hilt的用法比较简单,主要分为两部分,一部分为使用注解表明什么地方需要注入以及注入的对象的作用域。另外一部分就是被注入的对象如何产生。

Hilt接入:
首先,将 hilt-android-gradle-plugin 插件添加到项目的根级 build.gradle 文件中

plugins {...id 'com.google.dagger.hilt.android' version '2.44' apply false
}

然后,应用 Gradle 插件并在 app/build.gradle 文件中添加以下依赖项:

...
plugins {id 'kotlin-kapt'id 'com.google.dagger.hilt.android'
}android {...
}dependencies {implementation "com.google.dagger:hilt-android:2.44"kapt "com.google.dagger:hilt-compiler:2.44"
}// Allow references to generated code
kapt {correctErrorTypes true
}

所有使用 Hilt 的应用都必须包含一个带有 @HiltAndroidApp 注解的 Application 类。

@HiltAndroidApp 会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。

@HiltAndroidApp
class ExampleApplication : Application() { ... }

Hilt使用如下:

//被注入的对象如何产生//标记这是一个module.可以通过module模块向 Hilt 提供绑定信息。
@Module
//标记绑定作用域限定到ViewModel
@InstallIn(ViewModelComponent::class)
object RepositoryModule {//标记方法,提供依赖返回值,即产生对象@Providesfun provideTasksRepository(): UserRepository {return DefaultUserRepository()}
}//表明什么地方需要注入//表明这是一个被注入ViewModel
@HiltViewModel
//注入到构造函数的repository
class TestViewModel @Inject constructor(private val repository: UserRepository
) : BaseViewModel()

更多Hilt的使用方法参考链接:
https://developer.android.google.cn/training/dependency-injection/hilt-android?hl=zh-cn

网络实现

一般而言,如果没有特殊情况的话,Android网络请求一般采用的都是OkHttp来实现Http请求,数据格式一般采用Json。当然也有些项目为了性能采用RPC和Protobuf来进行数据通讯。

这里我们采用OkHttp ,Retrofit , Flow 来构建我们的数据传输,模拟的接口就采用玩Android的开放API来举例,接口文档地址:
https://www.wanandroid.com/blog/show/2

第一步导入依赖:

dependencies {...//OkHttpimplementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))implementation("com.squareup.okhttp3:okhttp")implementation("com.squareup.okhttp3:logging-interceptor")//retrofitimplementation "com.squareup.retrofit2:retrofit:2.9.0"implementation "com.squareup.retrofit2:converter-gson:2.9.0"
}

第二步构建OkHttp和Retrofit实例,结合Hilt实现


object NetUrlConst {const val BASE_URL = "https://www.wanandroid.com"/*** 登录*/const val LOGIN = "/user/login"
}@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {private const val TAG = "NetworkModule"@Provides@Singletonfun providesOKHttpClient():OkHttpClient{Log.i(TAG,"providesOKHttpClient")val okHttpClient = OkHttpClient().newBuilder().addInterceptor(HttpLoggingInterceptor { message ->Log.i(TAG, message)}.setLevel(HttpLoggingInterceptor.Level.BODY)).build()return okHttpClient}@Provides@Singletonfun providesRetrofit(client: OkHttpClient):Retrofit{Log.i(TAG,"providesRetrofit")return Retrofit.Builder().baseUrl(NetUrlConst.BASE_URL).client(client).addConverterFactory(GsonConverterFactory.create()).build()}
}

上面代码就实现了OkHttp和Retrofit的单例,使用的话在需要的地方使用Hilt注入即可。
第三步定义错误码以及返回结果

/**错误码枚举*/
enum class NetCode(val value: Int){ERROR(-1),NORMAL(0)
}
/**返回实体基类*/
data class BaseEntity<T>(val data: T,val errorCode: Int,val errorMsg: String
) {val isSuccessget() = errorCode == NetCode.NORMAL.value
}
/**用户实体类*/
data class UserEntity(val id: Int,val nickname: String,val password: String,val publicName: String,val username: String
)

第四步生成Service

interface UserService {@POST(NetUrlConst.LOGIN)suspend fun login(@Query("username") username: String,@Query("password") password: String): BaseEntity<UserEntity>
}@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {...@Providesfun providesNetUserService(retrofit: Retrofit):UserService{Log.i(TAG,"providesNetUserService")return retrofit.create(UserService::class.java)}
}

Repository

接下来我们需要定义,提供数据的Model层的实现形式。
一般而言在Model层会提供一个抽象的Repository来进行数据的提供,来源包括本地以及网络数据。在Repository中分别有不同的DataSource来提供数据,而Repository则屏蔽这些具体的细节统一封装数据返回给ViewModel。
如下图所示
在这里插入图片描述
同时由于我们数据流采用的是Flow,那么我们需要定义一个协程的返回基类,用于包含我们成功的信息以及错误的时候的异常信息。
代码如下:

/*** Author: huangtao* Date: 2023/1/19* Desc: 用于协程内容的封装类* Error用来处理程序异常,不处理业务错误*/
sealed class Result<out R> {data class Success<out T>(val data: T) : Result<T>()data class Error(val exception: Exception) : Result<Nothing>()override fun toString(): String {return when (this) {is Success<*> -> "Success[data=$data]"is Error -> "Error[exception=$exception]"}}
}val Result<*>.succeededget() = this is Result.Success && data != null

有了Result之后我们就可以方便地定义Repository的接口了
我们这边只接入用户登录的功能,那么方法也就一个。

/*** Desc: 登录的数据接口*/
interface UserRepository {/*** 登录*/suspend fun signIn(username: String, password: String): Result<BaseEntity<UserEntity>>
}/*** Desc: 登录的数据源接口*/
interface UserDataSource {/*** 登录*/suspend fun signIn(username: String, password: String): Result<BaseEntity<UserEntity>>
}//由于登录功能只有远程的实现,所以这边本地实现略
/*** Desc: 网络数据源*/
class RemoteDataSource(private val service: UserService,private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : UserDataSource {override suspend fun signIn(username: String, password: String) = withContext(ioDispatcher) {try {return@withContext Result.Success(service.login(username, password))} catch (e: Exception) {return@withContext Result.Error(e)}}
}/*** Desc: 默认LoginRepository实现*/
class DefaultUserRepository(private val remoteDataSource: UserDataSource,private val localDataSource: UserDataSource = null,
) : UserRepository {override suspend fun signIn(username: String, password: String): Result<BaseEntity<UserEntity>> {val result = remoteDataSource.signIn(username, password)if (result is Result.Success && result.data.isSuccess) {//TODO 一般而言登录成功后,会进行一些数据缓存的逻辑等}return result}
}

相关实现写好后,我们通过Hilt的依赖注入暴露出去

/*** Desc: 用户模块的依赖注入*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Remote@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Local@Module
@InstallIn(ViewModelComponent::class)
object RepositoryModule {@Providesfun provideTasksRepository(@Remote remoteDataSource: UserDataSource): UserRepository {return DefaultUserRepository(remoteDataSource)}
}@Module
@InstallIn(ViewModelComponent::class)
object DataSourceModule {@Remote@Providesfun provideUserRemoteDataSource(userService: UserService): UserDataSource {return RemoteDataSource(userService)}//	  本地数据源,如果有的话
//    @Local
//    @Provides
//    fun provideTasksLocalDataSource(): UserDataSource {
//        return LocalDataSource()
//    }
}

那么至此,Model层就算实现完成了。

View and ViewModel

通过上面的一系列工作,那么我们现在就可以愉快地写界面以及业务逻辑。
我们还是通过简单的用户登录这个界面来举例。
首先是我们的布局界面,简简单单一个按钮,两个输入框。
activity_login.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=".LoginActivity"><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/gl"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="horizontal"app:layout_constraintGuide_percent="0.33" /><EditTextandroid:id="@+id/ed_name"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="68dp"android:layout_marginEnd="68dp"android:background="@null"android:hint="输入用户名"android:padding="6dp"android:textSize="18sp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/gl" /><EditTextandroid:id="@+id/ed_password"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="68dp"android:layout_marginTop="18dp"android:layout_marginEnd="68dp"android:background="@null"android:hint="输入密码"android:inputType="textPassword"android:padding="6dp"android:textSize="18sp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/ed_name" /><Buttonandroid:id="@+id/bt_login"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="68dp"android:layout_marginTop="18dp"android:layout_marginEnd="68dp"android:padding="6dp"android:text="登录"android:textSize="18sp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/ed_password" /></androidx.constraintlayout.widget.ConstraintLayout>

View层activity

@AndroidEntryPoint
class LoginActivity : BaseActivity() {private val binding: ActivityLoginBinding by binding()private val mViewModel: LoginViewModel by viewModels()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)lifecycleScope.launchWhenResumed {mViewModel.loginFlow.collect {if (it == null) return@collectToast.makeText(this@LoginActivity, "返回的数据=$it", Toast.LENGTH_LONG).show()}}binding.btLogin.click {loginLogic()}}private fun loginLogic() {val name = binding.edName.text.toString().trim()val password = binding.edPassword.text.toString().trim()if (!CheckUtils.checkName(name)) {Toast.makeText(this, "用户名${Constant.NAME_LENGTH}位", Toast.LENGTH_LONG).show()return}if (!CheckUtils.checkPassWord(password)) {Toast.makeText(this, "密码${Constant.PASSWORD_LENGTH}位", Toast.LENGTH_LONG).show()return}mViewModel.login(name, password)}}

ViewModel

@HiltViewModel
class LoginViewModel @Inject constructor(private val repository: UserRepository
) : BaseViewModel() {private val mLoginFlow = MutableStateFlow<Result<BaseEntity<UserEntity>>?>(null)val loginFlow: Flow<Result<BaseEntity<UserEntity>>?> get() = mLoginFlowfun login(name: String, password: String) {viewModelScope.launch {mLoginFlow.value = repository.signIn(name, password)}}
}

到此一个简单的MVVM架构就实现了

  • 如果你有疑问或者更好的想法,欢迎进群讨论Android 学习交流群

源码传送门:
https://github.com/huangtaoOO/TaoComponent


http://chatgpt.dhexx.cn/article/yHvepYGi.shtml

相关文章

Android MVI框架搭建与使用

MVI框架搭建与使用 前言正文一、创建项目① 配置AndroidManifest.xml② 配置app的build.gradle 二、网络请求① 生成数据类② 接口类③ 网络请求工具类 三、意图与状态① 创建意图② 创建状态 四、ViewModel① 创建存储库② 创建ViewModel③ 创建ViewModel工厂 五、UI① 列表适…

Android MVVN 使用入门

MVVM&#xff08;Model-View-ViewModel&#xff09;是一种基于数据绑定的设计模式&#xff0c;它与传统的 MVC 和 MVP 模式相比&#xff0c;更加适合处理复杂的 UI 逻辑和数据展示。在 Android 开发中&#xff0c;MVVM 通常使用 Data Binding 和 ViewModel 实现。 下面是一个简…

mvnw的使用

1、什么是mvnw mvnw是Maven Wrapper的缩写。我们安装Maven时&#xff0c;默认系统所有项目都会使用全局安装的这个Maven版本。但对于某些项目来说&#xff0c;它可能必须使用某个特定的Maven版本&#xff0c;这时就可以使用Maven Wrapper&#xff0c;它可以负责给这个特定的项…

快速查找参考文献影响因子——ScholarScope

前言&#xff1a; 最初看到的关于查看影响因子的插件有&#xff1a;pubmedy, pubmed plus 和 scholar scope. 试了pubmedy&#xff0c;找到的版本没有用。 PubMed 是一个提供生物医学方面的论文搜寻以及摘要&#xff0c;并且免费搜寻的数据库。它的数据库来源为MEDLINE。其核心…

新手刚学js遇到的ie6问题

2019独角兽企业重金招聘Python工程师标准>>> 1.前段时间遇到一个需求&#xff0c;需要让图片在点击tab的时候加载。如果那个tab是由a标签组成的&#xff0c;这时候你就需要在click之后return false。不然坑爹的ie6是没法显示图片的。 2.有个需求是&#xff0c;做一…

查看文章影响因子的插件_查询文献可实时显示影响因子与分区排名的2个强大浏览器插件...

首先,看下我们普通的PubMed文献查找页面,是下图这样子的: 可是装了两个国产神器之后,文献的检索结果列表是下图这样的,可以实时查看文章的影响因子、研究领域的排名,以及全文下载链接、引文格式等。 而这两个神器其实只是两款非常小的浏览器插件:PubMedy和Scholarscope。…

python 贪吃蛇游戏代码

第一步&#xff1a;蛇形 运行IDLE&#xff0c;打开一个新的文本编辑窗口。输入以下的代码&#xff1a; # -*- coding: UTF-8 -*- # 1 - 引入模块 import pygame from pygame.locals import * import sys,random,time,math# 2 - 初始化pygame pygame.init() fpsClock pygame.…

简单的贪吃蛇

最近都在忙着复习考试&#xff0c;忙里偷闲&#xff0c;抽出时间写了个贪吃蛇&#xff0c;没时间写详细的思路了&#xff0c;代码里有比较详细的注释&#xff0c;有兴趣的同学可以自己看看。&#xff08;感觉写的相对来说还是比较简短的&#xff0c;如果有什么写的不好或是不对…

简单的贪吃蛇代码,可上机运行

贪吃蛇无敌版&#xff0c;可穿墙&#xff0c;英文输入法小写字母wasd操作。 #include<stdio.h> #include<string.h> #include<windows.h> #include<time.h> #include<conio.h>#define up w #define down s #define left a #define right d #def…

cmd贪吃蛇(cmd贪吃蛇怎么做)

贪吃蛇代码-贪吃蛇的围墙代码怎么&#xff1f;贪吃蛇的围墙代码怎么写 哈哈……避邪[哈哈] 贪吃蛇在哪下载啊 我的工享里有 在dos环境下c语言编程编一个贪吃蛇游戏 程序设计及说明 该类规定游戏的范围大小。 Snake 用该类生成一个实例蛇 snake 该类用于实现对蛇的操作控制&…

C++实现cmd界面简单贪吃蛇游戏

贪吃蛇的玩法我想应该大家都是耳熟能详了。但是这游戏虽然简单&#xff0c;但是编写的难度对一个刚刚学完c,准备考研的苦逼大学生来说却是一件非常艰难的事情。 date:10月3日&#xff0c;国庆节的头3天&#xff0c;大家在外玩耍我却苦逼的在这里写代码痛苦ing&#xff0c;不知…

手敲最基础C语言代码----“贪吃蛇”

C语言创作游戏----第二弹----贪吃蛇&#xff08;无限吃&#xff09; 主函数系列&#xff1a; 创建引入头文件----方便查看代码&#xff01;&#xff01; #include<stdio.h> #include<Windows.h> #include<stdlib.h> #include<conio.h> #include<tim…

创建链表和遍历链表算法演示

#include <stdio.h> #include <malloc.h> #include <string.h> #include <stdlib.h>typedef struct Node {int data; //数据域struct Node * pNext; //指针域}Node, *pNode;//函数声明 pNode create_list(); void traverse_list(pNode pHead); int…

C++ 创建链表

本文旨在解决两个问题&#xff1a; 1、如何写一个创建链表函数 2、为什么对于单个节点必须要new&#xff0c;而不能使用& 1、如何写一个创建链表函数 代码如下 ListNode* createListNode(vector<int> input) {ListNode dummy ListNode(-1);ListNode* pre &d…

单链表创建

单链表的创建与操作 链表作为基本的数据结构&#xff0c;学习好链表的创建与操作是数据结构入门的基础。 &#xff08;小白make for myself&#xff09; 单链表的创建 typedef struct Node {int data;struct Node* next; }Node;//结构体创建&#xff0c;也可以使用*Node取址…

动态链表的创建

#include <stdio.h> //List结构样式 typedef struct node { int data; struct node *next; }Node; //创建head的空链 Node *createList() { Node *head (Node *)malloc(sizeof(Node)); if(NULL head) exit(-1); head->next NULL; return head; } Node *insertList(…

C++创建一个链表

这个是在参加面试的时候遇到的题目&#xff0c;说句实话&#xff0c;我当时不懂。 后面查了资料&#xff0c;里面写的比较仔细就不多说了。 #include <iostream> using namespace std; struct node {int data;node* next;node(int data, node* next NULL) {this->d…

如何在Python中创建与使用链表(单链表)

如何在Python中创建与使用链表&#xff08;单链表&#xff09; 最近用Python语言在Leetcode中刷题&#xff0c;接触到不少关于链表的题&#xff0c;学校目前还没有开设数据结构的课程&#xff08;开设的话应该也是以C/C语言写的&#xff09;。 因为不太了解链表使用方式&#…

循环链表的创建

循环链表的创建以及基本操作 上篇我们讲了运用头插法和尾插法创建单链表的方法&#xff0c;和两种方法的比较。 接着我们学习循环链表的创建。 只要学会了单链表的创建&#xff0c;循环链表的创建就变得很简单。 循环链表创建 单链表的结构&#xff1a; 循环链表&#xff1a…

单链表的创建

单链表类型定义 单链表是由一串结点组成的&#xff0c;其中每个结点都包含指向下一个结点的指针&#xff0c;最后一个结点的指针为空&#xff1b; 假设结点只包括一个整数和指向下一结点的指针 typedef struct node{int data;struct node *next; }LNode,*LinkList; //LNode为…