Android | Android应用架构之MVVM模式_mvvm框架 android-程序员宅基地

技术标签: android  Android架构  

前言

早期的Android应用开发中,Activity/Fragment承担了过多的职责,它们不仅负责了应用界面的显示,而且负责了业务逻辑的处理。这样一来,Activity/Fragment很容易就变得臃肿、复杂,造成应用难以测试、维护和扩展。随着Android应用开发技术的不断发展和成熟,Android应用架构的设计得到了越来越多开发人员的关注和重视。目前,Android的应用架构主要有MVC、MVP和MVVM模式,本文将介绍一下MVVM模式。

相关知识

学习项目

MVP模式

MVVM模式可以说是MVP模式的进一步发展,所以先来了解一下MVP模式。

MVP (Model-View-Presenter) 模式的结构如下图所示:

MVP模式.png

MVP模式将应用分为三层:Model层主要负责数据的提供,View层主要负责界面的显示,Presenter层主要负责业务逻辑的处理。

在MVP模式中,Model层和View层不能直接通信,Presenter层负责充当中间人,实现Model层和View层之间的间接通信。View层和Presenter层互相持有对方的引用,实现View层和Presenter层之间的通信。

MVP模式的主要优点是:分离了Model层和View层,分离了视图操作和业务逻辑,降低了耦合。

MVVM模式

MVVM (Model-View-ViewModel) 模式的结构如下图所示:

MVVM模式.png

MVVM模式与MVP模式一样,也将应用分为三层,并且各个对应的层的职责相似:

  • Model层,主要负责数据的提供。Model层提供业务逻辑的数据结构(比如,实体类),提供数据的获取(比如,从本地数据库或者远程网络获取数据),提供数据的存储。
  • View层,主要负责界面的显示。View层不涉及任何的业务逻辑处理,它持有ViewModel层的引用,当需要进行业务逻辑处理时通知ViewModel层。
  • ViewModel层,主要负责业务逻辑的处理。ViewModel层不涉及任何的视图操作。通过官方提供的Data Binding库,View层和ViewModel层中的数据可以实现绑定,ViewModel层中数据的变化可以自动通知View层进行更新,因此ViewModel层不需要持有View层的引用。ViewModel层可以看作是View层的数据模型和Presenter层的结合。

MVVM模式与MVP模式最大的区别在于:ViewModel层不持有View层的引用。这样进一步降低了耦合,View层代码的改变不会影响到ViewModel层。

MVVM模式相对于MVP模式主要有如下优点:

  • 进一步降低了耦合。ViewModel层不持有View层的引用,当View层发生改变时,只要View层绑定的数据不变,那么ViewModel层就不需要改变。而在MVP模式下,当View层发生改变时,操作视图的接口就要进行相应的改变,那么Presenter层就需要修改了。
  • 不用再编写很多样板代码。通过官方的Data Binding库,UI和数据之间可以实现绑定,不用再编写大量的findViewById()和操作视图的代码了。总之,Activity/Fragment的代码可以做到相当简洁。

例子

下面举一个简单的例子来实践MVVM模式。完整的项目代码可以去GitHub上查看:

https://github.com/chongyucaiyan/MVVMDemo

例子实现的主要功能是:点击按钮网络查询天气,查询成功后在界面上显示天气信息。主界面如下图所示:

MVVMDemo界面.png

MVVM模式的代码组织结构建议按照 业务功能 进行划分,具体操作是:每个业务功能独立一个包存放,每个业务功能包下面再按Model、View、ViewModel分包存放。所有的Model存放在model包下面,所有的Activity和Fragment存放在activity包下面,所有的ViewModel存放在viewmodel包下面。该例子比较简单,只有一个weather业务功能模块,最终的代码组织结构如下图所示:

MVVMDemo代码组织结构.png

编写Model

查询杭州天气的URL为:

http://www.weather.com.cn/data/cityinfo/101210101.html

