MVVMHabit

Project Url: goldze/MVVMHabit
Introduction: 基于 DataBinding 框架,MVVM 设计模式的一套快速开发库,整合 Okhttp+RxJava+Retrofit+Glide 等主流库,满足日常开发需求。使用该框架可以快速开发一个 Android 应用。
More: Author   ReportBugs   
Tags:

v4.0.0:2021 年 07 月 16 日

  • 迁移 AndroidX 分支作为主线分支;
  • 升级第三方框架依赖版本;
  • 升级 gradle 插件版本支持;
  • 优化框架代码,解决已知 Bug;
  • 修改文档说明。

    更多日志


注:

3.x:Support 版(最后版本:3.1.6)

4.x:AndroidX 版(最后版本:4.0.0) 建议使用当前版本

原文地址: https://github.com/goldze/MVVMHabit

②群 MVVMHabit-Family2

①群 MVVMHabit-Family(已满)

MVVMHabit

#

目前,android 流行的 MVC、MVP 模式的开发框架很多,然而一款基于 MVVM 模式开发框架却很少。MVVMHabit 是以谷歌 DataBinding+LiveData+ViewModel 框架为基础,整合 Okhttp+RxJava+Retrofit+Glide 等流行模块,加上各种原生控件自定义的 BindingAdapter,让事件与数据源完美绑定的一款容易上瘾的实用性 MVVM 快速开发框架。从此告别 findViewById(),告别 setText(),告别 setOnClickListener()...

框架流程

框架特点

  • 快速开发

    只需要写项目的业务逻辑,不用再去关心网络请求、权限申请、View 的生命周期等问题,撸起袖子就是干。

  • 维护方便

    MVVM 开发模式,低耦合,逻辑分明。Model 层负责将请求的数据交给 ViewModel;ViewModel 层负责将请求到的数据做业务逻辑处理,最后交给 View 层去展示,与 View 一一对应;View 层只负责界面绘制刷新,不处理业务逻辑,非常适合分配独立模块开发。

  • 流行框架

    retrofit+okhttp+rxJava负责网络请求;gson负责解析 json 数据;glide负责加载图片;rxlifecycle负责管理 view 的生命周期;与网络请求共存亡;rxbinding结合 databinding 扩展 UI 事件;rxpermissions负责 Android 6.0 权限申请;material-dialogs一个漂亮的、流畅的、可定制的 material design 风格的对话框。

  • 数据绑定

    满足 google 目前控件支持的 databinding 双向绑定,并扩展原控件一些不支持的数据绑定。例如将图片的 url 路径绑定到 ImageView 控件中,在 BindingAdapter 方法里面则使用 Glide 加载图片;View 的 OnClick 事件在 BindingAdapter 中方法使用 RxView 防重复点击,再把事件回调到 ViewModel 层,实现 xml 与 ViewModel 之间数据和事件的绑定(框架里面部分扩展控件和回调命令使用的是@kelin 原创的)。

  • 基类封装

    专门针对 MVVM 模式打造的 BaseActivity、BaseFragment、BaseViewModel,在 View 层中不再需要定义 ViewDataBinding 和 ViewModel,直接在 BaseActivity、BaseFragment 上限定泛型即可使用。普通界面只需要编写 Fragment,然后使用 ContainerActivity 盛装(代理),这样就不需要每个界面都在 AndroidManifest 中注册一遍。

  • 全局操作

    1. 全局的 Activity 堆栈式管理,在程序任何地方可以打开、结束指定的 Activity,一键退出应用程序。
    2. LoggingInterceptor 全局拦截网络请求日志,打印 Request 和 Response,格式化 json、xml 数据显示,方便与后台调试接口。
    3. 全局 Cookie,支持 SharedPreferences 和内存两种管理模式。
    4. 通用的网络请求异常监听,根据不同的状态码或异常设置相应的 message。
    5. 全局的异常捕获,程序发生异常时不会崩溃,可跳入异常界面重启应用。
    6. 全局事件回调,提供 RxBus、Messenger 两种回调方式。
    7. 全局任意位置一行代码实现文件下载进度监听(暂不支持多文件进度监听)。
    8. 全局点击事件防抖动处理,防止点击过快。

1、准备工作

网上的很多有关 MVVM 的资料,在此就不再阐述什么是 MVVM 了,不清楚的朋友可以先去了解一下。todo-mvvm-live

