Android三大架构(MVC、MVP、MVVM)笔记

发布时间:2025-06-24 16:42:30  作者:北方职教升学中心  阅读量:749


一 .Android架构

(一)MVC架构(已淘汰)

MVC架构的经典实现方式

1.什么是MVC架构

MVC的全称是Model-View-Controller,即模型-视图-控制器,它是MVC、MVP、MVVM这三者中最早产生的框架,其他两个框架是以它为基础发展而来的。

MVC的目的就是将M和V的代码分离,且MVC是单向通信,必须通过Controller来承上启下。

M(Model):主要处理数据的存储、获取、解析

V(View):即Fragement、Activity、View等XML文件

C(Controller):主要功能为控制View层数据的显示,通常与写在Activity类、Fragment类、View类中,通过接口与Modle层进行通信,并将数据显示到View上。

2.MVC架构的优缺点
(1)优点

实现了View层和Model层的分离,使得View层与Modle层独立,一个View可以连接多个Modle,一定程度上实现Modle的复用,且修改View层的代码不影响Modle层的,反之同理。代码易于维护和修改。

(2)缺点(在Android开发中的缺点)

MVC架构的Controller层的与Activity、Fragment、View等类写在一起,一旦代码逻辑复杂则会导致Activity、Fragment类臃肿冗余,难以维护。

(二)MVP架构

1.什么是MVP架构

MVP的全称为Model-View-Presenter,即模型-视图-表示器。它是为了解决MVC架构的缺点而产生的,将MVC架构中的Controller层的代码与View类分开,单独抽取为Presenter层,View层与Moudel层互相独立,通过中间层Presenter来进行通信。

M(模型):负责处理数据和业务逻辑的组件。模型独立于视图和表示器,用于处理数据的获取、存储、验证和操作等任务。

V(视图):应用程序的用户界面,负责显示数据和接收用户输入。视图通常是被动监听的,通过表示器接收来自Modle层的数据并将用户操作输入传递给表示器进行处理。例如Activity类、Fragment类、自定义View类、Adapter类,以及与Presenter层进行通信的接口也都会存放到View层中。

P(表示器):模型和视图之间的中间层,负责协调和处理交互的组件,用于控制View层的数据显示。表示器接收用户输入,通过模型获取数据,并通过接口将数据传递给视图进行显示。它还可以响应视图的事件,调用相应的模型方法来更新数据。

这三个组件共同工作,实现了模块化、可维护和可测试的代码结构,提供了更好的代码分离和职责分配。MVP架构在Android开发中被广泛使用,帮助开发人员构建结构清晰、可测试和易于扩展的应用程序。

MVP架构中最重要的点:自定义接口回调

因为View层和Presenter层之间的通信是通过接口来实现的,在请求Presenter层请求数据时,要确保Presenter层持有View层的引用,才能够在数据请求回来的时候调用View层中的方法,将数据通知回UI层。

2.MVP架构的优缺点
(1)优点:
  1. 将控制视图显示的代码单独抽取为Presnter层,解决了MVC架构中由于Controller层的代码与View类的代码混在一起导致View类代码臃肿冗余的缺点。

  2. 面向接口编程,便于多人协作开发

  3. 易于维护

