Jetpack MVVM - Android架构探索!

一  开发架构 是什么?

我们先来理解开发架构的本质是什么,维基百科对软件架构的描述如下:

软件架构是一个系统的草图。软件架构描述的对象是直接构成系统的抽象组件。各个组件之间的连接则明确和相对细致地描述组件之间的通讯。在实现阶段,这些抽象组件被细化为实际的组件,比如具体某个类或者对象。在面向对象领域中,组件之间的连接通常用接口来实现。

拆分开来就是三条:

  1. 针对的是一个完整系统,此系统可以实现某种功能。
  2. 系统包含多个模块,模块间有一些关系和连接。
  3. 架构是实现此系统的实施描述:模块责任、模块间的连接。

为啥要做开发架构设计呢?

  1. 模块化责任具体化,使得每个模块专注自己内部
  2. 模块间的关联简单化,减少耦合
  3. 易于使用、维护性好
  4. 提高开发效率

架构模式最终都是 服务于开发者。如果代码职责和逻辑混乱,维护成本就会相应地上升。

宏观上来说,开发架构是一种思想,每个领域都有一些成熟的架构模式,选择适合自己项目即可。

二  Android开发中的架构

具体到Android开发中,开发架构就是描述 视图层逻辑层数据层 三者之间的关系和实施:

  • 视图层:用户界面,即界面的展示、以及交互事件的响应。
  • 逻辑层:为了实现系统功能而进行的必要逻辑。
  • 数据层:数据的获取和存储,含本地、server。

正常的开发流程中,开始写代码之前 都会有架构设计这一过程。这就需要你选择使用何种架构模式了。

我的Android开发之路完整地经过了 MVC、MVP、MVVM,相信很多开发者和我一样都是这样一个过程,先来回顾下三者。

2.1 MVC

MVC,Model-View-Controller,职责分类如下:

  • Model,模型层,即数据模型,用于获取和存储数据。
  • View,视图层,即xml布局
  • Controller,控制层,负责业务逻辑。

View层 接收到用户操作事件,通知到 Controller 进行对应的逻辑处理,然后通知 Model去获取/更新数据,Model 再把新的数据 通知到 View 更新界面。这就是一个完整 MVC 的数据流向。

但在Android中,因为xml布局能力很弱,View的很多操作是在Activity/Fragment中的,而业务逻辑同样也是写在Activity/Fragment中

所以,MVC 的问题点 如下:

  1. Activity/Fragment 责任不明,同时负责View、Controller,就会导致其中代码量大,不满足单一职责。
  2. Model耦合View,View 的修改会导致 Controller 和 Model 都进行改动,不满足最少知识原则。

2.2 MVP

MVP,Model-View-Presenter,职责分类如下:

  • Model,模型层,即数据模型,用于获取和存储数据。
  • View,视图层,即Activity/Fragment
  • Presenter,控制层,负责业务逻辑。

MVP解决了MVC的问题:1.View责任明确,逻辑不再写在Activity中,而是在Presenter中;2.Model不再持有View。

View层 接收到用户操作事件,通知到Presenter,Presenter进行逻辑处理,然后通知Model更新数据,Model 把更新的数据给到Presenter,Presenter再通知到 View 更新界面。

MVP的实现思路:

  • UI逻辑抽象成IView接口,由具体的Activity实现类来完成。且调用Presenter进行逻辑操作。
  • 业务逻辑抽象成IPresenter接口,由具体的Presenter实现类来完成。逻辑操作完成后调用IView接口方法刷新UI。

MVP 本质是面向接口编程,实现了依赖倒置原则。MVP解决了View层责任不明的问题,但并没有解决代码耦合的问题,View和Presenter之间相互持有。

所以 MVP 有问题点 如下:

  1. 会引入大量的IView、IPresenter接口,增加实现的复杂度。
  2. View和Presenter相互持有,形成耦合。

2.3 MVVM