1.1、启用 databinding

在主工程 app 的 build.gradle 的 android {}中加入:

dataBinding {
    enabled true
}

1.2、依赖 Library

从远程依赖:

在根目录的 build.gradle 中加入

allprojects {
    repositories {
        ...
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

在主项目 app 的 build.gradle 中依赖

dependencies {
    ...
    implementation 'com.github.goldze:MVVMHabit:4.0.0'
}

下载例子程序,在主项目 app 的 build.gradle 中依赖例子程序中的mvvmhabit

dependencies {    
    ...
    implementation project(':mvvmhabit')
}

1.3、配置 config.gradle

如果不是远程依赖,而是下载的例子程序,那么还需要将例子程序中的 config.gradle 放入你的主项目根目录中,然后在根目录 build.gradle 的第一行加入:

apply from: "config.gradle"

注意: config.gradle 中的

android = [] 是你的开发相关版本配置,可自行修改

support = [] 是你的 support 相关配置,可自行修改

dependencies = [] 是依赖第三方库的配置,可以加新库,但不要去修改原有第三方库的版本号,不然可能会编译不过

1.4、配置 AndroidManifest

添加权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

配置 Application:

继承mvvmhabit中的 BaseApplication,或者调用

BaseApplication.setApplication(this);

来初始化你的 Application

可以在你的自己 AppApplication 中配置

//是否开启日志打印
KLog.init(true);
//配置全局异常崩溃操作
CaocConfig.Builder.create()
    .backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //背景模式,开启沉浸式
    .enabled(true) //是否启动全局异常捕获
    .showErrorDetails(true) //是否显示错误详细信息
    .showRestartButton(true) //是否显示重启按钮
    .trackActivities(true) //是否跟踪 Activity
    .minTimeBetweenCrashesMs(2000) //崩溃的间隔时间(毫秒)
    .errorDrawable(R.mipmap.ic_launcher) //错误图标
    .restartActivity(LoginActivity.class) //重新启动后的 activity
    //.errorActivity(YourCustomErrorActivity.class) //崩溃后的错误 activity
    //.eventListener(new YourCustomEventListener()) //崩溃后的错误监听
    .apply();

2、快速上手

2.1、第一个 Activity

以大家都熟悉的登录操作为例:三个文件LoginActivty.javaLoginViewModel.javaactivity_login.xml

2.1.1、关联 ViewModel

在 activity_login.xml 中关联 LoginViewModel。

<layout>
    <data>
        <variable
            type="com.goldze.mvvmhabit.ui.login.LoginViewModel"
            name="viewModel"
        />
    </data>
    .....

</layout>

variable - type:类的全路径
variable - name:变量名

2.1.2、继承 BaseActivity

LoginActivity 继承 BaseActivity


public class LoginActivity extends BaseActivity<ActivityLoginBinding, LoginViewModel> {
    //ActivityLoginBinding 类是 databinding 框架自定生成的,对 activity_login.xml
    @Override
    public int initContentView(Bundle savedInstanceState) {
        return R.layout.activity_login;
    }

    @Override
    public int initVariableId() {
        return BR.viewModel;
    }

    @Override
    public LoginViewModel initViewModel() {
        //View 持有 ViewModel 的引用,如果没有特殊业务处理,这个方法可以不重写
        return ViewModelProviders.of(this).get(LoginViewModel.class);
    }
}

保存 activity_login.xml 后 databinding 会生成一个 ActivityLoginBinding 类。(如果没有生成,试着点击 Build->Clean Project)

BaseActivity 是一个抽象类,有两个泛型参数,一个是 ViewDataBinding,另一个是 BaseViewModel,上面的 ActivityLoginBinding 则是继承的 ViewDataBinding 作为第一个泛型约束,LoginViewModel 继承 BaseViewModel 作为第二个泛型约束。

重写 BaseActivity 的二个抽象方法

initContentView() 返回界面 layout 的 id
initVariableId() 返回变量的 id,对应 activity_login 中 name="viewModel",就像一个控件的 id,可以使用 R.id.xxx,这里的 BR 跟 R 文件一样,由系统生成,使用 BR.xxx 找到这个 ViewModel 的 id。

选择性重写 initViewModel()方法,返回 ViewModel 对象

@Override
public LoginViewModel initViewModel() {
    //View 持有 ViewModel 的引用,如果没有特殊业务处理,这个方法可以不重写
    return ViewModelProviders.of(this).get(LoginViewModel.class);
}

注意: 不重写 initViewModel(),默认会创建 LoginActivity 中第二个泛型约束的 LoginViewModel,如果没有指定第二个泛型,则会创建 BaseViewModel

2.1.3、继承 BaseViewModel

LoginViewModel 继承 BaseViewModel

public class LoginViewModel extends BaseViewModel {
    public LoginViewModel(@NonNull Application application) {
        super(application);
    }
    ....
}

BaseViewModel 与 BaseActivity 通过 LiveData 来处理常用 UI 逻辑,即可在 ViewModel 中使用父类的 showDialog()、startActivity()等方法。在这个 LoginViewModel 中就可以尽情的写你的逻辑了!

BaseFragment 的使用和 BaseActivity 一样,详情参考 Demo。

2.2、数据绑定

拥有 databinding 框架自带的双向绑定,也有扩展

2.2.1、传统绑定

绑定用户名:

在 LoginViewModel 中定义

//用户名的绑定
public ObservableField<String> userName = new ObservableField<>("");

在用户名 EditText 标签中绑定

android:text="@={viewModel.userName}"

这样一来,输入框中输入了什么,userName.get()的内容就是什么,userName.set("")设置什么,输入框中就显示什么。 注意: @符号后面需要加=号才能达到双向绑定效果;userName 需要是 public 的,不然 viewModel 无法找到它。

点击事件绑定:

在 LoginViewModel 中定义

//登录按钮的点击事件
public View.OnClickListener loginOnClick = new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
};

在登录按钮标签中绑定

android:onClick="@{viewModel.loginOnClick}"

这样一来,用户的点击事件直接被回调到 ViewModel 层了,更好的维护了业务逻辑

这就是强大的 databinding 框架双向绑定的特性,不用再给控件定义 id,setText(),setOnClickListener()。

但是,光有这些,完全满足不了我们复杂业务的需求啊!MVVMHabit 闪亮登场:它有一套自定义的绑定规则,可以满足大部分的场景需求,请继续往下看。

2.2.2、自定义绑定

还拿点击事件说吧,不用传统的绑定方式,使用自定义的点击事件绑定。

在 LoginViewModel 中定义

//登录按钮的点击事件
public BindingCommand loginOnClickCommand = new BindingCommand(new BindingAction() {
    @Override
    public void call() {

    }
});

在 activity_login 中定义命名空间

xmlns:binding="http://schemas.android.com/apk/res-auto"

在登录按钮标签中绑定

binding:onClickCommand="@{viewModel.loginOnClickCommand}"

这和原本传统的绑定不是一样吗?不,这其实是有差别的。使用这种形式的绑定,在原本事件绑定的基础之上,带有防重复点击的功能,1 秒内多次点击也只会执行一次操作。如果不需要防重复点击,可以加入这条属性

binding:isThrottleFirst="@{Boolean.TRUE}"

那这功能是在哪里做的呢?答案在下面的代码中。

//防重复点击间隔(秒)
public static final int CLICK_INTERVAL = 1;

/**
* requireAll 是意思是是否需要绑定全部参数, false 为否
* View 的 onClick 事件绑定
* onClickCommand 绑定的命令,
* isThrottleFirst 是否开启防止过快点击
*/
@BindingAdapter(value = {"onClickCommand", "isThrottleFirst"}, requireAll = false)
public static void onClickCommand(View view, final BindingCommand clickCommand, final boolean isThrottleFirst) {
    if (isThrottleFirst) {
        RxView.clicks(view)
        .subscribe(new Consumer<Object>() {
            @Override
            public void accept(Object object) throws Exception {
                if (clickCommand != null) {
                    clickCommand.execute();
                }
            }
        });
    } else {
        RxView.clicks(view)
        .throttleFirst(CLICK_INTERVAL, TimeUnit.SECONDS)//1 秒钟内只允许点击 1 次
        .subscribe(new Consumer<Object>() {
            @Override
            public void accept(Object object) throws Exception {
                if (clickCommand != null) {
                    clickCommand.execute();
                }
            }
        });
    }
}

onClickCommand 方法是自定义的,使用@BindingAdapter 注解来标明这是一个绑定方法。在方法中使用了 RxView 来增强 view 的 clicks 事件,.throttleFirst()限制订阅者在指定的时间内重复执行,最后通过 BindingCommand 将事件回调出去,就好比有一种拦截器,在点击时先做一下判断,然后再把事件沿着他原有的方向传递。

是不是觉得有点意思,好戏还在后头呢!

2.2.3、自定义 ImageView 图片加载

绑定图片路径:

在 ViewModel 中定义

public String imgUrl = "http://img0.imgtn.bdimg.com/it/u=2183314203,562241301&fm=26&gp=0.jpg";

在 ImageView 标签中

binding:url="@{viewModel.imgUrl}"

url 是图片路径,这样绑定后,这个 ImageView 就会去显示这张图片,不限网络图片还是本地图片。

如果需要给一个默认加载中的图片,可以加这一句

binding:placeholderRes="@{R.mipmap.ic_launcher_round}"

R 文件需要在 data 标签中导入使用,如:<import type="com.goldze.mvvmhabit.R" />

BindingAdapter 中的实现

@BindingAdapter(value = {"url", "placeholderRes"}, requireAll = false)
public static void setImageUri(ImageView imageView, String url, int placeholderRes) {
    if (!TextUtils.isEmpty(url)) {
        //使用 Glide 框架加载图片
        Glide.with(imageView.getContext())
            .load(url)
            .placeholder(placeholderRes)
            .into(imageView);
    }
}

很简单就自定义了一个 ImageView 图片加载的绑定,学会这种方式,可自定义扩展。

如果你对这些感兴趣,可以下载源码,在 binding 包中可以看到各类控件的绑定实现方式

2.2.4、RecyclerView 绑定

RecyclerView 也是很常用的一种控件,传统的方式需要针对各种业务要写各种 Adapter,如果你使用了 mvvmhabit,则可大大简化这种工作量,从此告别 setAdapter()。

在 ViewModel 中定义:

//给 RecyclerView 添加 items
public final ObservableList<NetWorkItemViewModel> observableList = new ObservableArrayList<>();
//给 RecyclerView 添加 ItemBinding
public final ItemBinding<NetWorkItemViewModel> itemBinding = ItemBinding.of(BR.viewModel, R.layout.item_network);

ObservableList<>和 ItemBinding<>的泛型是 Item 布局所对应的 ItemViewModel

在 xml 中绑定

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    binding:itemBinding="@{viewModel.itemBinding}"
    binding:items="@{viewModel.observableList}"
    binding:layoutManager="@{LayoutManagers.linear()}"
    binding:lineManager="@{LineManagers.horizontal()}" />

layoutManager 控制是线性(包含水平和垂直)排列还是网格排列,lineManager 是设置分割线

网格布局的写法:binding:layoutManager="@{LayoutManagers.grid(3)}
水平布局的写法:binding:layoutManager="@{LayoutManagers.linear(LinearLayoutManager.HORIZONTAL,Boolean.FALSE)}"

使用到相关类,则需要导入该类才能使用,和导入 Java 类相似

<import type="me.tatarka.bindingcollectionadapter2.LayoutManagers" />
<import type="me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LineManagers" />
<import type="android.support.v7.widget.LinearLayoutManager" />

这样绑定后,在 ViewModel 中调用 ObservableList 的 add()方法,添加一个 ItemViewModel,界面上就会实时绘制出一个 Item。在 Item 对应的 ViewModel 中,同样可以以绑定的形式完成逻辑

可以在请求到数据后,循环添加observableList.add(new NetWorkItemViewModel(NetWorkViewModel.this, entity));详细可以参考例子程序中 NetWorkViewModel 类。

注意: 在以前的版本中,ItemViewModel 是继承 BaseViewModel,传入 Context,新版本 3.x 中可继承 ItemViewModel,传入当前页面的 ViewModel

更多 RecyclerView、ListView、ViewPager 等绑定方式,请参考 https://github.com/evant/binding-collection-adapter

2.3、网络请求

网络请求一直都是一个项目的核心,现在的项目基本都离不开网络,一个好用网络请求框架可以让开发事半功倍。

2.3.1、Retrofit+Okhttp+RxJava

现今,这三个组合基本是网络请求的标配,如果你对这三个框架不了解,建议先去查阅相关资料。

square 出品的框架,用起来确实非常方便。MVVMHabit中引入了

api "com.squareup.okhttp3:okhttp:3.10.0"
api "com.squareup.retrofit2:retrofit:2.4.0"
api "com.squareup.retrofit2:converter-gson:2.4.0"
api "com.squareup.retrofit2:adapter-rxjava2:2.4.0"

构建 Retrofit 时加入

Retrofit retrofit = new Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

或者直接使用例子程序中封装好的 RetrofitClient

2.3.2、网络拦截器

LoggingInterceptor: 全局拦截请求信息,格式化打印 Request、Response,可以清晰的看到与后台接口对接的数据,

LoggingInterceptor mLoggingInterceptor = new LoggingInterceptor
    .Builder()//构建者模式
    .loggable(true) //是否开启日志打印
    .setLevel(Level.BODY) //打印的等级
    .log(Platform.INFO) // 打印类型
    .request("Request") // request 的 Tag
    .response("Response")// Response 的 Tag
    .addHeader("version", BuildConfig.VERSION_NAME)//打印版本
    .build()

构建 okhttp 时加入

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .addInterceptor(mLoggingInterceptor)
    .build();

CacheInterceptor: 缓存拦截器,当没有网络连接的时候自动读取缓存中的数据,缓存存放时间默认为 3 天。
创建缓存对象

//缓存时间
int CACHE_TIMEOUT = 10 * 1024 * 1024
//缓存存放的文件
File httpCacheDirectory = new File(mContext.getCacheDir(), "goldze_cache");
//缓存对象
Cache cache = new Cache(httpCacheDirectory, CACHE_TIMEOUT);

构建 okhttp 时加入

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor(new CacheInterceptor(mContext))
    .build();

MVVMHabit提供两种 CookieStore:PersistentCookieStore (SharedPreferences 管理)和MemoryCookieStore (内存管理),可以根据自己的业务需求,在构建 okhttp 时加入相应的 cookieJar

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cookieJar(new CookieJarImpl(new PersistentCookieStore(mContext)))
    .build();

或者

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cookieJar(new CookieJarImpl(new MemoryCookieStore()))
    .build();

2.3.4、绑定生命周期

请求在 ViewModel 层。默认在 BaseActivity 中注入了 LifecycleProvider 对象到 ViewModel,用于绑定请求的生命周期,View 与请求共存亡。

RetrofitClient.getInstance().create(DemoApiService.class)
    .demoGet()
    .compose(RxUtils.bindToLifecycle(getLifecycleProvider())) // 请求与 View 周期同步
    .compose(RxUtils.schedulersTransformer())  // 线程调度
    .compose(RxUtils.exceptionTransformer())   // 网络错误的异常转换
    .subscribe(new Consumer<BaseResponse<DemoEntity>>() {
        @Override
        public void accept(BaseResponse<DemoEntity> response) throws Exception {

        }
    }, new Consumer<ResponseThrowable>() {
        @Override
        public void accept(ResponseThrowable throwable) throws Exception {

        }
    });

在请求时关键需要加入组合操作符.compose(RxUtils.bindToLifecycle(getLifecycleProvider()))
注意: 由于 BaseActivity/BaseFragment 都实现了 LifecycleProvider 接口,并且默认注入到 ViewModel 中,所以在调用请求方法时可以直接调用 getLifecycleProvider()拿到生命周期接口。如果你没有使用 mvvmabit 里面的 BaseActivity 或 BaseFragment,使用自己定义的 Base,那么需要让你自己的 Activity 继承 RxAppCompatActivity、Fragment 继承 RxFragment 才能用RxUtils.bindToLifecycle(lifecycle)方法。

2.3.5、网络异常处理

网络异常在网络请求中非常常见,比如请求超时、解析错误、资源不存在、服务器内部错误等,在客户端则需要做相应的处理(当然,你可以把一部分异常甩锅给网络,比如当出现 code 500 时,提示:请求超时,请检查网络连接,此时偷偷将异常信息发送至后台(手动滑稽))。

在使用 Retrofit 请求时,加入组合操作符.compose(RxUtils.exceptionTransformer()),当发生网络异常时,回调 onError(ResponseThrowable)方法,可以拿到异常的 code 和 message,做相应处理。

mvvmhabit 中自定义了一个ExceptionHandle,已为你完成了大部分网络异常的判断,也可自行根据项目的具体需求调整逻辑。

注意: 这里的网络异常 code,并非是与服务端协议约定的 code。网络异常可以分为两部分,一部分是协议异常,即出现 code = 404、500 等,属于 HttpException,另一部分为请求异常,即出现:连接超时、解析错误、证书验证失等。而与服务端约定的 code 规则,它不属于网络异常,它是属于一种业务异常。在请求中可以使用 RxJava 的 filter(过滤器),也可以自定义 BaseSubscriber 统一处理网络请求的业务逻辑异常。由于每个公司的业务协议不一样,所以具体需要你自己来处理该类异常。

3、辅助功能

一个完整的快速开发框架,当然也少不了常用的辅助类。下面来介绍一下MVVMabit中有哪些辅助功能。

3.1、事件总线

事件总线存在的优点想必大家都很清楚了,android 自带的广播机制对于组件间的通信而言,使用非常繁琐,通信组件彼此之间的订阅和发布的耦合也比较严重,特别是对于事件的定义,广播机制局限于序列化的类(通过 Intent 传递),不够灵活。

3.3.1、RxBus

RxBus 并不是一个库,而是一种模式。相信大多数开发者都使用过 EventBus,对 RxBus 也是很熟悉。由于MVVMabit中已经加入 RxJava,所以采用了 RxBus 代替 EventBus 作为事件总线通信,以减少库的依赖。

使用方法:

在 ViewModel 中重写 registerRxBus()方法来注册 RxBus,重写 removeRxBus()方法来移除 RxBus

//订阅者
private Disposable mSubscription;
//注册 RxBus
@Override
public void registerRxBus() {
    super.registerRxBus();
    mSubscription = RxBus.getDefault().toObservable(String.class)
        .subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {

            }
        });
    //将订阅者加入管理站
    RxSubscriptions.add(mSubscription);
}