访问该URL将返回一串JSON字符串,如下所示:

{"weatherinfo":{"city":"杭州","cityid":"101210101","temp1":"5℃","temp2":"20℃","weather":"晴转多云","img1":"n0.gif","img2":"d1.gif","ptime":"18:00"}}

按照此JSON字符串,可以编写相应的实体类。WeatherData类的代码如下所示:

public class WeatherData {
    

    private WeatherInfo weatherinfo;

    public WeatherInfo getWeatherinfo() {
    
        return weatherinfo;
    }

    public void setWeatherinfo(WeatherInfo weatherinfo) {
    
        this.weatherinfo = weatherinfo;
    }
}

WeatherInfo类的代码如下所示:

public class WeatherInfo {
    

    private String city;

    private String cityid;

    private String temp1;

    private String temp2;

    private String weather;

    private String img1;

    private String img2;

    private String ptime;

    public String getCity() {
    
        return city;
    }

    public void setCity(String city) {
    
        this.city = city;
    }

    public String getCityid() {
    
        return cityid;
    }

    public void setCityid(String cityid) {
    
        this.cityid = cityid;
    }

    public String getTemp1() {
    
        return temp1;
    }

    public void setTemp1(String temp1) {
    
        this.temp1 = temp1;
    }

    public String getTemp2() {
    
        return temp2;
    }

    public void setTemp2(String temp2) {
    
        this.temp2 = temp2;
    }

    public String getWeather() {
    
        return weather;
    }

    public void setWeather(String weather) {
    
        this.weather = weather;
    }

    public String getImg1() {
    
        return img1;
    }

    public void setImg1(String img1) {
    
        this.img1 = img1;
    }

    public String getImg2() {
    
        return img2;
    }

    public void setImg2(String img2) {
    
        this.img2 = img2;
    }

    public String getPtime() {
    
        return ptime;
    }

    public void setPtime(String ptime) {
    
        this.ptime = ptime;
    }
}

编写ViewModel

ViewModel不涉及任何的视图操作,只进行业务逻辑的处理。通过官方提供的Data Binding库,当ViewModel中的数据发生变化时,UI将自动更新。QueryWeatherViewModel的代码如下所示:

public class QueryWeatherViewModel {
    

    private static final String TAG = "QueryWeatherViewModel";

    public final ObservableBoolean loading = new ObservableBoolean(false);

    public final ObservableBoolean loadingSuccess = new ObservableBoolean(false);

    public final ObservableBoolean loadingFailure = new ObservableBoolean(false);

    public final ObservableField<String> city = new ObservableField<>();

    public final ObservableField<String> cityId = new ObservableField<>();

    public final ObservableField<String> temp1 = new ObservableField<>();

    public final ObservableField<String> temp2 = new ObservableField<>();

    public final ObservableField<String> weather = new ObservableField<>();

    public final ObservableField<String> time = new ObservableField<>();

    private Call<WeatherData> mCall;

    public QueryWeatherViewModel() {
    

    }

    public void queryWeather() {
    
        loading.set(true);
        loadingSuccess.set(false);
        loadingFailure.set(false);

        mCall = RetrofitManager.get()
                .create(QueryWeatherRequest.class)
                .queryWeather();
        mCall.enqueue(new Callback<WeatherData>() {
    

            @Override
            public void onResponse(Call<WeatherData> call, Response<WeatherData> response) {
    
                WeatherInfo weatherInfo = response.body().getWeatherinfo();
                city.set(weatherInfo.getCity());
                cityId.set(weatherInfo.getCityid());
                temp1.set(weatherInfo.getTemp1());
                temp2.set(weatherInfo.getTemp2());
                weather.set(weatherInfo.getWeather());
                time.set(weatherInfo.getPtime());

                loading.set(false);
                loadingSuccess.set(true);
            }

            @Override
            public void onFailure(Call<WeatherData> call, Throwable t) {
    
                if (call.isCanceled()) {
    
                    Log.i(TAG, "call is canceled.");
                } else {
    
                    loading.set(false);
                    loadingFailure.set(true);
                }
            }
        });
    }