MVVM,Model-View-ViewModel,职责分类如下:

  • Model,模型层,即数据模型,用于获取和存储数据。
  • View,视图,即Activity/Fragment
  • ViewModel,视图模型,负责业务逻辑。

注意,MVVM这里的ViewModel就是一个名称,可以理解为MVP中的Presenter。不等同于上一篇中的 ViewModel组件 ,Jetpack ViewModel组件是 对 MVVM的ViewModel 的具体实施方案。

MVVM 的本质是 数据驱动,把解耦做的更彻底,viewModel不持有view 。

View 产生事件,使用 ViewModel进行逻辑处理后,通知Model更新数据,Model把更新的数据给ViewModel,ViewModel自动通知View更新界面而不是主动调用View的方法


MVVM在Android开发中是如何实现的呢?接着看~

到这里你会发现,所谓的架构模式本质上理解很简单。比如MVP,甚至你都可以忽略这个名字,理解成 在更高的层面上 面向接口编程,实现了 依赖倒置 原则,就是这么简单。

三  MVVM 的实现 - Jetpack MVVM

前面提到,架构模式选择适合自己项目的即可。话虽如此,但Google官方推荐的架构模式 是适合大多数情况,是非常值得我们学习和实践的。

好了,下面我们就来详细介绍 Jetpack MVVM 架构。

3.1 Jetpack MVVM 理解

Jetpack MVVM 是 MVVM 模式在 Android 开发中的一个具体实现,是 Android中 Google 官方提供并推荐的 MVVM实现方式。

不仅通过数据驱动完成彻底解耦,还兼顾了 Android 页面开发中其他不可预期的错误,例如Lifecycle 能在妥善处理 页面生命周期 避免view空指针问题,ViewModel使得UI发生重建时 无需重新向后台请求数据,节省了开销,让视图重建时更快展示数据。

首先,请查看下图,该图显示了所有模块应如何彼此交互:

各模块对应MVVM架构:

  • View层:Activity/Fragment
  • ViewModel层:Jetpack ViewModel + Jetpack LivaData
  • Model层:Repository仓库,包含 本地持久性数据 和 服务端数据

View层 包含了我们平时写的Activity/Fragment/布局文件等与界面相关的东西。

ViewModel层 用于持有和UI元素相关的数据,以保证这些数据在屏幕旋转时不会丢失,并且还要提供接口给View层调用以及和仓库层进行通信。

仓库层 要做的主要工作是判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获取到的数据返回给调用方。本地数据源可以使用数据库、SharedPreferences等持久化技术来实现,而网络数据源则通常使用Retrofit访问服务器提供的Webservice接口来实现。

另外,图中所有的箭头都是单向的,例如View层指向了ViewModel层,表示View层会持有ViewModel层的引用,但是反过来ViewModel层却不能持有View层的引用。除此之外,引用也不能跨层持有,比如View层不能持有仓库层的引用,谨记每一层的组件都只能与它相邻层的组件进行交互。

这种设计打造了一致且愉快的用户体验。无论用户上次使用应用是在几分钟前还是几天之前,现在回到应用时都会立即看到应用在本地保留的数据。如果此数据已过期,则应用的Repository将开始在后台更新数据。

3.2 实施

我们来举个完整的例子 - 在当用户更新笔记时, 笔记列表进行更新UI,来说明 Jetpack MVVM 的具体实施。

3.2.1 Model层