//移除 RxBus
@Override
public void removeRxBus() {
    super.removeRxBus();
    //将订阅者从管理站中移除
    RxSubscriptions.remove(mSubscription);
}

在需要执行回调的地方发送

RxBus.getDefault().post(object);

3.3.2、Messenger

Messenger 是一个轻量级全局的消息通信工具,在我们的复杂业务中,难免会出现一些交叉的业务,比如 ViewModel 与 ViewModel 之间需要有数据交换,这时候可以轻松地使用 Messenger 发送一个实体或一个空消息,将事件从一个 ViewModel 回调到另一个 ViewModel 中。

使用方法:

定义一个静态 String 类型的字符串 token

public static final String TOKEN_LOGINVIEWMODEL_REFRESH = "token_loginviewmodel_refresh";

在 ViewModel 中注册消息监听

//注册一个空消息监听 
//参数 1:接受人(上下文)
//参数 2:定义的 token
//参数 3:执行的回调监听
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, new BindingAction() {
    @Override
    public void call() {

    }
});

//注册一个带数据回调的消息监听 
//参数 1:接受人(上下文)
//参数 2:定义的 token
//参数 3:实体的泛型约束
//参数 4:执行的回调监听
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, String.class, new BindingConsumer<String>() {
    @Override
    public void call(String s) {

    }
});