(2)缺点:
  1. 由于View层与Presenter层之间、Presenter层与Moudel层之间都是通过接口来进行通信的,故而若项目逻辑复杂,会需要定义大量的接口。

  2. 需要考虑空指针异常的问题和内存泄漏的问题,由于MVP架构中的通信是通过自定义接口来持有某一方的引用实现通信的,故而需要考虑被持有引用的一方是否已经被销毁,需要判空,且在被持有引用的一方中的销毁的生命周期方法中取消另一方的引用持有避免内容泄漏。例如在Presenter层与View层进行通信时,在Presenter层拿到数据需要返回给View层时,需要考虑到此时用户是否已经退出了界面,使界面被销毁回收,故而需要在View类中的视图销毁的生命周期方法中取消其对应Presenter持有的引用(取消注册),避免内存泄漏,同时在Presenter层需对持有的View层的引用进行判空避免空指针异常。

  3. 若同一个地方需要用到同一个Presenter,可能会导致Presenter中的一些接口方法用不上,导致接口方法的浪费和代码冗余。例如在开发一个音乐APP时,完整播放页面和悬浮窗页面都需要用到同一个Presenter层来获取控制界面的显示,而完整界面需要显示的内容有播放的状态、歌曲的标题、歌曲的封面,故而需要在Presenter层中定义三个接口方法来获取并控制显示对应的控件,而悬浮窗页面只需要显示歌曲的播放状态这一个内容,此时若对同一个Presenter进行复用,就需要实现其余两个在此界面中无需用到的方法,导致接口方法的浪费和代码冗余。

(三)MVVM架构(Kotlin代码示例)

1.LifeCycle
(1)功能及作用

监听View类的生命周期,解决MVP架构中空指针异常和内存泄漏的问题。

(2)如何使用LifeCycle监听View类的生命周期

1.由于LifeCycle本身已经被Android项目中默认的多个依赖所依赖,故无需我们手动导入依赖就可以直接使用

2.监听方式:

由于lifecycle.addObserver()方法的参数类型为:LifecycleObserver接口

而此接口为空实现,故而监听方式有多种,若传入的参数为LifecycleObserver的对象,则需要使用以下第二种方式进行监听,否则没有意义,因为其为空实现;(但考虑到第二种方法已弃用,故不推荐使用此种方法)

又因为LifecycleObserver接口拥有子接口:LifecycleEventObserver 和 DefaultLifecycleObserver 。故可考虑传入这两个子接口作为参数。

LifecycleEventObserver 这个接口中有onStateChanged()方法专门用于监听View类的生命周期,故而可以使用此子接口作为参数,并重写onStateChanged()方法来实现监听,也就是以下的第一种方式

DefaultLifecycleObserver 这个接口中有对于各个生命周期的方法,可选择性进行重写以监听View类的生命周期,也就是以下第三种方式

1)直接在View类中使用lifecycle调用addObserver()方法并创建LifecycleEventObserver匿名内部类,重写其中的onStateChanged方法进行监听

class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)​        lifecycle.addObserver(object : LifecycleEventObserver{            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {                println(event.name)            }        })    }}

其中event.name表示的是View类的生命周期状态,owner表示的是被监听的View类本身,因为在Android中View类的底层都是实现了LifeCycleowner接口的。

2)在View类中使用lifecycle调用addObserve()方法并创建LifecycleObserver匿名内部类,在其中使用注解的方式监听特定的某个生命周期:使用@OnLifecycleEvent监听特定生命周期(反射)(由于使用的是反射,考虑到安全性问题,目前此方法已弃用)

lifecycle.addObserver(object : LifecycleObserver{            @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)            fun on_Create(){                println("anotation____onCreate")            }​            @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)            fun on_Resume(){                println("anotation____onCreate")            }        })

3)在View类中使用lifecycle调用addObserve()方法并创建DefaultLifecycleObserver匿名内部类,在其中重写需要监听的生命周期方法即可。

lifecycle.addObserver(object : DefaultLifecycleObserver{            override fun onCreate(owner: LifecycleOwner) {                println("DefaultLifecycleObserver____onCreate")            }        })

4)使用APT(自动生成的adapter类)实现(回调)。

接口适配器设计模式

可见,上述三种方法都是使用匿名内部类的形式实现生命周期监听的,是直接写在View类中来实现监听View类生命周期的,而此种方法不是,它是借助于ViewModle类的回调来实现的监听,使ViewModel类也可感知到View类的生命周期。使用此种方法,系统会自动生成一个对应的adapter类来实现回调监听,故而区别于匿名内部类的方式实现的