    public void cancelRequest() {
    
        if (mCall != null) {
    
            mCall.cancel();
        }
    }
}

编写View

View不涉及任何的业务逻辑处理,只进行界面的显示。在xml布局文件中,通过官方提供的Data Binding库,将UI与ViewModel中的数据进行绑定,当ViewModel中的数据发生变化时,UI将自动更新。xml布局文件的代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <import type="android.view.View" />

        <variable
            name="viewModel"
            type="com.github.cyc.mvvmdemo.weather.viewmodel.QueryWeatherViewModel" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/default_content_padding"
        tools:context="com.github.cyc.mvvmdemo.weather.activity.QueryWeatherActivity">

        <Button
            android:id="@+id/btn_query_weather"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:text="@string/query_weather"
            android:enabled="@{viewModel.loading ? false : true}"
            android:onClick="@{() -> viewModel.queryWeather()}" />

        <RelativeLayout
            android:id="@+id/vg_weather_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/btn_query_weather"
            android:layout_marginTop="@dimen/query_weather_margin"
            android:visibility="@{viewModel.loadingSuccess ? View.VISIBLE : View.GONE}">

            <TextView
                android:id="@+id/tv_city"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textStyle="bold"
                android:text="@string/city" />

            <TextView
                android:id="@+id/tv_city_value"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/tv_city"
                android:layout_alignBottom="@id/tv_city"
                android:text="@{viewModel.city}"
                tools:text="杭州" />

            <TextView
                android:id="@+id/tv_city_id"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/tv_city"
                android:layout_marginTop="@dimen/query_weather_margin"
                android:textStyle="bold"
                android:text="@string/city_id" />

            <TextView
                android:id="@+id/tv_city_id_value"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/tv_city_id"
                android:layout_alignBottom="@id/tv_city_id"
                android:text="@{viewModel.cityId}"
                tools:text="101210101" />

            <TextView
                android:id="@+id/tv_temp"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/tv_city_id"
                android:layout_marginTop="@dimen/query_weather_margin"
                android:textStyle="bold"
                android:text="@string/temperature" />

            <TextView
                android:id="@+id/tv_temp1_value"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/tv_temp"
                android:layout_alignBottom="@id/tv_temp"
                android:text="@{viewModel.temp1}"
                tools:text="5℃" />

            <TextView
                android:id="@+id/tv_tilde"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/tv_temp1_value"
                android:layout_alignBottom="@id/tv_temp"
                android:text="@string/tilde" />

            <TextView
                android:id="@+id/tv_temp2_value"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/tv_tilde"
                android:layout_alignBottom="@id/tv_temp"
                android:text="@{viewModel.temp2}"
                tools:text="10℃" />

            <TextView
                android:id="@+id/tv_weather"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/tv_temp"
                android:layout_marginTop="@dimen/query_weather_margin"
                android:textStyle="bold"
                android:text="@string/weather" />

            <TextView
                android:id="@+id/tv_weather_value"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/tv_weather"
                android:layout_alignBottom="@id/tv_weather"
                android:text="@{viewModel.weather}"
                tools:text="" />

            <TextView
                android:id="@+id/tv_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/tv_weather"
                android:layout_marginTop="@dimen/query_weather_margin"
                android:textStyle="bold"
                android:text="@string/release_time" />

            <TextView
                android:id="@+id/tv_time_value"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/tv_time"
                android:layout_alignBottom="@id/tv_time"
                android:text="@{viewModel.time}"
                tools:text="10:00" />
        </RelativeLayout>

        <ProgressBar
            android:id="@+id/pb_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" />

        <TextView
            android:id="@+id/tv_query_failure"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="@string/query_failure"
            android:visibility="@{viewModel.loadingFailure ? View.VISIBLE : View.GONE}" />
    </RelativeLayout>