在需要回调的地方使用 token 发送消息

//发送一个空消息
//参数 1:定义的 token
Messenger.getDefault().sendNoMsg(LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);

//发送一个带数据回调消息
//参数 1:回调的实体
//参数 2:定义的 token
Messenger.getDefault().send("refresh",LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);

token 最好不要重名,不然可能就会出现逻辑上的 bug,为了更好的维护和清晰逻辑,建议以aa_bb_cc的格式来定义 token。aa:TOKEN,bb:ViewModel 的类名,cc:动作名(功能名)。

为了避免大量使用 Messenger,建议只在 ViewModel 与 ViewModel 之间使用,View 与 ViewModel 之间采用 ObservableField 去监听 UI 上的逻辑,可在继承了 Base 的 Activity 或 Fragment 中重写 initViewObservable()方法来初始化 UI 的监听

注册了监听,当然也要解除它。在 BaseActivity、BaseFragment 的 onDestroy()方法里已经调用Messenger.getDefault().unregister(viewModel);解除注册,所以不用担心忘记解除导致的逻辑错误和内存泄漏。

3.2、文件下载

文件下载几乎是每个 app 必备的功能,图文的下载,软件的升级等都要用到,mvvmhabit 使用 Retrofit+Okhttp+RxJava+RxBus 实现一行代码监听带进度的文件下载。