在MVVM架构中,每个View类都会有对应的ViewModel类,而ViewModel类也是需要感知View类生命周期的,故而我们可以让ViewModel类直接实现LifecycleEventObserver接口或DefaultLifecycleObserver接口,直接重写其中对应的监听生命周期方法即可实现监听。

以下以实现DefaultLifecycleObserver接口为例

//MainActivity中,直接传入对应ViewModle的对象即可,因为其已经实现了LifecycleObserver接口的子接口(多态)class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        //使用ADT回调实现对View类的监听        lifecycle.addObserver(MainViewModel())    }}​class MainViewModel:ViewModel(),DefaultLifecycleObserver {    override fun onCreate(owner: LifecycleOwner) {        println("ADT-----onCreate")    }​    override fun onStart(owner: LifecycleOwner) {        println("ADT-----onStart")    }}
(3)实际MVVM架构中是如何使用Lifecycle

一般会采用第二点实现方式中的第四种实现方法,但由于每个View类都会有对应的ViewModel类,故而若直接让每个View类对应的ViewModel类直接实现Lifecycle的子接口来实现监听,就会导致产生很多的冗余代码。因而在实际MVVM架构的使用过程中,我们一般会再抽取一个BaseViewModel类来实现Lifecycle的子接口,再使各个ViewModel类继承自BaseModel类;抑或是:在BaseViewModel的基础上再抽取出一个IBaseViewModel接口,再次简化抽取代码。

2.Databinding
(1)功能及作用

11)省略模式化代码,如:对每个控件进行findViewById()

因为使用DataBinding对布局控件进行数据绑定之后,会自动生成一个BR类,此类中含有对所有数据绑定变量的id,而控件与数据绑定变量是绑定的,故而也就无需再为了在界面中找到控件,而对每个控件声明id了

2)字段单向数据绑定(Model与View单向绑定:Model->View)

当Model层数据变化时,更新View层界面

3)字段双向数据绑定(Model与View双向绑定:Model<->View)

当Model数据变化时,同步到View层界面;

当View层界面数据变化时(如用户的界面输入),同步更新到Model层

(2)如何用Databinding实现数据绑定
1)开启Databinding依赖

①在bulid.gradle(Module:app)文件中的插件引入模块中引入kapt注解处理器:

plugins {    id 'com.android.application'    id 'org.jetbrains.kotlin.android'    id 'org.jetbrains.kotlin.kapt'    //注解处理器,要使用Databinding必须引入此插件}

②在bulid.gradle(Module:app)文件中的android模块中写入以下模块来开启dataBinding依赖:

android {    //...    //使用dataBinding,必须手写此模块,来开启依赖    buildFeatures{        dataBinding = true;    }    //...}
2)在需要绑定的布局文件中的第一行使用alt+enter快捷键,将布局转化为dataBinding布局,转化成功后形式如下:
<?xml version="1.0" encoding="utf-8"?><layout 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">        <!--编写与界面中的控件进行数据绑定的数据变量-->    <data>        //输入框控件数据绑定变量        <variable            name="account"            type="String" />        //按钮控件数据绑定变量        <variable            name="randomButton"            type="android.view.View.OnClickListener" />    </data>    <!--界面布局-->    <androidx.constraintlayout.widget.ConstraintLayout        <LinearLayout            android:id="@+id/layout1"            android:layout_width="0dp"            android:layout_height="wrap_content"            app:layout_constraintTop_toTopOf="parent"            app:layout_constraintStart_toStartOf="parent"            app:layout_constraintEnd_toEndOf="parent"            android:gravity="center_horizontal">​            <EditText                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_weight="8"                android:hint="账号"                //将控件与account变量进行绑定                android:text="@{account}"/>​            <Button                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_weight="2"                android:text="随机"                //将控件与randomButton变量进行绑定                android:onClick="@{randomButton}"/>        </LinearLayout>    </androidx.constraintlayout.widget.ConstraintLayout></layout>
3)使用Databinding对布局控件进行数据绑定之后,在View类中引入布局的方式(初始化)和改变数据绑定变量值的方式也要改变,有以下两种方法:

①通用方法:

var account:String = "admin"val binding = DataBindingUtil.inflate<ViewDataBinding>(            layoutInflater,            R.layout.activity_main,            null,            false        )//改变数据绑定变量的值binding.setVariable(BR.account,account)        binding.setVariable(BR.randomButton,View.OnClickListener { binding.setVariable(BR.account,"66666") })setContentView(binding.root)

②特定方法(每个View类都不同):(通常使用这种方式)

因为使用DataBinding之后每个xml文件都会生成对应的binding类,命名方式为:xml文件+Binding

var account:String = "admin"val binding = ActivityMainBinding.inflate(layoutInflater)//改变数据绑定变量的值binding.account = accountbinding.randomButton = View.OnClickListener {            binding.account = "hfsidf"        }setContentView(binding.root)

上述示例代码为一次性单向数据绑定,还不算严格意义上的单向数据绑定。

我们只是人为手动的为binding类中的变量进行了赋值,数据的来源并不是Modle层。

即View类中的本地变量(可视为Model层存储的数据)并没有与binding类中的数据变量(可视为界面显示的数据)进行绑定,当View类中的本地变量改变时(如account变量进行二次修改时),binding类中的数据绑定变量并不会发生变化,这样会导致界面中的数据无法二次更新,只能实现第一次的数据更新

在实际应用时,并不会采用这种一次性的绑定方式,而是会将本地变量包装成一个可观察的变量,以实现本地变量与binding类中的变量绑定的效果

4)单向数据绑定

基于上述代码进行修改:

①将View类中的本地变量包装为可供binding类观察的变量

var account = ObservableField("admin")//注意:此时account变量的类型已经改变了,不再是String类型了,而是ObservableField<String!>类型了,故还需将布局文件中的数据绑定变量类型进行修改

②修改布局文件中对应的数据绑定变量的变量类型

<data>        <variable    name="account"   //注意此处的:&lt;和&gt; 是代表泛型的符号<>         type="androidx.databinding.Observable&lt;String&gt;" />       </data>

③由于本地变量类型的改变,故而之后对其进行修改时,都不能直接赋值,而是需要调用其对应的set()方法进行修改。

基于上述方式修改,即可实现真正意义上的单向绑定了,之后直接修改本地变量,即可实现binding类中对应变量的数据修改了,即实现界面更新。

但当用户对界面的数据进行修改时,即修改了binding中的数据时,本地变量是不会改变的,若还想实现界面数据变化时通知本地数据变量,则需要实现双向数据绑定

5)双向数据绑定

实现双向数据绑定十分简单,只需要在基于单向数据绑定的基础上,修改控件与数据绑定变量的绑定方式即可(android:text="@={account}"多了一个“=”),示例代码如下:

<?xml version="1.0" encoding="utf-8"?><layout 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">        <!--编写与界面中的控件进行数据绑定的数据变量-->    <data>        //输入框控件数据绑定变量        <variable            name="account"            type="androidx.databinding.ObservableField&lt;String&gt;" />         //按钮控件数据绑定变量        <variable            name="randomButton"            type="android.view.View.OnClickListener" />    </data>    <!--界面布局-->    <androidx.constraintlayout.widget.ConstraintLayout        <LinearLayout            android:id="@+id/layout1"            android:layout_width="0dp"            android:layout_height="wrap_content"            app:layout_constraintTop_toTopOf="parent"            app:layout_constraintStart_toStartOf="parent"            app:layout_constraintEnd_toEndOf="parent"            android:gravity="center_horizontal">​            <EditText                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_weight="8"                android:hint="账号"                //将控件与account变量进行双向数据绑定,注意比单向数据绑定多了一个=                android:text="@={account}"/>​            <Button                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_weight="2"                android:text="随机"                //将控件与randomButton变量进行绑定                android:onClick="@{randomButton}"/>        </LinearLayout>    </androidx.constraintlayout.widget.ConstraintLayout></layout>