@Database(entities = {EntityNote.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
    private static final String DB_NAME = "note.db";
    private static volatile AppDatabase instance;//创建单例

    public static synchronized AppDatabase getInstance() {
        if (instance == null) {
            instance = create();
        }
        return instance;
    }

    /**
     * 创建数据库
     */
    private static AppDatabase create() {
        return Room.databaseBuilder(MyApplication.getInstance(), AppDatabase.class, DB_NAME)
                .allowMainThreadQueries()
                .fallbackToDestructiveMigration()
                .build();
    }

    public abstract NoteDao noteDao();
}
@Dao
public interface NoteDao {
    @Query("select * from note")
    LiveData<List<EntityNote>> getAll();

    @Update
    int update(EntityNote note);

    @Delete
    int delete(EntityNote note);

    @Insert
    void insert(EntityNote note);
}
@Entity(tableName = "note")
public class EntityNote {
    @PrimaryKey(autoGenerate = true) // PrimaryKey 主键;autoGenerate自增长
    public int id;

    //下面定义表中包含的列(字段)
    @ColumnInfo(name = "uuid") // ColumnInfo 列信息
    public String uuid;

    @ColumnInfo(name = "title")
    public String title;

    @ColumnInfo(name = "content")
    public String content;

    @ColumnInfo(name = "searchContent")
    public String searchContent;
}

3.2.2 ViewModel 层

// 继承AndroidViewModel,带有Application环境
public class NoteViewModel extends AndroidViewModel {
    private MediatorLiveData<List<EntityNote >> mMediatorLiveData;

    public NoteViewModel(@NonNull Application application) {
        super(application);
        mMediatorLiveData = new MediatorLiveData<>();
        LiveData<List<EntityNote>> EntityNoteLiveData = AppDatabase.getInstance().noteDao().getAll();
        mMediatorLiveData.addSource(EntityNoteLiveData, new Observer<List<EntityNote>>() {
            private List<EntityNote> mLastEntityNoteList;

            @Override
            public void onChanged(List<EntityNote> entityNotes) {
                if (mLastEntityNoteList == null) {
                    mLastEntityNoteList = entityNotes;
                    return;
                }
                if (entityNotes == null) {
                    setValue(new ArrayList<>());
                    return;
                }
                int lastSize = mLastEntityNoteList.size();
                int size = entityNotes.size();
                if (lastSize != size) {
                    setValue(entityNotes);
                    return;
                }
                for (int i = 0; i < size; i++) {
                    EntityNote lastNote = mLastEntityNoteList.get(i);
                    EntityNote note = entityNotes.get(i);
                    if (!isSameNote(lastNote, note)) {
                        setValue(entityNotes);
                        break;
                    }
                }
                // 没有变化不setValue不触发onChanged
                mLastEntityNoteList = entityNotes;
            }

            private void setValue(List<EntityNote> entityNotes) {
                mMediatorLiveData.setValue(entityNotes);
                mLastEntityNoteList = entityNotes;
            }

            private boolean isSameNote(EntityNote first, EntityNote second) {
                if (first == null || second == null) {
                    return false;
                }
                return first.uuid.equals(second.uuid) && first.title.equals(second.title)
                        && first.id == second.id && first.content.equals(second.content);
            }

        });
    }

    //查(所有)
    public MediatorLiveData<List<EntityNote>> getNoteListLiveData() {
        return mMediatorLiveData;
    }
}

3.2.3 View层

public class NoteActivity extends AppCompatActivity {
    private NoteViewModel mViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mViewModel = new ViewModelProvider.AndroidViewModelFactory(MyApplication.getInstance()).
                create(NoteViewModel.class);
        mViewModel.getNoteListLiveData().observe(this, new Observer<List<EntityNote>>() {
            @Override
            public void onChanged(List<EntityNote> entityNotes) {
                // 更新UI
            }
        });
    }
}

3.3 注意点

  1. 在应用的各个模块之间设定明确定义的职责界限

  2. ViewModel 不能持有 View层引用,包括Context也不能持有。

  3. 将一个数据源指定为单一可信来源。 每当需要访问数据时,都应一律源于此单一可信来源。 例如 UserRepository会将网络服务响应保存在数据库中。这样一来,对数据库的更改将触发对活跃 LiveData 对象的回调。数据库会充当单一可信来源

  4. 保留尽可能多的相关数据和最新数据。 这样,即使用户的设备处于离线模式,他们也可以使用您应用的功能。请注意,并非所有用户都能享受到稳定的高速连接。

  5. 显示页面状态。 例如例子中的加载进度条,就是观察 ViewModel中的MutableLiveData loadingLiveData 进行操作的。