下载文件

String loadUrl = "你的文件下载路径";
String destFileDir = context.getCacheDir().getPath();  //文件存放的路径
String destFileName = System.currentTimeMillis() + ".apk";//文件存放的名称
DownLoadManager.getInstance().load(loadUrl, new ProgressCallBack<ResponseBody>(destFileDir, destFileName) {
    @Override
    public void onStart() {
        //RxJava 的 onStart()
    }

    @Override
    public void onCompleted() {
        //RxJava 的 onCompleted()
    }

    @Override
    public void onSuccess(ResponseBody responseBody) {
        //下载成功的回调
    }

    @Override
    public void progress(final long progress, final long total) {
        //下载中的回调 progress:当前进度 ,total:文件总大小
    }

    @Override
    public void onError(Throwable e) {
        //下载错误回调
    }
});

在 ProgressResponseBody 中使用了 RxBus,发送下载进度信息到 ProgressCallBack 中,继承 ProgressCallBack 就可以监听到下载状态。回调方法全部执行在主线程,方便 UI 的更新,详情请参考例子程序。

3.3、ContainerActivity

一个盛装 Fragment 的一个容器(代理)Activity,普通界面只需要编写 Fragment,使用此 Activity 盛装,这样就不需要每个界面都在 AndroidManifest 中注册一遍