注意:

通常我们只会对会被用户修改的控件才会使用双向数据绑定(如EditText),因为其他使用没有意义

(3)实际MVVM架构中是如何使用Databinding的

在实际MVVM架构中,每个View类都会有对应的ViewModel类,而View层的逻辑也会放在ViewModel层实现,故而绑定数据的变量和方法也会放在ViewModel层中,示例代码如下:

MainActivity文件:

package com.example.mvvmdemo​class MainActivity : AppCompatActivity() {​    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)​        //设置布局文件和修改变量的方式2        val binding = ActivityMainBinding.inflate(layoutInflater)        //创建并为布局文件中的viewmodel变量赋值        //binding.viewmodle = MainViewModel()//也可采用这种方式创建ViewModel类的对象,但不推荐,因为这种方式创建的ViewModel不可复用        binding.viewmodle = ViewModelProvider(this).get(MainViewModel::class.java)        //引入布局文件        setContentView(binding.root)​         }​}

MainViewModel文件:

package com.example.mvvmdemo​class MainViewModel:BaseViewModel() {    //数据绑定变量    //这里最好是使用val数据类型,因为使用Observable要尽量避免开箱或封箱操作,在Java里声明也应该是pubc final属性    //Observable的实现类里都提供了get()和set()函数来修改具体的值,故无需担心定义为val类型无法修改变量    val account = ObservableField("admin")    val password = ObservableField("123")​    //按钮控件绑定的方法    fun randomCLick(view:View){        account.set("6666666")    }​    fun login(view:View){        if(account.get().equals("admin") && password.get().equals("123")){            println("登陆成功")        }        println(account.get())    }​}

activity_main文件:

<?xml version="1.0" encoding="utf-8"?><layout 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">    <data>        //只需要绑定对应的ViewModel即可        <variable            name="viewmodle"            type="com.example.mvvmdemo.MainViewModel" />    </data>​    <androidx.constraintlayout.widget.ConstraintLayout        android:layout_width="match_parent"        android:layout_height="match_parent"        tools:context=".MainActivity">​        <LinearLayout            android:id="@+id/layout1"            android:layout_width="0dp"            android:layout_height="wrap_content"            app:layout_constraintTop_toTopOf="parent"            app:layout_constraintStart_toStartOf="parent"            app:layout_constraintEnd_toEndOf="parent"            android:gravity="center_horizontal">​            <EditText                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_weight="8"                android:hint="账号"                //改为绑定对应的ViewModel中的变量                android:text="@={viewmodle.account}"/>​            <Button                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_weight="2"                android:text="随机"                 //改为绑定对应的ViewModel中的方法                android:onClick="@{viewmodle.randomCLick}"/>        </LinearLayout>​        <LinearLayout            android:id="@+id/layout2"            android:layout_width="0dp"            android:layout_height="wrap_content"            app:layout_constraintStart_toStartOf="parent"            app:layout_constraintEnd_toEndOf="parent"            android:gravity="center_horizontal"            app:layout_constraintTop_toBottomOf="@id/layout1">​            <EditText                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:hint="密码"                android:text="@{viewmodle.password}"/>        </LinearLayout>​        <Button            android:layout_width="0dp"            android:layout_height="wrap_content"            android:layout_weight="2"            android:text="登录"            app:layout_constraintTop_toBottomOf="@id/layout2"            app:layout_constraintStart_toStartOf="parent"            app:layout_constraintEnd_toEndOf="parent"            android:onClick="@{viewmodle.login}"/>​    </androidx.constraintlayout.widget.ConstraintLayout></layout>
(4)其他操作(自行了解吧~~~我也不是很熟悉哈哈哈)
1)绑定内容表达式支持

在布局文件中控件绑定变量的格式:@{} 其实是一个表达式,其中可以支持多种运算包括:==、>、<、>=、<=、?:、!= 、??、&&、||、!、&、|、~、instanceof、<<、>>、>>>、() + - * /

注意,若在@{}表达式中需要用到字符串的拼接应该使用以下格式:

android:text='@{"account" + viewmodle.account}''//外面为单引号,里面为双引号
2)导入其他类

在转化为databinding格式后的布局文件中的data模块中使用import关键字可以导入其他类,用于绑定内容表达式中。如:若绑定内容表达式中需要使用到InputType类中的某个属性,则需要导入此类

3)在布局中引入其他布局

f使用include关键字即可

3.Viewbinding
(1)ViewBInding的作用

使用ViewBinding之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类(类名命名规则为:xml文件名+Binding)。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用,无需对每个控件进行findViewById()来找到控件,简化视图绑定的过程代码。

(2)如何使用Viewbinding进行视图绑定

1)在build.gradle(Module:app)里加入如下配置,开启Viewbinding依赖

android {    ...    buildFeatures {        viewBinding true    }}

2)在对应的View类中引入布局(初始化)和修改控件数据的方式

与Databinding的第二套引入布局和修改控件数据方式相似

class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        //通过自动生成的Binding类引入布局并设置布局        val binding = ActivityMainBinding.inflate(layoutInflater)        setContentView(binding.root)        //为控件设置点击事件        binding.btn.setOnClickListener {            Toast.makeText(this, "click", Toast.LENGTH_SHORT).show()        }        //为控件设置数据        binding.tv.text = "setText"    }}
(3)ViewBinding与Databinding的区别!!!!

1)包含关系:

DataBinding也有ViewBinding的功能,也可以省去findViewById()方法。

2)目的不同:

①Viewbinding只是单纯的为了绑定视图,省略findViewById的步骤的;

②Databinding是为了绑定界面中的数据而存在的,使用Databinding能够把视图的数据和代码变量绑定起来,并且实现自动更新。这个特性使得DataBinding能和MVVM框架进行很好的配合。

3)初始化方式不同

①Databinding有两种初始化方式:通用方式和特定方式

②Viewbinding只有一种方式

4.ViewModel
(1) ViewModel类的作用:

在MVVM架构中每个View类都会有对应的ViewaModel类,甚至会出现一对多或多对一的情况。(如微信中的一个界面中有多个Fragment,则这多个Fragment需要共享一个ViewModel的数据)。

ViewModel类一般用于解决以下问题:

  1. 处理View类的业务逻辑

  2. View类的数据保存和处理

  3. 多个View类之间的数据共享(少用)

  4. 旋转屏幕后无需再重新进行网络请求刷新数据或数据库读取,而是直接复用ViewModel类缓存的即可

(2)如何创建ViewModel类:

自定义ViewModel类一般继承自ViewModel类或者AndroidViewModel类,这两者不同的是,AndroidViewModel类没有空构造方法,必须实现其对应的构造方法

(3)如何创建ViewModel类的对象:
  • 直接创建对象 (不推荐使用)

    var manViewModel:MainViewModel = MainViewModel()

  • 使用ViewModelProvider类中的get()方法创建对象(推荐使用)

    var manViewModel:MainViewModel =ViewModelProvider(this).get(MainViewModel::class.java)

    上述两种创建方法的区别:

    在实际应用中我们一般会采取第二种创建方法,因为采用第二种创建方法时,会在调用get()方法时对ViewModel对象进行缓存,故而当我们进行旋转屏幕等操作后,不用再重新创建ViewModel对象,而是直接复用缓存中的ViewModel对象。而第一种方式不能起到缓存作用。

    有什么不对的地方欢迎指正~~~