3.4 MVP改造MVVM

了解了Jetpack MVVM的实现,再来改造 MVP 是很简单的了。

步骤如下:

  1. 去除Presener 对View、context的引用。
  2. Presener 替换成ViewModel的实现,获取的数据以 LivaData呈现
  3. 删除定义的IView等接口,Activity/Fragment中 获取ViewModel实例,调用其方法获取数据。
  4. Activity/Fragment 观察需要的 LivaData 然后刷新UI

这样就已经成为了MVVM。当然也要检查下 原MVP的 Model层的实现,是否满足上面的要求。


 

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

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

相关文章

选择算法之冒泡排序【图文详解】

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 博主主页&#xff1a;LiUEEEEE                        …

Java——变量

一、变量介绍 变量就是申请内存来存储值。也就是说&#xff0c;当创建变量的时候&#xff0c;需要在内存中申请空间。内存管理系统根据变量的类型为变量分配存储空间&#xff0c;分配的空间只能用来储存该类型数据。 1、变量声明和初始化 变量的声明&#xff1a; int a; i…

2021JSP普及组第三题:插入排序

2021JSP普及组第三题 题目&#xff1a; 思路&#xff1a; 题目要求排序后根据操作进行对应操作。 操作一需要显示某位置数据排序后的位置&#xff0c;所以需要定义结构体数组储存原数据的位置和数据本身排序后所得数据要根据原位置输出排序后的位置&#xff0c;所以建立一个新…

字典树,AcWing 5726. 连续子序列

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 5726. 连续子序列 - AcWing题库 二、解题报告 1、思路分析 字典树存储前缀和 考虑边遍历计算前缀和&#xff0c;边查询字典树 查询流程&#xff1a; 记当前前缀和为s 如果当前位k为1&#xff0c;那么s …

Qt6 mathgl数学函数绘图

1. 程序环境 Qt6.5.1, mingw11.2mathgl 8.0.1: https://sourceforge.net/projects/mathgl/,推荐下载mathgl-8.0.LGPL-mingw.win64.7z,Windows环境尝试自己编译mathgl会缺失一些库,补充完整也可以自己编译,路径"D:\mathgl-8.0.LGPL-mingw.win64\bin"添加至系统环境…

关于Golang中自定义包的简单使用-Go Mod

1. go env 查看 GO111MODULE 是否为 on&#xff0c;不是修改成on go env -w GO111MODULEon 2 .自定义包的目录格式 3. test.go 内容 package calc func Add(x, y int) int { // 首字母大写表示公有方法return x y }func Sub(x, y int) int {return x - y } 4.生成calc目…

RedisSearch与Elasticsearch:技术对比与选择指南

码到三十五 &#xff1a; 个人主页 数据时代&#xff0c;全文搜索已经成为许多应用程序中不可或缺的一部分。RedisSearch和Elasticsearch是两个流行的搜索解决方案&#xff0c;它们各自具有独特的特点和优势。本文简单探讨一些RedisSearch和Elasticsearch之间的技术差异。 目录…

AndroidStudio使用高德地图API获取手机定位

一、高德地图API申请 首先去高德注册开发者账号 下面这两个选项&#xff0c;也是我们项目成功的关键 1.1怎么获取SHA1指纹密码 ①使用AS自带的签名文件 你的用户文件下面会有一个.android文件夹,进入文件夹,在这个路径下打开cmd 如果.android下面没有签名文件参考创建文章 …

CSS Canvas鼠标点击特效之天女散花(文本粒子动画)