使用方法:

在 ViewModel 中调用 BaseViewModel 的方法开一个 Fragment

startContainerActivity(你的 Fragment 类名.class.getCanonicalName())

在 ViewModel 中调用 BaseViewModel 的方法,携带一个序列化实体打开一个 Fragment

Bundle mBundle = new Bundle();
mBundle.putParcelable("entity", entity);
startContainerActivity(你的 Fragment 类名.class.getCanonicalName(), mBundle);

在你的 Fragment 中取出实体

Bundle mBundle = getArguments();
if (mBundle != null) {
    entity = mBundle.getParcelable("entity");
}

3.4、6.0 权限申请

对 RxPermissions 已经熟悉的朋友可以跳过。

使用方法:

例如请求相机权限,在 ViewModel 中调用

//请求打开相机权限
RxPermissions rxPermissions = new RxPermissions((Activity) context);
rxPermissions.request(Manifest.permission.CAMERA)
    .subscribe(new Consumer<Boolean>() {
        @Override
        public void accept(Boolean aBoolean) throws Exception {
            if (aBoolean) {
                ToastUtils.showShort("权限已经打开,直接跳入相机");
            } else {
                ToastUtils.showShort("权限被拒绝");
            }
        }
    });

更多权限申请方式请参考RxPermissions 原项目地址