</layout>

在Activity中,通过官方提供的Data Binding库加载布局文件,创建ViewModel,并绑定View和ViewModel。QueryWeatherActivity的代码如下所示:

public class QueryWeatherActivity extends AppCompatActivity {
    

    // ViewModel
    private QueryWeatherViewModel mViewModel;

    // DataBinding
    private ActivityQueryWeatherBinding mDataBinding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
        mDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_query_weather);
        // 创建ViewModel
        mViewModel = new QueryWeatherViewModel();
        // 绑定View和ViewModel
        mDataBinding.setViewModel(mViewModel);
    }

    @Override
    protected void onDestroy() {
    
        super.onDestroy();
        // 取消请求
        mViewModel.cancelRequest();
    }
}

总结

MVVM模式将应用分为三层:Model层主要负责数据的提供,View层主要负责界面的显示,ViewModel层主要负责业务逻辑的处理。各个层职责单一,结构清晰,应用可以很方便地进行测试、维护和扩展。

参考

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u012317510/article/details/80247756

智能推荐

Another industry person close to Foxit Wu Yu added-程序员宅基地

文章浏览阅读227次。Another industry person close to Foxit Wu Yu added that Foxit’s business with Google can kill two birds with one stone.Open source is a self-certification of its technical strength. For example, Microsoft spent 7.5 billion US dollars to acquire open sourc._another industry

AIX登录账号被锁_aix查看用户锁定情况-程序员宅基地

文章浏览阅读2k次。限制密码重试次数,超过限制次数后锁定用户本文PDF版下载http://xunzhaoxz.itpub.net/resource/40016/29789作者:xunzhao【转载时请以超链接形式标明文章出处和作者信息】链接:http://xunzhaoxz.itpub.net/post/40016/488626加强系统安全,其中一大要务就是保护好系_aix查看用户锁定情况

BUUCTF Reverse/[MRCTF2020]hello_world_go_buuctfhello worldgo-程序员宅基地

文章浏览阅读512次。BUUCTF Reverse/[MRCTF2020]hello_world_go先看文件信息,没有加壳看到题目有go,推测这是go语言编写的程序下载好IDAGolangHelper,然后按alt + f7 选择 go_entry.py上面两个是查看go语言信息的检测到可能是这几个版本选择一个版本,然后点击剩下的三个按钮,然后点OK,但是我的IDA(7.5版本)加载失败了,然后找了好久的解决方法还是搞不定然后直接搜索字符串flag跟进这里按a_buuctfhello worldgo

jvm-sandbox-repeater在docker上的部署_jvm sandbox docker-程序员宅基地

文章浏览阅读649次。repeater在单机上部署,很简单。但是在docker上部署,则有些问题。这里记录下部署过程:制作安装包如果对repeater代码有修改,则使用代码工程里的install-lcoal.sh脚本来生成安装包。如果没修改,本地有代码工程的情况下,也可以使用install-local.sh脚本安装,否则,使用install-repeater.sh来在线安装。安装完成后,用户目录下会生成sandbox和.sandbox-module两个文件夹,将两个文件夹打包成zip包,repeater的安装包就做好了。_jvm sandbox docker

java--竖线分隔符|_竖分隔符-程序员宅基地

文章浏览阅读2.2w次。在数据中使用"|"分隔符,在解析的时候,使用split("|"),得到的结果和预期不一致,是因为|在正则表达式中有含义,需要使用转义符来表示比如数据是 str= "red|yellow|black";String[] str_sp = str.split("\\|")_竖分隔符

【转】【C#】ZIP、RAR 压缩与解压缩-程序员宅基地

文章浏览阅读12次。压缩文件夹源码如下using System;using System.Data;using System.Configuration;using System.Web;using System.Web.Security;using System.Web.UI;using System.IO;using ICSharpCode.SharpZipLib.Checksums...

随便推点

内存测试的常用方法_内存测试算法-程序员宅基地

文章浏览阅读6.4k次,点赞6次,收藏23次。# 内存信息查看命令:Freetopcat /proc/meminfo常用第三方内存测试工具:mbw--内存带宽测试工具memtster--内存压力测试工具lmbench--内存综合性能测试工具一、mbw:下载地址:http://github.com/raas/mbw./mbw –q –n 1 200参数说明:-q 表示隐藏日志 , -n 1表示运行1次,200表示测试所用的内存大小,单位为MB主要测试内存拷贝的速度,主要分三部分:memcpy(内存拷贝)_内存测试算法

第四届“蓝帽杯”全国大学生网络安全 技能大赛 Writeup_highlight_file(__file__); include("./check.php");-程序员宅基地

文章浏览阅读5.4k次,点赞9次,收藏13次。第四届“蓝帽杯”全国大学生网络安全 技能大赛 WriteupMisc签到sudo熟悉的解密Web文件包含绕过easiestSQLiSoitgoesInclusionCryptozucMisc签到观色010editor查看,发现文件头是GIF89a,是gif文件Stegsolve 调色道,出现前半flagps打开,分离两个图层Stegsolve分析,得到后半flag,即可得到完整flagsudo喜欢玩数独吗,一起来玩吧。nc 47.93.204.245 120001-9数字玩99_highlight_file(__file__); include("./check.php");

c语言生日快乐爱心,C语言 生日快乐-程序员宅基地

文章浏览阅读2k次,点赞3次,收藏10次。#include #include #include #define I 20#define R 340int main(){system("mode con cols=80 lines=60 ");system("title 向你致以最诚挚的祝福!");char data[200][60]={{"祝你生日快乐!祝你生日快乐 !"},{"Happy birthday to you!"},{"Ha..._用char data[60]

RaSa2.5.x之一:Rasa Playground_rasa 2.5.x demo-程序员宅基地

文章浏览阅读251次。目录一、构建自己的对话助手(简单示例)1. NLU数据(NLU data)2. 响应(Responses)3. 故事(Stories)4. 表单(Forms)5. 规则(Rules)安装(Installation)快速安装(Quick Installation)从源码安装(Building from Source)附加依赖项(Additional Dependencies)Dependencies for spaCyDependencies for MITIE调整自己的NLU模型(Tuning Your N_rasa 2.5.x demo

心形线面积公式推导_五个步骤制作出让人秒懂的“梯形面积公式推导”动图——GeoGebra制作教程...-程序员宅基地

文章浏览阅读1.2k次。前言:在网络上,容易搜索到梯形面积公式推导的过程。但是,简洁明了、可下载的却少之又少。那么,不如自己动手制作吧!梯形面积公式推导的方法有多种,本文选取割补法——将梯形转化为三角形的方法。先看适合初学者的动态演示效果(用汉字书写,并特意显示三角形、梯形的字眼):以直角梯形为例,进行演示再看简洁的动态演示效果(用字母书写):以等腰梯形、一般梯形为例,进行演示上图中,只要拉动相应的滑动条,就可以改变梯形..._心形线证明等腰梯形

python实践:让所有奇数都在偶数前面,而且奇数升序排列,偶数降序排序_python序列调序要求调整元素顺序,把所有奇数都放到前面,偶数都放到后面-程序员宅基地

文章浏览阅读3.6k次。"""给定一个任意长度数组,实现一个函数让所有奇数都在偶数前面,而且奇数升序排列,偶数降序排序,如字符串'1982376455',变成'1355798642'"""class Solution(): def SortNum(self,num_list): """ :param num_list: 传入数组 :return: rlis..._python序列调序要求调整元素顺序,把所有奇数都放到前面,偶数都放到后面

推荐文章

热门文章

相关标签