1.效果 2.代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><style>body,html {margin: 0;padding: 0;wi…

一个班有n个学生,需要把每个学生的简单材料(姓名和学号)输入计算机保存。然后可以通过输入某一学生的姓名查找其有关资料。

当输入一个姓名后&#xff0c;程序就查找该班中有无此学生&#xff0c;如果有&#xff0c;则输出他的姓名和学号&#xff0c;如果查不到&#xff0c;则输出"本班无此人"。 为解此问题&#xff0c;可以分别编写两个函数&#xff0c;函数input_data用来输人n个…

Spring系统学习 - Spring入门

什么是Spring&#xff1f; Spring翻译过来就是春天的意思&#xff0c;字面意思&#xff0c;冠以Spring的意思就是想表示使用这个框架&#xff0c;代表程序员的春天来了&#xff0c;实际上就是让开发更加简单方便&#xff0c;实际上Spring确实做到了。 官网地址&#xff1a;ht…

vmdx 文件如何打开

如果在网上下载到了 vmdx 文件要如何使用呢 首先开启 vmware 新建一个虚拟机 选择高级模式 选择你要给他的配置 在选择磁盘文件的时候,找到这个vmdx 如果要升级的话最好选择【不升级】 然后就可以用了

倪师哲学。不要浪费时间去做无谓的事情

再比如你像我拍这个视频&#xff0c;在我的视频下方啊&#xff0c;评论区啊&#xff0c;经常性的会有一些人去评论&#xff0c;一些那种不痛不痒的事&#xff0c;我给你找几条&#xff0c;你看一下就知道了. 就比如&#xff0c;今天早上&#xff0c;我翻到倪海夏老师的第一篇图…

如何获取SSL证书,消除网站不安全警告

获取SSL证书通常涉及以下几个步骤&#xff1a; 选择证书颁发机构&#xff08;CA&#xff09;&#xff1a; 你需要从受信任的SSL证书颁发机构中选择一个&#xff0c;比如DigiCert、GlobalSign、JoySSL等。部分云服务商如阿里云、腾讯云也提供免费或付费的SSL证书服务。 生成证…

命运方舟台服注册 命运方舟台服怎么注册?不会操作看这里

命运方舟台服注册 命运方舟台服怎么注册&#xff1f;不会操作看这里 命运方舟作为今年备受瞩目的一款MMORPG类型游戏&#xff0c;在上线前的预约数量已经一次又一次创下新高。这款游戏的开发商Smile gate真是给玩家们带来了一款让人眼前一亮的作品。游戏创建在虚幻引擎的基础…

为什么要学习数据结构和算法

前言 控制专业转码学习记录&#xff0c;本科没学过这门课&#xff0c;但是要从事软件行业通过相关面试笔试基础还是要打牢固的&#xff0c;所以通过写博客记录一下。 必要性 1.越是厉害的公司&#xff0c;越是注重考察数据结构与算法这类基础知识 2.作为业务开发&#xff0c…

40号渐变灰色背景证件照要求,手机拍照轻松拍干部照片

灰色渐变背景的证件照是一种常见的照片类型&#xff0c;在干部档案、事业单位工作人员信息采集、履历及升迁公示等阶段会用到&#xff0c;按照规范需要使用40号渐变灰色背景。很多朋友不清楚40号灰色是哪种灰色&#xff0c;以及照片的尺寸要求&#xff0c;下面就重点介绍40号渐…

一篇文章讲透数据结构之树

一.树 1.1树的定义 树是一种非线性的数据结构&#xff0c;它是有n个有限结点组成的一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根在上&#xff0c;叶在下的。 在树中有一个特殊的结点&#xff0c;称为根结点&#xff0c;根结点…

vue3可以快速简单的操作dom元素了

再也不需要用document.getElementById("myElement")的这种方式来对dom元素进行操作了 我们需要使用模板引用——也就是指向模板中一个 DOM 元素的 ref。我们需要通过这个特殊的 ref attribute 来实现模板引用&#xff1a; <script setup> import { ref, onMo…

【linux】docker安装下载器:aria2、gopeed、thunder迅雷

一、aria2 1、下载aria2服务镜像 docker pull p3terx/aria2-pro 2、下载ariang页面服务 docker pull p3terx/ariang 3、启动aria2服务 docker run -d --name aria2 \ --restart unless-stopped \ --log-opt max-size1m \ -e PUID$UID \ -e PGID$GID \ -e UMASK_SET022 \ -…