3.5、图片压缩

为了节约用户流量和加快图片上传的速度,某些场景将图片在本地压缩后再传给后台,所以特此提供一个图片压缩的辅助功能。

使用方法:

RxJava 的方式压缩单张图片,得到一个压缩后的图片文件对象

String filePath = "mnt/sdcard/1.png";
ImageUtils.compressWithRx(filePath, new Consumer<File>() {
    @Override
    public void accept(File file) throws Exception {
        //将文件放入 RequestBody
        ...
    }
});

RxJava 的方式压缩多张图片,按集合顺序每压缩成功一张,都将在 onNext 方法中得到一个压缩后的图片文件对象

List<String> filePaths = new ArrayList<>();
filePaths.add("mnt/sdcard/1.png");
filePaths.add("mnt/sdcard/2.png");
ImageUtils.compressWithRx(filePaths, new Subscriber() {
    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onNext(File file) {

    }
});

3.6、其他辅助类

ToastUtils: 吐司工具类

MaterialDialogUtils: Material 风格对话框工具类

SPUtils: SharedPreferences 工具类

SDCardUtils: SD 卡相关工具类

ConvertUtils: 转换相关工具类

StringUtils: 字符串相关工具类

RegexUtils: 正则相关工具类

KLog: 日志打印,含 json 格式打印

4、附加

4.1、编译错误解决方法

使用 databinding 其实有个缺点,就是会遇到一些编译错误,而 AS 不能很好的定位到错误的位置,这对于刚开始使用 databinding 的开发者来说是一个比较郁闷的事。那么我在此把我自己在开发中遇到的各种编译问题的解决方法分享给大家,希望这对你会有所帮助。

4.1.1、绑定错误

绑定错误是一个很常见的错误,基本都会犯。比如 TextView 的 android:text="" ,本来要绑定的是一个 String 类型,结果你不小心,可能绑了一个 Boolean 上去,或者变量名写错了,这时候编辑器不会报红错,而是在点编译运行的时候,在 AS 的 Messages 中会出现错误提示,如下图:

解决方法:把错误提示拉到最下面 (上面的提示找不到 BR 类这个不要管它),看最后一个错误 ,这里会提示是哪个 xml 出了错,并且会定位到行数,按照提示找到对应位置,即可解决该编译错误的问题。

注意: 行数要+1,意思是上面报出第 33 行错误,实际是第 34 行错误,AS 定位的不准确 (这可能是它的一个 bug)

4.1.2、xml 导包错误

在 xml 中需要导入 ViewModel 或者一些业务相关的类,假如在 xml 中导错了类,那一行则会报红,但是 res/layout 却没有错误提示,有一种场景,非常特殊,不容易找出错误位置。就是你写了一个 xml,导入了一个类,比如 XXXUtils,后来因为业务需求,把那个 XXXUtils 删了,这时候 res/layout 下不会出现任何错误,而你在编译运行的时候,才会出现错误日志。苦逼的是,不会像上面那样提示哪一个 xml 文件,哪一行出错了,最后一个错误只是一大片的报错报告。如下图:

解决方法:同样找到最后一个错误提示,找到 Cannot resolve type for xxx这一句 (xxx 是类名),然后使用全局搜索 (Ctrl+H) ,搜索哪个 xml 引用了这个类,跟踪点击进去,在 xml 就会出现一个红错,看到错误你就会明白了,这样就可解决该编译错误的问题。

4.1.3、build 错误

构建多 module 工程时,如出现【4.1.1、绑定错误】,且你能确定这个绑定是没有问题的,经过修改后出现下图错误:

解决方法: 这种是 databinding 比较大的坑,清理、重构和删 build 都不起作用,网上很难找到方法。经过试验,解决办法是手动创建异常中提到的文件夹,或者拷贝上一个没有报错的版本中对应的文件夹,可以解决这个异常

4.1.4、自动生成类错误

有时候在写完 xml 时,databinding 没有自动生成对应的 Binding 类及属性。比如新建了一个 activity_login.xml,按照 databinding 的写法加入<layout> <variable>后,理论上会自动对应生成 ActivityLoginBinding.java 类和 variable 的属性,可能是 as 对 databding 的支持还不够吧,有时候偏偏就不生成,导致 BR.xxx 报红等一些莫名的错误。

解决方法:其实确保自己的写法没有问题,是可以直接运行的,报红不一定是你写的有问题,也有可能是编译器抽风了。或者使用下面的办法
第一招:Build->Clean Project;
第二招:Build->Rebuild Project;
第三招:重启大法。

4.1.5、gradle 错误

如果遇到以下编译问题:

错误: 无法将类 BindingRecyclerViewAdapters 中的方法 setAdapter 应用到给定类型; 需要: RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory 找到: RecyclerView,ItemBinding,ObservableList,BindingRecyclerViewAdapter,ItemIds,ViewHolderFactory 原因: 推断类型不符合等式约束条件 推断: CAP#1 等式约束条件: CAP#1,NetWorkItemViewModel 其中, T 是类型变量: T 扩展已在方法 setAdapter(RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory)中声明的 Object 其中, CAP#1 是新类型变量: CAP#1 从?的捕获扩展 Object

一般是由于 gradle plugin 版本 3.5.1 造成的,请换成 gradle plugin 3.5.0 以下版本

混淆

例子程序中给出了最新的【MVVMHabit 混淆规则】,包含 MVVMHabit 中依赖的所有第三方 library,可以将规则直接拷贝到自己 app 的混淆规则中。在此基础上你只需要关注自己业务代码以及自己引入第三方的混淆,【MVVMHabit 混淆规则】请参考 app 目录下的proguard-rules.pro文件。

组件化

进阶 Android 组件化方案,请移步:MVVMHabitComponent

About

goldze: 本人喜欢尝试新的技术,以后发现有好用的东西,我将会在企业项目中实战,没有问题了就会把它引入到MVVMHabit中,一直维护着这套框架,谢谢各位朋友的支持。如果觉得这套框架不错的话,麻烦点个 star,你的支持则是我前进的动力!

QQ 群:84692105

Thank

感谢【zhangxiaoxiao】小伙伴长期提供的技术支持与帮助,为项目开源做出了很多的贡献。

License

Copyright 2017 goldze(曾宪泽)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools