[
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\nkey.jks\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n/.idea\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n/app/release\n/mvvmhabit/bintray.gradle\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2017 goldze(曾宪泽)\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "## 最新日志\n**v4.0.0：2021年07月16日**\n\n- 迁移AndroidX分支作为主线分支；\n- 升级第三方框架依赖版本；\n- 升级gradle插件版本支持；\n- 优化框架代码，解决已知Bug；\n- 修改文档说明。\n#### [更多日志](./UpdateLog.md)\n\n***\n\n**注：** \n\n[3.x：Support版（最后版本：3.1.6）](https://github.com/goldze/MVVMHabit/tree/20210716_v3.1.6_android) \n\n[4.x：AndroidX版（最后版本：4.0.0）](https://github.com/goldze/MVVMHabit) 建议使用当前版本\n\n> **原文地址：** [https://github.com/goldze/MVVMHabit](https://github.com/goldze/MVVMHabit)\n\n②群 <a target=\"_blank\" href=\"https://jq.qq.com/?_wv=1027&k=V3luKUW7\"><img border=\"0\" src=\"http://pub.idqqimg.com/wpa/images/group.png\" alt=\"MVVMHabit-Family2\" title=\"MVVMHabit-Family2\"></a>\n\n①群 <a target=\"_blank\" href=\"https://jq.qq.com/?_wv=1027&k=Fv9et98F\"><img border=\"0\" src=\"http://pub.idqqimg.com/wpa/images/group.png\" alt=\"MVVMHabit-Family\" title=\"MVVMHabit-Family\"></a>（已满）\n\n# MVVMHabit\n##\n目前，android流行的MVC、MVP模式的开发框架很多，然而一款基于MVVM模式开发框架却很少。**MVVMHabit是以谷歌DataBinding+LiveData+ViewModel框架为基础，整合Okhttp+RxJava+Retrofit+Glide等流行模块，加上各种原生控件自定义的BindingAdapter，让事件与数据源完美绑定的一款容易上瘾的实用性MVVM快速开发框架**。从此告别findViewById()，告别setText()，告别setOnClickListener()...\n\n## 框架流程\n![](./img/fc.png) \n\n## 框架特点\n- **快速开发**\n\n\t只需要写项目的业务逻辑，不用再去关心网络请求、权限申请、View的生命周期等问题，撸起袖子就是干。\n\n- **维护方便**\n\n\tMVVM开发模式，低耦合，逻辑分明。Model层负责将请求的数据交给ViewModel；ViewModel层负责将请求到的数据做业务逻辑处理，最后交给View层去展示，与View一一对应；View层只负责界面绘制刷新，不处理业务逻辑，非常适合分配独立模块开发。\n\n- **流行框架**\n\n\t[retrofit](https://github.com/square/retrofit)+[okhttp](https://github.com/square/okhttp)+[rxJava](https://github.com/ReactiveX/RxJava)负责网络请求；[gson](https://github.com/google/gson)负责解析json数据；[glide](https://github.com/bumptech/glide)负责加载图片；[rxlifecycle](https://github.com/trello/RxLifecycle)负责管理view的生命周期；与网络请求共存亡；[rxbinding](https://github.com/JakeWharton/RxBinding)结合databinding扩展UI事件；[rxpermissions](https://github.com/tbruyelle/RxPermissions)负责Android 6.0权限申请；[material-dialogs](https://github.com/afollestad/material-dialogs)一个漂亮的、流畅的、可定制的material design风格的对话框。\n\n- **数据绑定**\n\n\t满足google目前控件支持的databinding双向绑定，并扩展原控件一些不支持的数据绑定。例如将图片的url路径绑定到ImageView控件中，在BindingAdapter方法里面则使用Glide加载图片；View的OnClick事件在BindingAdapter中方法使用RxView防重复点击，再把事件回调到ViewModel层，实现xml与ViewModel之间数据和事件的绑定(框架里面部分扩展控件和回调命令使用的是@kelin原创的)。\n\n- **基类封装**\n\n\t专门针对MVVM模式打造的BaseActivity、BaseFragment、BaseViewModel，在View层中不再需要定义ViewDataBinding和ViewModel，直接在BaseActivity、BaseFragment上限定泛型即可使用。普通界面只需要编写Fragment，然后使用ContainerActivity盛装(代理)，这样就不需要每个界面都在AndroidManifest中注册一遍。\n\n- **全局操作**\n\t1. 全局的Activity堆栈式管理，在程序任何地方可以打开、结束指定的Activity，一键退出应用程序。\n\t2. LoggingInterceptor全局拦截网络请求日志，打印Request和Response，格式化json、xml数据显示，方便与后台调试接口。\n\t3. 全局Cookie，支持SharedPreferences和内存两种管理模式。\n\t4. 通用的网络请求异常监听，根据不同的状态码或异常设置相应的message。\n\t5. 全局的异常捕获，程序发生异常时不会崩溃，可跳入异常界面重启应用。\n\t6. 全局事件回调，提供RxBus、Messenger两种回调方式。\n\t7. 全局任意位置一行代码实现文件下载进度监听（暂不支持多文件进度监听）。\n    8. 全局点击事件防抖动处理，防止点击过快。\n\n\n## 1、准备工作\n> 网上的很多有关MVVM的资料，在此就不再阐述什么是MVVM了，不清楚的朋友可以先去了解一下。[todo-mvvm-live](https://github.com/googlesamples/android-architecture/tree/todo-mvvm-live)\n### 1.1、启用databinding\n在主工程app的build.gradle的android {}中加入：\n```gradle\ndataBinding {\n    enabled true\n}\n```\n### 1.2、依赖Library\n从远程依赖：\n\n在根目录的build.gradle中加入\n```gradle\nallprojects {\n    repositories {\n\t\t...\n        google()\n        jcenter()\n        maven { url 'https://jitpack.io' }\n    }\n}\n```\n在主项目app的build.gradle中依赖\n```gradle\ndependencies {\n    ...\n    implementation 'com.github.goldze:MVVMHabit:4.0.0'\n}\n```\n或\n\n下载例子程序，在主项目app的build.gradle中依赖例子程序中的**mvvmhabit**：\n```gradle\ndependencies {\t\n    ...\n    implementation project(':mvvmhabit')\n}\n```\n\n### 1.3、配置config.gradle\n如果不是远程依赖，而是下载的例子程序，那么还需要将例子程序中的config.gradle放入你的主项目根目录中，然后在根目录build.gradle的第一行加入：\n\n```gradle\napply from: \"config.gradle\"\n```\n\n**注意：** config.gradle中的 \n\nandroid = [] 是你的开发相关版本配置，可自行修改\n\nsupport = [] 是你的support相关配置，可自行修改\n\ndependencies = [] 是依赖第三方库的配置，可以加新库，但不要去修改原有第三方库的版本号，不然可能会编译不过\n### 1.4、配置AndroidManifest\n添加权限：\n```xml\n<uses-permission android:name=\"android.permission.INTERNET\" />\n<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n```\n配置Application：\n\n继承**mvvmhabit**中的BaseApplication，或者调用\n\n```java\nBaseApplication.setApplication(this);\n```\n来初始化你的Application\n\n可以在你的自己AppApplication中配置\n\n```java\n//是否开启日志打印\nKLog.init(true);\n//配置全局异常崩溃操作\nCaocConfig.Builder.create()\n    .backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //背景模式,开启沉浸式\n    .enabled(true) //是否启动全局异常捕获\n    .showErrorDetails(true) //是否显示错误详细信息\n    .showRestartButton(true) //是否显示重启按钮\n    .trackActivities(true) //是否跟踪Activity\n    .minTimeBetweenCrashesMs(2000) //崩溃的间隔时间(毫秒)\n    .errorDrawable(R.mipmap.ic_launcher) //错误图标\n    .restartActivity(LoginActivity.class) //重新启动后的activity\n    //.errorActivity(YourCustomErrorActivity.class) //崩溃后的错误activity\n    //.eventListener(new YourCustomEventListener()) //崩溃后的错误监听\n    .apply();\n```\n\n## 2、快速上手\n\n### 2.1、第一个Activity\n> 以大家都熟悉的登录操作为例：三个文件**LoginActivty.java**、**LoginViewModel.java**、**activity_login.xml**\n\n##### 2.1.1、关联ViewModel\n在activity_login.xml中关联LoginViewModel。\n```xml\n<layout>\n    <data>\n        <variable\n            type=\"com.goldze.mvvmhabit.ui.login.LoginViewModel\"\n            name=\"viewModel\"\n        />\n    </data>\n    .....\n\n</layout>\n```\n\n> variable - type：类的全路径 <br>variable - name：变量名\n\n##### 2.1.2、继承BaseActivity\n\nLoginActivity继承BaseActivity\n```java\n\npublic class LoginActivity extends BaseActivity<ActivityLoginBinding, LoginViewModel> {\n    //ActivityLoginBinding类是databinding框架自定生成的,对activity_login.xml\n    @Override\n    public int initContentView(Bundle savedInstanceState) {\n        return R.layout.activity_login;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public LoginViewModel initViewModel() {\n        //View持有ViewModel的引用，如果没有特殊业务处理，这个方法可以不重写\n        return ViewModelProviders.of(this).get(LoginViewModel.class);\n    }\n}\n```\n> 保存activity_login.xml后databinding会生成一个ActivityLoginBinding类。（如果没有生成，试着点击Build->Clean Project）\n\nBaseActivity是一个抽象类，有两个泛型参数，一个是ViewDataBinding，另一个是BaseViewModel，上面的ActivityLoginBinding则是继承的ViewDataBinding作为第一个泛型约束，LoginViewModel继承BaseViewModel作为第二个泛型约束。\n\n重写BaseActivity的二个抽象方法\n\ninitContentView() 返回界面layout的id<br>\ninitVariableId() 返回变量的id，对应activity_login中name=\"viewModel\"，就像一个控件的id，可以使用R.id.xxx，这里的BR跟R文件一样，由系统生成，使用BR.xxx找到这个ViewModel的id。<br>\n\n选择性重写initViewModel()方法，返回ViewModel对象\n```java\n@Override\npublic LoginViewModel initViewModel() {\n    //View持有ViewModel的引用，如果没有特殊业务处理，这个方法可以不重写\n    return ViewModelProviders.of(this).get(LoginViewModel.class);\n}\n```\n\n**注意：** 不重写initViewModel()，默认会创建LoginActivity中第二个泛型约束的LoginViewModel，如果没有指定第二个泛型，则会创建BaseViewModel\n\n##### 2.1.3、继承BaseViewModel\n\nLoginViewModel继承BaseViewModel\n```java\npublic class LoginViewModel extends BaseViewModel {\n    public LoginViewModel(@NonNull Application application) {\n        super(application);\n    }\n    ....\n}\n```\nBaseViewModel与BaseActivity通过LiveData来处理常用UI逻辑，即可在ViewModel中使用父类的showDialog()、startActivity()等方法。在这个LoginViewModel中就可以尽情的写你的逻辑了！\n> BaseFragment的使用和BaseActivity一样，详情参考Demo。\n\n### 2.2、数据绑定\n> 拥有databinding框架自带的双向绑定，也有扩展\n##### 2.2.1、传统绑定\n绑定用户名：\n\n在LoginViewModel中定义\n```java\n//用户名的绑定\npublic ObservableField<String> userName = new ObservableField<>(\"\");\n```\n在用户名EditText标签中绑定\n```xml\nandroid:text=\"@={viewModel.userName}\"\n```\n这样一来，输入框中输入了什么，userName.get()的内容就是什么，userName.set(\"\")设置什么，输入框中就显示什么。\n**注意：** @符号后面需要加=号才能达到双向绑定效果；userName需要是public的，不然viewModel无法找到它。\n\n点击事件绑定：\n\n在LoginViewModel中定义\n```java\n//登录按钮的点击事件\npublic View.OnClickListener loginOnClick = new View.OnClickListener() {\n    @Override\n    public void onClick(View v) {\n            \n    }\n};\n```\n在登录按钮标签中绑定\n```xml\nandroid:onClick=\"@{viewModel.loginOnClick}\"\n```\n这样一来，用户的点击事件直接被回调到ViewModel层了，更好的维护了业务逻辑\n\n这就是强大的databinding框架双向绑定的特性，不用再给控件定义id，setText()，setOnClickListener()。\n\n**但是，光有这些，完全满足不了我们复杂业务的需求啊！MVVMHabit闪亮登场：它有一套自定义的绑定规则，可以满足大部分的场景需求，请继续往下看。**\n\n##### 2.2.2、自定义绑定\n还拿点击事件说吧，不用传统的绑定方式，使用自定义的点击事件绑定。\n\n在LoginViewModel中定义\n```java\n//登录按钮的点击事件\npublic BindingCommand loginOnClickCommand = new BindingCommand(new BindingAction() {\n    @Override\n    public void call() {\n            \n    }\n});\n```\n在activity_login中定义命名空间\n```xml\nxmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n```\n在登录按钮标签中绑定\n```xml\nbinding:onClickCommand=\"@{viewModel.loginOnClickCommand}\"\n```\n这和原本传统的绑定不是一样吗？不，这其实是有差别的。使用这种形式的绑定，在原本事件绑定的基础之上，带有防重复点击的功能，1秒内多次点击也只会执行一次操作。如果不需要防重复点击，可以加入这条属性\n```xml\nbinding:isThrottleFirst=\"@{Boolean.TRUE}\"\n```\n那这功能是在哪里做的呢？答案在下面的代码中。\n```java\n//防重复点击间隔(秒)\npublic static final int CLICK_INTERVAL = 1;\n\n/**\n* requireAll 是意思是是否需要绑定全部参数, false为否\n* View的onClick事件绑定\n* onClickCommand 绑定的命令,\n* isThrottleFirst 是否开启防止过快点击\n*/\n@BindingAdapter(value = {\"onClickCommand\", \"isThrottleFirst\"}, requireAll = false)\npublic static void onClickCommand(View view, final BindingCommand clickCommand, final boolean isThrottleFirst) {\n    if (isThrottleFirst) {\n        RxView.clicks(view)\n        .subscribe(new Consumer<Object>() {\n            @Override\n            public void accept(Object object) throws Exception {\n                if (clickCommand != null) {\n                    clickCommand.execute();\n                }\n            }\n        });\n    } else {\n        RxView.clicks(view)\n        .throttleFirst(CLICK_INTERVAL, TimeUnit.SECONDS)//1秒钟内只允许点击1次\n        .subscribe(new Consumer<Object>() {\n            @Override\n            public void accept(Object object) throws Exception {\n                if (clickCommand != null) {\n                    clickCommand.execute();\n                }\n            }\n        });\n    }\n}\n```\nonClickCommand方法是自定义的，使用@BindingAdapter注解来标明这是一个绑定方法。在方法中使用了RxView来增强view的clicks事件，.throttleFirst()限制订阅者在指定的时间内重复执行，最后通过BindingCommand将事件回调出去，就好比有一种拦截器，在点击时先做一下判断，然后再把事件沿着他原有的方向传递。\n\n是不是觉得有点意思，好戏还在后头呢！\n##### 2.2.3、自定义ImageView图片加载\n绑定图片路径：\n\n在ViewModel中定义\n```java\npublic String imgUrl = \"http://img0.imgtn.bdimg.com/it/u=2183314203,562241301&fm=26&gp=0.jpg\";\n```\n在ImageView标签中\n```xml\nbinding:url=\"@{viewModel.imgUrl}\"\n```\nurl是图片路径，这样绑定后，这个ImageView就会去显示这张图片，不限网络图片还是本地图片。\n\n如果需要给一个默认加载中的图片，可以加这一句\n```xml\nbinding:placeholderRes=\"@{R.mipmap.ic_launcher_round}\"\n```\n> R文件需要在data标签中导入使用，如：`<import type=\"com.goldze.mvvmhabit.R\" />`\n\nBindingAdapter中的实现\n```java\n@BindingAdapter(value = {\"url\", \"placeholderRes\"}, requireAll = false)\npublic static void setImageUri(ImageView imageView, String url, int placeholderRes) {\n    if (!TextUtils.isEmpty(url)) {\n        //使用Glide框架加载图片\n        Glide.with(imageView.getContext())\n            .load(url)\n            .placeholder(placeholderRes)\n            .into(imageView);\n    }\n}\n```\n很简单就自定义了一个ImageView图片加载的绑定，学会这种方式，可自定义扩展。\n> 如果你对这些感兴趣，可以下载源码，在binding包中可以看到各类控件的绑定实现方式\n\n##### 2.2.4、RecyclerView绑定\n> RecyclerView也是很常用的一种控件，传统的方式需要针对各种业务要写各种Adapter，如果你使用了mvvmhabit，则可大大简化这种工作量，从此告别setAdapter()。\n\n在ViewModel中定义：\n```java\n//给RecyclerView添加items\npublic final ObservableList<NetWorkItemViewModel> observableList = new ObservableArrayList<>();\n//给RecyclerView添加ItemBinding\npublic final ItemBinding<NetWorkItemViewModel> itemBinding = ItemBinding.of(BR.viewModel, R.layout.item_network);\n```\nObservableList<>和ItemBinding<>的泛型是Item布局所对应的ItemViewModel\n\n在xml中绑定\n```xml\n<android.support.v7.widget.RecyclerView\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    binding:itemBinding=\"@{viewModel.itemBinding}\"\n    binding:items=\"@{viewModel.observableList}\"\n    binding:layoutManager=\"@{LayoutManagers.linear()}\"\n    binding:lineManager=\"@{LineManagers.horizontal()}\" />\n```\nlayoutManager控制是线性(包含水平和垂直)排列还是网格排列，lineManager是设置分割线\n\n网格布局的写法：`binding:layoutManager=\"@{LayoutManagers.grid(3)}`</br>\n水平布局的写法：`binding:layoutManager=\"@{LayoutManagers.linear(LinearLayoutManager.HORIZONTAL,Boolean.FALSE)}\"`</br>\n\n使用到相关类，则需要导入该类才能使用，和导入Java类相似\n\n> `<import type=\"me.tatarka.bindingcollectionadapter2.LayoutManagers\" />`</br>\n> `<import type=\"me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LineManagers\" />`</br>\n> `<import type=\"android.support.v7.widget.LinearLayoutManager\" />`\n\n\n这样绑定后，在ViewModel中调用ObservableList的add()方法，添加一个ItemViewModel，界面上就会实时绘制出一个Item。在Item对应的ViewModel中，同样可以以绑定的形式完成逻辑\n> 可以在请求到数据后，循环添加`observableList.add(new NetWorkItemViewModel(NetWorkViewModel.this, entity));`详细可以参考例子程序中NetWorkViewModel类。\n\n**注意：** 在以前的版本中，ItemViewModel是继承BaseViewModel，传入Context，新版本3.x中可继承ItemViewModel，传入当前页面的ViewModel\n\n更多RecyclerView、ListView、ViewPager等绑定方式，请参考 [https://github.com/evant/binding-collection-adapter](https://github.com/evant/binding-collection-adapter)\n\n### 2.3、网络请求\n> 网络请求一直都是一个项目的核心，现在的项目基本都离不开网络，一个好用网络请求框架可以让开发事半功倍。\n#### 2.3.1、Retrofit+Okhttp+RxJava\n> 现今，这三个组合基本是网络请求的标配，如果你对这三个框架不了解，建议先去查阅相关资料。\n\nsquare出品的框架，用起来确实非常方便。**MVVMHabit**中引入了\n```gradle\napi \"com.squareup.okhttp3:okhttp:3.10.0\"\napi \"com.squareup.retrofit2:retrofit:2.4.0\"\napi \"com.squareup.retrofit2:converter-gson:2.4.0\"\napi \"com.squareup.retrofit2:adapter-rxjava2:2.4.0\"\n```\n构建Retrofit时加入\n```java\nRetrofit retrofit = new Retrofit.Builder()\n    .addConverterFactory(GsonConverterFactory.create())\n    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())\n    .build();\n```\n或者直接使用例子程序中封装好的RetrofitClient\n#### 2.3.2、网络拦截器\n**LoggingInterceptor：** 全局拦截请求信息，格式化打印Request、Response，可以清晰的看到与后台接口对接的数据，\n```java\nLoggingInterceptor mLoggingInterceptor = new LoggingInterceptor\n    .Builder()//构建者模式\n    .loggable(true) //是否开启日志打印\n    .setLevel(Level.BODY) //打印的等级\n    .log(Platform.INFO) // 打印类型\n    .request(\"Request\") // request的Tag\n    .response(\"Response\")// Response的Tag\n    .addHeader(\"version\", BuildConfig.VERSION_NAME)//打印版本\n    .build()\n```\n构建okhttp时加入\n```java\nOkHttpClient okHttpClient = new OkHttpClient.Builder()\n    .addInterceptor(mLoggingInterceptor)\n    .build();\n```\n**CacheInterceptor：** 缓存拦截器，当没有网络连接的时候自动读取缓存中的数据，缓存存放时间默认为3天。</br>\n创建缓存对象\n```java\n//缓存时间\nint CACHE_TIMEOUT = 10 * 1024 * 1024\n//缓存存放的文件\nFile httpCacheDirectory = new File(mContext.getCacheDir(), \"goldze_cache\");\n//缓存对象\nCache cache = new Cache(httpCacheDirectory, CACHE_TIMEOUT);\n```\n构建okhttp时加入\n```java\nOkHttpClient okHttpClient = new OkHttpClient.Builder()\n    .cache(cache)\n    .addInterceptor(new CacheInterceptor(mContext))\n    .build();\n```\n#### 2.3.3、Cookie管理\n**MVVMHabit**提供两种CookieStore：**PersistentCookieStore** (SharedPreferences管理)和**MemoryCookieStore** (内存管理)，可以根据自己的业务需求，在构建okhttp时加入相应的cookieJar\n```java\nOkHttpClient okHttpClient = new OkHttpClient.Builder()\n    .cookieJar(new CookieJarImpl(new PersistentCookieStore(mContext)))\n    .build();\n```\n或者\n```java\nOkHttpClient okHttpClient = new OkHttpClient.Builder()\n    .cookieJar(new CookieJarImpl(new MemoryCookieStore()))\n    .build();\n```\n#### 2.3.4、绑定生命周期\n请求在ViewModel层。默认在BaseActivity中注入了LifecycleProvider对象到ViewModel，用于绑定请求的生命周期，View与请求共存亡。\n```java\nRetrofitClient.getInstance().create(DemoApiService.class)\n    .demoGet()\n    .compose(RxUtils.bindToLifecycle(getLifecycleProvider())) // 请求与View周期同步\n    .compose(RxUtils.schedulersTransformer())  // 线程调度\n    .compose(RxUtils.exceptionTransformer())   // 网络错误的异常转换\n    .subscribe(new Consumer<BaseResponse<DemoEntity>>() {\n        @Override\n        public void accept(BaseResponse<DemoEntity> response) throws Exception {\n                       \n        }\n    }, new Consumer<ResponseThrowable>() {\n        @Override\n        public void accept(ResponseThrowable throwable) throws Exception {\n                        \n        }\n    });\n\n```\n在请求时关键需要加入组合操作符`.compose(RxUtils.bindToLifecycle(getLifecycleProvider()))`<br>\n**注意：** 由于BaseActivity/BaseFragment都实现了LifecycleProvider接口，并且默认注入到ViewModel中，所以在调用请求方法时可以直接调用getLifecycleProvider()拿到生命周期接口。如果你没有使用 **mvvmabit** 里面的BaseActivity或BaseFragment，使用自己定义的Base，那么需要让你自己的Activity继承RxAppCompatActivity、Fragment继承RxFragment才能用`RxUtils.bindToLifecycle(lifecycle)`方法。\n#### 2.3.5、网络异常处理\n网络异常在网络请求中非常常见，比如请求超时、解析错误、资源不存在、服务器内部错误等，在客户端则需要做相应的处理(当然，你可以把一部分异常甩锅给网络，比如当出现code 500时，提示：请求超时，请检查网络连接，此时偷偷将异常信息发送至后台(手动滑稽))。<br>\n\n在使用Retrofit请求时，加入组合操作符`.compose(RxUtils.exceptionTransformer())`，当发生网络异常时，回调onError(ResponseThrowable)方法，可以拿到异常的code和message，做相应处理。<br>\n\n> mvvmhabit中自定义了一个[ExceptionHandle](./mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/ExceptionHandle.java)，已为你完成了大部分网络异常的判断，也可自行根据项目的具体需求调整逻辑。<br>\n\n**注意：** 这里的网络异常code，并非是与服务端协议约定的code。网络异常可以分为两部分，一部分是协议异常，即出现code = 404、500等，属于HttpException，另一部分为请求异常，即出现：连接超时、解析错误、证书验证失等。而与服务端约定的code规则，它不属于网络异常，它是属于一种业务异常。在请求中可以使用RxJava的filter(过滤器)，也可以自定义BaseSubscriber统一处理网络请求的业务逻辑异常。由于每个公司的业务协议不一样，所以具体需要你自己来处理该类异常。\n## 3、辅助功能\n> 一个完整的快速开发框架，当然也少不了常用的辅助类。下面来介绍一下**MVVMabit**中有哪些辅助功能。\n### 3.1、事件总线\n> 事件总线存在的优点想必大家都很清楚了，android自带的广播机制对于组件间的通信而言，使用非常繁琐，通信组件彼此之间的订阅和发布的耦合也比较严重，特别是对于事件的定义，广播机制局限于序列化的类（通过Intent传递），不够灵活。\n#### 3.3.1、RxBus\nRxBus并不是一个库，而是一种模式。相信大多数开发者都使用过EventBus，对RxBus也是很熟悉。由于**MVVMabit**中已经加入RxJava，所以采用了RxBus代替EventBus作为事件总线通信，以减少库的依赖。\n\n使用方法：\n\n在ViewModel中重写registerRxBus()方法来注册RxBus，重写removeRxBus()方法来移除RxBus\n```java\n//订阅者\nprivate Disposable mSubscription;\n//注册RxBus\n@Override\npublic void registerRxBus() {\n    super.registerRxBus();\n    mSubscription = RxBus.getDefault().toObservable(String.class)\n        .subscribe(new Consumer<String>() {\n            @Override\n            public void accept(String s) throws Exception {\n\n            }\n        });\n    //将订阅者加入管理站\n    RxSubscriptions.add(mSubscription);\n}\n\n//移除RxBus\n@Override\npublic void removeRxBus() {\n    super.removeRxBus();\n    //将订阅者从管理站中移除\n    RxSubscriptions.remove(mSubscription);\n}\n```\n在需要执行回调的地方发送\n```java\nRxBus.getDefault().post(object);\n```\n#### 3.3.2、Messenger\nMessenger是一个轻量级全局的消息通信工具，在我们的复杂业务中，难免会出现一些交叉的业务，比如ViewModel与ViewModel之间需要有数据交换，这时候可以轻松地使用Messenger发送一个实体或一个空消息，将事件从一个ViewModel回调到另一个ViewModel中。\n\n使用方法：\n\n定义一个静态String类型的字符串token\n```java\npublic static final String TOKEN_LOGINVIEWMODEL_REFRESH = \"token_loginviewmodel_refresh\";\n```\n在ViewModel中注册消息监听\n```java\n//注册一个空消息监听 \n//参数1：接受人（上下文）\n//参数2：定义的token\n//参数3：执行的回调监听\nMessenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, new BindingAction() {\n    @Override\n    public void call() {\n\t\n    }\n});\n\n//注册一个带数据回调的消息监听 \n//参数1：接受人（上下文）\n//参数2：定义的token\n//参数3：实体的泛型约束\n//参数4：执行的回调监听\nMessenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, String.class, new BindingConsumer<String>() {\n    @Override\n    public void call(String s) {\n         \n    }\n});\n```\n在需要回调的地方使用token发送消息\n```java\n//发送一个空消息\n//参数1：定义的token\nMessenger.getDefault().sendNoMsg(LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);\n\n//发送一个带数据回调消息\n//参数1：回调的实体\n//参数2：定义的token\nMessenger.getDefault().send(\"refresh\",LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);\n```\n> token最好不要重名，不然可能就会出现逻辑上的bug，为了更好的维护和清晰逻辑，建议以`aa_bb_cc`的格式来定义token。aa：TOKEN，bb：ViewModel的类名，cc：动作名（功能名）。\n\n> 为了避免大量使用Messenger，建议只在ViewModel与ViewModel之间使用，View与ViewModel之间采用ObservableField去监听UI上的逻辑，可在继承了Base的Activity或Fragment中重写initViewObservable()方法来初始化UI的监听\n\n\n注册了监听，当然也要解除它。在BaseActivity、BaseFragment的onDestroy()方法里已经调用`Messenger.getDefault().unregister(viewModel);`解除注册，所以不用担心忘记解除导致的逻辑错误和内存泄漏。\n### 3.2、文件下载\n文件下载几乎是每个app必备的功能，图文的下载，软件的升级等都要用到，mvvmhabit使用Retrofit+Okhttp+RxJava+RxBus实现一行代码监听带进度的文件下载。\n\n下载文件\n```java\nString loadUrl = \"你的文件下载路径\";\nString destFileDir = context.getCacheDir().getPath();  //文件存放的路径\nString destFileName = System.currentTimeMillis() + \".apk\";//文件存放的名称\nDownLoadManager.getInstance().load(loadUrl, new ProgressCallBack<ResponseBody>(destFileDir, destFileName) {\n    @Override\n    public void onStart() {\n        //RxJava的onStart()\n    }\n\n    @Override\n    public void onCompleted() {\n        //RxJava的onCompleted()\n    }\n\n    @Override\n    public void onSuccess(ResponseBody responseBody) {\n        //下载成功的回调\n    }\n\n    @Override\n    public void progress(final long progress, final long total) {\n        //下载中的回调 progress：当前进度 ，total：文件总大小\n    }\n\n    @Override\n    public void onError(Throwable e) {\n        //下载错误回调\n    }\n});\n```\n> 在ProgressResponseBody中使用了RxBus，发送下载进度信息到ProgressCallBack中，继承ProgressCallBack就可以监听到下载状态。回调方法全部执行在主线程，方便UI的更新，详情请参考例子程序。\n### 3.3、ContainerActivity\n一个盛装Fragment的一个容器(代理)Activity，普通界面只需要编写Fragment，使用此Activity盛装，这样就不需要每个界面都在AndroidManifest中注册一遍\n\n使用方法：\n\n在ViewModel中调用BaseViewModel的方法开一个Fragment\n```java\nstartContainerActivity(你的Fragment类名.class.getCanonicalName())\n```\n在ViewModel中调用BaseViewModel的方法，携带一个序列化实体打开一个Fragment\n```java\nBundle mBundle = new Bundle();\nmBundle.putParcelable(\"entity\", entity);\nstartContainerActivity(你的Fragment类名.class.getCanonicalName(), mBundle);\n```\n在你的Fragment中取出实体\n```java\nBundle mBundle = getArguments();\nif (mBundle != null) {\n    entity = mBundle.getParcelable(\"entity\");\n}\n```\n### 3.4、6.0权限申请\n> 对RxPermissions已经熟悉的朋友可以跳过。\n\n使用方法：\n\n例如请求相机权限，在ViewModel中调用\n```java\n//请求打开相机权限\nRxPermissions rxPermissions = new RxPermissions((Activity) context);\nrxPermissions.request(Manifest.permission.CAMERA)\n    .subscribe(new Consumer<Boolean>() {\n        @Override\n        public void accept(Boolean aBoolean) throws Exception {\n            if (aBoolean) {\n                ToastUtils.showShort(\"权限已经打开，直接跳入相机\");\n            } else {\n                ToastUtils.showShort(\"权限被拒绝\");\n            }\n        }\n    });\n```\n更多权限申请方式请参考[RxPermissions原项目地址](https://github.com/tbruyelle/RxPermissions)\n### 3.5、图片压缩\n> 为了节约用户流量和加快图片上传的速度，某些场景将图片在本地压缩后再传给后台，所以特此提供一个图片压缩的辅助功能。\n\n使用方法：\n\nRxJava的方式压缩单张图片，得到一个压缩后的图片文件对象\n```java\nString filePath = \"mnt/sdcard/1.png\";\nImageUtils.compressWithRx(filePath, new Consumer<File>() {\n    @Override\n    public void accept(File file) throws Exception {\n        //将文件放入RequestBody\n        ...\n    }\n});\n```\nRxJava的方式压缩多张图片，按集合顺序每压缩成功一张，都将在onNext方法中得到一个压缩后的图片文件对象\n```java\nList<String> filePaths = new ArrayList<>();\nfilePaths.add(\"mnt/sdcard/1.png\");\nfilePaths.add(\"mnt/sdcard/2.png\");\nImageUtils.compressWithRx(filePaths, new Subscriber() {\n    @Override\n    public void onCompleted() {\n\t\n    }\n\t\n    @Override\n    public void onError(Throwable e) {\n\t\n    }\n\t\n    @Override\n    public void onNext(File file) {\n\n    }\n});\n```\n### 3.6、其他辅助类\n**ToastUtils：** 吐司工具类\n\n**MaterialDialogUtils：** Material风格对话框工具类\n\n**SPUtils：** SharedPreferences工具类\n\n**SDCardUtils：** SD卡相关工具类\n\n**ConvertUtils：** 转换相关工具类\n\n**StringUtils：** 字符串相关工具类\n\n**RegexUtils：** 正则相关工具类\n\n**KLog：** 日志打印，含json格式打印\n\n## 4、附加\n\n### 4.1、编译错误解决方法\n> 使用databinding其实有个缺点，就是会遇到一些编译错误，而AS不能很好的定位到错误的位置，这对于刚开始使用databinding的开发者来说是一个比较郁闷的事。那么我在此把我自己在开发中遇到的各种编译问题的解决方法分享给大家，希望这对你会有所帮助。\n\n##### 4.1.1、绑定错误\n绑定错误是一个很常见的错误，基本都会犯。比如TextView的 `android:text=\"\"` ，本来要绑定的是一个String类型，结果你不小心，可能绑了一个Boolean上去，或者变量名写错了，这时候编辑器不会报红错，而是在点编译运行的时候，在AS的Messages中会出现错误提示，如下图：\n\n<img src=\"./img/error1.png\" width=\"640\" hegiht=\"640\" align=center />\n\n解决方法：把错误提示拉到最下面 (上面的提示找不到BR类这个不要管它)，看最后一个错误 ，这里会提示是哪个xml出了错，并且会定位到行数，按照提示找到对应位置，即可解决该编译错误的问题。\n\n**注意：** 行数要+1，意思是上面报出第33行错误，实际是第34行错误，AS定位的不准确 (这可能是它的一个bug)\n\n##### 4.1.2、xml导包错误\n在xml中需要导入ViewModel或者一些业务相关的类，假如在xml中导错了类，那一行则会报红，但是res/layout却没有错误提示，有一种场景，非常特殊，不容易找出错误位置。就是你写了一个xml，导入了一个类，比如XXXUtils，后来因为业务需求，把那个XXXUtils删了，这时候res/layout下不会出现任何错误，而你在编译运行的时候，才会出现错误日志。苦逼的是，不会像上面那样提示哪一个xml文件，哪一行出错了，最后一个错误只是一大片的报错报告。如下图：\n\n<img src=\"./img/error2.png\" width=\"640\" hegiht=\"640\" align=center />\n\n解决方法：同样找到最后一个错误提示，找到Cannot resolve type for **xxx**这一句 (xxx是类名)，然后使用全局搜索 (Ctrl+H) ，搜索哪个xml引用了这个类，跟踪点击进去，在xml就会出现一个红错，看到错误你就会明白了，这样就可解决该编译错误的问题。\n\n##### 4.1.3、build错误\n构建多module工程时，如出现【4.1.1、绑定错误】，且你能确定这个绑定是没有问题的，经过修改后出现下图错误：\n\n<img src=\"./img/error3.png\" width=\"640\" hegiht=\"640\" align=center />\n\n解决方法：\n这种是databinding比较大的坑，清理、重构和删build都不起作用，网上很难找到方法。经过试验，解决办法是手动创建异常中提到的文件夹，或者拷贝上一个没有报错的版本中对应的文件夹，可以解决这个异常\n\n##### 4.1.4、自动生成类错误\n有时候在写完xml时，databinding没有自动生成对应的Binding类及属性。比如新建了一个activity_login.xml，按照databinding的写法加入```<layout> <variable>```后，理论上会自动对应生成ActivityLoginBinding.java类和variable的属性，可能是as对databding的支持还不够吧，有时候偏偏就不生成，导致BR.xxx报红等一些莫名的错误。\n\n解决方法：其实确保自己的写法没有问题，是可以直接运行的，报红不一定是你写的有问题，也有可能是编译器抽风了。或者使用下面的办法</br>\n第一招：Build->Clean Project；</br>第二招：Build->Rebuild Project；</br>第三招：重启大法。\n\n##### 4.1.5、gradle错误\n如果遇到以下编译问题：\n\n错误: 无法将类 BindingRecyclerViewAdapters中的方法 setAdapter应用到给定类型;\n需要: RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory\n找到: RecyclerView,ItemBinding,ObservableList,BindingRecyclerViewAdapter<CAP#1>,ItemIds,ViewHolderFactory\n原因: 推断类型不符合等式约束条件\n推断: CAP#1\n等式约束条件: CAP#1,NetWorkItemViewModel\n其中, T是类型变量:\nT扩展已在方法 setAdapter(RecyclerView,ItemBinding,List,BindingRecyclerViewAdapter,ItemIds<? super T>,ViewHolderFactory)中声明的Object\n其中, CAP#1是新类型变量:\nCAP#1从?的捕获扩展Object\n\n一般是由于gradle plugin版本3.5.1造成的，请换成gradle plugin 3.5.0以下版本\n\n## 混淆\n例子程序中给出了最新的【MVVMHabit混淆规则】，包含MVVMHabit中依赖的所有第三方library，可以将规则直接拷贝到自己app的混淆规则中。在此基础上你只需要关注自己业务代码以及自己引入第三方的混淆，【MVVMHabit混淆规则】请参考app目录下的[proguard-rules.pro](./app/proguard-rules.pro)文件。\n\n## 组件化\n进阶Android组件化方案，请移步：[MVVMHabitComponent](https://github.com/goldze/MVVMHabitComponent)\n\n## About\n**goldze：** 本人喜欢尝试新的技术，以后发现有好用的东西，我将会在企业项目中实战，没有问题了就会把它引入到**MVVMHabit**中，一直维护着这套框架，谢谢各位朋友的支持。如果觉得这套框架不错的话，麻烦点个 **star**，你的支持则是我前进的动力！\n\n**QQ群**：84692105\n\n## Thank\n感谢[【zhangxiaoxiao】](https://github.com/zhanghacker)小伙伴长期提供的技术支持与帮助，为项目开源做出了很多的贡献。\n\n## License\n\n    Copyright 2017 goldze(曾宪泽)\n \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n \n        http://www.apache.org/licenses/LICENSE-2.0\n \n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n"
  },
  {
    "path": "UpdateLog.md",
    "content": "## 更新日志\n**v4.0.0：2021年07月16日**\n\n- 迁移AndroidX分支作为主线分支；\n- 升级第三方框架依赖版本；\n- 升级gradle插件版本支持；\n- 优化框架代码，解决已知Bug；\n- 修改文档说明。\n***\n**v3.0.7：2019年1月25日**\n\n- 优化框架代码，解决已知Bug；\n- 新增ViewPager+Fragment例子；\n- 新增RecycleView多布局例子；\n- 升级第三方依赖库；\n- 修改文档说明。\n***\n**v3.0.0：2018年10月8日**\n\n- 全面升级AAC，引入谷歌lifecycle组件；\n- 修改Base基类，满足新一套模式；\n- 升级第三方依赖库；\n- 修改例子程序；\n- 修改文档说明。\n***\n**v2.0.6：2018年7月19日**\n\n- 优化框架性能、基类逻辑，新增绑定命令；\n- 补充例子程序及注释；\n- 升级/修改第三方依赖库；\n- 补充文档说明。\n***\n**v2.0.0：2018年4月10日**\n\n- 全面升级RxJava2；\n- 优化绑定回调方式；\n- 升级第三方依赖库；\n- 微调例子程序。\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion rootProject.ext.android.compileSdkVersion\n    defaultConfig {\n        applicationId rootProject.ext.android.applicationId\n        minSdkVersion rootProject.ext.android.minSdkVersion\n        targetSdkVersion rootProject.ext.android.targetSdkVersion\n        versionCode rootProject.ext.android.versionCode\n        versionName rootProject.ext.android.versionName\n    }\n    buildTypes {\n        release {\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n        debug {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    dataBinding {\n        enabled true\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    //support\n    implementation rootProject.ext.support[\"design\"]\n    //下拉刷新,上拉加载\n    implementation 'com.lcodecorex:tkrefreshlayout:1.0.7'\n    //底部tabBar\n    implementation('me.majiajie:pager-bottom-tab-strip:2.2.5') {\n        exclude group: 'com.android.support'\n    }\n    //MVVMHabit\n    implementation project(':mvvmhabit')\n//    implementation rootProject.ext.dependencies.MVVMHabit\n    //内存泄漏测试\n    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'\n    debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in D:\\AndroidSDK/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n\n#------------------------------------------主项目混淆规则----------------------------------------------\n#实体类不参与混淆\n-keep class com.goldze.mvvmhabit.entity.** { *; }\n\n#tkrefreshlayout\n-keep class com.lcodecore.tkrefreshlayout.** { *; }\n-dontwarn com.lcodecore.tkrefreshlayout.**\n\n#-------------------------------------------MVVMHabit混淆规则----------------------------------------------\n#---------------------------------1.实体类---------------------------------\n\n-keep class me.goldze.mvvmhabit.http.BaseResponse { *; }\n\n#-------------------------------------------------------------------------\n\n#--------------------------------2.第三方包-------------------------------\n#support\n-keep class android.support.** { *; }\n-keep interface android.support.** { *; }\n-dontwarn android.support.**\n\n#databinding\n-keep class android.databinding.** { *; }\n-dontwarn android.databinding.**\n\n#annotation\n-keep class android.support.annotation.** { *; }\n-keep interface android.support.annotation.** { *; }\n\n#retrofit\n-dontwarn retrofit2.**\n-keep class retrofit2.** { *; }\n-keepattributes Signature\n-keepattributes Exceptions\n\n#gson\n-keepattributes Signature\n-keepattributes *Annotation*\n-keep class sun.misc.Unsafe { *; }\n-keep class com.google.gson.stream.** { *; }\n-keep class com.sunloto.shandong.bean.** { *; }\n\n#glide\n-keep public class * implements com.bumptech.glide.module.AppGlideModule\n-keep public class * implements com.bumptech.glide.module.LibraryGlideModule\n-keep class com.bumptech.glide.** { *; }\n-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {\n    **[] $VALUES;\n    public *;\n}\n\n#glide-transformations\n-keep class jp.wasabeef.glide.transformations.** {*;}\n-dontwarn jp.wasabeef.glide.transformations.**\n\n#okhttp\n-keepattributes Signature\n-keepattributes *Annotation*\n-keep class com.squareup.okhttp.** { *; }\n-keep interface com.squareup.okhttp.** { *; }\n-keep class okhttp3.** { *; }\n-keep interface okhttp3.** { *; }\n-dontwarn com.squareup.okhttp.**\n-dontwarn okhttp3.**\n-dontwarn okio.**\n\n#RxJava RxAndroid\n-dontwarn rx.*\n-dontwarn sun.misc.**\n\n-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {\n   long producerIndex;\n   long consumerIndex;\n}\n\n-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {\n    rx.internal.util.atomic.LinkedQueueNode producerNode;\n}\n\n-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {\n    rx.internal.util.atomic.LinkedQueueNode consumerNode;\n}\n\n#RxLifecycle\n-keep class com.trello.rxlifecycle2.** { *; }\n-keep interface com.trello.rxlifecycle2.** { *; }\n-dontwarn com.trello.rxlifecycle2.**\n\n#RxPermissions\n-keep class com.tbruyelle.rxpermissions2.** { *; }\n-keep interface com.tbruyelle.rxpermissions2.** { *; }\n\n#material-dialogs\n-keep class com.afollestad.materialdialogs.** { *; }\n-dontwarn om.afollestad.materialdialogs.**\n\n#=====================bindingcollectionadapter=====================\n-keep class me.tatarka.bindingcollectionadapter.** { *; }\n-dontwarn me.tatarka.bindingcollectionadapter.**\n\n\n#---------------------------------------------------------------------------\n\n#---------------------------------3.与js互相调用的类------------------------\n\n#无\n\n#----------------------------------------------------------------------------\n\n#---------------------------------4.反射相关的类和方法-----------------------\n-keep public class * extends me.goldze.mvvmhabit.base.BaseActivity{ *; }\n-keep public class * extends me.goldze.mvvmhabit.base.BaseFragment{ *; }\n-keep public class * extends me.goldze.mvvmhabit.binding.command.BindingCommand{ *; }\n-keep public class * extends me.goldze.mvvmhabit.binding.command.ResponseCommand{ *; }\n\n\n#----------------------------------------------------------------------------\n\n#---------------------------------5.自定义控件------------------------------\n\n-keep class me.goldze.mvvmhabit.widget.** { *; }\n\n#----------------------------------------------------------------------------\n#---------------------------------6.其他定制区-------------------------------\n#native方法不被混淆\n-keepclasseswithmembernames class * {\n    native <methods>;\n}\n#Parcelable 不被混淆\n-keep class * implements android.os.Parcelable {\n  public static final android.os.Parcelable$Creator *;\n}\n#Serializable 不被混淆\n-keepnames class * implements java.io.Serializable\n#Serializable 不被混淆并且enum 类也不被混淆\n-keepclassmembers class * implements java.io.Serializable {\n    static final long serialVersionUID;\n    private static final java.io.ObjectStreamField[] serialPersistentFields;\n    !static !transient <fields>;\n    !private <fields>;\n    !private <methods>;\n    private void writeObject(java.io.ObjectOutputStream);\n    private void readObject(java.io.ObjectInputStream);\n    java.lang.Object writeReplace();\n    java.lang.Object readResolve();\n}\n#保持枚举 enum 类不被混淆 如果混淆报错，建议直接使用上面的 -keepclassmembers class * implements java.io.Serializable即可\n-keepclassmembers enum * {\n  public static **[] values();\n  public static ** valueOf(java.lang.String);\n}\n-keepclassmembers class * {\n    public void *ButtonClicked(android.view.View);\n}\n#不混淆资源类\n-keepclassmembers class **.R$* {\n    public static <fields>;\n}\n#保持类中的所有方法名\n-keepclassmembers class * {\n    public <methods>;\n    private <methods>;\n}\n\n#----------------------------------------------------------------------------\n\n#---------------------------------基本指令区---------------------------------\n# 抑制警告\n-ignorewarnings\n#指定代码的压缩级别\n-optimizationpasses 5\n#包名不混合大小写\n-dontusemixedcaseclassnames\n#不去忽略非公共的库类\n-dontskipnonpubliclibraryclasses\n#指定不去忽略非公共库的类成员\n-dontskipnonpubliclibraryclassmembers\n #优化  不优化输入的类文件\n-dontoptimize\n #预校验\n-dontpreverify\n #混淆时是否记录日志\n-verbose\n # 混淆时所采用的算法\n-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*\n\n#混淆包路径\n-repackageclasses ''\n-flattenpackagehierarchy ''\n\n#保护注解\n-keepattributes *Annotation*\n\n#避免混淆泛型 如果混淆报错建议关掉\n-keepattributes Signature\n\n#保留SourceFile和LineNumber属性\n-keepattributes SourceFile,LineNumberTable\n\n#忽略警告\n#-ignorewarning\n#----------记录生成的日志数据,gradle build时在本项目根目录输出---------\n#apk 包内所有 class 的内部结构\n-dump class_files.txt\n#未混淆的类和成员\n-printseeds seeds.txt\n#列出从 apk 中删除的代码\n-printusage unused.txt\n#混淆前后的映射\n-printmapping mapping.txt\n#----------------------------------------------------------------------------\n\n#---------------------------------默认保留区---------------------------------\n-keep public class * extends android.app.Activity\n-keep public class * extends android.app.Application\n-keep public class * extends android.app.Service\n-keep public class * extends android.content.BroadcastReceiver\n-keep public class * extends android.content.ContentProvider\n-keep public class * extends android.app.backup.BackupAgentHelper\n-keep public class * extends android.preference.Preference\n-keep public class * extends android.view.View\n-keep public class com.android.vending.licensing.ILicensingService\n-keep class android.support.** {*;}\n\n-keep public class * extends android.view.View{\n    *** get*();\n    void set*(***);\n    public <init>(android.content.Context);\n    public <init>(android.content.Context, android.util.AttributeSet);\n    public <init>(android.content.Context, android.util.AttributeSet, int);\n}\n-keepclasseswithmembers class * {\n    public <init>(android.content.Context, android.util.AttributeSet);\n    public <init>(android.content.Context, android.util.AttributeSet, int);\n}\n-keepclassmembers class * implements java.io.Serializable {\n    static final long serialVersionUID;\n    private static final java.io.ObjectStreamField[] serialPersistentFields;\n    private void writeObject(java.io.ObjectOutputStream);\n    private void readObject(java.io.ObjectInputStream);\n    java.lang.Object writeReplace();\n    java.lang.Object readResolve();\n}\n-keep class **.R$* {\n *;\n}\n-keepclassmembers class * {\n    void *(**On*Event);\n}\n#----------------------------------------------------------------------------\n\n#---------------------------------webview------------------------------------\n-keepclassmembers class fqcn.of.javascript.interface.for.Webview {\n   public *;\n}\n-keepclassmembers class * extends android.webkit.WebViewClient {\n    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);\n    public boolean *(android.webkit.WebView, java.lang.String);\n}\n-keepclassmembers class * extends android.webkit.WebViewClient {\n    public void *(android.webkit.WebView, jav.lang.String);\n}\n#----------------------------------------------------------------------------\n#----------------------------------------------------------------------------"
  },
  {
    "path": "app/src/androidTest/java/com/goldze/mvvmhabit/ExampleInstrumentedTest.java",
    "content": "package com.goldze.mvvmhabit;\n\nimport android.content.Context;\nimport android.support.test.InstrumentationRegistry;\nimport android.support.test.runner.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumentation test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() throws Exception {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getTargetContext();\n\n        assertEquals(\"com.goldze.mvvmhabit\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.goldze.mvvmhabit\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\n    <application\n        android:name=\".app.AppApplication\"\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\">\n        <activity android:name=\".ui.login.LoginActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n        <activity android:name=\".ui.main.DemoActivity\"></activity>\n        <activity android:name=\".ui.tab_bar.activity.TabBarActivity\"></activity>\n        <activity android:name=\".ui.viewpager.activity.ViewPagerActivity\"></activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/app/AppApplication.java",
    "content": "package com.goldze.mvvmhabit.app;\n\nimport com.goldze.mvvmhabit.BuildConfig;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.ui.login.LoginActivity;\nimport com.squareup.leakcanary.LeakCanary;\n\nimport me.goldze.mvvmhabit.base.BaseApplication;\nimport me.goldze.mvvmhabit.crash.CaocConfig;\nimport me.goldze.mvvmhabit.utils.KLog;\n\n/**\n * Created by goldze on 2017/7/16.\n */\n\npublic class AppApplication extends BaseApplication {\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        //是否开启打印日志\n        KLog.init(BuildConfig.DEBUG);\n        //初始化全局异常崩溃\n        initCrash();\n        //内存泄漏检测\n        if (!LeakCanary.isInAnalyzerProcess(this)) {\n            LeakCanary.install(this);\n        }\n    }\n\n    private void initCrash() {\n        CaocConfig.Builder.create()\n                .backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //背景模式,开启沉浸式\n                .enabled(true) //是否启动全局异常捕获\n                .showErrorDetails(true) //是否显示错误详细信息\n                .showRestartButton(true) //是否显示重启按钮\n                .trackActivities(true) //是否跟踪Activity\n                .minTimeBetweenCrashesMs(2000) //崩溃的间隔时间(毫秒)\n                .errorDrawable(R.mipmap.ic_launcher) //错误图标\n                .restartActivity(LoginActivity.class) //重新启动后的activity\n//                .errorActivity(YourCustomErrorActivity.class) //崩溃后的错误activity\n//                .eventListener(new YourCustomEventListener()) //崩溃后的错误监听\n                .apply();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/app/AppViewModelFactory.java",
    "content": "package com.goldze.mvvmhabit.app;\n\nimport android.annotation.SuppressLint;\nimport android.app.Application;\n\nimport com.goldze.mvvmhabit.data.DemoRepository;\nimport com.goldze.mvvmhabit.ui.login.LoginViewModel;\nimport com.goldze.mvvmhabit.ui.network.NetWorkViewModel;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.VisibleForTesting;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\n/**\n * Created by goldze on 2019/3/26.\n */\npublic class AppViewModelFactory extends ViewModelProvider.NewInstanceFactory {\n    @SuppressLint(\"StaticFieldLeak\")\n    private static volatile AppViewModelFactory INSTANCE;\n    private final Application mApplication;\n    private final DemoRepository mRepository;\n\n    public static AppViewModelFactory getInstance(Application application) {\n        if (INSTANCE == null) {\n            synchronized (AppViewModelFactory.class) {\n                if (INSTANCE == null) {\n                    INSTANCE = new AppViewModelFactory(application, Injection.provideDemoRepository());\n                }\n            }\n        }\n        return INSTANCE;\n    }\n\n    @VisibleForTesting\n    public static void destroyInstance() {\n        INSTANCE = null;\n    }\n\n    private AppViewModelFactory(Application application, DemoRepository repository) {\n        this.mApplication = application;\n        this.mRepository = repository;\n    }\n\n    @NonNull\n    @Override\n    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {\n        if (modelClass.isAssignableFrom(NetWorkViewModel.class)) {\n            return (T) new NetWorkViewModel(mApplication, mRepository);\n        } else if (modelClass.isAssignableFrom(LoginViewModel.class)) {\n            return (T) new LoginViewModel(mApplication, mRepository);\n        }\n        throw new IllegalArgumentException(\"Unknown ViewModel class: \" + modelClass.getName());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/app/Injection.java",
    "content": "package com.goldze.mvvmhabit.app;\n\nimport com.goldze.mvvmhabit.data.DemoRepository;\nimport com.goldze.mvvmhabit.data.source.HttpDataSource;\nimport com.goldze.mvvmhabit.data.source.LocalDataSource;\nimport com.goldze.mvvmhabit.data.source.http.HttpDataSourceImpl;\nimport com.goldze.mvvmhabit.data.source.http.service.DemoApiService;\nimport com.goldze.mvvmhabit.data.source.local.LocalDataSourceImpl;\nimport com.goldze.mvvmhabit.utils.RetrofitClient;\n\n\n/**\n * 注入全局的数据仓库，可以考虑使用Dagger2。（根据项目实际情况搭建，千万不要为了架构而架构）\n * Created by goldze on 2019/3/26.\n */\npublic class Injection {\n    public static DemoRepository provideDemoRepository() {\n        //网络API服务\n        DemoApiService apiService = RetrofitClient.getInstance().create(DemoApiService.class);\n        //网络数据源\n        HttpDataSource httpDataSource = HttpDataSourceImpl.getInstance(apiService);\n        //本地数据源\n        LocalDataSource localDataSource = LocalDataSourceImpl.getInstance();\n        //两条分支组成一个数据仓库\n        return DemoRepository.getInstance(httpDataSource, localDataSource);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/binding/twinklingrefreshlayout/ViewAdapter.java",
    "content": "package com.goldze.mvvmhabit.binding.twinklingrefreshlayout;\n\nimport com.lcodecore.tkrefreshlayout.RefreshListenerAdapter;\nimport com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout;\n\nimport androidx.databinding.BindingAdapter;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n\n/**\n * Created by goldze on 2017/6/16.\n * TwinklingRefreshLayout列表刷新的绑定适配器\n */\npublic class ViewAdapter {\n\n    @BindingAdapter(value = {\"onRefreshCommand\", \"onLoadMoreCommand\"}, requireAll = false)\n    public static void onRefreshAndLoadMoreCommand(TwinklingRefreshLayout layout, final BindingCommand onRefreshCommand, final BindingCommand onLoadMoreCommand) {\n        layout.setOnRefreshListener(new RefreshListenerAdapter() {\n            @Override\n            public void onRefresh(TwinklingRefreshLayout refreshLayout) {\n                super.onRefresh(refreshLayout);\n                if (onRefreshCommand != null) {\n                    onRefreshCommand.execute();\n                }\n            }\n\n            @Override\n            public void onLoadMore(TwinklingRefreshLayout refreshLayout) {\n                super.onLoadMore(refreshLayout);\n                if (onLoadMoreCommand != null) {\n                    onLoadMoreCommand.execute();\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/data/DemoRepository.java",
    "content": "package com.goldze.mvvmhabit.data;\n\nimport com.goldze.mvvmhabit.data.source.HttpDataSource;\nimport com.goldze.mvvmhabit.data.source.LocalDataSource;\nimport com.goldze.mvvmhabit.entity.DemoEntity;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.VisibleForTesting;\nimport io.reactivex.Observable;\nimport me.goldze.mvvmhabit.base.BaseModel;\nimport me.goldze.mvvmhabit.http.BaseResponse;\n\n/**\n * MVVM的Model层，统一模块的数据仓库，包含网络数据和本地数据（一个应用可以有多个Repositor）\n * Created by goldze on 2019/3/26.\n */\npublic class DemoRepository extends BaseModel implements HttpDataSource, LocalDataSource {\n    private volatile static DemoRepository INSTANCE = null;\n    private final HttpDataSource mHttpDataSource;\n\n    private final LocalDataSource mLocalDataSource;\n\n    private DemoRepository(@NonNull HttpDataSource httpDataSource,\n                           @NonNull LocalDataSource localDataSource) {\n        this.mHttpDataSource = httpDataSource;\n        this.mLocalDataSource = localDataSource;\n    }\n\n    public static DemoRepository getInstance(HttpDataSource httpDataSource,\n                                             LocalDataSource localDataSource) {\n        if (INSTANCE == null) {\n            synchronized (DemoRepository.class) {\n                if (INSTANCE == null) {\n                    INSTANCE = new DemoRepository(httpDataSource, localDataSource);\n                }\n            }\n        }\n        return INSTANCE;\n    }\n\n    @VisibleForTesting\n    public static void destroyInstance() {\n        INSTANCE = null;\n    }\n\n\n    @Override\n    public Observable<Object> login() {\n        return mHttpDataSource.login();\n    }\n\n    @Override\n    public Observable<DemoEntity> loadMore() {\n        return mHttpDataSource.loadMore();\n    }\n\n    @Override\n    public Observable<BaseResponse<DemoEntity>> demoGet() {\n        return mHttpDataSource.demoGet();\n    }\n\n    @Override\n    public Observable<BaseResponse<DemoEntity>> demoPost(String catalog) {\n        return mHttpDataSource.demoPost(catalog);\n    }\n\n    @Override\n    public void saveUserName(String userName) {\n        mLocalDataSource.saveUserName(userName);\n    }\n\n    @Override\n    public void savePassword(String password) {\n        mLocalDataSource.savePassword(password);\n    }\n\n    @Override\n    public String getUserName() {\n        return mLocalDataSource.getUserName();\n    }\n\n    @Override\n    public String getPassword() {\n        return mLocalDataSource.getPassword();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/data/source/HttpDataSource.java",
    "content": "package com.goldze.mvvmhabit.data.source;\n\nimport com.goldze.mvvmhabit.entity.DemoEntity;\n\nimport io.reactivex.Observable;\nimport me.goldze.mvvmhabit.http.BaseResponse;\n\n/**\n * Created by goldze on 2019/3/26.\n */\npublic interface HttpDataSource {\n    //模拟登录\n    Observable<Object> login();\n\n    //模拟上拉加载\n    Observable<DemoEntity> loadMore();\n\n    Observable<BaseResponse<DemoEntity>> demoGet();\n\n    Observable<BaseResponse<DemoEntity>> demoPost(String catalog);\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/data/source/LocalDataSource.java",
    "content": "package com.goldze.mvvmhabit.data.source;\n\n/**\n * Created by goldze on 2019/3/26.\n */\npublic interface LocalDataSource {\n    /**\n     * 保存用户名\n     */\n    void saveUserName(String userName);\n\n    /**\n     * 保存用户密码\n     */\n\n    void savePassword(String password);\n\n    /**\n     * 获取用户名\n     */\n    String getUserName();\n\n    /**\n     * 获取用户密码\n     */\n    String getPassword();\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/data/source/http/HttpDataSourceImpl.java",
    "content": "package com.goldze.mvvmhabit.data.source.http;\n\nimport com.goldze.mvvmhabit.data.source.HttpDataSource;\nimport com.goldze.mvvmhabit.data.source.http.service.DemoApiService;\nimport com.goldze.mvvmhabit.entity.DemoEntity;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableEmitter;\nimport io.reactivex.ObservableOnSubscribe;\nimport me.goldze.mvvmhabit.http.BaseResponse;\n\n/**\n * Created by goldze on 2019/3/26.\n */\npublic class HttpDataSourceImpl implements HttpDataSource {\n    private DemoApiService apiService;\n    private volatile static HttpDataSourceImpl INSTANCE = null;\n\n    public static HttpDataSourceImpl getInstance(DemoApiService apiService) {\n        if (INSTANCE == null) {\n            synchronized (HttpDataSourceImpl.class) {\n                if (INSTANCE == null) {\n                    INSTANCE = new HttpDataSourceImpl(apiService);\n                }\n            }\n        }\n        return INSTANCE;\n    }\n\n    public static void destroyInstance() {\n        INSTANCE = null;\n    }\n\n    private HttpDataSourceImpl(DemoApiService apiService) {\n        this.apiService = apiService;\n    }\n\n    @Override\n    public Observable<Object> login() {\n        return Observable.just(new Object()).delay(3, TimeUnit.SECONDS); //延迟3秒\n    }\n\n    @Override\n    public Observable<DemoEntity> loadMore() {\n        return Observable.create(new ObservableOnSubscribe<DemoEntity>() {\n            @Override\n            public void subscribe(ObservableEmitter<DemoEntity> observableEmitter) throws Exception {\n                DemoEntity entity = new DemoEntity();\n                List<DemoEntity.ItemsEntity> itemsEntities = new ArrayList<>();\n                //模拟一部分假数据\n                for (int i = 0; i < 10; i++) {\n                    DemoEntity.ItemsEntity item = new DemoEntity.ItemsEntity();\n                    item.setId(-1);\n                    item.setName(\"模拟条目\");\n                    itemsEntities.add(item);\n                }\n                entity.setItems(itemsEntities);\n                observableEmitter.onNext(entity);\n            }\n        }).delay(3, TimeUnit.SECONDS); //延迟3秒\n    }\n\n    @Override\n    public Observable<BaseResponse<DemoEntity>> demoGet() {\n        return apiService.demoGet();\n    }\n\n    @Override\n    public Observable<BaseResponse<DemoEntity>> demoPost(String catalog) {\n        return apiService.demoPost(catalog);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/data/source/http/service/DemoApiService.java",
    "content": "package com.goldze.mvvmhabit.data.source.http.service;\n\nimport com.goldze.mvvmhabit.entity.DemoEntity;\n\nimport io.reactivex.Observable;\nimport me.goldze.mvvmhabit.http.BaseResponse;\nimport retrofit2.http.Field;\nimport retrofit2.http.FormUrlEncoded;\nimport retrofit2.http.GET;\nimport retrofit2.http.POST;\n\n/**\n * Created by goldze on 2017/6/15.\n */\n\npublic interface DemoApiService {\n    @GET(\"action/apiv2/banner?catalog=1\")\n    Observable<BaseResponse<DemoEntity>> demoGet();\n\n    @FormUrlEncoded\n    @POST(\"action/apiv2/banner\")\n    Observable<BaseResponse<DemoEntity>> demoPost(@Field(\"catalog\") String catalog);\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/data/source/local/LocalDataSourceImpl.java",
    "content": "package com.goldze.mvvmhabit.data.source.local;\n\nimport com.goldze.mvvmhabit.data.source.LocalDataSource;\n\nimport me.goldze.mvvmhabit.utils.SPUtils;\n\n/**\n * 本地数据源，可配合Room框架使用\n * Created by goldze on 2019/3/26.\n */\npublic class LocalDataSourceImpl implements LocalDataSource {\n    private volatile static LocalDataSourceImpl INSTANCE = null;\n\n    public static LocalDataSourceImpl getInstance() {\n        if (INSTANCE == null) {\n            synchronized (LocalDataSourceImpl.class) {\n                if (INSTANCE == null) {\n                    INSTANCE = new LocalDataSourceImpl();\n                }\n            }\n        }\n        return INSTANCE;\n    }\n\n    public static void destroyInstance() {\n        INSTANCE = null;\n    }\n\n    private LocalDataSourceImpl() {\n        //数据库Helper构建\n    }\n\n    @Override\n    public void saveUserName(String userName) {\n        SPUtils.getInstance().put(\"UserName\", userName);\n    }\n\n    @Override\n    public void savePassword(String password) {\n        SPUtils.getInstance().put(\"password\", password);\n    }\n\n    @Override\n    public String getUserName() {\n        return SPUtils.getInstance().getString(\"UserName\");\n    }\n\n    @Override\n    public String getPassword() {\n        return SPUtils.getInstance().getString(\"password\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/entity/DemoEntity.java",
    "content": "package com.goldze.mvvmhabit.entity;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport java.util.List;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class DemoEntity {\n    private String nextPageToken;\n    private String prevPageToken;\n    private int requestCount;\n    private int responseCount;\n    private int totalResults;\n    private List<ItemsEntity> items;\n\n    public String getNextPageToken() {\n        return nextPageToken;\n    }\n\n    public void setNextPageToken(String nextPageToken) {\n        this.nextPageToken = nextPageToken;\n    }\n\n    public String getPrevPageToken() {\n        return prevPageToken;\n    }\n\n    public void setPrevPageToken(String prevPageToken) {\n        this.prevPageToken = prevPageToken;\n    }\n\n    public int getRequestCount() {\n        return requestCount;\n    }\n\n    public void setRequestCount(int requestCount) {\n        this.requestCount = requestCount;\n    }\n\n    public int getResponseCount() {\n        return responseCount;\n    }\n\n    public void setResponseCount(int responseCount) {\n        this.responseCount = responseCount;\n    }\n\n    public int getTotalResults() {\n        return totalResults;\n    }\n\n    public void setTotalResults(int totalResults) {\n        this.totalResults = totalResults;\n    }\n\n    public List<ItemsEntity> getItems() {\n        return items;\n    }\n\n    public void setItems(List<ItemsEntity> items) {\n        this.items = items;\n    }\n\n    public static class ItemsEntity implements Parcelable{\n        private String detail;\n        private String href;\n        private int id;\n        private String img;\n        private String name;\n        private String pubDate;\n        private int type;\n\n        public String getDetail() {\n            return detail;\n        }\n\n        public void setDetail(String detail) {\n            this.detail = detail;\n        }\n\n        public String getHref() {\n            return href;\n        }\n\n        public void setHref(String href) {\n            this.href = href;\n        }\n\n        public int getId() {\n            return id;\n        }\n\n        public void setId(int id) {\n            this.id = id;\n        }\n\n        public String getImg() {\n            return img;\n        }\n\n        public void setImg(String img) {\n            this.img = img;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public void setName(String name) {\n            this.name = name;\n        }\n\n        public String getPubDate() {\n            return pubDate;\n        }\n\n        public void setPubDate(String pubDate) {\n            this.pubDate = pubDate;\n        }\n\n        public int getType() {\n            return type;\n        }\n\n        public void setType(int type) {\n            this.type = type;\n        }\n\n        @Override\n        public int describeContents() {\n            return 0;\n        }\n\n        @Override\n        public void writeToParcel(Parcel dest, int flags) {\n            dest.writeString(this.detail);\n            dest.writeString(this.href);\n            dest.writeInt(this.id);\n            dest.writeString(this.img);\n            dest.writeString(this.name);\n            dest.writeString(this.pubDate);\n            dest.writeInt(this.type);\n        }\n\n        public ItemsEntity() {\n        }\n\n        protected ItemsEntity(Parcel in) {\n            this.detail = in.readString();\n            this.href = in.readString();\n            this.id = in.readInt();\n            this.img = in.readString();\n            this.name = in.readString();\n            this.pubDate = in.readString();\n            this.type = in.readInt();\n        }\n\n        public static final Creator<ItemsEntity> CREATOR = new Creator<ItemsEntity>() {\n            @Override\n            public ItemsEntity createFromParcel(Parcel source) {\n                return new ItemsEntity(source);\n            }\n\n            @Override\n            public ItemsEntity[] newArray(int size) {\n                return new ItemsEntity[size];\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/entity/FormEntity.java",
    "content": "package com.goldze.mvvmhabit.entity;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.databinding.BaseObservable;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class FormEntity extends BaseObservable implements Parcelable {\n    private String id;\n    private String name;\n    private String sex;\n    private String Bir;\n    private String hobby;\n    private Boolean isMarry;\n\n    public String getId() {\n        return id;\n    }\n\n    public void setId(String id) {\n        this.id = id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getSex() {\n        return sex;\n    }\n\n    public void setSex(String sex) {\n        this.sex = sex;\n    }\n\n    public String getBir() {\n        return Bir;\n    }\n\n    public void setBir(String bir) {\n        Bir = bir;\n    }\n\n    public String getHobby() {\n        return hobby;\n    }\n\n    public void setHobby(String hobby) {\n        this.hobby = hobby;\n    }\n\n    public FormEntity() {\n    }\n\n    public Boolean getMarry() {\n        return isMarry;\n    }\n\n    public void setMarry(Boolean marry) {\n        isMarry = marry;\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeString(this.id);\n        dest.writeString(this.name);\n        dest.writeString(this.sex);\n        dest.writeString(this.Bir);\n        dest.writeString(this.hobby);\n        dest.writeValue(this.isMarry);\n    }\n\n    protected FormEntity(Parcel in) {\n        this.id = in.readString();\n        this.name = in.readString();\n        this.sex = in.readString();\n        this.Bir = in.readString();\n        this.hobby = in.readString();\n        this.isMarry = (Boolean) in.readValue(Boolean.class.getClassLoader());\n    }\n\n    public static final Creator<FormEntity> CREATOR = new Creator<FormEntity>() {\n        @Override\n        public FormEntity createFromParcel(Parcel source) {\n            return new FormEntity(source);\n        }\n\n        @Override\n        public FormEntity[] newArray(int size) {\n            return new FormEntity[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/entity/SpinnerItemData.java",
    "content": "package com.goldze.mvvmhabit.entity;\n\nimport me.goldze.mvvmhabit.binding.viewadapter.spinner.IKeyAndValue;\n\n/**\n * Created by goldze on 2017/7/17.\n * Spinner条目数据实体\n * 该实体类可以自定义,比如该类是数据库实体类. 或者是数据字典实体类, 但需要实现IKeyAndValue接口, 返回key和value两个值就可以在Spinner中绑定使用了\n */\n\npublic class SpinnerItemData implements IKeyAndValue {\n    //key是下拉显示的文字\n    private String key;\n    //value是对应需要上传给后台的值, 这个可以根据具体业务具体定义\n    private String value;\n\n    public SpinnerItemData(String key, String value) {\n        this.key = key;\n        this.value = value;\n    }\n\n    @Override\n    public String getKey() {\n        return key;\n    }\n\n    @Override\n    public String getValue() {\n        return value;\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/base/adapter/BaseFragmentPagerAdapter.java",
    "content": "package com.goldze.mvvmhabit.ui.base.adapter;\n\nimport java.util.List;\n\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.fragment.app.FragmentPagerAdapter;\n\n/**\n * Created by goldze on 2017/7/17.\n * FragmentPager适配器\n */\n\npublic class BaseFragmentPagerAdapter extends FragmentPagerAdapter {\n    private List<Fragment> list;//ViewPager要填充的fragment列表\n    private List<String> title;//tab中的title文字列表\n\n    //使用构造方法来将数据传进去\n    public BaseFragmentPagerAdapter(FragmentManager fm, List<Fragment> list, List<String> title) {\n        super(fm);\n        this.list = list;\n        this.title = title;\n    }\n\n    @Override\n    public Fragment getItem(int position) {//获得position中的fragment来填充\n        return list.get(position);\n    }\n\n    @Override\n    public int getCount() {//返回FragmentPager的个数\n        return list.size();\n    }\n\n    //FragmentPager的标题,如果重写这个方法就显示不出tab的标题内容\n    @Override\n    public CharSequence getPageTitle(int position) {\n        return title.get(position);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/base/fragment/BasePagerFragment.java",
    "content": "package com.goldze.mvvmhabit.ui.base.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.databinding.FragmentBasePagerBinding;\nimport com.goldze.mvvmhabit.ui.base.adapter.BaseFragmentPagerAdapter;\nimport com.google.android.material.tabs.TabLayout;\n\nimport java.util.List;\n\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\nimport me.goldze.mvvmhabit.base.BaseFragment;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\n\n/**\n * Created by goldze on 2017/7/17.\n * 抽取的二级BasePagerFragment\n */\n\npublic abstract class BasePagerFragment extends BaseFragment<FragmentBasePagerBinding, BaseViewModel> {\n\n    private List<Fragment> mFragments;\n    private List<String> titlePager;\n\n    protected abstract List<Fragment> pagerFragment();\n\n    protected abstract List<String> pagerTitleString();\n\n    @Override\n    public int initContentView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return R.layout.fragment_base_pager;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public void initData() {\n        mFragments = pagerFragment();\n        titlePager = pagerTitleString();\n        //设置Adapter\n        BaseFragmentPagerAdapter pagerAdapter = new BaseFragmentPagerAdapter(getChildFragmentManager(), mFragments, titlePager);\n        binding.viewPager.setAdapter(pagerAdapter);\n        binding.tabs.setupWithViewPager(binding.viewPager);\n        binding.viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabs));\n    }\n\n    @Override\n    public void initViewObservable() {\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/base/viewmodel/ToolbarViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.base.viewmodel;\n\nimport android.app.Application;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableField;\nimport androidx.databinding.ObservableInt;\nimport me.goldze.mvvmhabit.base.BaseModel;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/03\n * Description： 对应include标题的ToolbarViewModel\n * Toolbar的封装方式有很多种，具体封装需根据项目实际业务和习惯来编写\n * 所有例子仅做参考,业务多种多样,可能我这里写的例子和你的需求不同，理解如何使用才最重要。\n */\n\npublic class ToolbarViewModel<M extends BaseModel> extends BaseViewModel<M> {\n    //标题文字\n    public ObservableField<String> titleText = new ObservableField<>(\"\");\n    //右边文字\n    public ObservableField<String> rightText = new ObservableField<>(\"更多\");\n    //右边文字的观察者\n    public ObservableInt rightTextVisibleObservable = new ObservableInt(View.GONE);\n    //右边图标的观察者\n    public ObservableInt rightIconVisibleObservable = new ObservableInt(View.GONE);\n    //兼容databinding，去泛型化\n    public ToolbarViewModel toolbarViewModel;\n\n    public ToolbarViewModel(@NonNull Application application) {\n        this(application, null);\n    }\n\n    public ToolbarViewModel(@NonNull Application application, M model) {\n        super(application, model);\n        toolbarViewModel = this;\n    }\n\n    /**\n     * 设置标题\n     *\n     * @param text 标题文字\n     */\n    public void setTitleText(String text) {\n        titleText.set(text);\n    }\n\n    /**\n     * 设置右边文字\n     *\n     * @param text 右边文字\n     */\n    public void setRightText(String text) {\n        rightText.set(text);\n    }\n\n    /**\n     * 设置右边文字的显示和隐藏\n     *\n     * @param visibility\n     */\n    public void setRightTextVisible(int visibility) {\n        rightTextVisibleObservable.set(visibility);\n    }\n\n    /**\n     * 设置右边图标的显示和隐藏\n     *\n     * @param visibility\n     */\n    public void setRightIconVisible(int visibility) {\n        rightIconVisibleObservable.set(visibility);\n    }\n\n    /**\n     * 返回按钮的点击事件\n     */\n    public final BindingCommand backOnClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            finish();\n        }\n    });\n\n    public BindingCommand rightTextOnClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            rightTextOnClick();\n        }\n    });\n    public BindingCommand rightIconOnClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            rightIconOnClick();\n        }\n    });\n\n    /**\n     * 右边文字的点击事件，子类可重写\n     */\n    protected void rightTextOnClick() {\n    }\n\n    /**\n     * 右边图标的点击事件，子类可重写\n     */\n    protected void rightIconOnClick() {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/form/FormFragment.java",
    "content": "package com.goldze.mvvmhabit.ui.form;\n\nimport android.app.DatePickerDialog;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\nimport android.widget.DatePicker;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.databinding.FragmentFormBinding;\nimport com.goldze.mvvmhabit.entity.FormEntity;\n\nimport java.util.Calendar;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.databinding.Observable;\nimport androidx.lifecycle.Observer;\nimport me.goldze.mvvmhabit.base.BaseFragment;\nimport me.goldze.mvvmhabit.utils.MaterialDialogUtils;\n\n/**\n * Created by goldze on 2017/7/17.\n * 表单提交/编辑界面\n */\n\npublic class FormFragment extends BaseFragment<FragmentFormBinding, FormViewModel> {\n\n    private FormEntity entity = new FormEntity();\n\n    @Override\n    public void initParam() {\n        //获取列表传入的实体\n        Bundle mBundle = getArguments();\n        if (mBundle != null) {\n            entity = mBundle.getParcelable(\"entity\");\n        }\n    }\n\n    @Override\n    public int initContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        return R.layout.fragment_form;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public void initData() {\n        //通过binding拿到toolbar控件, 设置给Activity\n        ((AppCompatActivity) getActivity()).setSupportActionBar(binding.include.toolbar);\n        //View层传参到ViewModel层\n        viewModel.setFormEntity(entity);\n        //初始化标题\n        viewModel.initToolbar();\n    }\n\n    @Override\n    public void initViewObservable() {\n        //监听日期选择\n        viewModel.uc.showDateDialogObservable.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {\n            @Override\n            public void onPropertyChanged(Observable sender, int propertyId) {\n                final Calendar calendar = Calendar.getInstance();\n                int year = calendar.get(Calendar.YEAR);\n                int month = calendar.get(Calendar.MONTH);\n                int day = calendar.get(Calendar.DAY_OF_MONTH);\n                DatePickerDialog datePickerDialog = new DatePickerDialog(getContext(), new DatePickerDialog.OnDateSetListener() {\n                    @Override\n                    public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {\n                        viewModel.setBir(year, month, dayOfMonth);\n                    }\n                }, year, month, day);\n                datePickerDialog.setMessage(\"生日选择\");\n                datePickerDialog.show();\n            }\n        });\n        viewModel.entityJsonLiveData.observe(this, new Observer<String>() {\n            @Override\n            public void onChanged(@Nullable String submitJson) {\n                MaterialDialogUtils.showBasicDialog(getContext(), \"提交的json实体数据：\\r\\n\" + submitJson).show();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/form/FormViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.form;\n\nimport android.app.Application;\nimport android.text.TextUtils;\nimport android.view.View;\n\nimport com.goldze.mvvmhabit.entity.FormEntity;\nimport com.goldze.mvvmhabit.entity.SpinnerItemData;\nimport com.goldze.mvvmhabit.ui.base.viewmodel.ToolbarViewModel;\nimport com.google.gson.Gson;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableBoolean;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.binding.command.BindingConsumer;\nimport me.goldze.mvvmhabit.binding.viewadapter.spinner.IKeyAndValue;\nimport me.goldze.mvvmhabit.bus.event.SingleLiveEvent;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class FormViewModel extends ToolbarViewModel {\n    public FormEntity entity;\n\n    public List<IKeyAndValue> sexItemDatas;\n    public SingleLiveEvent<String> entityJsonLiveData = new SingleLiveEvent<>();\n    //封装一个界面发生改变的观察者\n    public UIChangeObservable uc;\n\n    public class UIChangeObservable {\n        //显示日期对话框\n        public ObservableBoolean showDateDialogObservable;\n\n        public UIChangeObservable() {\n            showDateDialogObservable = new ObservableBoolean(false);\n        }\n    }\n\n    public FormViewModel(@NonNull Application application) {\n        super(application);\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        uc = new UIChangeObservable();\n        //sexItemDatas 一般可以从本地Sqlite数据库中取出数据字典对象集合，让该对象实现IKeyAndValue接口\n        sexItemDatas = new ArrayList<>();\n        sexItemDatas.add(new SpinnerItemData(\"男\", \"1\"));\n        sexItemDatas.add(new SpinnerItemData(\"女\", \"2\"));\n    }\n\n    /**\n     * 初始化Toolbar\n     */\n    public void initToolbar() {\n        //初始化标题栏\n        setRightTextVisible(View.VISIBLE);\n        if (TextUtils.isEmpty(entity.getId())) {\n            //ID为空是新增\n            setTitleText(\"表单提交\");\n        } else {\n            //ID不为空是修改\n            setTitleText(\"表单编辑\");\n        }\n    }\n\n    @Override\n    public void rightTextOnClick() {\n        ToastUtils.showShort(\"更多\");\n    }\n\n    public void setFormEntity(FormEntity entity) {\n        if (this.entity == null) {\n            this.entity = entity;\n        }\n    }\n\n    //性别选择的监听\n    public BindingCommand<IKeyAndValue> onSexSelectorCommand = new BindingCommand<>(new BindingConsumer<IKeyAndValue>() {\n        @Override\n        public void call(IKeyAndValue iKeyAndValue) {\n            entity.setSex(iKeyAndValue.getValue());\n        }\n    });\n    //生日选择的监听\n    public BindingCommand onBirClickCommand = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //回调到view层(Fragment)中显示日期对话框\n            uc.showDateDialogObservable.set(!uc.showDateDialogObservable.get());\n        }\n    });\n    //是否已婚Switch点状态改变回调\n    public BindingCommand<Boolean> onMarryCheckedChangeCommand = new BindingCommand<>(new BindingConsumer<Boolean>() {\n        @Override\n        public void call(Boolean isChecked) {\n            entity.setMarry(isChecked);\n        }\n    });\n    //提交按钮点击事件\n    public BindingCommand onCmtClickCommand = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            String submitJson = new Gson().toJson(entity);\n            entityJsonLiveData.setValue(submitJson);\n        }\n    });\n\n    public void setBir(int year, int month, int dayOfMonth) {\n        //设置数据到实体中，自动刷新界面\n        entity.setBir(year + \"年\" + (month + 1) + \"月\" + dayOfMonth + \"日\");\n        //刷新实体,驱动界面更新\n        entity.notifyChange();\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/login/LoginActivity.java",
    "content": "package com.goldze.mvvmhabit.ui.login;\n\nimport android.os.Bundle;\nimport android.text.method.HideReturnsTransformationMethod;\nimport android.text.method.PasswordTransformationMethod;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.app.AppViewModelFactory;\nimport com.goldze.mvvmhabit.databinding.ActivityLoginBinding;\n\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModelProviders;\nimport me.goldze.mvvmhabit.base.BaseActivity;\n\n/**\n * 一个MVVM模式的登陆界面\n */\npublic class LoginActivity extends BaseActivity<ActivityLoginBinding, LoginViewModel> {\n    //ActivityLoginBinding类是databinding框架自定生成的,对应activity_login.xml\n    @Override\n    public int initContentView(Bundle savedInstanceState) {\n        return R.layout.activity_login;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public LoginViewModel initViewModel() {\n        //使用自定义的ViewModelFactory来创建ViewModel，如果不重写该方法，则默认会调用LoginViewModel(@NonNull Application application)构造方法\n        AppViewModelFactory factory = AppViewModelFactory.getInstance(getApplication());\n        return ViewModelProviders.of(this, factory).get(LoginViewModel.class);\n    }\n\n    @Override\n    public void initViewObservable() {\n        //监听ViewModel中pSwitchObservable的变化, 当ViewModel中执行【uc.pSwitchObservable.set(!uc.pSwitchObservable.get());】时会回调该方法\n        viewModel.uc.pSwitchEvent.observe(this, new Observer<Boolean>() {\n            @Override\n            public void onChanged(@Nullable Boolean aBoolean) {\n                //pSwitchObservable是boolean类型的观察者,所以可以直接使用它的值改变密码开关的图标\n                if (viewModel.uc.pSwitchEvent.getValue()) {\n                    //密码可见\n                    //在xml中定义id后,使用binding可以直接拿到这个view的引用,不再需要findViewById去找控件了\n                    binding.ivSwichPasswrod.setImageResource(R.mipmap.show_psw);\n                    binding.etPassword.setTransformationMethod(HideReturnsTransformationMethod.getInstance());\n                } else {\n                    //密码不可见\n                    binding.ivSwichPasswrod.setImageResource(R.mipmap.show_psw_press);\n                    binding.etPassword.setTransformationMethod(PasswordTransformationMethod.getInstance());\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/login/LoginViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.login;\n\nimport android.app.Application;\nimport android.text.TextUtils;\nimport android.view.View;\n\nimport com.goldze.mvvmhabit.data.DemoRepository;\nimport com.goldze.mvvmhabit.ui.main.DemoActivity;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableField;\nimport androidx.databinding.ObservableInt;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.functions.Consumer;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.binding.command.BindingConsumer;\nimport me.goldze.mvvmhabit.bus.event.SingleLiveEvent;\nimport me.goldze.mvvmhabit.utils.RxUtils;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class LoginViewModel extends BaseViewModel<DemoRepository> {\n    //用户名的绑定\n    public ObservableField<String> userName = new ObservableField<>(\"\");\n    //密码的绑定\n    public ObservableField<String> password = new ObservableField<>(\"\");\n    //用户名清除按钮的显示隐藏绑定\n    public ObservableInt clearBtnVisibility = new ObservableInt();\n    //封装一个界面发生改变的观察者\n    public UIChangeObservable uc = new UIChangeObservable();\n\n    public class UIChangeObservable {\n        //密码开关观察者\n        public SingleLiveEvent<Boolean> pSwitchEvent = new SingleLiveEvent<>();\n    }\n\n    public LoginViewModel(@NonNull Application application, DemoRepository repository) {\n        super(application, repository);\n        //从本地取得数据绑定到View层\n        userName.set(model.getUserName());\n        password.set(model.getPassword());\n    }\n\n    //清除用户名的点击事件, 逻辑从View层转换到ViewModel层\n    public BindingCommand clearUserNameOnClickCommand = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            userName.set(\"\");\n        }\n    });\n    //密码显示开关  (你可以尝试着狂按这个按钮,会发现它有防多次点击的功能)\n    public BindingCommand passwordShowSwitchOnClickCommand = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //让观察者的数据改变,逻辑从ViewModel层转到View层，在View层的监听则会被调用\n            uc.pSwitchEvent.setValue(uc.pSwitchEvent.getValue() == null || !uc.pSwitchEvent.getValue());\n        }\n    });\n    //用户名输入框焦点改变的回调事件\n    public BindingCommand<Boolean> onFocusChangeCommand = new BindingCommand<>(new BindingConsumer<Boolean>() {\n        @Override\n        public void call(Boolean hasFocus) {\n            if (hasFocus) {\n                clearBtnVisibility.set(View.VISIBLE);\n            } else {\n                clearBtnVisibility.set(View.INVISIBLE);\n            }\n        }\n    });\n    //登录按钮的点击事件\n    public BindingCommand loginOnClickCommand = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            login();\n        }\n    });\n\n    /**\n     * 网络模拟一个登陆操作\n     **/\n    private void login() {\n        if (TextUtils.isEmpty(userName.get())) {\n            ToastUtils.showShort(\"请输入账号！\");\n            return;\n        }\n        if (TextUtils.isEmpty(password.get())) {\n            ToastUtils.showShort(\"请输入密码！\");\n            return;\n        }\n        //RaJava模拟登录\n        addSubscribe(model.login()\n                .compose(RxUtils.schedulersTransformer()) //线程调度\n                .doOnSubscribe(new Consumer<Disposable>() {\n                    @Override\n                    public void accept(Disposable disposable) throws Exception {\n                        showDialog();\n                    }\n                })\n                .subscribe(new Consumer<Object>() {\n                    @Override\n                    public void accept(Object o) throws Exception {\n                        dismissDialog();\n                        //保存账号密码\n                        model.saveUserName(userName.get());\n                        model.savePassword(password.get());\n                        //进入DemoActivity页面\n                        startActivity(DemoActivity.class);\n                        //关闭页面\n                        finish();\n                    }\n                }));\n\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/main/DemoActivity.java",
    "content": "package com.goldze.mvvmhabit.ui.main;\n\nimport android.Manifest;\nimport android.app.ProgressDialog;\nimport android.content.pm.ActivityInfo;\nimport android.os.Bundle;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.databinding.ActivityDemoBinding;\nimport com.tbruyelle.rxpermissions2.RxPermissions;\n\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.Observer;\nimport io.reactivex.functions.Consumer;\nimport me.goldze.mvvmhabit.base.BaseActivity;\nimport me.goldze.mvvmhabit.http.DownLoadManager;\nimport me.goldze.mvvmhabit.http.download.ProgressCallBack;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\nimport okhttp3.ResponseBody;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class DemoActivity extends BaseActivity<ActivityDemoBinding, DemoViewModel> {\n    @Override\n    public void initParam() {\n        super.initParam();\n        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n    }\n\n    @Override\n    public int initContentView(Bundle savedInstanceState) {\n        return R.layout.activity_demo;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public void initViewObservable() {\n        //注册监听相机权限的请求\n        viewModel.requestCameraPermissions.observe(this, new Observer<Boolean>() {\n            @Override\n            public void onChanged(@Nullable Boolean aBoolean) {\n                requestCameraPermissions();\n            }\n        });\n        //注册文件下载的监听\n        viewModel.loadUrlEvent.observe(this, new Observer<String>() {\n            @Override\n            public void onChanged(@Nullable String url) {\n                downFile(url);\n            }\n        });\n    }\n\n    /**\n     * 请求相机权限\n     */\n    private void requestCameraPermissions() {\n        //请求打开相机权限\n        RxPermissions rxPermissions = new RxPermissions(DemoActivity.this);\n        rxPermissions.request(Manifest.permission.CAMERA)\n                .subscribe(new Consumer<Boolean>() {\n                    @Override\n                    public void accept(Boolean aBoolean) throws Exception {\n                        if (aBoolean) {\n                            ToastUtils.showShort(\"相机权限已经打开，直接跳入相机\");\n                        } else {\n                            ToastUtils.showShort(\"权限被拒绝\");\n                        }\n                    }\n                });\n    }\n\n    private void downFile(String url) {\n        String destFileDir = getApplication().getCacheDir().getPath();\n        String destFileName = System.currentTimeMillis() + \".apk\";\n        final ProgressDialog progressDialog = new ProgressDialog(this);\n        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);\n        progressDialog.setTitle(\"正在下载...\");\n        progressDialog.setCancelable(false);\n        progressDialog.show();\n        DownLoadManager.getInstance().load(url, new ProgressCallBack<ResponseBody>(destFileDir, destFileName) {\n            @Override\n            public void onStart() {\n                super.onStart();\n            }\n\n            @Override\n            public void onCompleted() {\n                progressDialog.dismiss();\n            }\n\n            @Override\n            public void onSuccess(ResponseBody responseBody) {\n                ToastUtils.showShort(\"文件下载完成！\");\n            }\n\n            @Override\n            public void progress(final long progress, final long total) {\n                progressDialog.setMax((int) total);\n                progressDialog.setProgress((int) progress);\n            }\n\n            @Override\n            public void onError(Throwable e) {\n                e.printStackTrace();\n                ToastUtils.showShort(\"文件下载失败！\");\n                progressDialog.dismiss();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/main/DemoViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.main;\n\nimport android.app.Application;\nimport android.os.Bundle;\n\nimport com.goldze.mvvmhabit.entity.FormEntity;\nimport com.goldze.mvvmhabit.ui.form.FormFragment;\nimport com.goldze.mvvmhabit.ui.network.NetWorkFragment;\nimport com.goldze.mvvmhabit.ui.rv_multi.MultiRecycleViewFragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.activity.TabBarActivity;\nimport com.goldze.mvvmhabit.ui.viewpager.activity.ViewPagerActivity;\nimport com.goldze.mvvmhabit.ui.vp_frg.ViewPagerGroupFragment;\n\nimport androidx.annotation.NonNull;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.bus.event.SingleLiveEvent;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class DemoViewModel extends BaseViewModel {\n    //使用Observable\n    public SingleLiveEvent<Boolean> requestCameraPermissions = new SingleLiveEvent<>();\n    //使用LiveData\n    public SingleLiveEvent<String> loadUrlEvent = new SingleLiveEvent<>();\n\n    public DemoViewModel(@NonNull Application application) {\n        super(application);\n    }\n\n    //网络访问点击事件\n    public BindingCommand netWorkClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            startContainerActivity(NetWorkFragment.class.getCanonicalName());\n        }\n    });\n    //RecycleView多布局\n    public BindingCommand rvMultiClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            startContainerActivity(MultiRecycleViewFragment.class.getCanonicalName());\n        }\n    });\n    //进入TabBarActivity\n    public BindingCommand startTabBarClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            startActivity(TabBarActivity.class);\n        }\n    });\n    //ViewPager绑定\n    public BindingCommand viewPagerBindingClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            startActivity(ViewPagerActivity.class);\n        }\n    });\n    //ViewPager+Fragment\n    public BindingCommand viewPagerGroupBindingClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            startContainerActivity(ViewPagerGroupFragment.class.getCanonicalName());\n        }\n    });\n    //表单提交点击事件\n    public BindingCommand formSbmClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            startContainerActivity(FormFragment.class.getCanonicalName());\n        }\n    });\n    //表单修改点击事件\n    public BindingCommand formModifyClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //模拟一个修改的实体数据\n            FormEntity entity = new FormEntity();\n            entity.setId(\"12345678\");\n            entity.setName(\"goldze\");\n            entity.setSex(\"1\");\n            entity.setBir(\"xxxx年xx月xx日\");\n            entity.setMarry(true);\n            //传入实体数据\n            Bundle mBundle = new Bundle();\n            mBundle.putParcelable(\"entity\", entity);\n            startContainerActivity(FormFragment.class.getCanonicalName(), mBundle);\n        }\n    });\n    //权限申请\n    public BindingCommand permissionsClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            requestCameraPermissions.call();\n        }\n    });\n\n    //全局异常捕获\n    public BindingCommand exceptionClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //伪造一个异常\n            Integer.parseInt(\"goldze\");\n        }\n    });\n    //文件下载\n    public BindingCommand fileDownLoadClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            loadUrlEvent.setValue(\"http://gdown.baidu.com/data/wisegame/dc8a46540c7960a2/baidushoujizhushou_16798087.apk\");\n        }\n    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/network/NetWorkFragment.java",
    "content": "package com.goldze.mvvmhabit.ui.network;\n\nimport android.content.pm.ActivityInfo;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.app.AppViewModelFactory;\nimport com.goldze.mvvmhabit.databinding.FragmentNetworkBinding;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModelProviders;\nimport me.goldze.mvvmhabit.base.BaseFragment;\nimport me.goldze.mvvmhabit.utils.MaterialDialogUtils;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\nimport me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter;\n\n/**\n * Created by goldze on 2017/7/17.\n * 网络请求列表界面\n */\n\npublic class NetWorkFragment extends BaseFragment<FragmentNetworkBinding, NetWorkViewModel> {\n    @Override\n    public void initParam() {\n        super.initParam();\n        getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n    }\n\n    @Override\n    public int initContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        return R.layout.fragment_network;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public NetWorkViewModel initViewModel() {\n        //使用自定义的ViewModelFactory来创建ViewModel，如果不重写该方法，则默认会调用NetWorkViewModel(@NonNull Application application)构造方法\n        AppViewModelFactory factory = AppViewModelFactory.getInstance(getActivity().getApplication());\n        return ViewModelProviders.of(this, factory).get(NetWorkViewModel.class);\n    }\n\n    @Override\n    public void initData() {\n        //请求网络数据\n        viewModel.requestNetWork();\n    }\n\n    @Override\n    public void initViewObservable() {\n        //监听下拉刷新完成\n        viewModel.uc.finishRefreshing.observe(this, new Observer() {\n            @Override\n            public void onChanged(@Nullable Object o) {\n                //结束刷新\n                binding.twinklingRefreshLayout.finishRefreshing();\n            }\n        });\n        //监听上拉加载完成\n        viewModel.uc.finishLoadmore.observe(this, new Observer() {\n            @Override\n            public void onChanged(@Nullable Object o) {\n                //结束刷新\n                binding.twinklingRefreshLayout.finishLoadmore();\n            }\n        });\n        //监听删除条目\n        viewModel.deleteItemLiveData.observe(this, new Observer<NetWorkItemViewModel>() {\n            @Override\n            public void onChanged(@Nullable final NetWorkItemViewModel netWorkItemViewModel) {\n                int index = viewModel.getItemPosition(netWorkItemViewModel);\n                //删除选择对话框\n                MaterialDialogUtils.showBasicDialog(getContext(), \"提示\", \"是否删除【\" + netWorkItemViewModel.entity.get().getName() + \"】？ position：\" + index)\n                        .onNegative(new MaterialDialog.SingleButtonCallback() {\n                            @Override\n                            public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                                ToastUtils.showShort(\"取消\");\n                            }\n                        }).onPositive(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        viewModel.deleteItem(netWorkItemViewModel);\n                    }\n                }).show();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/network/NetWorkItemViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.network;\n\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\n\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.entity.DemoEntity;\nimport com.goldze.mvvmhabit.ui.network.detail.DetailFragment;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\nimport androidx.databinding.ObservableField;\nimport me.goldze.mvvmhabit.base.ItemViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class NetWorkItemViewModel extends ItemViewModel<NetWorkViewModel> {\n    public ObservableField<DemoEntity.ItemsEntity> entity = new ObservableField<>();\n    public Drawable drawableImg;\n\n    public NetWorkItemViewModel(@NonNull NetWorkViewModel viewModel, DemoEntity.ItemsEntity entity) {\n        super(viewModel);\n        this.entity.set(entity);\n        //ImageView的占位图片，可以解决RecyclerView中图片错误问题\n        drawableImg = ContextCompat.getDrawable(viewModel.getApplication(), R.mipmap.ic_launcher);\n    }\n\n    /**\n     * 获取position的方式有很多种,indexOf是其中一种，常见的还有在Adapter中、ItemBinding.of回调里\n     *\n     * @return\n     */\n    public int getPosition() {\n        return viewModel.getItemPosition(this);\n    }\n\n    //条目的点击事件\n    public BindingCommand itemClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //这里可以通过一个标识,做出判断，已达到跳入不同界面的逻辑\n            if (entity.get().getId() == -1) {\n                viewModel.deleteItemLiveData.setValue(NetWorkItemViewModel.this);\n            } else {\n                //跳转到详情界面,传入条目的实体对象\n                Bundle mBundle = new Bundle();\n                mBundle.putParcelable(\"entity\", entity.get());\n                viewModel.startContainerActivity(DetailFragment.class.getCanonicalName(), mBundle);\n            }\n        }\n    });\n    //条目的长按事件\n    public BindingCommand itemLongClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //以前是使用Messenger发送事件，在NetWorkViewModel中完成删除逻辑\n//            Messenger.getDefault().send(NetWorkItemViewModel.this, NetWorkViewModel.TOKEN_NETWORKVIEWMODEL_DELTE_ITEM);\n            //现在ItemViewModel中存在ViewModel引用，可以直接拿到LiveData去做删除\n            ToastUtils.showShort(entity.get().getName());\n        }\n    });\n//    /**\n//     * 可以在xml中使用binding:currentView=\"@{viewModel.titleTextView}\" 拿到这个控件的引用, 但是强烈不推荐这样做，避免内存泄漏\n//     **/\n//    private TextView tv;\n//    //将标题TextView控件回调到ViewModel中\n//    public BindingCommand<TextView> titleTextView = new BindingCommand(new BindingConsumer<TextView>() {\n//        @Override\n//        public void call(TextView tv) {\n//            NetWorkItemViewModel.this.tv = tv;\n//        }\n//    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/network/NetWorkViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.network;\n\nimport android.app.Application;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.data.DemoRepository;\nimport com.goldze.mvvmhabit.entity.DemoEntity;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableArrayList;\nimport androidx.databinding.ObservableList;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.functions.Action;\nimport io.reactivex.functions.Consumer;\nimport io.reactivex.observers.DisposableObserver;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.bus.event.SingleLiveEvent;\nimport me.goldze.mvvmhabit.http.BaseResponse;\nimport me.goldze.mvvmhabit.http.ResponseThrowable;\nimport me.goldze.mvvmhabit.utils.RxUtils;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\nimport me.tatarka.bindingcollectionadapter2.ItemBinding;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class NetWorkViewModel extends BaseViewModel<DemoRepository> {\n    public SingleLiveEvent<NetWorkItemViewModel> deleteItemLiveData = new SingleLiveEvent<>();\n    //封装一个界面发生改变的观察者\n    public UIChangeObservable uc = new UIChangeObservable();\n\n    public class UIChangeObservable {\n        //下拉刷新完成\n        public SingleLiveEvent finishRefreshing = new SingleLiveEvent<>();\n        //上拉加载完成\n        public SingleLiveEvent finishLoadmore = new SingleLiveEvent<>();\n    }\n\n    public NetWorkViewModel(@NonNull Application application, DemoRepository repository) {\n        super(application, repository);\n    }\n\n    //给RecyclerView添加ObservableList\n    public ObservableList<NetWorkItemViewModel> observableList = new ObservableArrayList<>();\n    //给RecyclerView添加ItemBinding\n    public ItemBinding<NetWorkItemViewModel> itemBinding = ItemBinding.of(BR.viewModel, R.layout.item_network);\n    //下拉刷新\n    public BindingCommand onRefreshCommand = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            ToastUtils.showShort(\"下拉刷新\");\n            requestNetWork();\n        }\n    });\n    //上拉加载\n    public BindingCommand onLoadMoreCommand = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            if (observableList.size() > 50) {\n                ToastUtils.showLong(\"兄dei，你太无聊啦~崩是不可能的~\");\n                uc.finishLoadmore.call();\n                return;\n            }\n            //模拟网络上拉加载更多\n            model.loadMore()\n                    .compose(RxUtils.schedulersTransformer()) //线程调度\n                    .doOnSubscribe(NetWorkViewModel.this) //请求与ViewModel周期同步\n                    .doOnSubscribe(new Consumer<Disposable>() {\n                        @Override\n                        public void accept(Disposable disposable) throws Exception {\n                            ToastUtils.showShort(\"上拉加载\");\n                        }\n                    })\n                    .subscribe(new Consumer<DemoEntity>() {\n                        @Override\n                        public void accept(DemoEntity entity) throws Exception {\n                            for (DemoEntity.ItemsEntity itemsEntity : entity.getItems()) {\n                                NetWorkItemViewModel itemViewModel = new NetWorkItemViewModel(NetWorkViewModel.this, itemsEntity);\n                                //双向绑定动态添加Item\n                                observableList.add(itemViewModel);\n                            }\n                            //刷新完成收回\n                            uc.finishLoadmore.call();\n                        }\n                    });\n        }\n    });\n\n    /**\n     * 网络请求方法，在ViewModel中调用Model层，通过Okhttp+Retrofit+RxJava发起请求\n     */\n    public void requestNetWork() {\n        //可以调用addSubscribe()添加Disposable，请求与View周期同步\n        model.demoGet()\n                .compose(RxUtils.schedulersTransformer()) //线程调度\n                .compose(RxUtils.exceptionTransformer()) // 网络错误的异常转换, 这里可以换成自己的ExceptionHandle\n                .doOnSubscribe(this)//请求与ViewModel周期同步\n                .doOnSubscribe(new Consumer<Disposable>() {\n                    @Override\n                    public void accept(Disposable disposable) throws Exception {\n                        showDialog(\"正在请求...\");\n                    }\n                })\n                .subscribe(new DisposableObserver<BaseResponse<DemoEntity>>() {\n                    @Override\n                    public void onNext(BaseResponse<DemoEntity> response) {\n                        //清除列表\n                        observableList.clear();\n                        //请求成功\n                        if (response.getCode() == 1) {\n                            for (DemoEntity.ItemsEntity entity : response.getResult().getItems()) {\n                                NetWorkItemViewModel itemViewModel = new NetWorkItemViewModel(NetWorkViewModel.this, entity);\n                                //双向绑定动态添加Item\n                                observableList.add(itemViewModel);\n                            }\n                        } else {\n                            //code错误时也可以定义Observable回调到View层去处理\n                            ToastUtils.showShort(\"数据错误\");\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable throwable) {\n                        //关闭对话框\n                        dismissDialog();\n                        //请求刷新完成收回\n                        uc.finishRefreshing.call();\n                        if (throwable instanceof ResponseThrowable) {\n                            ToastUtils.showShort(((ResponseThrowable) throwable).message);\n                        }\n                    }\n\n                    @Override\n                    public void onComplete() {\n                        //关闭对话框\n                        dismissDialog();\n                        //请求刷新完成收回\n                        uc.finishRefreshing.call();\n                    }\n                });\n    }\n\n    /**\n     * 删除条目\n     *\n     * @param netWorkItemViewModel\n     */\n    public void deleteItem(NetWorkItemViewModel netWorkItemViewModel) {\n        //点击确定，在 observableList 绑定中删除，界面立即刷新\n        observableList.remove(netWorkItemViewModel);\n    }\n\n    /**\n     * 获取条目下标\n     *\n     * @param netWorkItemViewModel\n     * @return\n     */\n    public int getItemPosition(NetWorkItemViewModel netWorkItemViewModel) {\n        return observableList.indexOf(netWorkItemViewModel);\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/network/detail/DetailFragment.java",
    "content": "package com.goldze.mvvmhabit.ui.network.detail;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.databinding.FragmentDetailBinding;\nimport com.goldze.mvvmhabit.entity.DemoEntity;\n\nimport me.goldze.mvvmhabit.base.BaseFragment;\n\n/**\n * Created by goldze on 2017/7/17.\n * 详情界面\n */\n\npublic class DetailFragment extends BaseFragment<FragmentDetailBinding, DetailViewModel> {\n\n    private DemoEntity.ItemsEntity entity;\n\n    @Override\n    public void initParam() {\n        //获取列表传入的实体\n        Bundle mBundle = getArguments();\n        if (mBundle != null) {\n            entity = mBundle.getParcelable(\"entity\");\n        }\n    }\n\n    @Override\n    public int initContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        return R.layout.fragment_detail;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public void initData() {\n        viewModel.setDemoEntity(entity);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/network/detail/DetailViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.network.detail;\n\nimport android.app.Application;\n\nimport com.goldze.mvvmhabit.entity.DemoEntity;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableField;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\n\n/**\n * Created by goldze on 2017/7/17.\n */\n\npublic class DetailViewModel extends BaseViewModel {\n    public ObservableField<DemoEntity.ItemsEntity> entity = new ObservableField<>();\n\n    public DetailViewModel(@NonNull Application application) {\n        super(application);\n    }\n\n    public void setDemoEntity(DemoEntity.ItemsEntity entity) {\n        this.entity.set(entity);\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        entity = null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/rv_multi/MultiRecycleHeadViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.rv_multi;\n\nimport androidx.annotation.NonNull;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.goldze.mvvmhabit.base.MultiItemViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/25\n * Description：\n */\n\npublic class MultiRecycleHeadViewModel extends MultiItemViewModel {\n\n    public MultiRecycleHeadViewModel(@NonNull BaseViewModel viewModel) {\n        super(viewModel);\n    }\n\n    //条目的点击事件\n    public BindingCommand itemClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            ToastUtils.showShort(\"我是头布局\");\n        }\n    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/rv_multi/MultiRecycleLeftItemViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.rv_multi;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableField;\nimport me.goldze.mvvmhabit.base.MultiItemViewModel;\n\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/25\n * Description：\n */\n\npublic class MultiRecycleLeftItemViewModel extends MultiItemViewModel<MultiRecycleViewModel> {\n    public ObservableField<String> text = new ObservableField<>(\"\");\n\n    public MultiRecycleLeftItemViewModel(@NonNull MultiRecycleViewModel viewModel, String text) {\n        super(viewModel);\n        this.text.set(text);\n    }\n\n    //条目的点击事件\n    public BindingCommand itemClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //拿到position\n            int position = viewModel.observableList.indexOf(MultiRecycleLeftItemViewModel.this);\n            ToastUtils.showShort(\"position：\" + position);\n        }\n    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/rv_multi/MultiRecycleRightItemViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.rv_multi;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableField;\nimport me.goldze.mvvmhabit.base.MultiItemViewModel;\n\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/25\n * Description：\n */\n\npublic class MultiRecycleRightItemViewModel extends MultiItemViewModel<MultiRecycleViewModel> {\n    public ObservableField<String> text = new ObservableField<>(\"\");\n\n    public MultiRecycleRightItemViewModel(@NonNull MultiRecycleViewModel viewModel, String text) {\n        super(viewModel);\n        this.text.set(text);\n    }\n\n    //条目的点击事件\n    public BindingCommand itemClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //拿到position\n            int position = viewModel.observableList.indexOf(MultiRecycleRightItemViewModel.this);\n            ToastUtils.showShort(\"position：\" + position);\n        }\n    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/rv_multi/MultiRecycleViewFragment.java",
    "content": "package com.goldze.mvvmhabit.ui.rv_multi;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.databinding.FragmentMultiRvBinding;\n\nimport androidx.annotation.Nullable;\nimport me.goldze.mvvmhabit.base.BaseFragment;\nimport me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/25\n * Description：RecycleView多布局实现\n */\n\npublic class MultiRecycleViewFragment extends BaseFragment<FragmentMultiRvBinding, MultiRecycleViewModel> {\n    @Override\n    public int initContentView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return R.layout.fragment_multi_rv;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public void initData() {\n        super.initData();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/rv_multi/MultiRecycleViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.rv_multi;\n\nimport android.app.Application;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableArrayList;\nimport androidx.databinding.ObservableList;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.goldze.mvvmhabit.base.MultiItemViewModel;\nimport me.tatarka.bindingcollectionadapter2.ItemBinding;\nimport me.tatarka.bindingcollectionadapter2.OnItemBind;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/25\n * Description：\n */\n\npublic class MultiRecycleViewModel extends BaseViewModel {\n    private static final String MultiRecycleType_Head = \"head\";\n    private static final String MultiRecycleType_Left = \"left\";\n    private static final String MultiRecycleType_Right = \"right\";\n\n    public MultiRecycleViewModel(@NonNull Application application) {\n        super(application);\n        //模拟10个条目，数据源可以来自网络\n        for (int i = 0; i < 20; i++) {\n            if (i == 0) {\n                MultiItemViewModel item = new MultiRecycleHeadViewModel(this);\n                //条目类型为头布局\n                item.multiItemType(MultiRecycleType_Head);\n                observableList.add(item);\n            } else {\n                String text = \"我是第\" + i + \"条\";\n                if (i % 2 == 0) {\n                    MultiItemViewModel item = new MultiRecycleLeftItemViewModel(this, text);\n                    //条目类型为左布局\n                    item.multiItemType(MultiRecycleType_Left);\n                    observableList.add(item);\n                } else {\n                    MultiItemViewModel item = new MultiRecycleRightItemViewModel(this, text);\n                    //条目类型为右布局\n                    item.multiItemType(MultiRecycleType_Right);\n                    observableList.add(item);\n                }\n             }\n        }\n    }\n\n    //给RecyclerView添加ObservableList\n    public ObservableList<MultiItemViewModel> observableList = new ObservableArrayList<>();\n    //RecyclerView多布局添加ItemBinding\n    public ItemBinding<MultiItemViewModel> itemBinding = ItemBinding.of(new OnItemBind<MultiItemViewModel>() {\n        @Override\n        public void onItemBind(ItemBinding itemBinding, int position, MultiItemViewModel item) {\n            //通过item的类型, 动态设置Item加载的布局\n            String itemType = (String) item.getItemType();\n            if (MultiRecycleType_Head.equals(itemType)) {\n                //设置头布局\n                itemBinding.set(BR.viewModel, R.layout.item_multi_head);\n            } else if (MultiRecycleType_Left.equals(itemType)) {\n                //设置左布局\n                itemBinding.set(BR.viewModel, R.layout.item_multi_rv_left);\n            } else if (MultiRecycleType_Right.equals(itemType)) {\n                //设置右布局\n                itemBinding.set(BR.viewModel, R.layout.item_multi_rv_right);\n            }\n        }\n    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/tab_bar/activity/TabBarActivity.java",
    "content": "package com.goldze.mvvmhabit.ui.tab_bar.activity;\n\nimport android.os.Bundle;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.databinding.ActivityTabBarBinding;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar1Fragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar2Fragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar3Fragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar4Fragment;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport androidx.core.content.ContextCompat;\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentTransaction;\nimport me.goldze.mvvmhabit.base.BaseActivity;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.majiajie.pagerbottomtabstrip.NavigationController;\nimport me.majiajie.pagerbottomtabstrip.listener.OnTabItemSelectedListener;\n\n/**\n * 底部tab按钮的例子\n * 所有例子仅做参考,理解如何使用才最重要。\n * Created by goldze on 2018/7/18.\n */\n\npublic class TabBarActivity extends BaseActivity<ActivityTabBarBinding, BaseViewModel> {\n    private List<Fragment> mFragments;\n\n    @Override\n    public int initContentView(Bundle savedInstanceState) {\n        return R.layout.activity_tab_bar;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n    @Override\n    public void initData() {\n        //初始化Fragment\n        initFragment();\n        //初始化底部Button\n        initBottomTab();\n    }\n\n    private void initFragment() {\n        mFragments = new ArrayList<>();\n        mFragments.add(new TabBar1Fragment());\n        mFragments.add(new TabBar2Fragment());\n        mFragments.add(new TabBar3Fragment());\n        mFragments.add(new TabBar4Fragment());\n        //默认选中第一个\n        commitAllowingStateLoss(0);\n    }\n\n    private void initBottomTab() {\n        NavigationController navigationController = binding.pagerBottomTab.material()\n                .addItem(R.mipmap.yingyong, \"应用\")\n                .addItem(R.mipmap.huanzhe, \"工作\")\n                .addItem(R.mipmap.xiaoxi_select, \"消息\")\n                .addItem(R.mipmap.wode_select, \"我的\")\n                .setDefaultColor(ContextCompat.getColor(this, R.color.textColorVice))\n                .build();\n        //底部按钮的点击事件监听\n        navigationController.addTabItemSelectedListener(new OnTabItemSelectedListener() {\n            @Override\n            public void onSelected(int index, int old) {\n//                FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();\n//                transaction.replace(R.id.frameLayout, mFragments.get(index));\n//                transaction.commitAllowingStateLoss();\n                commitAllowingStateLoss(index);\n            }\n\n            @Override\n            public void onRepeat(int index) {\n            }\n        });\n    }\n    private void commitAllowingStateLoss(int position) {\n        hideAllFragment();\n        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();\n        Fragment currentFragment = getSupportFragmentManager().findFragmentByTag(position + \"\");\n        if (currentFragment != null) {\n            transaction.show(currentFragment);\n        } else {\n            currentFragment = mFragments.get(position);\n            transaction.add(R.id.frameLayout, currentFragment, position + \"\");\n        }\n        transaction.commitAllowingStateLoss();\n    }\n\n    //隐藏所有Fragment\n    private void hideAllFragment() {\n        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();\n        for (int i = 0; i < mFragments.size(); i++) {\n            Fragment currentFragment = getSupportFragmentManager().findFragmentByTag(i + \"\");\n            if (currentFragment != null) {\n                transaction.hide(currentFragment);\n            }\n        }\n        transaction.commitAllowingStateLoss();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/tab_bar/fragment/TabBar1Fragment.java",
    "content": "package com.goldze.mvvmhabit.ui.tab_bar.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\n\nimport androidx.annotation.Nullable;\nimport me.goldze.mvvmhabit.base.BaseFragment;\n\n/**\n * Created by goldze on 2018/7/18.\n */\n\npublic class TabBar1Fragment extends BaseFragment {\n    @Override\n    public int initContentView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return R.layout.fragment_tab_bar_1;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/tab_bar/fragment/TabBar2Fragment.java",
    "content": "package com.goldze.mvvmhabit.ui.tab_bar.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\n\nimport androidx.annotation.Nullable;\nimport me.goldze.mvvmhabit.base.BaseFragment;\n\n/**\n * Created by goldze on 2018/7/18.\n */\n\npublic class TabBar2Fragment extends BaseFragment {\n    @Override\n    public int initContentView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return R.layout.fragment_tab_bar_2;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/tab_bar/fragment/TabBar3Fragment.java",
    "content": "package com.goldze.mvvmhabit.ui.tab_bar.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\n\nimport androidx.annotation.Nullable;\nimport me.goldze.mvvmhabit.base.BaseFragment;\n\n/**\n * Created by goldze on 2018/7/18.\n */\n\npublic class TabBar3Fragment extends BaseFragment{\n    @Override\n    public int initContentView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return R.layout.fragment_tab_bar_3;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/tab_bar/fragment/TabBar4Fragment.java",
    "content": "package com.goldze.mvvmhabit.ui.tab_bar.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\n\nimport androidx.annotation.Nullable;\nimport me.goldze.mvvmhabit.base.BaseFragment;\n\n/**\n * Created by goldze on 2018/7/18.\n */\n\npublic class TabBar4Fragment extends BaseFragment {\n    @Override\n    public int initContentView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        return R.layout.fragment_tab_bar_4;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/viewpager/activity/ViewPagerActivity.java",
    "content": "package com.goldze.mvvmhabit.ui.viewpager.activity;\n\nimport android.os.Bundle;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\nimport com.goldze.mvvmhabit.databinding.FragmentViewpagerBinding;\nimport com.goldze.mvvmhabit.ui.viewpager.adapter.ViewPagerBindingAdapter;\nimport com.goldze.mvvmhabit.ui.viewpager.vm.ViewPagerViewModel;\nimport com.google.android.material.tabs.TabLayout;\n\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.Observer;\nimport me.goldze.mvvmhabit.base.BaseActivity;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\n\n/**\n * ViewPager绑定的例子, 更多绑定方式，请参考 https://github.com/evant/binding-collection-adapter\n * 所有例子仅做参考,千万不要把它当成一种标准,毕竟主打的不是例子,业务场景繁多,理解如何使用才最重要。\n * Created by goldze on 2018/7/18.\n */\n\npublic class ViewPagerActivity extends BaseActivity<FragmentViewpagerBinding, ViewPagerViewModel> {\n\n    @Override\n    public int initContentView(Bundle savedInstanceState) {\n        return R.layout.fragment_viewpager;\n    }\n\n    @Override\n    public int initVariableId() {\n        return BR.viewModel;\n    }\n\n\n    @Override\n    public void initData() {\n        // 使用 TabLayout 和 ViewPager 相关联\n        binding.tabs.setupWithViewPager(binding.viewPager);\n        binding.viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabs));\n        //给ViewPager设置adapter\n        binding.setAdapter(new ViewPagerBindingAdapter());\n    }\n\n    @Override\n    public void initViewObservable() {\n        viewModel.itemClickEvent.observe(this, new Observer<String>() {\n            @Override\n            public void onChanged(@Nullable String text) {\n                ToastUtils.showShort(\"position：\" + text);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/viewpager/adapter/ViewPagerBindingAdapter.java",
    "content": "package com.goldze.mvvmhabit.ui.viewpager.adapter;\n\nimport android.view.ViewGroup;\n\nimport com.goldze.mvvmhabit.databinding.ItemViewpagerBinding;\nimport com.goldze.mvvmhabit.ui.viewpager.vm.ViewPagerItemViewModel;\n\nimport androidx.databinding.ViewDataBinding;\nimport me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter;\n\n/**\n * Created by goldze on 2018/6/21.\n */\n\npublic class ViewPagerBindingAdapter extends BindingViewPagerAdapter<ViewPagerItemViewModel> {\n\n    @Override\n    public void onBindBinding(final ViewDataBinding binding, int variableId, int layoutRes, final int position, ViewPagerItemViewModel item) {\n        super.onBindBinding(binding, variableId, layoutRes, position, item);\n        //这里可以强转成ViewPagerItemViewModel对应的ViewDataBinding，\n        ItemViewpagerBinding _binding = (ItemViewpagerBinding) binding;\n    }\n\n    @Override\n    public void destroyItem(ViewGroup container, int position, Object object) {\n        super.destroyItem(container, position, object);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/viewpager/vm/ViewPagerItemViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.viewpager.vm;\n\nimport androidx.annotation.NonNull;\nimport me.goldze.mvvmhabit.base.ItemViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * 所有例子仅做参考,千万不要把它当成一种标准,毕竟主打的不是例子,业务场景繁多,理解如何使用才最重要。\n * Created by goldze on 2018/7/18.\n */\n\npublic class ViewPagerItemViewModel extends ItemViewModel<ViewPagerViewModel> {\n    public String text;\n\n    public ViewPagerItemViewModel(@NonNull ViewPagerViewModel viewModel, String text) {\n        super(viewModel);\n        this.text = text;\n    }\n\n    public BindingCommand onItemClick = new BindingCommand(new BindingAction() {\n        @Override\n        public void call() {\n            //点击之后将逻辑转到activity中处理\n            viewModel.itemClickEvent.setValue(text);\n        }\n    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/viewpager/vm/ViewPagerViewModel.java",
    "content": "package com.goldze.mvvmhabit.ui.viewpager.vm;\n\nimport android.app.Application;\n\nimport com.goldze.mvvmhabit.BR;\nimport com.goldze.mvvmhabit.R;\n\nimport androidx.annotation.NonNull;\nimport androidx.databinding.ObservableArrayList;\nimport androidx.databinding.ObservableList;\nimport me.goldze.mvvmhabit.base.BaseViewModel;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\nimport me.goldze.mvvmhabit.binding.command.BindingConsumer;\nimport me.goldze.mvvmhabit.bus.event.SingleLiveEvent;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\nimport me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter;\nimport me.tatarka.bindingcollectionadapter2.ItemBinding;\n\n/**\n * 所有例子仅做参考,千万不要把它当成一种标准,毕竟主打的不是例子,业务场景繁多,理解如何使用才最重要。\n * Created by goldze on 2018/7/18.\n */\n\npublic class ViewPagerViewModel extends BaseViewModel {\n    public SingleLiveEvent<String> itemClickEvent = new SingleLiveEvent<>();\n    public ViewPagerViewModel(@NonNull Application application) {\n        super(application);\n        //模拟3个ViewPager页面\n        for (int i = 1; i <= 3; i++) {\n            ViewPagerItemViewModel itemViewModel = new ViewPagerItemViewModel(this, \"第\" + i + \"个页面\");\n            items.add(itemViewModel);\n        }\n    }\n\n    //给ViewPager添加ObservableList\n    public ObservableList<ViewPagerItemViewModel> items = new ObservableArrayList<>();\n    //给ViewPager添加ItemBinding\n    public ItemBinding<ViewPagerItemViewModel> itemBinding = ItemBinding.of(BR.viewModel, R.layout.item_viewpager);\n    //给ViewPager添加PageTitle\n    public final BindingViewPagerAdapter.PageTitles<ViewPagerItemViewModel> pageTitles = new BindingViewPagerAdapter.PageTitles<ViewPagerItemViewModel>() {\n        @Override\n        public CharSequence getPageTitle(int position, ViewPagerItemViewModel item) {\n            return \"条目\" + position;\n        }\n    };\n    //ViewPager切换监听\n    public BindingCommand<Integer> onPageSelectedCommand = new BindingCommand<>(new BindingConsumer<Integer>() {\n        @Override\n        public void call(Integer index) {\n            ToastUtils.showShort(\"ViewPager切换：\" + index);\n        }\n    });\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/ui/vp_frg/ViewPagerGroupFragment.java",
    "content": "package com.goldze.mvvmhabit.ui.vp_frg;\n\nimport com.goldze.mvvmhabit.ui.base.fragment.BasePagerFragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar1Fragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar2Fragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar3Fragment;\nimport com.goldze.mvvmhabit.ui.tab_bar.fragment.TabBar4Fragment;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport androidx.fragment.app.Fragment;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/25\n * Description：ViewPager+Fragment的实现\n */\n\npublic class ViewPagerGroupFragment extends BasePagerFragment {\n    @Override\n    protected List<Fragment> pagerFragment() {\n        List<Fragment> list = new ArrayList<>();\n        list.add(new TabBar1Fragment());\n        list.add(new TabBar2Fragment());\n        list.add(new TabBar3Fragment());\n        list.add(new TabBar4Fragment());\n        return list;\n    }\n\n    @Override\n    protected List<String> pagerTitleString() {\n        List<String> list = new ArrayList<>();\n        list.add(\"page1\");\n        list.add(\"page2\");\n        list.add(\"page3\");\n        list.add(\"page4\");\n        return list;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/utils/HttpsUtils.java",
    "content": "/*\n * Copyright 2016 jeasonlzy(廖子尧)\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.goldze.mvvmhabit.utils;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.security.KeyManagementException;\nimport java.security.KeyStore;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.cert.Certificate;\nimport java.security.cert.CertificateException;\nimport java.security.cert.CertificateFactory;\nimport java.security.cert.X509Certificate;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.KeyManager;\nimport javax.net.ssl.KeyManagerFactory;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLSession;\nimport javax.net.ssl.SSLSocketFactory;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.TrustManagerFactory;\nimport javax.net.ssl.X509TrustManager;\n\npublic class HttpsUtils {\n\n    public static class SSLParams {\n        public SSLSocketFactory sSLSocketFactory;\n        public X509TrustManager trustManager;\n    }\n\n    public static SSLParams getSslSocketFactory() {\n        return getSslSocketFactoryBase(null, null, null);\n    }\n\n    /**\n     * https单向认证\n     * 可以额外配置信任服务端的证书策略，否则默认是按CA证书去验证的，若不是CA可信任的证书，则无法通过验证\n     */\n    public static SSLParams getSslSocketFactory(X509TrustManager trustManager) {\n        return getSslSocketFactoryBase(trustManager, null, null);\n    }\n\n    /**\n     * https单向认证\n     * 用含有服务端公钥的证书校验服务端证书\n     */\n    public static SSLParams getSslSocketFactory(InputStream... certificates) {\n        return getSslSocketFactoryBase(null, null, null, certificates);\n    }\n\n    /**\n     * https双向认证\n     * bksFile 和 password -> 客户端使用bks证书校验服务端证书\n     * certificates -> 用含有服务端公钥的证书校验服务端证书\n     */\n    public static SSLParams getSslSocketFactory(InputStream bksFile, String password, InputStream... certificates) {\n        return getSslSocketFactoryBase(null, bksFile, password, certificates);\n    }\n\n    /**\n     * https双向认证\n     * bksFile 和 password -> 客户端使用bks证书校验服务端证书\n     * X509TrustManager -> 如果需要自己校验，那么可以自己实现相关校验，如果不需要自己校验，那么传null即可\n     */\n    public static SSLParams getSslSocketFactory(InputStream bksFile, String password, X509TrustManager trustManager) {\n        return getSslSocketFactoryBase(trustManager, bksFile, password);\n    }\n\n    private static SSLParams getSslSocketFactoryBase(X509TrustManager trustManager, InputStream bksFile, String password, InputStream... certificates) {\n        SSLParams sslParams = new SSLParams();\n        try {\n            KeyManager[] keyManagers = prepareKeyManager(bksFile, password);\n            TrustManager[] trustManagers = prepareTrustManager(certificates);\n            X509TrustManager manager;\n            if (trustManager != null) {\n                //优先使用用户自定义的TrustManager\n                manager = trustManager;\n            } else if (trustManagers != null) {\n                //然后使用默认的TrustManager\n                manager = chooseTrustManager(trustManagers);\n            } else {\n                //否则使用不安全的TrustManager\n                manager = UnSafeTrustManager;\n            }\n            // 创建TLS类型的SSLContext对象， that uses our TrustManager\n            SSLContext sslContext = SSLContext.getInstance(\"TLS\");\n            // 用上面得到的trustManagers初始化SSLContext，这样sslContext就会信任keyStore中的证书\n            // 第一个参数是授权的密钥管理器，用来授权验证，比如授权自签名的证书验证。第二个是被授权的证书管理器，用来验证服务器端的证书\n            sslContext.init(keyManagers, new TrustManager[]{manager}, null);\n            // 通过sslContext获取SSLSocketFactory对象\n            sslParams.sSLSocketFactory = sslContext.getSocketFactory();\n            sslParams.trustManager = manager;\n            return sslParams;\n        } catch (NoSuchAlgorithmException e) {\n            throw new AssertionError(e);\n        } catch (KeyManagementException e) {\n            throw new AssertionError(e);\n        }\n    }\n\n    private static KeyManager[] prepareKeyManager(InputStream bksFile, String password) {\n        try {\n            if (bksFile == null || password == null) return null;\n            KeyStore clientKeyStore = KeyStore.getInstance(\"BKS\");\n            clientKeyStore.load(bksFile, password.toCharArray());\n            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());\n            kmf.init(clientKeyStore, password.toCharArray());\n            return kmf.getKeyManagers();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    private static TrustManager[] prepareTrustManager(InputStream... certificates) {\n        if (certificates == null || certificates.length <= 0) return null;\n        try {\n            CertificateFactory certificateFactory = CertificateFactory.getInstance(\"X.509\");\n            // 创建一个默认类型的KeyStore，存储我们信任的证书\n            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());\n            keyStore.load(null);\n            int index = 0;\n            for (InputStream certStream : certificates) {\n                String certificateAlias = Integer.toString(index++);\n                // 证书工厂根据证书文件的流生成证书 cert\n                Certificate cert = certificateFactory.generateCertificate(certStream);\n                // 将 cert 作为可信证书放入到keyStore中\n                keyStore.setCertificateEntry(certificateAlias, cert);\n                try {\n                    if (certStream != null) certStream.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n            //我们创建一个默认类型的TrustManagerFactory\n            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());\n            //用我们之前的keyStore实例初始化TrustManagerFactory，这样tmf就会信任keyStore中的证书\n            tmf.init(keyStore);\n            //通过tmf获取TrustManager数组，TrustManager也会信任keyStore中的证书\n            return tmf.getTrustManagers();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    private static X509TrustManager chooseTrustManager(TrustManager[] trustManagers) {\n        for (TrustManager trustManager : trustManagers) {\n            if (trustManager instanceof X509TrustManager) {\n                return (X509TrustManager) trustManager;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * 为了解决客户端不信任服务器数字证书的问题，网络上大部分的解决方案都是让客户端不对证书做任何检查，\n     * 这是一种有很大安全漏洞的办法\n     */\n    public static X509TrustManager UnSafeTrustManager = new X509TrustManager() {\n        @Override\n        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {\n        }\n\n        @Override\n        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {\n        }\n\n        @Override\n        public X509Certificate[] getAcceptedIssuers() {\n            return new X509Certificate[]{};\n        }\n    };\n\n    /**\n     * 此类是用于主机名验证的基接口。 在握手期间，如果 URL 的主机名和服务器的标识主机名不匹配，\n     * 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。\n     * 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的，则返回 true\n     */\n    public static HostnameVerifier UnSafeHostnameVerifier = new HostnameVerifier() {\n        @Override\n        public boolean verify(String hostname, SSLSession session) {\n            return true;\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/com/goldze/mvvmhabit/utils/RetrofitClient.java",
    "content": "package com.goldze.mvvmhabit.utils;\n\nimport android.content.Context;\nimport android.text.TextUtils;\n\nimport com.goldze.mvvmhabit.BuildConfig;\n\nimport java.io.File;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\nimport io.reactivex.Observer;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\nimport me.goldze.mvvmhabit.http.cookie.CookieJarImpl;\nimport me.goldze.mvvmhabit.http.cookie.store.PersistentCookieStore;\nimport me.goldze.mvvmhabit.http.interceptor.BaseInterceptor;\nimport me.goldze.mvvmhabit.http.interceptor.CacheInterceptor;\nimport me.goldze.mvvmhabit.http.interceptor.logging.Level;\nimport me.goldze.mvvmhabit.http.interceptor.logging.LoggingInterceptor;\nimport me.goldze.mvvmhabit.utils.KLog;\nimport me.goldze.mvvmhabit.utils.Utils;\nimport okhttp3.Cache;\nimport okhttp3.ConnectionPool;\nimport okhttp3.OkHttpClient;\nimport okhttp3.internal.platform.Platform;\nimport retrofit2.Retrofit;\nimport retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;\nimport retrofit2.converter.gson.GsonConverterFactory;\n\n/**\n * Created by goldze on 2017/5/10.\n * RetrofitClient封装单例类, 实现网络请求\n */\npublic class RetrofitClient {\n    //超时时间\n    private static final int DEFAULT_TIMEOUT = 20;\n    //缓存时间\n    private static final int CACHE_TIMEOUT = 10 * 1024 * 1024;\n    //服务端根路径\n    public static String baseUrl = \"https://www.oschina.net/\";\n\n    private static Context mContext = Utils.getContext();\n\n    private static OkHttpClient okHttpClient;\n    private static Retrofit retrofit;\n\n    private Cache cache = null;\n    private File httpCacheDirectory;\n\n    private static class SingletonHolder {\n        private static RetrofitClient INSTANCE = new RetrofitClient();\n    }\n\n    public static RetrofitClient getInstance() {\n        return SingletonHolder.INSTANCE;\n    }\n\n    private RetrofitClient() {\n        this(baseUrl, null);\n    }\n\n    private RetrofitClient(String url, Map<String, String> headers) {\n\n        if (TextUtils.isEmpty(url)) {\n            url = baseUrl;\n        }\n\n        if (httpCacheDirectory == null) {\n            httpCacheDirectory = new File(mContext.getCacheDir(), \"goldze_cache\");\n        }\n\n        try {\n            if (cache == null) {\n                cache = new Cache(httpCacheDirectory, CACHE_TIMEOUT);\n            }\n        } catch (Exception e) {\n            KLog.e(\"Could not create http cache\", e);\n        }\n        HttpsUtils.SSLParams sslParams = HttpsUtils.getSslSocketFactory();\n        okHttpClient = new OkHttpClient.Builder()\n                .cookieJar(new CookieJarImpl(new PersistentCookieStore(mContext)))\n//                .cache(cache)\n                .addInterceptor(new BaseInterceptor(headers))\n                .addInterceptor(new CacheInterceptor(mContext))\n                .sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager)\n                .addInterceptor(new LoggingInterceptor\n                        .Builder()//构建者模式\n                        .loggable(BuildConfig.DEBUG) //是否开启日志打印\n                        .setLevel(Level.BASIC) //打印的等级\n                        .log(Platform.INFO) // 打印类型\n                        .request(\"Request\") // request的Tag\n                        .response(\"Response\")// Response的Tag\n                        .addHeader(\"log-header\", \"I am the log request header.\") // 添加打印头, 注意 key 和 value 都不能是中文\n                        .build()\n                )\n                .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)\n                .writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)\n                .connectionPool(new ConnectionPool(8, 15, TimeUnit.SECONDS))\n                // 这里你可以根据自己的机型设置同时连接的个数和时间，我这里8个，和每个保持时间为10s\n                .build();\n        retrofit = new Retrofit.Builder()\n                .client(okHttpClient)\n                .addConverterFactory(GsonConverterFactory.create())\n                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())\n                .baseUrl(url)\n                .build();\n\n    }\n\n    /**\n     * create you ApiService\n     * Create an implementation of the API endpoints defined by the {@code service} interface.\n     */\n    public <T> T create(final Class<T> service) {\n        if (service == null) {\n            throw new RuntimeException(\"Api service is null!\");\n        }\n        return retrofit.create(service);\n    }\n\n    /**\n     * /**\n     * execute your customer API\n     * For example:\n     * MyApiService service =\n     * RetrofitClient.getInstance(MainActivity.this).create(MyApiService.class);\n     * <p>\n     * RetrofitClient.getInstance(MainActivity.this)\n     * .execute(service.lgon(\"name\", \"password\"), subscriber)\n     * * @param subscriber\n     */\n\n    public static <T> T execute(Observable<T> observable, Observer<T> subscriber) {\n        observable.subscribeOn(Schedulers.io())\n                .unsubscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(subscriber);\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/login_clear_input.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@mipmap/registered_delete_grey\" android:state_pressed=\"false\"/>\n    <item android:drawable=\"@mipmap/registered_delete_bule\" android:state_pressed=\"true\"/>\n</selector>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_demo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.main.DemoViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"DemoViewModel\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"#202020\"\n        android:orientation=\"vertical\">\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"网络访问\"\n            binding:onClickCommand=\"@{viewModel.netWorkClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"RecycleView多布局\"\n            binding:onClickCommand=\"@{viewModel.rvMultiClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"底部Tab按钮\"\n            binding:onClickCommand=\"@{viewModel.startTabBarClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"ViewPager绑定\"\n            binding:onClickCommand=\"@{viewModel.viewPagerBindingClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"ViewPager+Fragment\"\n            binding:onClickCommand=\"@{viewModel.viewPagerGroupBindingClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"表单提交\"\n            binding:onClickCommand=\"@{viewModel.formSbmClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"表单编辑\"\n            binding:onClickCommand=\"@{viewModel.formModifyClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"权限申请\"\n            binding:onClickCommand=\"@{viewModel.permissionsClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"全局异常捕获\"\n            binding:onClickCommand=\"@{viewModel.exceptionClick}\" />\n\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"文件下载\"\n            binding:onClickCommand=\"@{viewModel.fileDownLoadClick}\" />\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/activity_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:binding=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"viewModel\"\n            type=\"com.goldze.mvvmhabit.ui.login.LoginViewModel\" />\n    </data>\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\">\n\n        <ImageView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentBottom=\"true\"\n            android:background=\"@mipmap/login_back\"\n            android:scaleType=\"fitXY\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:gravity=\"center\"\n            android:orientation=\"vertical\"\n            android:padding=\"20dp\">\n\n            <ImageView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:src=\"@mipmap/logo\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"34dp\"\n                android:background=\"@mipmap/user_edit\"\n                android:gravity=\"center\"\n                android:orientation=\"horizontal\"\n                android:padding=\"16sp\">\n\n                <ImageView\n                    android:layout_width=\"22dp\"\n                    android:layout_height=\"22dp\"\n                    android:src=\"@mipmap/user_icon\" />\n\n                <View\n                    android:layout_width=\"1dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginLeft=\"8dp\"\n                    android:layout_marginRight=\"8dp\"\n                    android:background=\"@color/textColorHint\" />\n\n                <EditText\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"1\"\n                    android:background=\"@null\"\n                    android:hint=\"请输入用户名\"\n                    android:text=\"@={viewModel.userName}\"\n                    android:textColor=\"@color/textColor\"\n                    android:textColorHint=\"@color/textColorHint\"\n                    android:textSize=\"16sp\"\n                    binding:onFocusChangeCommand=\"@{viewModel.onFocusChangeCommand}\" />\n\n                <ImageView\n                    android:layout_width=\"30dp\"\n                    android:layout_height=\"30dp\"\n                    android:padding=\"6dp\"\n                    android:src=\"@mipmap/clean_edit\"\n                    android:visibility=\"@{viewModel.clearBtnVisibility}\"\n                    binding:onClickCommand=\"@{viewModel.clearUserNameOnClickCommand}\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:background=\"@mipmap/user_edit\"\n                android:gravity=\"center\"\n                android:orientation=\"horizontal\"\n                android:padding=\"16sp\">\n\n                <ImageView\n                    android:layout_width=\"22dp\"\n                    android:layout_height=\"22dp\"\n                    android:src=\"@mipmap/password_icon\" />\n\n                <View\n                    android:layout_width=\"1dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginLeft=\"8dp\"\n                    android:layout_marginRight=\"8dp\"\n                    android:background=\"@color/textColorHint\" />\n\n                <EditText\n                    android:id=\"@+id/et_password\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"1\"\n                    android:background=\"@null\"\n                    android:hint=\"请输入密码\"\n                    android:inputType=\"textPassword\"\n                    android:text=\"@={viewModel.password}\"\n                    android:textColor=\"@color/textColor\"\n                    android:textColorHint=\"@color/textColorHint\"\n                    android:textSize=\"16sp\" />\n\n                <ImageView\n                    android:id=\"@+id/iv_swich_passwrod\"\n                    android:layout_width=\"30dp\"\n                    android:layout_height=\"30dp\"\n                    android:padding=\"6dp\"\n                    android:src=\"@mipmap/show_psw_press\"\n                    binding:onClickCommand=\"@{viewModel.passwordShowSwitchOnClickCommand}\" />\n            </LinearLayout>\n\n            <RelativeLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"8dp\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_alignParentRight=\"true\"\n                    android:layout_marginRight=\"8dp\"\n                    android:text=\"忘记密码\"\n                    android:textColor=\"@color/colorPrimaryDark\"\n                    android:textSize=\"16sp\" />\n            </RelativeLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"16dp\"\n                android:background=\"@mipmap/btn_login\"\n                android:orientation=\"vertical\"\n                android:padding=\"6dp\">\n\n                <Button\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"?selectableItemBackground\"\n                    android:text=\"登录\"\n                    android:textColor=\"@color/white\"\n                    android:textSize=\"18sp\"\n                    binding:onClickCommand=\"@{viewModel.loginOnClickCommand}\" />\n            </LinearLayout>\n\n            <TextView\n                android:gravity=\"center\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"模拟一个登陆操作,随便输入账号密码点击登录即可进入\"\n                android:textColor=\"#EE1010\" />\n        </LinearLayout>\n    </RelativeLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/activity_tab_bar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"viewModel\"\n            type=\"me.goldze.mvvmhabit.base.BaseViewModel\" />\n    </data>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\"\n        android:orientation=\"vertical\">\n\n        <FrameLayout\n            android:id=\"@+id/frameLayout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\" />\n\n        <View\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"1dp\"\n            android:background=\"#F0F0F0\" />\n\n        <me.majiajie.pagerbottomtabstrip.PageBottomTabLayout\n            android:id=\"@+id/pager_bottom_tab\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"56dp\"\n            app:elevation=\"8dp\" />\n    </LinearLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_base_pager.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"me.goldze.mvvmhabit.base.BaseViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"BaseViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\"\n        android:orientation=\"vertical\">\n\n        <com.google.android.material.tabs.TabLayout\n            android:id=\"@+id/tabs\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"@color/white\"\n            app:tabGravity=\"fill\"\n            app:tabIndicatorColor=\"@color/colorPrimary\"\n            app:tabSelectedTextColor=\"@color/colorPrimary\"\n            app:tabTextColor=\"@android:color/black\" />\n\n        <View\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"1dp\"\n            android:background=\"#F0F0F0\" />\n\n        <androidx.viewpager.widget.ViewPager\n            android:id=\"@+id/viewPager\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\" />\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.network.detail.DetailViewModel\" />\n\n        <variable\n                name=\"viewModel\"\n                type=\"com.goldze.mvvmhabit.ui.network.detail.DetailViewModel\"\n                />\n        <import type=\"com.goldze.mvvmhabit.R\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n            xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"10dp\"\n            android:gravity=\"center\"\n            android:orientation=\"vertical\"\n            >\n\n        <ImageView\n                android:layout_width=\"280dp\"\n                android:layout_height=\"140dp\"\n                android:src=\"@mipmap/ic_launcher\"\n                binding:url=\"@{viewModel.entity.img}\"\n                binding:placeholderRes=\"@{R.mipmap.ic_launcher_round}\"\n                />\n\n        <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\"\n                >\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"id\"\n                    android:textSize=\"18sp\"\n                    />\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"3\"\n                    android:text=\"@{String.valueOf(viewModel.entity.id)}\"\n                    android:textSize=\"18sp\"\n                    />\n        </LinearLayout>\n\n        <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:orientation=\"horizontal\"\n                >\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"名称\"\n                    android:textSize=\"18sp\"\n                    />\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"3\"\n                    android:text=\"@{viewModel.entity.name}\"\n                    android:textSize=\"18sp\"\n                    />\n        </LinearLayout>\n\n        <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:orientation=\"horizontal\"\n                >\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"日期\"\n                    android:textSize=\"18sp\"\n                    />\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"3\"\n                    android:text=\"@{viewModel.entity.pubDate}\"\n                    android:textSize=\"18sp\"\n                    />\n        </LinearLayout>\n\n        <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:orientation=\"horizontal\"\n                >\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"图片路径\"\n                    android:textSize=\"18sp\"\n                    />\n\n            <TextView\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"3\"\n                    android:text=\"@{viewModel.entity.img}\"\n                    android:textSize=\"18sp\"\n                    />\n        </LinearLayout>\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_form.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.form.FormViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"com.goldze.mvvmhabit.ui.form.FormViewModel\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <include\n            android:id=\"@+id/include\"\n            layout=\"@layout/layout_toolbar\"\n            binding:toolbarViewModel=\"@{viewModel.toolbarViewModel}\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\"\n            android:padding=\"10dp\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"姓名\" />\n\n            <EditText\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"3\"\n                android:text=\"@={viewModel.entity.name}\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\"\n            android:padding=\"10dp\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"性别\" />\n\n            <Spinner\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"3\"\n                binding:itemDatas=\"@{viewModel.sexItemDatas}\"\n                binding:onItemSelectedCommand=\"@{viewModel.onSexSelectorCommand}\"\n                binding:valueReply=\"@{viewModel.entity.sex}\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\"\n            android:padding=\"10dp\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"生日\" />\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"3\"\n                android:text=\"@={viewModel.entity.bir}\"\n                binding:onClickCommand=\"@{viewModel.onBirClickCommand}\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\"\n            android:padding=\"10dp\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"是否已婚\" />\n\n            <Switch\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"3\"\n                binding:onCheckedChangeCommand=\"@{viewModel.onMarryCheckedChangeCommand}\"\n                binding:switchState=\"@{viewModel.entity.marry}\" />\n        </LinearLayout>\n\n        <Button\n            android:id=\"@+id/btn_cmt\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"立即提交\"\n            binding:onClickCommand=\"@{viewModel.onCmtClickCommand}\" />\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_multi_rv.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.rv_multi.MultiRecycleViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"MultiRecycleViewModel\" />\n\n        <import type=\"me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LayoutManagers\" />\n\n        <import type=\"me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LineManagers\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            binding:itemBinding=\"@{viewModel.itemBinding}\"\n            binding:items=\"@{viewModel.observableList}\"\n            binding:layoutManager=\"@{LayoutManagers.linear()}\"\n            binding:lineManager=\"@{LineManagers.horizontal()}\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_network.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.network.NetWorkViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"NetWorkViewModel\" />\n\n        <import type=\"me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LayoutManagers\" />\n\n        <import type=\"me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LineManagers\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout\n            android:id=\"@+id/twinklingRefreshLayout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            binding:onLoadMoreCommand=\"@{viewModel.onLoadMoreCommand}\"\n            binding:onRefreshCommand=\"@{viewModel.onRefreshCommand}\"\n            binding:tr_head_height=\"80dp\"\n            binding:tr_wave_height=\"80dp\">\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                binding:itemBinding=\"@{viewModel.itemBinding}\"\n                binding:items=\"@{viewModel.observableList}\"\n                binding:layoutManager=\"@{LayoutManagers.linear()}\"\n                binding:lineManager=\"@{LineManagers.horizontal()}\" />\n\n        </com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout>\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_tab_bar_1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"me.goldze.mvvmhabit.base.BaseViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"BaseViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"top|left\"\n        android:orientation=\"vertical\"\n        android:padding=\"10dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"TabBar_1\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_tab_bar_2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"me.goldze.mvvmhabit.base.BaseViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"BaseViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"top|right\"\n        android:orientation=\"vertical\"\n        android:padding=\"10dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"TabBar_2\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_tab_bar_3.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"me.goldze.mvvmhabit.base.BaseViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"BaseViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"bottom|left\"\n        android:orientation=\"vertical\"\n        android:padding=\"10dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"TabBar_3\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_tab_bar_4.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"me.goldze.mvvmhabit.base.BaseViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"BaseViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"bottom|right\"\n        android:orientation=\"vertical\"\n        android:padding=\"10dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"TabBar_4\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_viewpager.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.viewpager.vm.ViewPagerViewModel\" />\n\n        <import type=\"com.goldze.mvvmhabit.ui.viewpager.adapter.ViewPagerBindingAdapter\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"ViewPagerViewModel\" />\n\n        <variable\n            name=\"adapter\"\n            type=\"ViewPagerBindingAdapter\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <com.google.android.material.tabs.TabLayout\n            android:id=\"@+id/tabs\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            binding:tabGravity=\"fill\"\n            binding:tabIndicatorColor=\"@color/colorPrimary\"\n            binding:tabSelectedTextColor=\"@color/colorPrimary\"\n            binding:tabTextColor=\"@android:color/black\" />\n\n        <View\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"1dp\"\n            android:background=\"#F0F0F0\" />\n\n        <androidx.viewpager.widget.ViewPager\n            android:id=\"@+id/viewPager\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\"\n            binding:adapter=\"@{adapter}\"\n            binding:itemBinding=\"@{viewModel.itemBinding}\"\n            binding:items=\"@{viewModel.items}\"\n            binding:onPageSelectedCommand=\"@{viewModel.onPageSelectedCommand}\"\n            binding:pageTitles=\"@{viewModel.pageTitles}\" />\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_multi_head.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.rv_multi.MultiRecycleHeadViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"MultiRecycleHeadViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"160dp\"\n        android:background=\"?android:selectableItemBackground\"\n        android:gravity=\"center\"\n        android:orientation=\"vertical\"\n        binding:onClickCommand=\"@{viewModel.itemClick}\">\n\n        <ImageView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:src=\"@mipmap/ic_launcher\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"我是头布局\"\n            android:textColor=\"@color/textColor\"\n            android:textSize=\"18sp\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_multi_rv_left.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.rv_multi.MultiRecycleLeftItemViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"MultiRecycleLeftItemViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"#DDDDDD\"\n        android:orientation=\"vertical\"\n        android:padding=\"10dp\"\n        binding:onClickCommand=\"@{viewModel.itemClick}\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@{viewModel.text}\"\n            android:textColor=\"@color/textColor\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_multi_rv_right.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.rv_multi.MultiRecycleRightItemViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"MultiRecycleRightItemViewModel\" />\n\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"#909090\"\n        android:gravity=\"right\"\n        android:orientation=\"vertical\"\n        android:padding=\"10dp\"\n        binding:onClickCommand=\"@{viewModel.itemClick}\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@{viewModel.text}\"\n            android:textColor=\"@color/textColor\" />\n\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_network.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.network.NetWorkItemViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"com.goldze.mvvmhabit.ui.network.NetWorkItemViewModel\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?android:selectableItemBackground\"\n        android:gravity=\"center_vertical\"\n        android:orientation=\"horizontal\"\n        android:padding=\"10dp\"\n        binding:onClickCommand=\"@{viewModel.itemClick}\"\n        binding:onLongClickCommand=\"@{viewModel.itemLongClick}\">\n\n        <ImageView\n            android:layout_width=\"50dp\"\n            android:layout_height=\"50dp\"\n            android:src=\"@{viewModel.drawableImg}\"\n            binding:url=\"@{viewModel.entity.img}\" />\n\n        <TextView\n            android:id=\"@+id/tv_title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"6dp\"\n            android:text=\"@{viewModel.entity.id == -1 ? viewModel.getPosition() + viewModel.entity.name : viewModel.entity.name}\" />\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/item_viewpager.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout>\n\n    <data>\n\n        <import type=\"com.goldze.mvvmhabit.ui.viewpager.vm.ViewPagerItemViewModel\" />\n\n        <variable\n            name=\"viewModel\"\n            type=\"ViewPagerItemViewModel\" />\n    </data>\n\n    <LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:binding=\"http://schemas.android.com/apk/res-auto\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"?android:selectableItemBackground\"\n        android:gravity=\"center\"\n        android:orientation=\"horizontal\">\n\n        <Button\n            android:id=\"@+id/tv_content\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"6dp\"\n            android:text=\"@{viewModel.text}\"\n            binding:onClickCommand=\"@{viewModel.onItemClick}\" />\n    </LinearLayout>\n</layout>"
  },
  {
    "path": "app/src/main/res/layout/layout_toolbar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:binding=\"http://schemas.android.com/apk/res-auto\">\n\n    <data>\n\n        <variable\n            name=\"toolbarViewModel\"\n            type=\"com.goldze.mvvmhabit.ui.base.viewmodel.ToolbarViewModel\" />\n    </data>\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"56dp\"\n        android:background=\"@color/white\"\n        binding:contentInsetStart=\"0dp\">\n\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <ImageView\n                android:id=\"@+id/iv_back\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:layout_alignParentLeft=\"true\"\n                android:layout_centerVertical=\"true\"\n                android:background=\"?selectableItemBackground\"\n                android:padding=\"12dp\"\n                android:src=\"@mipmap/back\"\n                binding:onClickCommand=\"@{toolbarViewModel.backOnClick}\" />\n\n            <TextView\n                android:id=\"@+id/tv_title\"\n                style=\"@style/TextAppearance.AppCompat.Widget.ActionBar.Title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerInParent=\"true\"\n                android:ellipsize=\"end\"\n                android:gravity=\"center\"\n                android:maxEms=\"12\"\n                android:singleLine=\"true\"\n                android:text=\"@{toolbarViewModel.titleText}\"\n                android:textColor=\"@color/textColor\"\n                android:textSize=\"18sp\" />\n\n            <TextView\n                android:id=\"@+id/tv_right_text\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:layout_alignParentRight=\"true\"\n                android:layout_centerVertical=\"true\"\n                android:background=\"?selectableItemBackground\"\n                android:gravity=\"center\"\n                android:padding=\"12dp\"\n                android:text=\"@{toolbarViewModel.rightText}\"\n                android:textColor=\"@color/textColor\"\n                android:textSize=\"18sp\"\n                android:visibility=\"@{toolbarViewModel.rightTextVisibleObservable}\"\n                binding:onClickCommand=\"@{toolbarViewModel.rightTextOnClick}\" />\n\n            <ImageView\n                android:id=\"@+id/iv_right_icon\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"match_parent\"\n                android:layout_alignParentRight=\"true\"\n                android:layout_centerVertical=\"true\"\n                android:background=\"?selectableItemBackground\"\n                android:gravity=\"center\"\n                android:padding=\"12dp\"\n                android:src=\"@mipmap/toolbar_more\"\n                android:visibility=\"@{toolbarViewModel.rightIconVisibleObservable}\"\n                binding:onClickCommand=\"@{toolbarViewModel.rightIconOnClick}\" />\n\n            <View\n                style=\"@style/ViewLineStyle\"\n                android:layout_alignParentBottom=\"true\" />\n        </RelativeLayout>\n    </androidx.appcompat.widget.Toolbar>\n</layout>\n\n"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"TwinklingRefreshLayout\">\n        <attr name=\"tr_wave_height\" format=\"dimension\" />\n        <attr name=\"tr_head_height\" format=\"dimension\" />\n        <attr name=\"tr_bottom_height\" format=\"dimension\" />\n        <attr name=\"tr_overscroll_height\" format=\"dimension\" />\n        <attr name=\"tr_enable_loadmore\" format=\"boolean\" />\n        <attr name=\"tr_pureScrollMode_on\" format=\"boolean\" />\n        <attr name=\"tr_show_overlay_refreshview\" format=\"boolean\" />\n        <attr name=\"tr_headerView\" format=\"dimension\" />\n        <attr name=\"tr_bottomView\" format=\"dimension\" />\n        <attr name=\"onRefreshCommand\" format=\"reference\" />\n        <attr name=\"onLoadMoreCommand\" format=\"reference\" />\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n    <color name=\"textColor\">#202020</color>\n    <color name=\"textColorHint\">#A0A0A0</color>\n    <color name=\"textColorVice\">#6C6C6C</color>\n    <color name=\"viewLineColor\">#F0F0F0</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">MVVMHabit-sample</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!--将ActionBar隐藏,这里使用ToolBar-->\n        <item name=\"windowActionBar\">false</item>\n        <!-- 使用 API Level 22以上编译的话，要拿掉前綴字 -->\n        <item name=\"windowNoTitle\">true</item>\n        <!--colorPrimary 对应ToolBar的颜色-->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <!--colorPrimaryDark对应状态栏的颜色-->\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <!--colorAccent 对应EditText编辑时、RadioButton选中、CheckBox等选中时的颜色。-->\n        <item name=\"colorAccent\">@color/colorPrimaryDark</item>\n        <item name=\"android:windowBackground\">@color/white</item>\n    </style>\n\n    <style name=\"ViewLineStyle\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">1dp</item>\n        <item name=\"android:background\">@color/viewLineColor</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\" />\n</network-security-config>\n"
  },
  {
    "path": "app/src/test/java/com/goldze/mvvmhabit/ExampleUnitTest.java",
    "content": "package com.goldze.mvvmhabit;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() throws Exception {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\napply from: \"config.gradle\"\nbuildscript {\n    repositories {\n        google()\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.5.2'\n//        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'\n//        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'\n//        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        jcenter()\n        maven { url 'https://jitpack.io' }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n\n\n"
  },
  {
    "path": "config.gradle",
    "content": "ext {\n    //android开发版本配置\n    android = [\n            compileSdkVersion: 28,\n            buildToolsVersion: \"28.0.0\",\n            applicationId    : \"com.goldze.mvvmhabit\",\n            minSdkVersion    : 15,\n            targetSdkVersion : 28,\n            versionCode      : 1,\n            versionName      : \"1.0\",\n    ]\n    //version配置\n    versions = [\n            \"support-version\": \"1.0.0\",\n            \"junit-version\"  : \"4.12\",\n    ]\n    //support配置\n    support = [\n            'support-v4'              : \"androidx.legacy:legacy-support-v4:${versions[\"support-version\"]}\",\n            'appcompat-v7'            : \"androidx.appcompat:appcompat:${versions[\"support-version\"]}\",\n            'recyclerview-v7'         : \"androidx.recyclerview:recyclerview:${versions[\"support-version\"]}\",\n            'support-v13'             : \"androidx.legacy:legacy-support-v13:${versions[\"support-version\"]}\",\n            'support-fragment'        : \"androidx.fragment:fragment:${versions[\"support-version\"]}\",\n            'design'                  : \"com.google.android.material:material:${versions[\"support-version\"]}\",\n            'animated-vector-drawable': \"androidx.vectordrawable:vectordrawable-animated:${versions[\"support-version\"]}\",\n            'junit'                   : \"junit:junit:${versions[\"junit-version\"]}\",\n    ]\n    //依赖第三方配置\n    dependencies = [\n            //rxjava\n            \"rxjava\"                               : \"io.reactivex.rxjava2:rxjava:2.2.3\",\n            \"rxandroid\"                            : \"io.reactivex.rxjava2:rxandroid:2.1.0\",\n            //rx系列与View生命周期同步\n            \"rxlifecycle\"                          : \"com.trello.rxlifecycle2:rxlifecycle:2.2.2\",\n            \"rxlifecycle-components\"               : \"com.trello.rxlifecycle2:rxlifecycle-components:2.2.2\",\n            //rxbinding\n            \"rxbinding\"                            : \"com.jakewharton.rxbinding2:rxbinding:2.1.1\",\n            //rx 6.0权限请求\n            \"rxpermissions\"                        : \"com.github.tbruyelle:rxpermissions:0.10.2\",\n            //network\n            \"okhttp\"                               : \"com.squareup.okhttp3:okhttp:3.10.0\",\n            \"retrofit\"                             : \"com.squareup.retrofit2:retrofit:2.4.0\",\n            \"converter-gson\"                       : \"com.squareup.retrofit2:converter-gson:2.4.0\",\n            \"adapter-rxjava\"                       : \"com.squareup.retrofit2:adapter-rxjava2:2.4.0\",\n            //glide图片加载\n            \"glide\"                                : \"com.github.bumptech.glide:glide:4.11.0\",\n            \"glide-compiler\"                       : \"com.github.bumptech.glide:compiler:4.11.0\",\n            //json解析\n            \"gson\"                                 : \"com.google.code.gson:gson:2.8.6\",\n            //material-dialogs\n            \"material-dialogs-core\"                : \"com.afollestad.material-dialogs:core:0.9.6.0\",\n            \"material-dialogs-commons\"             : \"com.afollestad.material-dialogs:commons:0.9.6.0\",\n            //recyclerview的databinding套装\n            \"bindingcollectionadapter\"             : \"me.tatarka.bindingcollectionadapter2:bindingcollectionadapter:4.0.0\",\n            \"bindingcollectionadapter-recyclerview\": \"me.tatarka.bindingcollectionadapter2:bindingcollectionadapter-recyclerview:4.0.0\",\n            \"bindingcollectionadapter-viewpager2\"  : \"me.tatarka.bindingcollectionadapter2:bindingcollectionadapter-viewpager2:4.0.0\",\n            //Google AAC\n            \"lifecycle-extensions\"                 : \"androidx.lifecycle:lifecycle-extensions:2.0.0\",\n            \"lifecycle-compiler\"                   : \"androidx.lifecycle:lifecycle-compiler:2.0.0\",\n            //MVVMHabit\n            \"MVVMHabit\"                            : \"com.github.goldze:MVVMHabit:4.0.0\",\n    ]\n}\n\n\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Tue Apr 09 21:09:52 CST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.4-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# android.databinding.enableV2=true\n\nandroid.useAndroidX=true\nandroid.enableJetifier=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "mvvmhabit/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "mvvmhabit/build.gradle",
    "content": "apply plugin: 'com.android.library'\nandroid {\n    compileSdkVersion rootProject.ext.android.compileSdkVersion\n    defaultConfig {\n        minSdkVersion rootProject.ext.android.minSdkVersion\n        targetSdkVersion rootProject.ext.android.targetSdkVersion\n        versionCode rootProject.ext.android.versionCode\n        versionName rootProject.ext.android.versionName\n    }\n    dataBinding {\n        enabled true\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    api fileTree(include: ['*.jar'], dir: 'libs')\n    //support\n    api rootProject.ext.support[\"support-v4\"]\n    api rootProject.ext.support[\"appcompat-v7\"]\n    api rootProject.ext.support[\"recyclerview-v7\"]\n    //rxjava\n    api rootProject.ext.dependencies.rxjava\n    api rootProject.ext.dependencies.rxandroid\n    //rx管理View的生命周期\n    api(rootProject.ext.dependencies.rxlifecycle) {\n        exclude group: 'com.android.support'\n    }\n    api(rootProject.ext.dependencies[\"rxlifecycle-components\"]) {\n        exclude group: 'com.android.support'\n    }\n    //rxbinding\n    api(rootProject.ext.dependencies.rxbinding) {\n        exclude group: 'com.android.support'\n    }\n    //rx权限请求\n    api(rootProject.ext.dependencies.rxpermissions) {\n        exclude group: 'com.android.support'\n    }\n    //network\n    api rootProject.ext.dependencies.okhttp\n    api rootProject.ext.dependencies.retrofit\n    api rootProject.ext.dependencies[\"converter-gson\"]\n    api rootProject.ext.dependencies[\"adapter-rxjava\"]\n    //json解析\n    api rootProject.ext.dependencies.gson\n    //material-dialogs\n    api(rootProject.ext.dependencies[\"material-dialogs-core\"]) {\n        exclude group: 'com.android.support'\n    }\n    api(rootProject.ext.dependencies[\"material-dialogs-commons\"]) {\n        exclude group: 'com.android.support'\n    }\n    //glide图片加载库\n    api (rootProject.ext.dependencies.glide){\n        exclude group: 'com.android.support'\n    }\n    annotationProcessor rootProject.ext.dependencies[\"glide-compiler\"]\n    //recyclerview的databinding套装\n    api(rootProject.ext.dependencies.bindingcollectionadapter) {\n        exclude group: 'com.android.support'\n    }\n    api(rootProject.ext.dependencies[\"bindingcollectionadapter-recyclerview\"]) {\n        exclude group: 'com.android.support'\n    }\n    api(rootProject.ext.dependencies[\"bindingcollectionadapter-viewpager2\"]) {\n        exclude group: 'com.android.support'\n    }\n    //Google LiveData和ViewModel组件\n    api rootProject.ext.dependencies[\"lifecycle-extensions\"]\n    annotationProcessor rootProject.ext.dependencies[\"lifecycle-compiler\"]\n}\n"
  },
  {
    "path": "mvvmhabit/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in D:\\AndroidSDK/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n\n"
  },
  {
    "path": "mvvmhabit/src/androidTest/java/me/goldze/mvvmhabit/ExampleInstrumentedTest.java",
    "content": "package me.goldze.mvvmhabit;\n\nimport android.content.Context;\nimport android.support.test.InstrumentationRegistry;\nimport android.support.test.runner.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumentation test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() throws Exception {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getTargetContext();\n\n        assertEquals(\"me.goldze.mvvmhabit.test\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\n    package=\"me.goldze.mvvmhabit\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <application>\n        <activity\n            android:name=\".base.ContainerActivity\"\n            android:configChanges=\"orientation|keyboardHidden\"></activity>\n        <activity\n            android:name=\".crash.DefaultErrorActivity\"\n            android:process=\":error_activity\" />\n\n        <provider\n            android:name=\".crash.CaocInitProvider\"\n            android:authorities=\"${applicationId}.customactivityoncrashinitprovider\"\n            android:exported=\"false\"\n            android:initOrder=\"101\" />\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/AppManager.java",
    "content": "package me.goldze.mvvmhabit.base;\n\nimport android.app.Activity;\n\nimport java.util.Stack;\n\nimport androidx.fragment.app.Fragment;\n\n/**\n * Created by goldze on 2017/6/15.\n * activity堆栈式管理\n */\npublic class AppManager {\n\n    private static Stack<Activity> activityStack;\n    private static Stack<Fragment> fragmentStack;\n    private static AppManager instance;\n\n    private AppManager() {\n    }\n\n    /**\n     * 单例模式\n     *\n     * @return AppManager\n     */\n    public static AppManager getAppManager() {\n        if (instance == null) {\n            instance = new AppManager();\n        }\n        return instance;\n    }\n\n    public static Stack<Activity> getActivityStack() {\n        return activityStack;\n    }\n\n    public static Stack<Fragment> getFragmentStack() {\n        return fragmentStack;\n    }\n\n\n    /**\n     * 添加Activity到堆栈\n     */\n    public void addActivity(Activity activity) {\n        if (activityStack == null) {\n            activityStack = new Stack<Activity>();\n        }\n        activityStack.add(activity);\n    }\n\n    /**\n     * 移除指定的Activity\n     */\n    public void removeActivity(Activity activity) {\n        if (activity != null) {\n            activityStack.remove(activity);\n        }\n    }\n\n\n    /**\n     * 是否有activity\n     */\n    public boolean isActivity() {\n        if (activityStack != null) {\n            return !activityStack.isEmpty();\n        }\n        return false;\n    }\n\n    /**\n     * 获取当前Activity（堆栈中最后一个压入的）\n     */\n    public Activity currentActivity() {\n        Activity activity = activityStack.lastElement();\n        return activity;\n    }\n\n    /**\n     * 结束当前Activity（堆栈中最后一个压入的）\n     */\n    public void finishActivity() {\n        Activity activity = activityStack.lastElement();\n        finishActivity(activity);\n    }\n\n    /**\n     * 结束指定的Activity\n     */\n    public void finishActivity(Activity activity) {\n        if (activity != null) {\n            if (!activity.isFinishing()) {\n                activity.finish();\n            }\n        }\n    }\n\n    /**\n     * 结束指定类名的Activity\n     */\n    public void finishActivity(Class<?> cls) {\n        for (Activity activity : activityStack) {\n            if (activity.getClass().equals(cls)) {\n                finishActivity(activity);\n                break;\n            }\n        }\n    }\n\n    /**\n     * 结束所有Activity\n     */\n    public void finishAllActivity() {\n        for (int i = 0, size = activityStack.size(); i < size; i++) {\n            if (null != activityStack.get(i)) {\n                finishActivity(activityStack.get(i));\n            }\n        }\n        activityStack.clear();\n    }\n\n    /**\n     * 获取指定的Activity\n     *\n     * @author kymjs\n     */\n    public Activity getActivity(Class<?> cls) {\n        if (activityStack != null)\n            for (Activity activity : activityStack) {\n                if (activity.getClass().equals(cls)) {\n                    return activity;\n                }\n            }\n        return null;\n    }\n\n\n    /**\n     * 添加Fragment到堆栈\n     */\n    public void addFragment(Fragment fragment) {\n        if (fragmentStack == null) {\n            fragmentStack = new Stack<Fragment>();\n        }\n        fragmentStack.add(fragment);\n    }\n\n    /**\n     * 移除指定的Fragment\n     */\n    public void removeFragment(Fragment fragment) {\n        if (fragment != null) {\n            fragmentStack.remove(fragment);\n        }\n    }\n\n\n    /**\n     * 是否有Fragment\n     */\n    public boolean isFragment() {\n        if (fragmentStack != null) {\n            return !fragmentStack.isEmpty();\n        }\n        return false;\n    }\n\n    /**\n     * 获取当前Activity（堆栈中最后一个压入的）\n     */\n    public Fragment currentFragment() {\n        if (fragmentStack != null) {\n            Fragment fragment = fragmentStack.lastElement();\n            return fragment;\n        }\n        return null;\n    }\n\n\n    /**\n     * 退出应用程序\n     */\n    public void AppExit() {\n        try {\n            finishAllActivity();\n            // 杀死该应用进程\n//          android.os.Process.killProcess(android.os.Process.myPid());\n//            调用 System.exit(n) 实际上等效于调用：\n//            Runtime.getRuntime().exit(n)\n//            finish()是Activity的类方法，仅仅针对Activity，当调用finish()时，只是将活动推向后台，并没有立即释放内存，活动的资源并没有被清理；当调用System.exit(0)时，退出当前Activity并释放资源（内存），但是该方法不可以结束整个App如有多个Activty或者有其他组件service等不会结束。\n//            其实android的机制决定了用户无法完全退出应用，当你的application最长时间没有被用过的时候，android自身会决定将application关闭了。\n            //System.exit(0);\n        } catch (Exception e) {\n            activityStack.clear();\n            e.printStackTrace();\n        }\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/BaseActivity.java",
    "content": "package me.goldze.mvvmhabit.base;\n\nimport android.content.Intent;\nimport android.os.Bundle;\n\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.trello.rxlifecycle2.components.support.RxAppCompatActivity;\n\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\nimport androidx.annotation.Nullable;\nimport androidx.databinding.DataBindingUtil;\nimport androidx.databinding.ViewDataBinding;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProviders;\nimport me.goldze.mvvmhabit.base.BaseViewModel.ParameterField;\nimport me.goldze.mvvmhabit.bus.Messenger;\nimport me.goldze.mvvmhabit.utils.MaterialDialogUtils;\n\n\n/**\n * Created by goldze on 2017/6/15.\n * 一个拥有DataBinding框架的基Activity\n * 这里根据项目业务可以换成你自己熟悉的BaseActivity, 但是需要继承RxAppCompatActivity,方便LifecycleProvider管理生命周期\n */\npublic abstract class BaseActivity<V extends ViewDataBinding, VM extends BaseViewModel> extends RxAppCompatActivity implements IBaseView {\n    protected V binding;\n    protected VM viewModel;\n    private int viewModelId;\n    private MaterialDialog dialog;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        //页面接受的参数方法\n        initParam();\n        //私有的初始化Databinding和ViewModel方法\n        initViewDataBinding(savedInstanceState);\n        //私有的ViewModel与View的契约事件回调逻辑\n        registorUIChangeLiveDataCallBack();\n        //页面数据初始化方法\n        initData();\n        //页面事件监听的方法，一般用于ViewModel层转到View层的事件注册\n        initViewObservable();\n        //注册RxBus\n        viewModel.registerRxBus();\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        //解除Messenger注册\n        Messenger.getDefault().unregister(viewModel);\n        if (viewModel != null) {\n            viewModel.removeRxBus();\n        }\n        if(binding != null){\n            binding.unbind();\n        }\n    }\n\n    /**\n     * 注入绑定\n     */\n    private void initViewDataBinding(Bundle savedInstanceState) {\n        //DataBindingUtil类需要在project的build中配置 dataBinding {enabled true }, 同步后会自动关联android.databinding包\n        binding = DataBindingUtil.setContentView(this, initContentView(savedInstanceState));\n        viewModelId = initVariableId();\n        viewModel = initViewModel();\n        if (viewModel == null) {\n            Class modelClass;\n            Type type = getClass().getGenericSuperclass();\n            if (type instanceof ParameterizedType) {\n                modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[1];\n            } else {\n                //如果没有指定泛型参数，则默认使用BaseViewModel\n                modelClass = BaseViewModel.class;\n            }\n            viewModel = (VM) createViewModel(this, modelClass);\n        }\n        //关联ViewModel\n        binding.setVariable(viewModelId, viewModel);\n        //支持LiveData绑定xml，数据改变，UI自动会更新\n        binding.setLifecycleOwner(this);\n        //让ViewModel拥有View的生命周期感应\n        getLifecycle().addObserver(viewModel);\n        //注入RxLifecycle生命周期\n        viewModel.injectLifecycleProvider(this);\n    }\n\n    //刷新布局\n    public void refreshLayout() {\n        if (viewModel != null) {\n            binding.setVariable(viewModelId, viewModel);\n        }\n    }\n\n\n    /**\n     * =====================================================================\n     **/\n    //注册ViewModel与View的契约UI回调事件\n    protected void registorUIChangeLiveDataCallBack() {\n        //加载对话框显示\n        viewModel.getUC().getShowDialogEvent().observe(this, new Observer<String>() {\n            @Override\n            public void onChanged(@Nullable String title) {\n                showDialog(title);\n            }\n        });\n        //加载对话框消失\n        viewModel.getUC().getDismissDialogEvent().observe(this, new Observer<Void>() {\n            @Override\n            public void onChanged(@Nullable Void v) {\n                dismissDialog();\n            }\n        });\n        //跳入新页面\n        viewModel.getUC().getStartActivityEvent().observe(this, new Observer<Map<String, Object>>() {\n            @Override\n            public void onChanged(@Nullable Map<String, Object> params) {\n                Class<?> clz = (Class<?>) params.get(ParameterField.CLASS);\n                Bundle bundle = (Bundle) params.get(ParameterField.BUNDLE);\n                startActivity(clz, bundle);\n            }\n        });\n        //跳入ContainerActivity\n        viewModel.getUC().getStartContainerActivityEvent().observe(this, new Observer<Map<String, Object>>() {\n            @Override\n            public void onChanged(@Nullable Map<String, Object> params) {\n                String canonicalName = (String) params.get(ParameterField.CANONICAL_NAME);\n                Bundle bundle = (Bundle) params.get(ParameterField.BUNDLE);\n                startContainerActivity(canonicalName, bundle);\n            }\n        });\n        //关闭界面\n        viewModel.getUC().getFinishEvent().observe(this, new Observer<Void>() {\n            @Override\n            public void onChanged(@Nullable Void v) {\n                finish();\n            }\n        });\n        //关闭上一层\n        viewModel.getUC().getOnBackPressedEvent().observe(this, new Observer<Void>() {\n            @Override\n            public void onChanged(@Nullable Void v) {\n                onBackPressed();\n            }\n        });\n    }\n\n    public void showDialog(String title) {\n        if (dialog != null) {\n            dialog = dialog.getBuilder().title(title).build();\n            dialog.show();\n        } else {\n            MaterialDialog.Builder builder = MaterialDialogUtils.showIndeterminateProgressDialog(this, title, true);\n            dialog = builder.show();\n        }\n    }\n\n    public void dismissDialog() {\n        if (dialog != null && dialog.isShowing()) {\n            dialog.dismiss();\n        }\n    }\n\n    /**\n     * 跳转页面\n     *\n     * @param clz 所跳转的目的Activity类\n     */\n    public void startActivity(Class<?> clz) {\n        startActivity(new Intent(this, clz));\n    }\n\n    /**\n     * 跳转页面\n     *\n     * @param clz    所跳转的目的Activity类\n     * @param bundle 跳转所携带的信息\n     */\n    public void startActivity(Class<?> clz, Bundle bundle) {\n        Intent intent = new Intent(this, clz);\n        if (bundle != null) {\n            intent.putExtras(bundle);\n        }\n        startActivity(intent);\n    }\n\n    /**\n     * 跳转容器页面\n     *\n     * @param canonicalName 规范名 : Fragment.class.getCanonicalName()\n     */\n    public void startContainerActivity(String canonicalName) {\n        startContainerActivity(canonicalName, null);\n    }\n\n    /**\n     * 跳转容器页面\n     *\n     * @param canonicalName 规范名 : Fragment.class.getCanonicalName()\n     * @param bundle        跳转所携带的信息\n     */\n    public void startContainerActivity(String canonicalName, Bundle bundle) {\n        Intent intent = new Intent(this, ContainerActivity.class);\n        intent.putExtra(ContainerActivity.FRAGMENT, canonicalName);\n        if (bundle != null) {\n            intent.putExtra(ContainerActivity.BUNDLE, bundle);\n        }\n        startActivity(intent);\n    }\n\n    /**\n     * =====================================================================\n     **/\n    @Override\n    public void initParam() {\n\n    }\n\n    /**\n     * 初始化根布局\n     *\n     * @return 布局layout的id\n     */\n    public abstract int initContentView(Bundle savedInstanceState);\n\n    /**\n     * 初始化ViewModel的id\n     *\n     * @return BR的id\n     */\n    public abstract int initVariableId();\n\n    /**\n     * 初始化ViewModel\n     *\n     * @return 继承BaseViewModel的ViewModel\n     */\n    public VM initViewModel() {\n        return null;\n    }\n\n    @Override\n    public void initData() {\n\n    }\n\n    @Override\n    public void initViewObservable() {\n\n    }\n\n    /**\n     * 创建ViewModel\n     *\n     * @param cls\n     * @param <T>\n     * @return\n     */\n    public <T extends ViewModel> T createViewModel(FragmentActivity activity, Class<T> cls) {\n        return ViewModelProviders.of(activity).get(cls);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/BaseApplication.java",
    "content": "package me.goldze.mvvmhabit.base;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport me.goldze.mvvmhabit.utils.Utils;\n\n/**\n * Created by goldze on 2017/6/15.\n */\n\npublic class BaseApplication extends Application {\n    private static Application sInstance;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        setApplication(this);\n    }\n\n    /**\n     * 当主工程没有继承BaseApplication时，可以使用setApplication方法初始化BaseApplication\n     *\n     * @param application\n     */\n    public static synchronized void setApplication(@NonNull Application application) {\n        sInstance = application;\n        //初始化工具类\n        Utils.init(application);\n        //注册监听每个activity的生命周期,便于堆栈式管理\n        application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {\n\n            @Override\n            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {\n                AppManager.getAppManager().addActivity(activity);\n            }\n\n            @Override\n            public void onActivityStarted(Activity activity) {\n            }\n\n            @Override\n            public void onActivityResumed(Activity activity) {\n            }\n\n            @Override\n            public void onActivityPaused(Activity activity) {\n            }\n\n            @Override\n            public void onActivityStopped(Activity activity) {\n            }\n\n            @Override\n            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {\n            }\n\n            @Override\n            public void onActivityDestroyed(Activity activity) {\n                AppManager.getAppManager().removeActivity(activity);\n            }\n        });\n    }\n\n    /**\n     * 获得当前app运行的Application\n     */\n    public static Application getInstance() {\n        if (sInstance == null) {\n            throw new NullPointerException(\"please inherit BaseApplication or call setApplication.\");\n        }\n        return sInstance;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/BaseFragment.java",
    "content": "package me.goldze.mvvmhabit.base;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.trello.rxlifecycle2.components.support.RxFragment;\n\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.util.Map;\n\nimport androidx.annotation.Nullable;\nimport androidx.databinding.DataBindingUtil;\nimport androidx.databinding.ViewDataBinding;\nimport androidx.fragment.app.Fragment;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProviders;\nimport me.goldze.mvvmhabit.base.BaseViewModel.ParameterField;\nimport me.goldze.mvvmhabit.bus.Messenger;\nimport me.goldze.mvvmhabit.utils.MaterialDialogUtils;\n\n/**\n * Created by goldze on 2017/6/15.\n */\npublic abstract class BaseFragment<V extends ViewDataBinding, VM extends BaseViewModel> extends RxFragment implements IBaseView {\n    protected V binding;\n    protected VM viewModel;\n    private int viewModelId;\n    private MaterialDialog dialog;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        initParam();\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n    }\n\n    @Nullable\n    @Override\n    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {\n        binding = DataBindingUtil.inflate(inflater, initContentView(inflater, container, savedInstanceState), container, false);\n        return binding.getRoot();\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        //解除Messenger注册\n        Messenger.getDefault().unregister(viewModel);\n        if (viewModel != null) {\n            viewModel.removeRxBus();\n        }\n        if (binding != null) {\n            binding.unbind();\n        }\n    }\n\n    @Override\n    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {\n        super.onViewCreated(view, savedInstanceState);\n        //私有的初始化Databinding和ViewModel方法\n        initViewDataBinding();\n        //私有的ViewModel与View的契约事件回调逻辑\n        registorUIChangeLiveDataCallBack();\n        //页面数据初始化方法\n        initData();\n        //页面事件监听的方法，一般用于ViewModel层转到View层的事件注册\n        initViewObservable();\n        //注册RxBus\n        viewModel.registerRxBus();\n    }\n\n    /**\n     * 注入绑定\n     */\n    private void initViewDataBinding() {\n        viewModelId = initVariableId();\n        viewModel = initViewModel();\n        if (viewModel == null) {\n            Class modelClass;\n            Type type = getClass().getGenericSuperclass();\n            if (type instanceof ParameterizedType) {\n                modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[1];\n            } else {\n                //如果没有指定泛型参数，则默认使用BaseViewModel\n                modelClass = BaseViewModel.class;\n            }\n            viewModel = (VM) createViewModel(this, modelClass);\n        }\n        binding.setVariable(viewModelId, viewModel);\n        //支持LiveData绑定xml，数据改变，UI自动会更新\n        binding.setLifecycleOwner(this);\n        //让ViewModel拥有View的生命周期感应\n        getLifecycle().addObserver(viewModel);\n        //注入RxLifecycle生命周期\n        viewModel.injectLifecycleProvider(this);\n    }\n\n    /**\n     * =====================================================================\n     **/\n    //注册ViewModel与View的契约UI回调事件\n    protected void registorUIChangeLiveDataCallBack() {\n        //加载对话框显示\n        viewModel.getUC().getShowDialogEvent().observe(this, new Observer<String>() {\n            @Override\n            public void onChanged(@Nullable String title) {\n                showDialog(title);\n            }\n        });\n        //加载对话框消失\n        viewModel.getUC().getDismissDialogEvent().observe(this, new Observer<Void>() {\n            @Override\n            public void onChanged(@Nullable Void v) {\n                dismissDialog();\n            }\n        });\n        //跳入新页面\n        viewModel.getUC().getStartActivityEvent().observe(this, new Observer<Map<String, Object>>() {\n            @Override\n            public void onChanged(@Nullable Map<String, Object> params) {\n                Class<?> clz = (Class<?>) params.get(ParameterField.CLASS);\n                Bundle bundle = (Bundle) params.get(ParameterField.BUNDLE);\n                startActivity(clz, bundle);\n            }\n        });\n        //跳入ContainerActivity\n        viewModel.getUC().getStartContainerActivityEvent().observe(this, new Observer<Map<String, Object>>() {\n            @Override\n            public void onChanged(@Nullable Map<String, Object> params) {\n                String canonicalName = (String) params.get(ParameterField.CANONICAL_NAME);\n                Bundle bundle = (Bundle) params.get(ParameterField.BUNDLE);\n                startContainerActivity(canonicalName, bundle);\n            }\n        });\n        //关闭界面\n        viewModel.getUC().getFinishEvent().observe(this, new Observer<Void>() {\n            @Override\n            public void onChanged(@Nullable Void v) {\n                getActivity().finish();\n            }\n        });\n        //关闭上一层\n        viewModel.getUC().getOnBackPressedEvent().observe(this, new Observer<Void>() {\n            @Override\n            public void onChanged(@Nullable Void v) {\n                getActivity().onBackPressed();\n            }\n        });\n    }\n\n    public void showDialog(String title) {\n        if (dialog != null) {\n            dialog = dialog.getBuilder().title(title).build();\n            dialog.show();\n        } else {\n            MaterialDialog.Builder builder = MaterialDialogUtils.showIndeterminateProgressDialog(getActivity(), title, true);\n            dialog = builder.show();\n        }\n    }\n\n    public void dismissDialog() {\n        if (dialog != null && dialog.isShowing()) {\n            dialog.dismiss();\n        }\n    }\n\n    /**\n     * 跳转页面\n     *\n     * @param clz 所跳转的目的Activity类\n     */\n    public void startActivity(Class<?> clz) {\n        startActivity(new Intent(getContext(), clz));\n    }\n\n    /**\n     * 跳转页面\n     *\n     * @param clz    所跳转的目的Activity类\n     * @param bundle 跳转所携带的信息\n     */\n    public void startActivity(Class<?> clz, Bundle bundle) {\n        Intent intent = new Intent(getContext(), clz);\n        if (bundle != null) {\n            intent.putExtras(bundle);\n        }\n        startActivity(intent);\n    }\n\n    /**\n     * 跳转容器页面\n     *\n     * @param canonicalName 规范名 : Fragment.class.getCanonicalName()\n     */\n    public void startContainerActivity(String canonicalName) {\n        startContainerActivity(canonicalName, null);\n    }\n\n    /**\n     * 跳转容器页面\n     *\n     * @param canonicalName 规范名 : Fragment.class.getCanonicalName()\n     * @param bundle        跳转所携带的信息\n     */\n    public void startContainerActivity(String canonicalName, Bundle bundle) {\n        Intent intent = new Intent(getContext(), ContainerActivity.class);\n        intent.putExtra(ContainerActivity.FRAGMENT, canonicalName);\n        if (bundle != null) {\n            intent.putExtra(ContainerActivity.BUNDLE, bundle);\n        }\n        startActivity(intent);\n    }\n\n    /**\n     * =====================================================================\n     **/\n\n    //刷新布局\n    public void refreshLayout() {\n        if (viewModel != null) {\n            binding.setVariable(viewModelId, viewModel);\n        }\n    }\n\n    @Override\n    public void initParam() {\n\n    }\n\n    /**\n     * 初始化根布局\n     *\n     * @return 布局layout的id\n     */\n    public abstract int initContentView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState);\n\n    /**\n     * 初始化ViewModel的id\n     *\n     * @return BR的id\n     */\n    public abstract int initVariableId();\n\n    /**\n     * 初始化ViewModel\n     *\n     * @return 继承BaseViewModel的ViewModel\n     */\n    public VM initViewModel() {\n        return null;\n    }\n\n    @Override\n    public void initData() {\n\n    }\n\n    @Override\n    public void initViewObservable() {\n\n    }\n\n    public boolean isBackPressed() {\n        return false;\n    }\n\n    /**\n     * 创建ViewModel\n     *\n     * @param cls\n     * @param <T>\n     * @return\n     */\n    public <T extends ViewModel> T createViewModel(Fragment fragment, Class<T> cls) {\n        return ViewModelProviders.of(fragment).get(cls);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/BaseModel.java",
    "content": "package me.goldze.mvvmhabit.base;\n\n/**\n * Created by goldze on 2017/6/15.\n */\npublic class BaseModel implements IModel {\n\n    public BaseModel() {\n    }\n\n    @Override\n    public void onCleared() {\n\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/BaseViewModel.java",
    "content": "package me.goldze.mvvmhabit.base;\n\nimport android.app.Application;\nimport android.os.Bundle;\n\nimport com.trello.rxlifecycle2.LifecycleProvider;\n\nimport java.lang.ref.WeakReference;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.AndroidViewModel;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.Observer;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.functions.Consumer;\nimport me.goldze.mvvmhabit.bus.event.SingleLiveEvent;\n\n/**\n * Created by goldze on 2017/6/15.\n */\npublic class BaseViewModel<M extends BaseModel> extends AndroidViewModel implements IBaseViewModel, Consumer<Disposable> {\n    protected M model;\n    private UIChangeLiveData uc;\n    //弱引用持有\n    private WeakReference<LifecycleProvider> lifecycle;\n    //管理RxJava，主要针对RxJava异步操作造成的内存泄漏\n    private CompositeDisposable mCompositeDisposable;\n\n    public BaseViewModel(@NonNull Application application) {\n        this(application, null);\n    }\n\n    public BaseViewModel(@NonNull Application application, M model) {\n        super(application);\n        this.model = model;\n        mCompositeDisposable = new CompositeDisposable();\n    }\n\n    protected void addSubscribe(Disposable disposable) {\n        if (mCompositeDisposable == null) {\n            mCompositeDisposable = new CompositeDisposable();\n        }\n        mCompositeDisposable.add(disposable);\n    }\n\n    /**\n     * 注入RxLifecycle生命周期\n     *\n     * @param lifecycle\n     */\n    public void injectLifecycleProvider(LifecycleProvider lifecycle) {\n        this.lifecycle = new WeakReference<>(lifecycle);\n    }\n\n    public LifecycleProvider getLifecycleProvider() {\n        return lifecycle.get();\n    }\n\n    public UIChangeLiveData getUC() {\n        if (uc == null) {\n            uc = new UIChangeLiveData();\n        }\n        return uc;\n    }\n\n    public void showDialog() {\n        showDialog(\"请稍后...\");\n    }\n\n    public void showDialog(String title) {\n        uc.showDialogEvent.postValue(title);\n    }\n\n    public void dismissDialog() {\n        uc.dismissDialogEvent.call();\n    }\n\n    /**\n     * 跳转页面\n     *\n     * @param clz 所跳转的目的Activity类\n     */\n    public void startActivity(Class<?> clz) {\n        startActivity(clz, null);\n    }\n\n    /**\n     * 跳转页面\n     *\n     * @param clz    所跳转的目的Activity类\n     * @param bundle 跳转所携带的信息\n     */\n    public void startActivity(Class<?> clz, Bundle bundle) {\n        Map<String, Object> params = new HashMap<>();\n        params.put(ParameterField.CLASS, clz);\n        if (bundle != null) {\n            params.put(ParameterField.BUNDLE, bundle);\n        }\n        uc.startActivityEvent.postValue(params);\n    }\n\n    /**\n     * 跳转容器页面\n     *\n     * @param canonicalName 规范名 : Fragment.class.getCanonicalName()\n     */\n    public void startContainerActivity(String canonicalName) {\n        startContainerActivity(canonicalName, null);\n    }\n\n    /**\n     * 跳转容器页面\n     *\n     * @param canonicalName 规范名 : Fragment.class.getCanonicalName()\n     * @param bundle        跳转所携带的信息\n     */\n    public void startContainerActivity(String canonicalName, Bundle bundle) {\n        Map<String, Object> params = new HashMap<>();\n        params.put(ParameterField.CANONICAL_NAME, canonicalName);\n        if (bundle != null) {\n            params.put(ParameterField.BUNDLE, bundle);\n        }\n        uc.startContainerActivityEvent.postValue(params);\n    }\n\n    /**\n     * 关闭界面\n     */\n    public void finish() {\n        uc.finishEvent.call();\n    }\n\n    /**\n     * 返回上一层\n     */\n    public void onBackPressed() {\n        uc.onBackPressedEvent.call();\n    }\n\n    @Override\n    public void onAny(LifecycleOwner owner, Lifecycle.Event event) {\n    }\n\n    @Override\n    public void onCreate() {\n    }\n\n    @Override\n    public void onDestroy() {\n    }\n\n    @Override\n    public void onStart() {\n    }\n\n    @Override\n    public void onStop() {\n    }\n\n    @Override\n    public void onResume() {\n    }\n\n    @Override\n    public void onPause() {\n    }\n\n    @Override\n    public void registerRxBus() {\n    }\n\n    @Override\n    public void removeRxBus() {\n    }\n\n    @Override\n    protected void onCleared() {\n        super.onCleared();\n        if (model != null) {\n            model.onCleared();\n        }\n        //ViewModel销毁时会执行，同时取消所有异步任务\n        if (mCompositeDisposable != null) {\n            mCompositeDisposable.clear();\n        }\n    }\n\n    @Override\n    public void accept(Disposable disposable) throws Exception {\n        addSubscribe(disposable);\n    }\n\n    public final class UIChangeLiveData extends SingleLiveEvent {\n        private SingleLiveEvent<String> showDialogEvent;\n        private SingleLiveEvent<Void> dismissDialogEvent;\n        private SingleLiveEvent<Map<String, Object>> startActivityEvent;\n        private SingleLiveEvent<Map<String, Object>> startContainerActivityEvent;\n        private SingleLiveEvent<Void> finishEvent;\n        private SingleLiveEvent<Void> onBackPressedEvent;\n\n        public SingleLiveEvent<String> getShowDialogEvent() {\n            return showDialogEvent = createLiveData(showDialogEvent);\n        }\n\n        public SingleLiveEvent<Void> getDismissDialogEvent() {\n            return dismissDialogEvent = createLiveData(dismissDialogEvent);\n        }\n\n        public SingleLiveEvent<Map<String, Object>> getStartActivityEvent() {\n            return startActivityEvent = createLiveData(startActivityEvent);\n        }\n\n        public SingleLiveEvent<Map<String, Object>> getStartContainerActivityEvent() {\n            return startContainerActivityEvent = createLiveData(startContainerActivityEvent);\n        }\n\n        public SingleLiveEvent<Void> getFinishEvent() {\n            return finishEvent = createLiveData(finishEvent);\n        }\n\n        public SingleLiveEvent<Void> getOnBackPressedEvent() {\n            return onBackPressedEvent = createLiveData(onBackPressedEvent);\n        }\n\n        private <T> SingleLiveEvent<T> createLiveData(SingleLiveEvent<T> liveData) {\n            if (liveData == null) {\n                liveData = new SingleLiveEvent<>();\n            }\n            return liveData;\n        }\n\n        @Override\n        public void observe(LifecycleOwner owner, Observer observer) {\n            super.observe(owner, observer);\n        }\n    }\n\n    public static final class ParameterField {\n        public static String CLASS = \"CLASS\";\n        public static String CANONICAL_NAME = \"CANONICAL_NAME\";\n        public static String BUNDLE = \"BUNDLE\";\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/ContainerActivity.java",
    "content": "package me.goldze.mvvmhabit.base;\n\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.WindowManager;\n\nimport com.trello.rxlifecycle2.components.support.RxAppCompatActivity;\n\nimport java.lang.ref.WeakReference;\n\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.fragment.app.FragmentTransaction;\nimport me.goldze.mvvmhabit.R;\n\n\n/**\n * 盛装Fragment的一个容器(代理)Activity\n * 普通界面只需要编写Fragment,使用此Activity盛装,这样就不需要每个界面都在AndroidManifest中注册一遍\n */\npublic class ContainerActivity extends RxAppCompatActivity {\n    private static final String FRAGMENT_TAG = \"content_fragment_tag\";\n    public static final String FRAGMENT = \"fragment\";\n    public static final String BUNDLE = \"bundle\";\n    protected WeakReference<Fragment> mFragment;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_container);\n        FragmentManager fm = getSupportFragmentManager();\n        Fragment fragment = null;\n        if (savedInstanceState != null) {\n            fragment = fm.getFragment(savedInstanceState, FRAGMENT_TAG);\n        }\n        if (fragment == null) {\n            fragment = initFromIntent(getIntent());\n        }\n        FragmentTransaction trans = getSupportFragmentManager()\n                .beginTransaction();\n        trans.replace(R.id.content, fragment);\n        trans.commitAllowingStateLoss();\n        mFragment = new WeakReference<>(fragment);\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        getSupportFragmentManager().putFragment(outState, FRAGMENT_TAG, mFragment.get());\n    }\n\n    protected Fragment initFromIntent(Intent data) {\n        if (data == null) {\n            throw new RuntimeException(\n                    \"you must provide a page info to display\");\n        }\n        try {\n            String fragmentName = data.getStringExtra(FRAGMENT);\n            if (fragmentName == null || \"\".equals(fragmentName)) {\n                throw new IllegalArgumentException(\"can not find page fragmentName\");\n            }\n            Class<?> fragmentClass = Class.forName(fragmentName);\n            Fragment fragment = (Fragment) fragmentClass.newInstance();\n            Bundle args = data.getBundleExtra(BUNDLE);\n            if (args != null) {\n                fragment.setArguments(args);\n            }\n            return fragment;\n        } catch (ClassNotFoundException e) {\n            e.printStackTrace();\n        } catch (InstantiationException e) {\n            e.printStackTrace();\n        } catch (IllegalAccessException e) {\n            e.printStackTrace();\n        }\n        throw new RuntimeException(\"fragment initialization failed!\");\n    }\n\n    @Override\n    public void onBackPressed() {\n        Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.content);\n        if (fragment instanceof BaseFragment) {\n            if (!((BaseFragment) fragment).isBackPressed()) {\n                super.onBackPressed();\n            }\n        } else {\n            super.onBackPressed();\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/IBaseView.java",
    "content": "package me.goldze.mvvmhabit.base;\n\n/**\n * Created by goldze on 2017/6/15.\n */\n\npublic interface IBaseView {\n    /**\n     * 初始化界面传递参数\n     */\n    void initParam();\n    /**\n     * 初始化数据\n     */\n    void initData();\n\n    /**\n     * 初始化界面观察者的监听\n     */\n    void initViewObservable();\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/IBaseViewModel.java",
    "content": "package me.goldze.mvvmhabit.base;\n\n\nimport androidx.lifecycle.Lifecycle;\nimport androidx.lifecycle.LifecycleObserver;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.OnLifecycleEvent;\n\n/**\n * Created by goldze on 2017/6/15.\n */\n\npublic interface IBaseViewModel extends LifecycleObserver {\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_ANY)\n    void onAny(LifecycleOwner owner, Lifecycle.Event event);\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)\n    void onCreate();\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)\n    void onDestroy();\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_START)\n    void onStart();\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)\n    void onStop();\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)\n    void onResume();\n\n    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)\n    void onPause();\n\n    /**\n     * 注册RxBus\n     */\n    void registerRxBus();\n\n    /**\n     * 移除RxBus\n     */\n    void removeRxBus();\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/IModel.java",
    "content": "package me.goldze.mvvmhabit.base;\n\n/**\n * Created by goldze on 2017/6/15.\n */\npublic interface IModel {\n    /**\n     * ViewModel销毁时清除Model，与ViewModel共消亡。Model层同样不能持有长生命周期对象\n     */\n    void onCleared();\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/ItemViewModel.java",
    "content": "package me.goldze.mvvmhabit.base;\n\n\nimport androidx.annotation.NonNull;\n\n/**\n * ItemViewModel\n * Created by goldze on 2018/10/3.\n */\n\npublic class ItemViewModel<VM extends BaseViewModel> {\n    protected VM viewModel;\n\n    public ItemViewModel(@NonNull VM viewModel) {\n        this.viewModel = viewModel;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/MultiItemViewModel.java",
    "content": "package me.goldze.mvvmhabit.base;\n\n\nimport androidx.annotation.NonNull;\n\n/**\n * Create Author：goldze\n * Create Date：2019/01/25\n * Description：RecycleView多布局ItemViewModel是封装\n */\n\npublic class MultiItemViewModel<VM extends BaseViewModel> extends ItemViewModel<VM> {\n    protected Object multiType;\n\n    public Object getItemType() {\n        return multiType;\n    }\n\n    public void multiItemType(@NonNull Object multiType) {\n        this.multiType = multiType;\n    }\n\n    public MultiItemViewModel(@NonNull VM viewModel) {\n        super(viewModel);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/base/ViewModelFactory.java",
    "content": "package me.goldze.mvvmhabit.base;\n\nimport android.annotation.SuppressLint;\nimport android.app.Application;\n\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.InvocationTargetException;\n\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\n/**\n * Created by goldze on 2018/9/30.\n */\n\npublic class ViewModelFactory extends ViewModelProvider.NewInstanceFactory {\n    @SuppressLint(\"StaticFieldLeak\")\n    private static volatile ViewModelFactory INSTANCE;\n\n    private final Application mApplication;\n\n    public static ViewModelFactory getInstance(Application application) {\n\n        if (INSTANCE == null) {\n            synchronized (ViewModelFactory.class) {\n                if (INSTANCE == null) {\n                    INSTANCE = new ViewModelFactory(application);\n                }\n            }\n        }\n        return INSTANCE;\n    }\n\n\n    private ViewModelFactory(Application application) {\n        mApplication = application;\n    }\n\n    @Override\n    public <T extends ViewModel> T create(Class<T> modelClass) {\n        if (modelClass.isAssignableFrom(BaseViewModel.class)) {\n            return (T) new BaseViewModel(mApplication);\n        }\n        //反射动态实例化ViewModel\n        try {\n            String className = modelClass.getCanonicalName();\n            Class<?> classViewModel = Class.forName(className);\n            Constructor<?> cons = classViewModel.getConstructor(Application.class);\n            ViewModel viewModel = (ViewModel) cons.newInstance(mApplication);\n            return (T) viewModel;\n        } catch (ClassNotFoundException e) {\n            e.printStackTrace();\n            throw new IllegalArgumentException(\"Unknown ViewModel class: \" + modelClass.getName());\n        } catch (IllegalAccessException e) {\n            e.printStackTrace();\n            throw new IllegalArgumentException(\"Unknown ViewModel class: \" + modelClass.getName());\n        } catch (InstantiationException e) {\n            e.printStackTrace();\n            throw new IllegalArgumentException(\"Unknown ViewModel class: \" + modelClass.getName());\n        } catch (NoSuchMethodException e) {\n            e.printStackTrace();\n            throw new IllegalArgumentException(\"Unknown ViewModel class: \" + modelClass.getName());\n        } catch (InvocationTargetException e) {\n            e.printStackTrace();\n            throw new IllegalArgumentException(\"Unknown ViewModel class: \" + modelClass.getName());\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/command/BindingAction.java",
    "content": "package me.goldze.mvvmhabit.binding.command;\n\n/**\n * A zero-argument action.\n */\n\npublic interface BindingAction {\n    void call();\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/command/BindingCommand.java",
    "content": "package me.goldze.mvvmhabit.binding.command;\n\n\n/**\n * About : kelin的ReplyCommand\n * 执行的命令回调, 用于ViewModel与xml之间的数据绑定\n */\npublic class BindingCommand<T> {\n    private BindingAction execute;\n    private BindingConsumer<T> consumer;\n    private BindingFunction<Boolean> canExecute0;\n\n    public BindingCommand(BindingAction execute) {\n        this.execute = execute;\n    }\n\n    /**\n     * @param execute 带泛型参数的命令绑定\n     */\n    public BindingCommand(BindingConsumer<T> execute) {\n        this.consumer = execute;\n    }\n\n    /**\n     * @param execute     触发命令\n     * @param canExecute0 true则执行,反之不执行\n     */\n    public BindingCommand(BindingAction execute, BindingFunction<Boolean> canExecute0) {\n        this.execute = execute;\n        this.canExecute0 = canExecute0;\n    }\n\n    /**\n     * @param execute     带泛型参数触发命令\n     * @param canExecute0 true则执行,反之不执行\n     */\n    public BindingCommand(BindingConsumer<T> execute, BindingFunction<Boolean> canExecute0) {\n        this.consumer = execute;\n        this.canExecute0 = canExecute0;\n    }\n\n    /**\n     * 执行BindingAction命令\n     */\n    public void execute() {\n        if (execute != null && canExecute0()) {\n            execute.call();\n        }\n    }\n\n    /**\n     * 执行带泛型参数的命令\n     *\n     * @param parameter 泛型参数\n     */\n    public void execute(T parameter) {\n        if (consumer != null && canExecute0()) {\n            consumer.call(parameter);\n        }\n    }\n\n    /**\n     * 是否需要执行\n     *\n     * @return true则执行, 反之不执行\n     */\n    private boolean canExecute0() {\n        if (canExecute0 == null) {\n            return true;\n        }\n        return canExecute0.call();\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/command/BindingConsumer.java",
    "content": "package me.goldze.mvvmhabit.binding.command;\n\n/**\n * A one-argument action.\n *\n * @param <T> the first argument type\n */\npublic interface BindingConsumer<T> {\n    void call(T t);\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/command/BindingFunction.java",
    "content": "package me.goldze.mvvmhabit.binding.command;\n\n/**\n * Represents a function with zero arguments.\n *\n * @param <T> the result type\n */\npublic interface BindingFunction<T> {\n    T call();\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/command/ResponseCommand.java",
    "content": "package me.goldze.mvvmhabit.binding.command;\n\nimport io.reactivex.functions.Function;\n\n/**\n * About : kelin的ResponseCommand\n * 执行的命令事件转换\n */\npublic class ResponseCommand<T, R> {\n\n    private BindingFunction<R> execute;\n    private Function<T, R> function;\n    private BindingFunction<Boolean> canExecute;\n\n    /**\n     * like {@link BindingCommand},but ResponseCommand can return result when command has executed!\n     *\n     * @param execute function to execute when event occur.\n     */\n    public ResponseCommand(BindingFunction<R> execute) {\n        this.execute = execute;\n    }\n\n\n    public ResponseCommand(Function<T, R> execute) {\n        this.function = execute;\n    }\n\n\n    public ResponseCommand(BindingFunction<R> execute, BindingFunction<Boolean> canExecute) {\n        this.execute = execute;\n        this.canExecute = canExecute;\n    }\n\n\n    public ResponseCommand(Function<T, R> execute, BindingFunction<Boolean> canExecute) {\n        this.function = execute;\n        this.canExecute = canExecute;\n    }\n\n\n    public R execute() {\n        if (execute != null && canExecute()) {\n            return execute.call();\n        }\n        return null;\n    }\n\n    private boolean canExecute() {\n        if (canExecute == null) {\n            return true;\n        }\n        return canExecute.call();\n    }\n\n\n    public R execute(T parameter) throws Exception {\n        if (function != null && canExecute()) {\n            return function.apply(parameter);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/checkbox/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.checkbox;\n\nimport android.widget.CheckBox;\nimport android.widget.CompoundButton;\n\nimport androidx.databinding.BindingAdapter;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/16.\n */\n\npublic class ViewAdapter {\n    /**\n     * @param bindingCommand //绑定监听\n     */\n    @SuppressWarnings(\"unchecked\")\n    @BindingAdapter(value = {\"onCheckedChangedCommand\"}, requireAll = false)\n    public static void setCheckedChanged(final CheckBox checkBox, final BindingCommand<Boolean> bindingCommand) {\n        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {\n            @Override\n            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {\n                bindingCommand.execute(b);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/edittext/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.edittext;\n\nimport android.content.Context;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.inputmethod.InputMethodManager;\nimport android.widget.EditText;\n\nimport androidx.databinding.BindingAdapter;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/16.\n */\n\npublic class ViewAdapter {\n    /**\n     * EditText重新获取焦点的事件绑定\n     */\n    @BindingAdapter(value = {\"requestFocus\"}, requireAll = false)\n    public static void requestFocusCommand(EditText editText, final Boolean needRequestFocus) {\n        if (needRequestFocus) {\n            editText.setSelection(editText.getText().length());\n            editText.requestFocus();\n            InputMethodManager imm = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);\n            imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);\n        }\n        editText.setFocusableInTouchMode(needRequestFocus);\n    }\n\n    /**\n     * EditText输入文字改变的监听\n     */\n    @BindingAdapter(value = {\"textChanged\"}, requireAll = false)\n    public static void addTextChangedListener(EditText editText, final BindingCommand<String> textChanged) {\n        editText.addTextChangedListener(new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {\n\n            }\n\n            @Override\n            public void onTextChanged(CharSequence text, int i, int i1, int i2) {\n                if (textChanged != null) {\n                    textChanged.execute(text.toString());\n                }\n            }\n\n            @Override\n            public void afterTextChanged(Editable editable) {\n\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/image/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.image;\n\nimport android.text.TextUtils;\nimport android.widget.ImageView;\n\nimport com.bumptech.glide.Glide;\nimport com.bumptech.glide.request.RequestOptions;\n\nimport androidx.databinding.BindingAdapter;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic final class ViewAdapter {\n    @BindingAdapter(value = {\"url\", \"placeholderRes\"}, requireAll = false)\n    public static void setImageUri(ImageView imageView, String url, int placeholderRes) {\n        if (!TextUtils.isEmpty(url)) {\n            //使用Glide框架加载图片\n            Glide.with(imageView.getContext())\n                    .load(url)\n                    .apply(new RequestOptions().placeholder(placeholderRes))\n                    .into(imageView);\n        }\n    }\n}\n\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/listview/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.listview;\n\nimport android.view.View;\nimport android.widget.AbsListView;\nimport android.widget.AdapterView;\nimport android.widget.ListView;\n\nimport java.util.concurrent.TimeUnit;\n\nimport androidx.databinding.BindingAdapter;\nimport io.reactivex.functions.Consumer;\nimport io.reactivex.subjects.PublishSubject;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic final class ViewAdapter {\n\n    @SuppressWarnings(\"unchecked\")\n    @BindingAdapter(value = {\"onScrollChangeCommand\", \"onScrollStateChangedCommand\"}, requireAll = false)\n    public static void onScrollChangeCommand(final ListView listView,\n                                             final BindingCommand<ListViewScrollDataWrapper> onScrollChangeCommand,\n                                             final BindingCommand<Integer> onScrollStateChangedCommand) {\n        listView.setOnScrollListener(new AbsListView.OnScrollListener() {\n            private int scrollState;\n\n            @Override\n            public void onScrollStateChanged(AbsListView view, int scrollState) {\n                this.scrollState = scrollState;\n                if (onScrollStateChangedCommand != null) {\n                    onScrollStateChangedCommand.execute(scrollState);\n                }\n            }\n\n            @Override\n            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n                if (onScrollChangeCommand != null) {\n                    onScrollChangeCommand.execute(new ListViewScrollDataWrapper(scrollState, firstVisibleItem, visibleItemCount, totalItemCount));\n                }\n            }\n        });\n\n    }\n\n\n    @BindingAdapter(value = {\"onItemClickCommand\"}, requireAll = false)\n    public static void onItemClickCommand(final ListView listView, final BindingCommand<Integer> onItemClickCommand) {\n        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {\n            @Override\n            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {\n                if (onItemClickCommand != null) {\n                    onItemClickCommand.execute(position);\n                }\n            }\n        });\n    }\n\n\n    @BindingAdapter({\"onLoadMoreCommand\"})\n    public static void onLoadMoreCommand(final ListView listView, final BindingCommand<Integer> onLoadMoreCommand) {\n        listView.setOnScrollListener(new OnScrollListener(listView, onLoadMoreCommand));\n\n    }\n\n    public static class OnScrollListener implements AbsListView.OnScrollListener {\n        private PublishSubject<Integer> methodInvoke = PublishSubject.create();\n        private BindingCommand<Integer> onLoadMoreCommand;\n        private ListView listView;\n\n        public OnScrollListener(ListView listView, final BindingCommand<Integer> onLoadMoreCommand) {\n            this.onLoadMoreCommand = onLoadMoreCommand;\n            this.listView = listView;\n            methodInvoke.throttleFirst(1, TimeUnit.SECONDS)\n                    .subscribe(new Consumer<Integer>() {\n                        @Override\n                        public void accept(Integer integer) throws Exception {\n                            onLoadMoreCommand.execute(integer);\n                        }\n                    });\n        }\n\n        @Override\n        public void onScrollStateChanged(AbsListView view, int scrollState) {\n\n        }\n\n        @Override\n        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n            if (firstVisibleItem + visibleItemCount >= totalItemCount\n                    && totalItemCount != 0\n                    && totalItemCount != listView.getHeaderViewsCount()\n                    + listView.getFooterViewsCount()) {\n                if (onLoadMoreCommand != null) {\n                    methodInvoke.onNext(totalItemCount);\n                }\n            }\n        }\n    }\n\n    public static class ListViewScrollDataWrapper {\n        public int firstVisibleItem;\n        public int visibleItemCount;\n        public int totalItemCount;\n        public int scrollState;\n\n        public ListViewScrollDataWrapper(int scrollState, int firstVisibleItem, int visibleItemCount, int totalItemCount) {\n            this.firstVisibleItem = firstVisibleItem;\n            this.visibleItemCount = visibleItemCount;\n            this.totalItemCount = totalItemCount;\n            this.scrollState = scrollState;\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/mswitch/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.mswitch;\n\nimport android.widget.CompoundButton;\nimport android.widget.Switch;\n\nimport androidx.databinding.BindingAdapter;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/18.\n */\n\npublic class ViewAdapter {\n    /**\n     * 设置开关状态\n     *\n     * @param mSwitch Switch控件\n     */\n    @BindingAdapter(\"switchState\")\n    public static void setSwitchState(Switch mSwitch, boolean isChecked) {\n        mSwitch.setChecked(isChecked);\n    }\n\n    /**\n     * Switch的状态改变监听\n     *\n     * @param mSwitch        Switch控件\n     * @param changeListener 事件绑定命令\n     */\n    @BindingAdapter(\"onCheckedChangeCommand\")\n    public static void onCheckedChangeCommand(final Switch mSwitch, final BindingCommand<Boolean> changeListener) {\n        if (changeListener != null) {\n            mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {\n                @Override\n                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {\n                    changeListener.execute(isChecked);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/radiogroup/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.radiogroup;\n\nimport android.widget.RadioButton;\nimport android.widget.RadioGroup;\n\nimport androidx.annotation.IdRes;\nimport androidx.databinding.BindingAdapter;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic class ViewAdapter {\n    @BindingAdapter(value = {\"onCheckedChangedCommand\"}, requireAll = false)\n    public static void onCheckedChangedCommand(final RadioGroup radioGroup, final BindingCommand<String> bindingCommand) {\n        radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {\n            @Override\n            public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {\n                RadioButton radioButton = (RadioButton) group.findViewById(checkedId);\n                bindingCommand.execute(radioButton.getText().toString());\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/recyclerview/DividerLine.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.recyclerview;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.view.View;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by goldze on 2017/6/16.\n */\npublic class DividerLine extends RecyclerView.ItemDecoration {\n    private static final String TAG = DividerLine.class.getCanonicalName();\n    //默认分隔线厚度为2dp\n    private static final int DEFAULT_DIVIDER_SIZE = 1;\n    //控制分隔线的属性,值为一个drawable\n    private static final int ATTRS[] = {android.R.attr.listDivider};\n    //divider对应的drawable\n    private Drawable dividerDrawable;\n    private Context mContext;\n    private int dividerSize;\n    //默认为null\n    private LineDrawMode mMode = null;\n\n    /**\n     * 分隔线绘制模式,水平，垂直，两者都绘制\n     */\n    public enum LineDrawMode {\n        HORIZONTAL, VERTICAL, BOTH\n    }\n\n    public DividerLine(Context context) {\n        mContext = context;\n        //获取样式中对应的属性值\n        TypedArray attrArray = context.obtainStyledAttributes(ATTRS);\n        dividerDrawable = attrArray.getDrawable(0);\n        attrArray.recycle();\n    }\n\n    public DividerLine(Context context, LineDrawMode mode) {\n        this(context);\n        mMode = mode;\n    }\n\n    public DividerLine(Context context, int dividerSize, LineDrawMode mode) {\n        this(context, mode);\n        this.dividerSize = dividerSize;\n    }\n\n    public int getDividerSize() {\n        return dividerSize;\n    }\n\n    public void setDividerSize(int dividerSize) {\n        this.dividerSize = dividerSize;\n    }\n\n    public LineDrawMode getMode() {\n        return mMode;\n    }\n\n    public void setMode(LineDrawMode mode) {\n        mMode = mode;\n    }\n\n    /**\n     * Item绘制完毕之后绘制分隔线\n     * 根据不同的模式绘制不同的分隔线\n     *\n     * @param c\n     * @param parent\n     * @param state\n     */\n    @Override\n    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {\n        super.onDrawOver(c, parent, state);\n        if (getMode() == null) {\n            throw new IllegalStateException(\"assign LineDrawMode,please!\");\n        }\n        switch (getMode()) {\n            case VERTICAL:\n                drawVertical(c, parent, state);\n                break;\n            case HORIZONTAL:\n                drawHorizontal(c, parent, state);\n                break;\n            case BOTH:\n                drawHorizontal(c, parent, state);\n                drawVertical(c, parent, state);\n                break;\n        }\n    }\n\n    /**\n     * 绘制垂直分隔线\n     *\n     * @param c\n     * @param parent\n     * @param state\n     */\n    private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state) {\n        final int childCount = parent.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            final View child = parent.getChildAt(i);\n            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child\n                    .getLayoutParams();\n            final int top = child.getTop() - params.topMargin;\n            final int bottom = child.getBottom() + params.bottomMargin;\n            final int left = child.getRight() + params.rightMargin;\n            final int right = getDividerSize() == 0 ? left + dip2px(mContext, DEFAULT_DIVIDER_SIZE) : left + getDividerSize();\n            dividerDrawable.setBounds(left, top, right, bottom);\n            dividerDrawable.draw(c);\n        }\n    }\n\n    /**\n     * 绘制水平分隔线\n     *\n     * @param c\n     * @param parent\n     * @param state\n     */\n    private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state) {\n        int childCount = parent.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            //分别为每个item绘制分隔线,首先要计算出item的边缘在哪里,给分隔线定位,定界\n            final View child = parent.getChildAt(i);\n            //RecyclerView的LayoutManager继承自ViewGroup,支持了margin\n            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();\n            //child的左边缘(也是分隔线的左边)\n            final int left = child.getLeft() - params.leftMargin;\n            //child的底边缘(恰好是分隔线的顶边)\n            final int top = child.getBottom() + params.topMargin;\n            //child的右边(也是分隔线的右边)\n            final int right = child.getRight() - params.rightMargin;\n            //分隔线的底边所在的位置(那就是分隔线的顶边加上分隔线的高度)\n            final int bottom = getDividerSize() == 0 ? top + dip2px(mContext, DEFAULT_DIVIDER_SIZE) : top + getDividerSize();\n            dividerDrawable.setBounds(left, top, right, bottom);\n            //画上去\n            dividerDrawable.draw(c);\n        }\n    }\n\n    @Override\n    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {\n        super.getItemOffsets(outRect, view, parent, state);\n//        outRect.bottom = getDividerSize() == 0 ? dip2px(mContext, DEFAULT_DIVIDER_SIZE) : getDividerSize();\n    }\n\n    /**\n     * 将dip或dp值转换为px值，保证尺寸大小不变\n     *\n     * @param dipValue\n     * @param context（DisplayMetrics类中属性density）\n     * @return\n     */\n    public static int dip2px(Context context, float dipValue) {\n        float scale = context.getResources().getDisplayMetrics().density;\n        return (int) (dipValue * scale + 0.5f);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/recyclerview/LayoutManagers.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.recyclerview;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\nimport androidx.annotation.IntDef;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\n/**\n * A collection of factories to create RecyclerView LayoutManagers so that you can easily set them\n * in your layout.\n */\npublic class LayoutManagers {\n    protected LayoutManagers() {\n    }\n\n    public interface LayoutManagerFactory {\n        RecyclerView.LayoutManager create(RecyclerView recyclerView);\n    }\n\n    /**\n     * A {@link LinearLayoutManager}.\n     */\n    public static LayoutManagerFactory linear() {\n        return new LayoutManagerFactory() {\n            @Override\n            public RecyclerView.LayoutManager create(RecyclerView recyclerView) {\n                return new LinearLayoutManager(recyclerView.getContext());\n            }\n        };\n    }\n\n    /**\n     * A {@link LinearLayoutManager} with the given orientation and reverseLayout.\n     */\n    public static LayoutManagerFactory linear(@Orientation final int orientation, final boolean reverseLayout) {\n        return new LayoutManagerFactory() {\n            @Override\n            public RecyclerView.LayoutManager create(RecyclerView recyclerView) {\n                return new LinearLayoutManager(recyclerView.getContext(), orientation, reverseLayout);\n            }\n        };\n    }\n\n    /**\n     * A {@link GridLayoutManager} with the given spanCount.\n     */\n    public static LayoutManagerFactory grid(final int spanCount) {\n        return new LayoutManagerFactory() {\n            @Override\n            public RecyclerView.LayoutManager create(RecyclerView recyclerView) {\n                return new GridLayoutManager(recyclerView.getContext(), spanCount);\n            }\n        };\n    }\n\n    /**\n     * A {@link GridLayoutManager} with the given spanCount, orientation and reverseLayout.\n     **/\n    public static LayoutManagerFactory grid(final int spanCount, @Orientation final int orientation, final boolean reverseLayout) {\n        return new LayoutManagerFactory() {\n            @Override\n            public RecyclerView.LayoutManager create(RecyclerView recyclerView) {\n                return new GridLayoutManager(recyclerView.getContext(), spanCount, orientation, reverseLayout);\n            }\n        };\n    }\n\n    /**\n     * A {@link StaggeredGridLayoutManager} with the given spanCount and orientation.\n     */\n    public static LayoutManagerFactory staggeredGrid(final int spanCount, @Orientation final int orientation) {\n        return new LayoutManagerFactory() {\n            @Override\n            public RecyclerView.LayoutManager create(RecyclerView recyclerView) {\n                return new StaggeredGridLayoutManager(spanCount, orientation);\n            }\n        };\n    }\n\n    @IntDef({LinearLayoutManager.HORIZONTAL, LinearLayoutManager.VERTICAL})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Orientation {\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/recyclerview/LineManagers.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.recyclerview;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by goldze on 2017/6/16.\n */\npublic class LineManagers {\n    protected LineManagers() {\n    }\n\n    public interface LineManagerFactory {\n        RecyclerView.ItemDecoration create(RecyclerView recyclerView);\n    }\n\n\n    public static LineManagerFactory both() {\n        return new LineManagerFactory() {\n            @Override\n            public RecyclerView.ItemDecoration create(RecyclerView recyclerView) {\n                return new DividerLine(recyclerView.getContext(), DividerLine.LineDrawMode.BOTH);\n            }\n        };\n    }\n\n    public static LineManagerFactory horizontal() {\n        return new LineManagerFactory() {\n            @Override\n            public RecyclerView.ItemDecoration create(RecyclerView recyclerView) {\n                return new DividerLine(recyclerView.getContext(), DividerLine.LineDrawMode.HORIZONTAL);\n            }\n        };\n    }\n\n    public static LineManagerFactory vertical() {\n        return new LineManagerFactory() {\n            @Override\n            public RecyclerView.ItemDecoration create(RecyclerView recyclerView) {\n                return new DividerLine(recyclerView.getContext(), DividerLine.LineDrawMode.VERTICAL);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/recyclerview/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.recyclerview;\n\nimport java.util.concurrent.TimeUnit;\n\nimport androidx.databinding.BindingAdapter;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport io.reactivex.functions.Consumer;\nimport io.reactivex.subjects.PublishSubject;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/16.\n */\npublic class ViewAdapter {\n\n    @BindingAdapter(\"lineManager\")\n    public static void setLineManager(RecyclerView recyclerView, LineManagers.LineManagerFactory lineManagerFactory) {\n        recyclerView.addItemDecoration(lineManagerFactory.create(recyclerView));\n    }\n\n    @BindingAdapter(\"layoutManager\")\n    public static void setLayoutManager(RecyclerView recyclerView, LayoutManagers.LayoutManagerFactory layoutManagerFactory) {\n        recyclerView.setLayoutManager(layoutManagerFactory.create(recyclerView));\n    }\n\n    @BindingAdapter(value = {\"onScrollChangeCommand\", \"onScrollStateChangedCommand\"}, requireAll = false)\n    public static void onScrollChangeCommand(final RecyclerView recyclerView,\n                                             final BindingCommand<ScrollDataWrapper> onScrollChangeCommand,\n                                             final BindingCommand<Integer> onScrollStateChangedCommand) {\n        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {\n            private int state;\n\n            @Override\n            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n                super.onScrolled(recyclerView, dx, dy);\n                if (onScrollChangeCommand != null) {\n                    onScrollChangeCommand.execute(new ScrollDataWrapper(dx, dy, state));\n                }\n            }\n\n            @Override\n            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {\n                super.onScrollStateChanged(recyclerView, newState);\n                state = newState;\n                if (onScrollStateChangedCommand != null) {\n                    onScrollStateChangedCommand.execute(newState);\n                }\n            }\n        });\n\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @BindingAdapter({\"onLoadMoreCommand\"})\n    public static void onLoadMoreCommand(final RecyclerView recyclerView, final BindingCommand<Integer> onLoadMoreCommand) {\n        RecyclerView.OnScrollListener listener = new OnScrollListener(onLoadMoreCommand);\n        recyclerView.addOnScrollListener(listener);\n\n    }\n\n    @BindingAdapter(\"itemAnimator\")\n    public static void setItemAnimator(RecyclerView recyclerView, RecyclerView.ItemAnimator animator) {\n        recyclerView.setItemAnimator(animator);\n    }\n\n    public static class OnScrollListener extends RecyclerView.OnScrollListener {\n\n        private PublishSubject<Integer> methodInvoke = PublishSubject.create();\n\n        private BindingCommand<Integer> onLoadMoreCommand;\n\n        public OnScrollListener(final BindingCommand<Integer> onLoadMoreCommand) {\n            this.onLoadMoreCommand = onLoadMoreCommand;\n            methodInvoke.throttleFirst(1, TimeUnit.SECONDS)\n                    .subscribe(new Consumer<Integer>() {\n                        @Override\n                        public void accept(Integer integer) throws Exception {\n                            onLoadMoreCommand.execute(integer);\n                        }\n                    });\n        }\n\n        @Override\n        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n            LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();\n            int visibleItemCount = layoutManager.getChildCount();\n            int totalItemCount = layoutManager.getItemCount();\n            int pastVisiblesItems = layoutManager.findFirstVisibleItemPosition();\n            if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {\n                if (onLoadMoreCommand != null) {\n                    methodInvoke.onNext(recyclerView.getAdapter().getItemCount());\n                }\n            }\n        }\n\n        @Override\n        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {\n            super.onScrollStateChanged(recyclerView, newState);\n        }\n\n\n    }\n\n    public static class ScrollDataWrapper {\n        public float scrollX;\n        public float scrollY;\n        public int state;\n\n        public ScrollDataWrapper(float scrollX, float scrollY, int state) {\n            this.scrollX = scrollX;\n            this.scrollY = scrollY;\n            this.state = state;\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/scrollview/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.scrollview;\n\nimport android.view.ViewTreeObserver;\nimport android.widget.ScrollView;\n\nimport androidx.core.widget.NestedScrollView;\nimport androidx.databinding.BindingAdapter;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic final class ViewAdapter {\n\n    @SuppressWarnings(\"unchecked\")\n    @BindingAdapter({\"onScrollChangeCommand\"})\n    public static void onScrollChangeCommand(final NestedScrollView nestedScrollView, final BindingCommand<NestScrollDataWrapper> onScrollChangeCommand) {\n        nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {\n            @Override\n            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {\n                if (onScrollChangeCommand != null) {\n                    onScrollChangeCommand.execute(new NestScrollDataWrapper(scrollX, scrollY, oldScrollX, oldScrollY));\n                }\n            }\n        });\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @BindingAdapter({\"onScrollChangeCommand\"})\n    public static void onScrollChangeCommand(final ScrollView scrollView, final BindingCommand<ScrollDataWrapper> onScrollChangeCommand) {\n        scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {\n            @Override\n            public void onScrollChanged() {\n                if (onScrollChangeCommand != null) {\n                    onScrollChangeCommand.execute(new ScrollDataWrapper(scrollView.getScrollX(), scrollView.getScrollY()));\n                }\n            }\n        });\n    }\n\n    public static class ScrollDataWrapper {\n        public float scrollX;\n        public float scrollY;\n\n        public ScrollDataWrapper(float scrollX, float scrollY) {\n            this.scrollX = scrollX;\n            this.scrollY = scrollY;\n        }\n    }\n\n    public static class NestScrollDataWrapper {\n        public int scrollX;\n        public int scrollY;\n        public int oldScrollX;\n        public int oldScrollY;\n\n        public NestScrollDataWrapper(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {\n            this.scrollX = scrollX;\n            this.scrollY = scrollY;\n            this.oldScrollX = oldScrollX;\n            this.oldScrollY = oldScrollY;\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/spinner/IKeyAndValue.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.spinner;\n\n/**\n * Created by goldze on 2017/6/18.\n * 下拉Spinner控件的键值对, 实现该接口,返回key,value值, 在xml绑定List<IKeyAndValue>\n */\npublic interface IKeyAndValue {\n    String getKey();\n\n    String getValue();\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/spinner/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.spinner;\n\nimport android.text.TextUtils;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.ArrayAdapter;\nimport android.widget.Spinner;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport androidx.databinding.BindingAdapter;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic class ViewAdapter {\n    /**\n     * 双向的SpinnerViewAdapter, 可以监听选中的条目,也可以回显选中的值\n     *\n     * @param spinner        控件本身\n     * @param itemDatas      下拉条目的集合\n     * @param valueReply     回显的value\n     * @param bindingCommand 条目点击的监听\n     */\n    @BindingAdapter(value = {\"itemDatas\", \"valueReply\", \"resource\", \"dropDownResource\", \"onItemSelectedCommand\"}, requireAll = false)\n    public static void onItemSelectedCommand(final Spinner spinner, final List<IKeyAndValue> itemDatas, String valueReply, int resource, int dropDownResource, final BindingCommand<IKeyAndValue> bindingCommand) {\n        if (itemDatas == null) {\n            throw new NullPointerException(\"this itemDatas parameter is null\");\n        }\n        List<String> lists = new ArrayList<>();\n        for (IKeyAndValue iKeyAndValue : itemDatas) {\n            lists.add(iKeyAndValue.getKey());\n        }\n        if (resource == 0) {\n            resource = android.R.layout.simple_spinner_item;\n        }\n        if (dropDownResource == 0) {\n            dropDownResource = android.R.layout.simple_spinner_dropdown_item;\n        }\n        ArrayAdapter<String> adapter = new ArrayAdapter(spinner.getContext(), resource, lists);\n        adapter.setDropDownViewResource(dropDownResource);\n        spinner.setAdapter(adapter);\n        spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {\n            @Override\n            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n                IKeyAndValue iKeyAndValue = itemDatas.get(position);\n                //将IKeyAndValue对象交给ViewModel\n                bindingCommand.execute(iKeyAndValue);\n            }\n\n            @Override\n            public void onNothingSelected(AdapterView<?> parent) {\n\n            }\n        });\n        //回显选中的值\n        if (!TextUtils.isEmpty(valueReply)) {\n            for (int i = 0; i < itemDatas.size(); i++) {\n                IKeyAndValue iKeyAndValue = itemDatas.get(i);\n                if (valueReply.equals(iKeyAndValue.getValue())) {\n                    spinner.setSelection(i);\n                    return;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/swiperefresh/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.swiperefresh;\n\nimport androidx.databinding.BindingAdapter;\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic class ViewAdapter {\n    //下拉刷新命令\n    @BindingAdapter({\"onRefreshCommand\"})\n    public static void onRefreshCommand(SwipeRefreshLayout swipeRefreshLayout, final BindingCommand onRefreshCommand) {\n        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {\n            @Override\n            public void onRefresh() {\n                if (onRefreshCommand != null) {\n                    onRefreshCommand.execute();\n                }\n            }\n        });\n    }\n\n    //是否刷新中\n    @BindingAdapter({\"refreshing\"})\n    public static void setRefreshing(SwipeRefreshLayout swipeRefreshLayout, boolean refreshing) {\n        swipeRefreshLayout.setRefreshing(refreshing);\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/view/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.view;\n\nimport android.view.View;\n\nimport com.jakewharton.rxbinding2.view.RxView;\n\nimport java.util.concurrent.TimeUnit;\n\nimport androidx.databinding.BindingAdapter;\nimport io.reactivex.functions.Consumer;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/16.\n */\n\npublic class ViewAdapter {\n    //防重复点击间隔(秒)\n    public static final int CLICK_INTERVAL = 1;\n\n    /**\n     * requireAll 是意思是是否需要绑定全部参数, false为否\n     * View的onClick事件绑定\n     * onClickCommand 绑定的命令,\n     * isThrottleFirst 是否开启防止过快点击\n     */\n    @BindingAdapter(value = {\"onClickCommand\", \"isThrottleFirst\"}, requireAll = false)\n    public static void onClickCommand(View view, final BindingCommand clickCommand, final boolean isThrottleFirst) {\n        if (isThrottleFirst) {\n            RxView.clicks(view)\n                    .subscribe(new Consumer<Object>() {\n                        @Override\n                        public void accept(Object object) throws Exception {\n                            if (clickCommand != null) {\n                                clickCommand.execute();\n                            }\n                        }\n                    });\n        } else {\n            RxView.clicks(view)\n                    .throttleFirst(CLICK_INTERVAL, TimeUnit.SECONDS)//1秒钟内只允许点击1次\n                    .subscribe(new Consumer<Object>() {\n                        @Override\n                        public void accept(Object object) throws Exception {\n                            if (clickCommand != null) {\n                                clickCommand.execute();\n                            }\n                        }\n                    });\n        }\n    }\n\n    /**\n     * view的onLongClick事件绑定\n     */\n    @BindingAdapter(value = {\"onLongClickCommand\"}, requireAll = false)\n    public static void onLongClickCommand(View view, final BindingCommand clickCommand) {\n        RxView.longClicks(view)\n                .subscribe(new Consumer<Object>() {\n                    @Override\n                    public void accept(Object object) throws Exception {\n                        if (clickCommand != null) {\n                            clickCommand.execute();\n                        }\n                    }\n                });\n    }\n\n    /**\n     * 回调控件本身\n     *\n     * @param currentView\n     * @param bindingCommand\n     */\n    @BindingAdapter(value = {\"currentView\"}, requireAll = false)\n    public static void replyCurrentView(View currentView, BindingCommand bindingCommand) {\n        if (bindingCommand != null) {\n            bindingCommand.execute(currentView);\n        }\n    }\n\n    /**\n     * view是否需要获取焦点\n     */\n    @BindingAdapter({\"requestFocus\"})\n    public static void requestFocusCommand(View view, final Boolean needRequestFocus) {\n        if (needRequestFocus) {\n            view.setFocusableInTouchMode(true);\n            view.requestFocus();\n        } else {\n            view.clearFocus();\n        }\n    }\n\n    /**\n     * view的焦点发生变化的事件绑定\n     */\n    @BindingAdapter({\"onFocusChangeCommand\"})\n    public static void onFocusChangeCommand(View view, final BindingCommand<Boolean> onFocusChangeCommand) {\n        view.setOnFocusChangeListener(new View.OnFocusChangeListener() {\n            @Override\n            public void onFocusChange(View v, boolean hasFocus) {\n                if (onFocusChangeCommand != null) {\n                    onFocusChangeCommand.execute(hasFocus);\n                }\n            }\n        });\n    }\n\n    /**\n     * view的显示隐藏\n     */\n    @BindingAdapter(value = {\"isVisible\"}, requireAll = false)\n    public static void isVisible(View view, final Boolean visibility) {\n        if (visibility) {\n            view.setVisibility(View.VISIBLE);\n        } else {\n            view.setVisibility(View.GONE);\n        }\n    }\n//    @BindingAdapter({\"onTouchCommand\"})\n//    public static void onTouchCommand(View view, final ResponseCommand<MotionEvent, Boolean> onTouchCommand) {\n//        view.setOnTouchListener(new View.OnTouchListener() {\n//            @Override\n//            public boolean onTouch(View v, MotionEvent event) {\n//                if (onTouchCommand != null) {\n//                    return onTouchCommand.execute(event);\n//                }\n//                return false;\n//            }\n//        });\n//    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/viewgroup/IBindingItemViewModel.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.viewgroup;\n\nimport androidx.databinding.ViewDataBinding;\n\n/**\n * Created by goldze on 2017/6/15.\n */\npublic interface IBindingItemViewModel<V extends ViewDataBinding> {\n    void injecDataBinding(V binding);\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/viewgroup/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.viewgroup;\n\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\n\nimport androidx.databinding.BindingAdapter;\nimport androidx.databinding.DataBindingUtil;\nimport androidx.databinding.ObservableList;\nimport androidx.databinding.ViewDataBinding;\nimport me.tatarka.bindingcollectionadapter2.ItemBinding;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic final class ViewAdapter {\n\n    @BindingAdapter({\"itemView\", \"observableList\"})\n    public static void addViews(ViewGroup viewGroup, final ItemBinding itemBinding, final ObservableList<IBindingItemViewModel> viewModelList) {\n        if (viewModelList != null && !viewModelList.isEmpty()) {\n            viewGroup.removeAllViews();\n            for (IBindingItemViewModel viewModel : viewModelList) {\n                ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()),\n                        itemBinding.layoutRes(), viewGroup, true);\n                binding.setVariable(itemBinding.variableId(), viewModel);\n                viewModel.injecDataBinding(binding);\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/viewpager/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.viewpager;\n\nimport androidx.databinding.BindingAdapter;\nimport androidx.viewpager.widget.ViewPager;\nimport me.goldze.mvvmhabit.binding.command.BindingCommand;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic class ViewAdapter {\n    @BindingAdapter(value = {\"onPageScrolledCommand\", \"onPageSelectedCommand\", \"onPageScrollStateChangedCommand\"}, requireAll = false)\n    public static void onScrollChangeCommand(final ViewPager viewPager,\n                                             final BindingCommand<ViewPagerDataWrapper> onPageScrolledCommand,\n                                             final BindingCommand<Integer> onPageSelectedCommand,\n                                             final BindingCommand<Integer> onPageScrollStateChangedCommand) {\n        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {\n            private int state;\n\n            @Override\n            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {\n                if (onPageScrolledCommand != null) {\n                    onPageScrolledCommand.execute(new ViewPagerDataWrapper(position, positionOffset, positionOffsetPixels, state));\n                }\n            }\n\n            @Override\n            public void onPageSelected(int position) {\n                if (onPageSelectedCommand != null) {\n                    onPageSelectedCommand.execute(position);\n                }\n            }\n\n            @Override\n            public void onPageScrollStateChanged(int state) {\n                this.state = state;\n                if (onPageScrollStateChangedCommand != null) {\n                    onPageScrollStateChangedCommand.execute(state);\n                }\n            }\n        });\n\n    }\n\n    public static class ViewPagerDataWrapper {\n        public float positionOffset;\n        public float position;\n        public int positionOffsetPixels;\n        public int state;\n\n        public ViewPagerDataWrapper(float position, float positionOffset, int positionOffsetPixels, int state) {\n            this.positionOffset = positionOffset;\n            this.position = position;\n            this.positionOffsetPixels = positionOffsetPixels;\n            this.state = state;\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/binding/viewadapter/webview/ViewAdapter.java",
    "content": "package me.goldze.mvvmhabit.binding.viewadapter.webview;\n\nimport android.text.TextUtils;\nimport android.webkit.WebView;\n\nimport androidx.databinding.BindingAdapter;\n\n/**\n * Created by goldze on 2017/6/18.\n */\npublic class ViewAdapter {\n    @BindingAdapter({\"render\"})\n    public static void loadHtml(WebView webView, final String html) {\n        if (!TextUtils.isEmpty(html)) {\n            webView.loadDataWithBaseURL(null, html, \"text/html\", \"UTF-8\", null);\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/bus/Messenger.java",
    "content": "package me.goldze.mvvmhabit.bus;\n\nimport java.lang.reflect.Type;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.List;\n\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingConsumer;\n\n/**\n * About : kelin的Messenger\n */\npublic class Messenger {\n\n    private static Messenger defaultInstance;\n\n    private HashMap<Type, List<WeakActionAndToken>> recipientsOfSubclassesAction;\n\n    private HashMap<Type, List<WeakActionAndToken>> recipientsStrictAction;\n\n    public static Messenger getDefault() {\n        if (defaultInstance == null) {\n            defaultInstance = new Messenger();\n        }\n        return defaultInstance;\n    }\n\n\n    public static void overrideDefault(Messenger newWeakMessenger) {\n        defaultInstance = newWeakMessenger;\n    }\n\n    public static void reset() {\n        defaultInstance = null;\n    }\n\n    /**\n     * @param recipient the receiver,if register in activity the recipient always set \"this\",\n     *                  and \"WeakMessenger.getDefault().unregister(this)\" in onDestroy,if in ViewModel,\n     *                  you can also register with Activity context and also in onDestroy to unregister.\n     * @param action    do something on message received\n     */\n    public void register(Object recipient, BindingAction action) {\n        register(recipient, null, false, action);\n    }\n\n    /**\n     * @param recipient                 the receiver,if register in activity the recipient always set \"this\",\n     *                                  and \"WeakMessenger.getDefault().unregister(this)\" in onDestroy,if in ViewModel,\n     *                                  you can also register with Activity context and also in onDestroy to unregister.\n     * @param receiveDerivedMessagesToo whether Derived class of recipient can receive the message\n     * @param action                    do something on message received\n     */\n    public void register(Object recipient, boolean receiveDerivedMessagesToo, BindingAction action) {\n        register(recipient, null, receiveDerivedMessagesToo, action);\n    }\n\n    /**\n     * @param recipient the receiver,if register in activity the recipient always set \"this\",\n     *                  and \"WeakMessenger.getDefault().unregister(this)\" in onDestroy,if in ViewModel,\n     *                  you can also register with Activity context and also in onDestroy to unregister.\n     * @param token     register with a unique token,when a messenger send a msg with same token,it\n     *                  will\n     *                  receive this msg\n     * @param action    do something on message received\n     */\n    public void register(Object recipient, Object token, BindingAction action) {\n        register(recipient, token, false, action);\n    }\n\n    /**\n     * @param recipient                 the receiver,if register in activity the recipient always set \"this\",\n     *                                  and \"WeakMessenger.getDefault().unregister(this)\" in onDestroy,if in ViewModel,\n     *                                  you can also register with Activity context and also in onDestroy to unregister.\n     * @param token                     register with a unique token,when a messenger send a msg with same token,it\n     *                                  will\n     *                                  receive this msg\n     * @param receiveDerivedMessagesToo whether Derived class of recipient can receive the message\n     * @param action                    do something on message received\n     */\n    public void register(Object recipient, Object token, boolean receiveDerivedMessagesToo, BindingAction action) {\n\n        Type messageType = NotMsgType.class;\n\n        HashMap<Type, List<WeakActionAndToken>> recipients;\n\n        if (receiveDerivedMessagesToo) {\n            if (recipientsOfSubclassesAction == null) {\n                recipientsOfSubclassesAction = new HashMap<Type, List<WeakActionAndToken>>();\n            }\n\n            recipients = recipientsOfSubclassesAction;\n        } else {\n            if (recipientsStrictAction == null) {\n                recipientsStrictAction = new HashMap<Type, List<WeakActionAndToken>>();\n            }\n\n            recipients = recipientsStrictAction;\n        }\n\n        List<WeakActionAndToken> list;\n\n        if (!recipients.containsKey(messageType)) {\n            list = new ArrayList<WeakActionAndToken>();\n            recipients.put(messageType, list);\n        } else {\n            list = recipients.get(messageType);\n        }\n\n        WeakAction weakAction = new WeakAction(recipient, action);\n\n        WeakActionAndToken item = new WeakActionAndToken(weakAction, token);\n        list.add(item);\n        cleanup();\n    }\n\n    /**\n     * @param recipient {}\n     * @param tClass    class of T\n     * @param action    this action has one params that type of tClass\n     * @param <T>       message data type\n     */\n    public <T> void register(Object recipient, Class<T> tClass, BindingConsumer<T> action) {\n        register(recipient, null, false, action, tClass);\n    }\n\n    /**\n     * see {}\n     *\n     * @param recipient                 receiver of message\n     * @param receiveDerivedMessagesToo whether derived class of recipient can receive the message\n     * @param tClass                    class of T\n     * @param action                    this action has one params that type of tClass\n     * @param <T>                       message data type\n     */\n    public <T> void register(Object recipient, boolean receiveDerivedMessagesToo, Class<T> tClass, BindingConsumer<T> action) {\n        register(recipient, null, receiveDerivedMessagesToo, action, tClass);\n    }\n\n    /**\n     * see {}\n     *\n     * @param recipient receiver of message\n     * @param token     register with a unique token,when a messenger send a msg with same token,it\n     *                  will\n     *                  receive this msg\n     * @param tClass    class of T for BindingConsumer\n     * @param action    this action has one params that type of tClass\n     * @param <T>       message data type\n     */\n    public <T> void register(Object recipient, Object token, Class<T> tClass, BindingConsumer<T> action) {\n        register(recipient, token, false, action, tClass);\n    }\n\n    /**\n     * see {}\n     *\n     * @param recipient                 receiver of message\n     * @param token                     register with a unique token,when a messenger send a msg with same token,it\n     *                                  will\n     *                                  receive this msg\n     * @param receiveDerivedMessagesToo whether derived class of recipient can receive the message\n     * @param action                    this action has one params that type of tClass\n     * @param tClass                    class of T for BindingConsumer\n     * @param <T>                       message data type\n     */\n    public <T> void register(Object recipient, Object token, boolean receiveDerivedMessagesToo, BindingConsumer<T> action, Class<T> tClass) {\n\n        Type messageType = tClass;\n\n        HashMap<Type, List<WeakActionAndToken>> recipients;\n\n        if (receiveDerivedMessagesToo) {\n            if (recipientsOfSubclassesAction == null) {\n                recipientsOfSubclassesAction = new HashMap<Type, List<WeakActionAndToken>>();\n            }\n\n            recipients = recipientsOfSubclassesAction;\n        } else {\n            if (recipientsStrictAction == null) {\n                recipientsStrictAction = new HashMap<Type, List<WeakActionAndToken>>();\n            }\n\n            recipients = recipientsStrictAction;\n        }\n\n        List<WeakActionAndToken> list;\n\n        if (!recipients.containsKey(messageType)) {\n            list = new ArrayList<WeakActionAndToken>();\n            recipients.put(messageType, list);\n        } else {\n            list = recipients.get(messageType);\n        }\n\n        WeakAction weakAction = new WeakAction<T>(recipient, action);\n\n        WeakActionAndToken item = new WeakActionAndToken(weakAction, token);\n        list.add(item);\n        cleanup();\n    }\n\n\n    private void cleanup() {\n        cleanupList(recipientsOfSubclassesAction);\n        cleanupList(recipientsStrictAction);\n    }\n\n    /**\n     * @param token send with a unique token,when a receiver has register with same token,it will\n     *              receive this msg\n     */\n    public void sendNoMsg(Object token) {\n        sendToTargetOrType(null, token);\n    }\n\n    /**\n     * send to recipient directly with has not any message\n     *\n     * @param target WeakMessenger.getDefault().register(this, ..) in a activity,if target set this\n     *               activity\n     *               it will receive the message\n     */\n    public void sendNoMsgToTarget(Object target) {\n        sendToTargetOrType(target.getClass(), null);\n    }\n\n    /**\n     * send message to target with token,when a receiver has register with same token,it will\n     * receive this msg\n     *\n     * @param token  send with a unique token,when a receiver has register with same token,it will\n     *               receive this msg\n     * @param target send to recipient directly with has not any message,\n     *               WeakMessenger.getDefault().register(this, ..) in a activity,if target set this activity\n     *               it will receive the message\n     */\n    public void sendNoMsgToTargetWithToken(Object token, Object target) {\n        sendToTargetOrType(target.getClass(), token);\n    }\n\n    /**\n     * send the message type of T, all receiver can receive the message\n     *\n     * @param message any object can to be a message\n     * @param <T>     message data type\n     */\n    public <T> void send(T message) {\n        sendToTargetOrType(message, null, null);\n    }\n\n    /**\n     * send the message type of T, all receiver can receive the message\n     *\n     * @param message any object can to be a message\n     * @param token   send with a unique token,when a receiver has register with same token,it will\n     *                receive this message\n     * @param <T>     message data type\n     */\n    public <T> void send(T message, Object token) {\n        sendToTargetOrType(message, null, token);\n    }\n\n    /**\n     * send message to recipient directly\n     *\n     * @param message any object can to be a message\n     * @param target  send to recipient directly with has not any message,\n     *                WeakMessenger.getDefault().register(this, ..) in a activity,if target set this activity\n     *                it will receive the message\n     * @param <T>     message data type\n     * @param <R>     target\n     */\n    public <T, R> void sendToTarget(T message, R target) {\n        sendToTargetOrType(message, target.getClass(), null);\n    }\n\n    /**\n     * Unregister the receiver such as:\n     * WeakMessenger.getDefault().unregister(this)\" in onDestroy in the Activity is required avoid\n     * to\n     * memory leak!\n     *\n     * @param recipient receiver of message\n     */\n    public void unregister(Object recipient) {\n        unregisterFromLists(recipient, recipientsOfSubclassesAction);\n        unregisterFromLists(recipient, recipientsStrictAction);\n        cleanup();\n    }\n\n\n    public <T> void unregister(Object recipient, Object token) {\n        unregisterFromLists(recipient, token, null, recipientsStrictAction);\n        unregisterFromLists(recipient, token, null, recipientsOfSubclassesAction);\n        cleanup();\n    }\n\n\n    private static <T> void sendToList(\n            T message,\n            Collection<WeakActionAndToken> list,\n            Type messageTargetType,\n            Object token) {\n        if (list != null) {\n            // Clone to protect from people registering in a \"receive message\" method\n            // Bug correction Messaging BL0004.007\n            ArrayList<WeakActionAndToken> listClone = new ArrayList<>();\n            listClone.addAll(list);\n\n            for (WeakActionAndToken item : listClone) {\n                WeakAction executeAction = item.getAction();\n                if (executeAction != null\n                        && item.getAction().isLive()\n                        && item.getAction().getTarget() != null\n                        && (messageTargetType == null\n                        || item.getAction().getTarget().getClass() == messageTargetType\n                        || classImplements(item.getAction().getTarget().getClass(), messageTargetType))\n                        && ((item.getToken() == null && token == null)\n                        || item.getToken() != null && item.getToken().equals(token))) {\n                    executeAction.execute(message);\n                }\n            }\n        }\n    }\n\n    private static void unregisterFromLists(Object recipient, HashMap<Type, List<WeakActionAndToken>> lists) {\n        if (recipient == null\n                || lists == null\n                || lists.size() == 0) {\n            return;\n        }\n        synchronized (lists) {\n            for (Type messageType : lists.keySet()) {\n                for (WeakActionAndToken item : lists.get(messageType)) {\n                    WeakAction weakAction = item.getAction();\n\n                    if (weakAction != null\n                            && recipient == weakAction.getTarget()) {\n                        weakAction.markForDeletion();\n                    }\n                }\n            }\n        }\n        cleanupList(lists);\n    }\n\n    private static <T> void unregisterFromLists(\n            Object recipient,\n            BindingConsumer<T> action,\n            HashMap<Type, List<WeakActionAndToken>> lists,\n            Class<T> tClass) {\n        Type messageType = tClass;\n\n        if (recipient == null\n                || lists == null\n                || lists.size() == 0\n                || !lists.containsKey(messageType)) {\n            return;\n        }\n\n        synchronized (lists) {\n            for (WeakActionAndToken item : lists.get(messageType)) {\n                WeakAction<T> weakActionCasted = (WeakAction<T>) item.getAction();\n\n                if (weakActionCasted != null\n                        && recipient == weakActionCasted.getTarget()\n                        && (action == null\n                        || action == weakActionCasted.getBindingConsumer())) {\n                    item.getAction().markForDeletion();\n                }\n            }\n        }\n    }\n\n    private static void unregisterFromLists(\n            Object recipient,\n            BindingAction action,\n            HashMap<Type, List<WeakActionAndToken>> lists\n    ) {\n        Type messageType = NotMsgType.class;\n\n        if (recipient == null\n                || lists == null\n                || lists.size() == 0\n                || !lists.containsKey(messageType)) {\n            return;\n        }\n\n        synchronized (lists) {\n            for (WeakActionAndToken item : lists.get(messageType)) {\n                WeakAction weakActionCasted = (WeakAction) item.getAction();\n\n                if (weakActionCasted != null\n                        && recipient == weakActionCasted.getTarget()\n                        && (action == null\n                        || action == weakActionCasted.getBindingAction())) {\n                    item.getAction().markForDeletion();\n                }\n            }\n        }\n    }\n\n\n    private static <T> void unregisterFromLists(\n            Object recipient,\n            Object token,\n            BindingConsumer<T> action,\n            HashMap<Type, List<WeakActionAndToken>> lists, Class<T> tClass) {\n        Type messageType = tClass;\n\n        if (recipient == null\n                || lists == null\n                || lists.size() == 0\n                || !lists.containsKey(messageType)) {\n            return;\n        }\n\n        synchronized (lists) {\n            for (WeakActionAndToken item : lists.get(messageType)) {\n                WeakAction<T> weakActionCasted = (WeakAction<T>) item.getAction();\n\n                if (weakActionCasted != null\n                        && recipient == weakActionCasted.getTarget()\n                        && (action == null\n                        || action == weakActionCasted.getBindingConsumer())\n                        && (token == null\n                        || token.equals(item.getToken()))) {\n                    item.getAction().markForDeletion();\n                }\n            }\n        }\n    }\n\n    private static void unregisterFromLists(\n            Object recipient,\n            Object token,\n            BindingAction action,\n            HashMap<Type, List<WeakActionAndToken>> lists) {\n        Type messageType = NotMsgType.class;\n\n        if (recipient == null\n                || lists == null\n                || lists.size() == 0\n                || !lists.containsKey(messageType)) {\n            return;\n        }\n\n        synchronized (lists) {\n            for (WeakActionAndToken item : lists.get(messageType)) {\n                WeakAction weakActionCasted = (WeakAction) item.getAction();\n\n                if (weakActionCasted != null\n                        && recipient == weakActionCasted.getTarget()\n                        && (action == null\n                        || action == weakActionCasted.getBindingAction())\n                        && (token == null\n                        || token.equals(item.getToken()))) {\n                    item.getAction().markForDeletion();\n                }\n            }\n        }\n    }\n\n    private static boolean classImplements(Type instanceType, Type interfaceType) {\n        if (interfaceType == null\n                || instanceType == null) {\n            return false;\n        }\n        Class[] interfaces = ((Class) instanceType).getInterfaces();\n        for (Class currentInterface : interfaces) {\n            if (currentInterface == interfaceType) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static void cleanupList(HashMap<Type, List<WeakActionAndToken>> lists) {\n        if (lists == null) {\n            return;\n        }\n        for (Iterator it = lists.entrySet().iterator(); it.hasNext(); ) {\n            Object key = it.next();\n            List<WeakActionAndToken> itemList = lists.get(key);\n            if (itemList != null) {\n                for (WeakActionAndToken item : itemList) {\n                    if (item.getAction() == null\n                            || !item.getAction().isLive()) {\n                        itemList.remove(item);\n                    }\n                }\n                if (itemList.size() == 0) {\n                    lists.remove(key);\n                }\n            }\n        }\n    }\n\n    private void sendToTargetOrType(Type messageTargetType, Object token) {\n        Class messageType = NotMsgType.class;\n        if (recipientsOfSubclassesAction != null) {\n            // Clone to protect from people registering in a \"receive message\" method\n            // Bug correction Messaging BL0008.002\n//            var listClone = recipientsOfSubclassesAction.Keys.Take(_recipientsOfSubclassesAction.Count()).ToList();\n            List<Type> listClone = new ArrayList<>();\n            listClone.addAll(recipientsOfSubclassesAction.keySet());\n            for (Type type : listClone) {\n                List<WeakActionAndToken> list = null;\n\n                if (messageType == type\n                        || ((Class) type).isAssignableFrom(messageType)\n                        || classImplements(messageType, type)) {\n                    list = recipientsOfSubclassesAction.get(type);\n                }\n\n                sendToList(list, messageTargetType, token);\n            }\n        }\n\n        if (recipientsStrictAction != null) {\n            if (recipientsStrictAction.containsKey(messageType)) {\n                List<WeakActionAndToken> list = recipientsStrictAction.get(messageType);\n                sendToList(list, messageTargetType, token);\n            }\n        }\n\n        cleanup();\n    }\n\n    private static void sendToList(\n            Collection<WeakActionAndToken> list,\n            Type messageTargetType,\n            Object token) {\n        if (list != null) {\n            // Clone to protect from people registering in a \"receive message\" method\n            // Bug correction Messaging BL0004.007\n            ArrayList<WeakActionAndToken> listClone = new ArrayList<>();\n            listClone.addAll(list);\n\n            for (WeakActionAndToken item : listClone) {\n                WeakAction executeAction = item.getAction();\n                if (executeAction != null\n                        && item.getAction().isLive()\n                        && item.getAction().getTarget() != null\n                        && (messageTargetType == null\n                        || item.getAction().getTarget().getClass() == messageTargetType\n                        || classImplements(item.getAction().getTarget().getClass(), messageTargetType))\n                        && ((item.getToken() == null && token == null)\n                        || item.getToken() != null && item.getToken().equals(token))) {\n                    executeAction.execute();\n                }\n            }\n        }\n    }\n\n    private <T> void sendToTargetOrType(T message, Type messageTargetType, Object token) {\n        Class messageType = message.getClass();\n\n\n        if (recipientsOfSubclassesAction != null) {\n            // Clone to protect from people registering in a \"receive message\" method\n            // Bug correction Messaging BL0008.002\n//            var listClone = recipientsOfSubclassesAction.Keys.Take(_recipientsOfSubclassesAction.Count()).ToList();\n            List<Type> listClone = new ArrayList<>();\n            listClone.addAll(recipientsOfSubclassesAction.keySet());\n            for (Type type : listClone) {\n                List<WeakActionAndToken> list = null;\n\n                if (messageType == type\n                        || ((Class) type).isAssignableFrom(messageType)\n                        || classImplements(messageType, type)) {\n                    list = recipientsOfSubclassesAction.get(type);\n                }\n\n                sendToList(message, list, messageTargetType, token);\n            }\n        }\n\n        if (recipientsStrictAction != null) {\n            if (recipientsStrictAction.containsKey(messageType)) {\n                List<WeakActionAndToken> list = recipientsStrictAction.get(messageType);\n                sendToList(message, list, messageTargetType, token);\n            }\n        }\n\n        cleanup();\n    }\n\n    private class WeakActionAndToken {\n        private WeakAction action;\n        private Object token;\n\n        public WeakActionAndToken(WeakAction action, Object token) {\n            this.action = action;\n            this.token = token;\n        }\n\n        public WeakAction getAction() {\n            return action;\n        }\n\n        public void setAction(WeakAction action) {\n            this.action = action;\n        }\n\n        public Object getToken() {\n            return token;\n        }\n\n        public void setToken(Object token) {\n            this.token = token;\n        }\n    }\n\n    public static class NotMsgType {\n\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/bus/RxBus.java",
    "content": "package me.goldze.mvvmhabit.bus;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableEmitter;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.subjects.PublishSubject;\nimport io.reactivex.subjects.Subject;\n\n\n/**\n * 只会把在订阅发生的时间点之后来自原始Observable的数据发射给观察者\n */\npublic class RxBus {\n    private static volatile RxBus mDefaultInstance;\n    private final Subject<Object> mBus;\n\n    private final Map<Class<?>, Object> mStickyEventMap;\n\n    public RxBus() {\n        mBus = PublishSubject.create().toSerialized();\n        mStickyEventMap = new ConcurrentHashMap<>();\n    }\n\n    public static RxBus getDefault() {\n        if (mDefaultInstance == null) {\n            synchronized (RxBus.class) {\n                if (mDefaultInstance == null) {\n                    mDefaultInstance = new RxBus();\n                }\n            }\n        }\n        return mDefaultInstance;\n    }\n\n    /**\n     * 发送事件\n     */\n    public void post(Object event) {\n        mBus.onNext(event);\n    }\n\n    /**\n     * 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者\n     */\n    public <T> Observable<T> toObservable(Class<T> eventType) {\n        return mBus.ofType(eventType);\n    }\n\n    /**\n     * 判断是否有订阅者\n     */\n    public boolean hasObservers() {\n        return mBus.hasObservers();\n    }\n\n    public void reset() {\n        mDefaultInstance = null;\n    }\n\n    /**\n     * Stciky 相关\n     */\n\n    /**\n     * 发送一个新Sticky事件\n     */\n    public void postSticky(Object event) {\n        synchronized (mStickyEventMap) {\n            mStickyEventMap.put(event.getClass(), event);\n        }\n        post(event);\n    }\n\n    /**\n     * 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者\n     */\n    public <T> Observable<T> toObservableSticky(final Class<T> eventType) {\n        synchronized (mStickyEventMap) {\n            Observable<T> observable = mBus.ofType(eventType);\n            final Object event = mStickyEventMap.get(eventType);\n\n            if (event != null) {\n                return Observable.merge(observable, Observable.create(new ObservableOnSubscribe<T>() {\n                    @Override\n                    public void subscribe(ObservableEmitter<T> emitter) throws Exception {\n                        emitter.onNext(eventType.cast(event));\n                    }\n                }));\n            } else {\n                return observable;\n            }\n        }\n    }\n\n    /**\n     * 根据eventType获取Sticky事件\n     */\n    public <T> T getStickyEvent(Class<T> eventType) {\n        synchronized (mStickyEventMap) {\n            return eventType.cast(mStickyEventMap.get(eventType));\n        }\n    }\n\n    /**\n     * 移除指定eventType的Sticky事件\n     */\n    public <T> T removeStickyEvent(Class<T> eventType) {\n        synchronized (mStickyEventMap) {\n            return eventType.cast(mStickyEventMap.remove(eventType));\n        }\n    }\n\n    /**\n     * 移除所有的Sticky事件\n     */\n    public void removeAllStickyEvents() {\n        synchronized (mStickyEventMap) {\n            mStickyEventMap.clear();\n        }\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/bus/RxBusSubscriber.java",
    "content": "package me.goldze.mvvmhabit.bus;\n\nimport io.reactivex.observers.DisposableObserver;\n\n/**\n * 为RxBus使用的Subscriber, 主要提供next事件的try,catch\n */\npublic abstract class RxBusSubscriber<T> extends DisposableObserver<T> {\n\n    @Override\n    public void onNext(T t) {\n        try {\n            onEvent(t);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    @Override\n    public void onComplete() {\n    }\n\n    @Override\n    public void onError(Throwable e) {\n        e.printStackTrace();\n    }\n\n    protected abstract void onEvent(T t);\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/bus/RxSubscriptions.java",
    "content": "package me.goldze.mvvmhabit.bus;\n\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\n\n/**\n * 管理 CompositeSubscription\n */\npublic class RxSubscriptions {\n    private static CompositeDisposable mSubscriptions = new CompositeDisposable ();\n\n    public static boolean isDisposed() {\n        return mSubscriptions.isDisposed();\n    }\n\n    public static void add(Disposable s) {\n        if (s != null) {\n            mSubscriptions.add(s);\n        }\n    }\n\n    public static void remove(Disposable s) {\n        if (s != null) {\n            mSubscriptions.remove(s);\n        }\n    }\n\n    public static void clear() {\n        mSubscriptions.clear();\n    }\n\n    public static void dispose() {\n        mSubscriptions.dispose();\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/bus/WeakAction.java",
    "content": "package me.goldze.mvvmhabit.bus;\n\nimport java.lang.ref.WeakReference;\n\nimport me.goldze.mvvmhabit.binding.command.BindingAction;\nimport me.goldze.mvvmhabit.binding.command.BindingConsumer;\n\n\n/**\n * About : kelin的WeakBindingAction\n */\npublic class WeakAction<T> {\n    private BindingAction action;\n    private BindingConsumer<T> consumer;\n    private boolean isLive;\n    private Object target;\n    private WeakReference reference;\n\n    public WeakAction(Object target, BindingAction action) {\n        reference = new WeakReference(target);\n        this.action = action;\n\n    }\n\n    public WeakAction(Object target, BindingConsumer<T> consumer) {\n        reference = new WeakReference(target);\n        this.consumer = consumer;\n    }\n\n    public void execute() {\n        if (action != null && isLive()) {\n            action.call();\n        }\n    }\n\n    public void execute(T parameter) {\n        if (consumer != null\n                && isLive()) {\n            consumer.call(parameter);\n        }\n    }\n\n    public void markForDeletion() {\n        reference.clear();\n        reference = null;\n        action = null;\n        consumer = null;\n    }\n\n    public BindingAction getBindingAction() {\n        return action;\n    }\n\n    public BindingConsumer getBindingConsumer() {\n        return consumer;\n    }\n\n    public boolean isLive() {\n        if (reference == null) {\n            return false;\n        }\n        if (reference.get() == null) {\n            return false;\n        }\n        return true;\n    }\n\n\n    public Object getTarget() {\n        if (reference != null) {\n            return reference.get();\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/bus/event/SingleLiveEvent.java",
    "content": "/*\n *  Copyright 2017 Google Inc.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage me.goldze.mvvmhabit.bus.event;\n\nimport android.util.Log;\n\nimport java.util.concurrent.atomic.AtomicBoolean;\n\nimport androidx.annotation.MainThread;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.MutableLiveData;\nimport androidx.lifecycle.Observer;\n\n/**\n * A lifecycle-aware observable that sends only new updates after subscription, used for events like\n * navigation and Snackbar messages.\n * <p>\n * This avoids a common problem with events: on configuration change (like rotation) an update\n * can be emitted if the observer is active. This LiveData only calls the observable if there's an\n * explicit call to setValue() or call().\n * <p>\n * Note that only one observer is going to be notified of changes.\n */\npublic class SingleLiveEvent<T> extends MutableLiveData<T> {\n    private static final String TAG = \"SingleLiveEvent\";\n\n    private final AtomicBoolean mPending = new AtomicBoolean(false);\n\n    @MainThread\n    public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer<? super T> observer) {\n\n        if (hasActiveObservers()) {\n            Log.w(TAG, \"Multiple observers registered but only one will be notified of changes.\");\n        }\n\n        // Observe the internal MutableLiveData\n        super.observe(owner, new Observer<T>() {\n            @Override\n            public void onChanged(@Nullable T t) {\n                if (mPending.compareAndSet(true, false)) {\n                    observer.onChanged(t);\n                }\n            }\n        });\n    }\n\n    @MainThread\n    public void setValue(@Nullable T t) {\n        mPending.set(true);\n        super.setValue(t);\n    }\n\n    /**\n     * Used for cases where T is Void, to make calls cleaner.\n     */\n    @MainThread\n    public void call() {\n        setValue(null);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/bus/event/SnackbarMessage.java",
    "content": "/*\n *  Copyright 2017 Google Inc.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n */\n\npackage me.goldze.mvvmhabit.bus.event;\n\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.Observer;\n\n/**\n * A SingleLiveEvent used for Snackbar messages. Like a {@link SingleLiveEvent} but also prevents\n * null messages and uses a custom observer.\n * <p>\n * Note that only one observer is going to be notified of changes.\n */\npublic class SnackbarMessage extends SingleLiveEvent<Integer> {\n\n    public void observe(LifecycleOwner owner, final SnackbarObserver observer) {\n        super.observe(owner, new Observer<Integer>() {\n            @Override\n            public void onChanged(@Nullable Integer t) {\n                if (t == null) {\n                    return;\n                }\n                observer.onNewMessage(t);\n            }\n        });\n    }\n\n    public interface SnackbarObserver {\n        /**\n         * Called when there is a new message to be shown.\n         * @param snackbarMessageResourceId The new message, non-null.\n         */\n        void onNewMessage(@StringRes int snackbarMessageResourceId);\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/crash/CaocConfig.java",
    "content": "/*\n * Copyright 2014-2017 Eduard Ereza Martínez\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * You may obtain a copy of the License at\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.goldze.mvvmhabit.crash;\n\nimport android.app.Activity;\n\nimport java.io.Serializable;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.reflect.Modifier;\n\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n\npublic class CaocConfig implements Serializable {\n\n    @IntDef({BACKGROUND_MODE_CRASH, BACKGROUND_MODE_SHOW_CUSTOM, BACKGROUND_MODE_SILENT})\n    @Retention(RetentionPolicy.SOURCE)\n    private @interface BackgroundMode {\n        //I hate empty blocks\n    }\n\n    public static final int BACKGROUND_MODE_SILENT = 0;\n    public static final int BACKGROUND_MODE_SHOW_CUSTOM = 1;\n    public static final int BACKGROUND_MODE_CRASH = 2;\n\n    private int backgroundMode = BACKGROUND_MODE_SHOW_CUSTOM;\n    private boolean enabled = true;\n    private boolean showErrorDetails = true;\n    private boolean showRestartButton = true;\n    private boolean trackActivities = false;\n    private int minTimeBetweenCrashesMs = 3000;\n    private Integer errorDrawable = null;\n    private Class<? extends Activity> errorActivityClass = null;\n    private Class<? extends Activity> restartActivityClass = null;\n    private CustomActivityOnCrash.EventListener eventListener = null;\n\n    @BackgroundMode\n    public int getBackgroundMode() {\n        return backgroundMode;\n    }\n\n    public void setBackgroundMode(@BackgroundMode int backgroundMode) {\n        this.backgroundMode = backgroundMode;\n    }\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    public void setEnabled(boolean enabled) {\n        this.enabled = enabled;\n    }\n\n    public boolean isShowErrorDetails() {\n        return showErrorDetails;\n    }\n\n    public void setShowErrorDetails(boolean showErrorDetails) {\n        this.showErrorDetails = showErrorDetails;\n    }\n\n    public boolean isShowRestartButton() {\n        return showRestartButton;\n    }\n\n    public void setShowRestartButton(boolean showRestartButton) {\n        this.showRestartButton = showRestartButton;\n    }\n\n    public boolean isTrackActivities() {\n        return trackActivities;\n    }\n\n    public void setTrackActivities(boolean trackActivities) {\n        this.trackActivities = trackActivities;\n    }\n\n    public int getMinTimeBetweenCrashesMs() {\n        return minTimeBetweenCrashesMs;\n    }\n\n    public void setMinTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {\n        this.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;\n    }\n\n    @Nullable\n    @DrawableRes\n    public Integer getErrorDrawable() {\n        return errorDrawable;\n    }\n\n    public void setErrorDrawable(@Nullable @DrawableRes Integer errorDrawable) {\n        this.errorDrawable = errorDrawable;\n    }\n\n    @Nullable\n    public Class<? extends Activity> getErrorActivityClass() {\n        return errorActivityClass;\n    }\n\n    public void setErrorActivityClass(@Nullable Class<? extends Activity> errorActivityClass) {\n        this.errorActivityClass = errorActivityClass;\n    }\n\n    @Nullable\n    public Class<? extends Activity> getRestartActivityClass() {\n        return restartActivityClass;\n    }\n\n    public void setRestartActivityClass(@Nullable Class<? extends Activity> restartActivityClass) {\n        this.restartActivityClass = restartActivityClass;\n    }\n\n    @Nullable\n    public CustomActivityOnCrash.EventListener getEventListener() {\n        return eventListener;\n    }\n\n    public void setEventListener(@Nullable CustomActivityOnCrash.EventListener eventListener) {\n        this.eventListener = eventListener;\n    }\n\n    public static class Builder {\n        private CaocConfig config;\n\n        @NonNull\n        public static Builder create() {\n            Builder builder = new Builder();\n            CaocConfig currentConfig = CustomActivityOnCrash.getConfig();\n\n            CaocConfig config = new CaocConfig();\n            config.backgroundMode = currentConfig.backgroundMode;\n            config.enabled = currentConfig.enabled;\n            config.showErrorDetails = currentConfig.showErrorDetails;\n            config.showRestartButton = currentConfig.showRestartButton;\n            config.trackActivities = currentConfig.trackActivities;\n            config.minTimeBetweenCrashesMs = currentConfig.minTimeBetweenCrashesMs;\n            config.errorDrawable = currentConfig.errorDrawable;\n            config.errorActivityClass = currentConfig.errorActivityClass;\n            config.restartActivityClass = currentConfig.restartActivityClass;\n            config.eventListener = currentConfig.eventListener;\n\n            builder.config = config;\n\n            return builder;\n        }\n\n        /**\n         * Defines if the error activity must be launched when the app is on background.\n         * BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM: launch the error activity when the app is in background,\n         * BackgroundMode.BACKGROUND_MODE_CRASH: launch the default system error when the app is in background,\n         * BackgroundMode.BACKGROUND_MODE_SILENT: crash silently when the app is in background,\n         * The default is BackgroundMode.BACKGROUND_MODE_SHOW_CUSTOM (the app will be brought to front when a crash occurs).\n         */\n        @NonNull\n        public Builder backgroundMode(@BackgroundMode int backgroundMode) {\n            config.backgroundMode = backgroundMode;\n            return this;\n        }\n\n        /**\n         * Defines if CustomActivityOnCrash crash interception mechanism is enabled.\n         * Set it to true if you want CustomActivityOnCrash to intercept crashes,\n         * false if you want them to be treated as if the library was not installed.\n         * The default is true.\n         */\n        @NonNull\n        public Builder enabled(boolean enabled) {\n            config.enabled = enabled;\n            return this;\n        }\n\n        /**\n         * Defines if the error activity must shown the error details button.\n         * Set it to true if you want to show the full stack trace and device info,\n         * false if you want it to be hidden.\n         * The default is true.\n         */\n        @NonNull\n        public Builder showErrorDetails(boolean showErrorDetails) {\n            config.showErrorDetails = showErrorDetails;\n            return this;\n        }\n\n        /**\n         * Defines if the error activity should show a restart button.\n         * Set it to true if you want to show a restart button,\n         * false if you want to show a close button.\n         * Note that even if restart is enabled but you app does not have any launcher activities,\n         * a close button will still be used by the default error activity.\n         * The default is true.\n         */\n        @NonNull\n        public Builder showRestartButton(boolean showRestartButton) {\n            config.showRestartButton = showRestartButton;\n            return this;\n        }\n\n        /**\n         * Defines if the activities visited by the user should be tracked\n         * so they are reported when an error occurs.\n         * The default is false.\n         */\n        @NonNull\n        public Builder trackActivities(boolean trackActivities) {\n            config.trackActivities = trackActivities;\n            return this;\n        }\n\n        /**\n         * Defines the time that must pass between app crashes to determine that we are not\n         * in a crash loop. If a crash has occurred less that this time ago,\n         * the error activity will not be launched and the system crash screen will be invoked.\n         * The default is 3000.\n         */\n        @NonNull\n        public Builder minTimeBetweenCrashesMs(int minTimeBetweenCrashesMs) {\n            config.minTimeBetweenCrashesMs = minTimeBetweenCrashesMs;\n            return this;\n        }\n\n        /**\n         * Defines which drawable to use in the default error activity image.\n         * Set this if you want to use an image other than the default one.\n         * The default is R.drawable.customactivityoncrash_error_image (a cute upside-down bug).\n         */\n        @NonNull\n        public Builder errorDrawable(@Nullable @DrawableRes Integer errorDrawable) {\n            config.errorDrawable = errorDrawable;\n            return this;\n        }\n\n        /**\n         * Sets the error activity class to launch when a crash occurs.\n         * If null, the default error activity will be used.\n         */\n        @NonNull\n        public Builder errorActivity(@Nullable Class<? extends Activity> errorActivityClass) {\n            config.errorActivityClass = errorActivityClass;\n            return this;\n        }\n\n        /**\n         * Sets the main activity class that the error activity must launch when a crash occurs.\n         * If not set or set to null, the default launch activity will be used.\n         * If your app has no launch activities and this is not set, the default error activity will close instead.\n         */\n        @NonNull\n        public Builder restartActivity(@Nullable Class<? extends Activity> restartActivityClass) {\n            config.restartActivityClass = restartActivityClass;\n            return this;\n        }\n\n        /**\n         * Sets an event listener to be called when events occur, so they can be reported\n         * by the app as, for example, Google Analytics events.\n         * If not set or set to null, no events will be reported.\n         *\n         * @param eventListener The event listener.\n         * @throws IllegalArgumentException if the eventListener is an inner or anonymous class\n         */\n        @NonNull\n        public Builder eventListener(@Nullable CustomActivityOnCrash.EventListener eventListener) {\n            if (eventListener != null && eventListener.getClass().getEnclosingClass() != null && !Modifier.isStatic(eventListener.getClass().getModifiers())) {\n                throw new IllegalArgumentException(\"The event listener cannot be an inner or anonymous class, because it will need to be serialized. Change it to a class of its own, or make it a static inner class.\");\n            } else {\n                config.eventListener = eventListener;\n            }\n            return this;\n        }\n\n        @NonNull\n        public CaocConfig get() {\n            return config;\n        }\n\n        public void apply() {\n            CustomActivityOnCrash.setConfig(config);\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/crash/CaocInitProvider.java",
    "content": "/*\n * Copyright 2014-2017 Eduard Ereza Martínez\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * You may obtain a copy of the License at\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.goldze.mvvmhabit.crash;\n\nimport android.content.ContentProvider;\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\n\npublic class CaocInitProvider extends ContentProvider {\n\n    public boolean onCreate() {\n        CustomActivityOnCrash.install(getContext());\n        return false;\n    }\n\n    @Nullable\n    @Override\n    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public String getType(@NonNull Uri uri) {\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {\n        return null;\n    }\n\n    @Override\n    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {\n        return 0;\n    }\n\n    @Override\n    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {\n        return 0;\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/crash/CustomActivityOnCrash.java",
    "content": "/*\n * Copyright 2014-2017 Eduard Ereza Martínez\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * You may obtain a copy of the License at\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.goldze.mvvmhabit.crash;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.app.Application;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.ApplicationInfo;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager;\nimport android.content.pm.ResolveInfo;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.util.Log;\n\nimport java.io.PrintWriter;\nimport java.io.Serializable;\nimport java.io.StringWriter;\nimport java.lang.ref.WeakReference;\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.ArrayDeque;\nimport java.util.Date;\nimport java.util.Deque;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RestrictTo;\n\n\npublic final class CustomActivityOnCrash {\n\n    private final static String TAG = \"CustomActivityOnCrash\";\n\n    //Extras passed to the error activity\n    private static final String EXTRA_CONFIG = \"cat.ereza.customactivityoncrash.EXTRA_CONFIG\";\n    private static final String EXTRA_STACK_TRACE = \"cat.ereza.customactivityoncrash.EXTRA_STACK_TRACE\";\n    private static final String EXTRA_ACTIVITY_LOG = \"cat.ereza.customactivityoncrash.EXTRA_ACTIVITY_LOG\";\n\n    //General constants\n    private static final String INTENT_ACTION_ERROR_ACTIVITY = \"cat.ereza.customactivityoncrash.ERROR\";\n    private static final String INTENT_ACTION_RESTART_ACTIVITY = \"cat.ereza.customactivityoncrash.RESTART\";\n    private static final String CAOC_HANDLER_PACKAGE_NAME = \"cat.ereza.customactivityoncrash\";\n    private static final String DEFAULT_HANDLER_PACKAGE_NAME = \"com.android.internal.os\";\n    private static final int MAX_STACK_TRACE_SIZE = 131071; //128 KB - 1\n    private static final int MAX_ACTIVITIES_IN_LOG = 50;\n\n    //Shared preferences\n    private static final String SHARED_PREFERENCES_FILE = \"custom_activity_on_crash\";\n    private static final String SHARED_PREFERENCES_FIELD_TIMESTAMP = \"last_crash_timestamp\";\n\n    //Internal variables\n    @SuppressLint(\"StaticFieldLeak\") //This is an application-wide component\n    private static Application application;\n    private static CaocConfig config = new CaocConfig();\n    private static Deque<String> activityLog = new ArrayDeque<>(MAX_ACTIVITIES_IN_LOG);\n    private static WeakReference<Activity> lastActivityCreated = new WeakReference<>(null);\n    private static boolean isInBackground = true;\n\n\n    /**\n     * Installs CustomActivityOnCrash on the application using the default error activity.\n     *\n     * @param context Context to use for obtaining the ApplicationContext. Must not be null.\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    public static void install(@Nullable final Context context) {\n        try {\n            if (context == null) {\n                Log.e(TAG, \"Install failed: context is null!\");\n            } else {\n                //INSTALL!\n                final Thread.UncaughtExceptionHandler oldHandler = Thread.getDefaultUncaughtExceptionHandler();\n\n                if (oldHandler != null && oldHandler.getClass().getName().startsWith(CAOC_HANDLER_PACKAGE_NAME)) {\n                    Log.e(TAG, \"CustomActivityOnCrash was already installed, doing nothing!\");\n                } else {\n                    if (oldHandler != null && !oldHandler.getClass().getName().startsWith(DEFAULT_HANDLER_PACKAGE_NAME)) {\n                        Log.e(TAG, \"IMPORTANT WARNING! You already have an UncaughtExceptionHandler, are you sure this is correct? If you use a custom UncaughtExceptionHandler, you must initialize it AFTER CustomActivityOnCrash! Installing anyway, but your original handler will not be called.\");\n                    }\n\n                    application = (Application) context.getApplicationContext();\n\n                    //We define a default exception handler that does what we want so it can be called from Crashlytics/ACRA\n                    Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {\n                        @Override\n                        public void uncaughtException(Thread thread, final Throwable throwable) {\n                            if (config.isEnabled()) {\n                                Log.e(TAG, \"App has crashed, executing CustomActivityOnCrash's UncaughtExceptionHandler\", throwable);\n\n                                if (hasCrashedInTheLastSeconds(application)) {\n                                    Log.e(TAG, \"App already crashed recently, not starting custom error activity because we could enter a restart loop. Are you sure that your app does not crash directly on init?\", throwable);\n                                    if (oldHandler != null) {\n                                        oldHandler.uncaughtException(thread, throwable);\n                                        return;\n                                    }\n                                } else {\n                                    setLastCrashTimestamp(application, new Date().getTime());\n\n                                    Class<? extends Activity> errorActivityClass = config.getErrorActivityClass();\n\n                                    if (errorActivityClass == null) {\n                                        errorActivityClass = guessErrorActivityClass(application);\n                                    }\n\n                                    if (isStackTraceLikelyConflictive(throwable, errorActivityClass)) {\n                                        Log.e(TAG, \"Your application class or your error activity have crashed, the custom activity will not be launched!\");\n                                        if (oldHandler != null) {\n                                            oldHandler.uncaughtException(thread, throwable);\n                                            return;\n                                        }\n                                    } else if (config.getBackgroundMode() == CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM || !isInBackground) {\n\n                                        final Intent intent = new Intent(application, errorActivityClass);\n                                        StringWriter sw = new StringWriter();\n                                        PrintWriter pw = new PrintWriter(sw);\n                                        throwable.printStackTrace(pw);\n                                        String stackTraceString = sw.toString();\n\n                                        //Reduce data to 128KB so we don't get a TransactionTooLargeException when sending the intent.\n                                        //The limit is 1MB on Android but some devices seem to have it lower.\n                                        //See: http://developer.android.com/reference/android/os/TransactionTooLargeException.html\n                                        //And: http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception#comment46697371_12809171\n                                        if (stackTraceString.length() > MAX_STACK_TRACE_SIZE) {\n                                            String disclaimer = \" [stack trace too large]\";\n                                            stackTraceString = stackTraceString.substring(0, MAX_STACK_TRACE_SIZE - disclaimer.length()) + disclaimer;\n                                        }\n                                        intent.putExtra(EXTRA_STACK_TRACE, stackTraceString);\n\n                                        if (config.isTrackActivities()) {\n                                            String activityLogString = \"\";\n                                            while (!activityLog.isEmpty()) {\n                                                activityLogString += activityLog.poll();\n                                            }\n                                            intent.putExtra(EXTRA_ACTIVITY_LOG, activityLogString);\n                                        }\n\n                                        if (config.isShowRestartButton() && config.getRestartActivityClass() == null) {\n                                            //We can set the restartActivityClass because the app will terminate right now,\n                                            //and when relaunched, will be null again by default.\n                                            config.setRestartActivityClass(guessRestartActivityClass(application));\n                                        }\n\n                                        intent.putExtra(EXTRA_CONFIG, config);\n                                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);\n                                        if (config.getEventListener() != null) {\n                                            config.getEventListener().onLaunchErrorActivity();\n                                        }\n                                        application.startActivity(intent);\n                                    } else if (config.getBackgroundMode() == CaocConfig.BACKGROUND_MODE_CRASH) {\n                                        if (oldHandler != null) {\n                                            oldHandler.uncaughtException(thread, throwable);\n                                            return;\n                                        }\n                                        //If it is null (should not be), we let it continue and kill the process or it will be stuck\n                                    }\n                                    //Else (BACKGROUND_MODE_SILENT): do nothing and let the following code kill the process\n                                }\n                                final Activity lastActivity = lastActivityCreated.get();\n                                if (lastActivity != null) {\n                                    //We finish the activity, this solves a bug which causes infinite recursion.\n                                    //See: https://github.com/ACRA/acra/issues/42\n                                    lastActivity.finish();\n                                    lastActivityCreated.clear();\n                                }\n                                killCurrentProcess();\n                            } else if (oldHandler != null) {\n                                oldHandler.uncaughtException(thread, throwable);\n                            }\n                        }\n                    });\n                    application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {\n                        int currentlyStartedActivities = 0;\n                        DateFormat dateFormat = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\", Locale.US);\n\n                        @Override\n                        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {\n                            if (activity.getClass() != config.getErrorActivityClass()) {\n                                // Copied from ACRA:\n                                // Ignore activityClass because we want the last\n                                // application Activity that was started so that we can\n                                // explicitly kill it off.\n                                lastActivityCreated = new WeakReference<>(activity);\n                            }\n                            if (config.isTrackActivities()) {\n                                activityLog.add(dateFormat.format(new Date()) + \": \" + activity.getClass().getSimpleName() + \" created\\n\");\n                            }\n                        }\n\n                        @Override\n                        public void onActivityStarted(Activity activity) {\n                            currentlyStartedActivities++;\n                            isInBackground = (currentlyStartedActivities == 0);\n                            //Do nothing\n                        }\n\n                        @Override\n                        public void onActivityResumed(Activity activity) {\n                            if (config.isTrackActivities()) {\n                                activityLog.add(dateFormat.format(new Date()) + \": \" + activity.getClass().getSimpleName() + \" resumed\\n\");\n                            }\n                        }\n\n                        @Override\n                        public void onActivityPaused(Activity activity) {\n                            if (config.isTrackActivities()) {\n                                activityLog.add(dateFormat.format(new Date()) + \": \" + activity.getClass().getSimpleName() + \" paused\\n\");\n                            }\n                        }\n\n                        @Override\n                        public void onActivityStopped(Activity activity) {\n                            //Do nothing\n                            currentlyStartedActivities--;\n                            isInBackground = (currentlyStartedActivities == 0);\n                        }\n\n                        @Override\n                        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {\n                            //Do nothing\n                        }\n\n                        @Override\n                        public void onActivityDestroyed(Activity activity) {\n                            if (config.isTrackActivities()) {\n                                activityLog.add(dateFormat.format(new Date()) + \": \" + activity.getClass().getSimpleName() + \" destroyed\\n\");\n                            }\n                        }\n                    });\n                }\n\n                Log.i(TAG, \"CustomActivityOnCrash has been installed.\");\n            }\n        } catch (Throwable t) {\n            Log.e(TAG, \"An unknown error occurred while installing CustomActivityOnCrash, it may not have been properly initialized. Please report this as a bug if needed.\", t);\n        }\n    }\n\n    /**\n     * Given an Intent, returns the stack trace extra from it.\n     *\n     * @param intent The Intent. Must not be null.\n     * @return The stacktrace, or null if not provided.\n     */\n    @NonNull\n    public static String getStackTraceFromIntent(@NonNull Intent intent) {\n        return intent.getStringExtra(CustomActivityOnCrash.EXTRA_STACK_TRACE);\n    }\n\n    /**\n     * Given an Intent, returns the config extra from it.\n     *\n     * @param intent The Intent. Must not be null.\n     * @return The config, or null if not provided.\n     */\n    @NonNull\n    public static CaocConfig getConfigFromIntent(@NonNull Intent intent) {\n        return (CaocConfig) intent.getSerializableExtra(CustomActivityOnCrash.EXTRA_CONFIG);\n    }\n\n    /**\n     * Given an Intent, returns the activity log extra from it.\n     *\n     * @param intent The Intent. Must not be null.\n     * @return The activity log, or null if not provided.\n     */\n    @Nullable\n    public static String getActivityLogFromIntent(@NonNull Intent intent) {\n        return intent.getStringExtra(CustomActivityOnCrash.EXTRA_ACTIVITY_LOG);\n    }\n\n    /**\n     * Given an Intent, returns several error details including the stack trace extra from the intent.\n     *\n     * @param context A valid context. Must not be null.\n     * @param intent  The Intent. Must not be null.\n     * @return The full error details.\n     */\n    @NonNull\n    public static String getAllErrorDetailsFromIntent(@NonNull Context context, @NonNull Intent intent) {\n        //I don't think that this needs localization because it's a development string...\n\n        Date currentDate = new Date();\n        DateFormat dateFormat = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\", Locale.US);\n\n        //Get build date\n        String buildDateAsString = getBuildDateAsString(context, dateFormat);\n\n        //Get app version\n        String versionName = getVersionName(context);\n\n        String errorDetails = \"\";\n\n        errorDetails += \"Build version: \" + versionName + \" \\n\";\n        if (buildDateAsString != null) {\n            errorDetails += \"Build date: \" + buildDateAsString + \" \\n\";\n        }\n        errorDetails += \"Current date: \" + dateFormat.format(currentDate) + \" \\n\";\n        //Added a space between line feeds to fix #18.\n        //Ideally, we should not use this method at all... It is only formatted this way because of coupling with the default error activity.\n        //We should move it to a method that returns a bean, and let anyone format it as they wish.\n        errorDetails += \"Device: \" + getDeviceModelName() + \" \\n \\n\";\n        errorDetails += \"Stack trace:  \\n\";\n        errorDetails += getStackTraceFromIntent(intent);\n\n        String activityLog = getActivityLogFromIntent(intent);\n\n        if (activityLog != null) {\n            errorDetails += \"\\nUser actions: \\n\";\n            errorDetails += activityLog;\n        }\n        return errorDetails;\n    }\n\n    /**\n     * Given an Intent, restarts the app and launches a startActivity to that intent.\n     * The flags NEW_TASK and CLEAR_TASK are set if the Intent does not have them, to ensure\n     * the app stack is fully cleared.\n     * If an event listener is provided, the restart app event is invoked.\n     * Must only be used from your error activity.\n     *\n     * @param activity The current error activity. Must not be null.\n     * @param intent   The Intent. Must not be null.\n     * @param config   The config object as obtained by calling getConfigFromIntent.\n     */\n    public static void restartApplicationWithIntent(@NonNull Activity activity, @NonNull Intent intent, @NonNull CaocConfig config) {\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);\n        if (intent.getComponent() != null) {\n            //If the class name has been set, we force it to simulate a Launcher launch.\n            //If we don't do this, if you restart from the error activity, then press home,\n            //and then launch the activity from the launcher, the main activity appears twice on the backstack.\n            //This will most likely not have any detrimental effect because if you set the Intent component,\n            //if will always be launched regardless of the actions specified here.\n            intent.setAction(Intent.ACTION_MAIN);\n            intent.addCategory(Intent.CATEGORY_LAUNCHER);\n        }\n        if (config.getEventListener() != null) {\n            config.getEventListener().onRestartAppFromErrorActivity();\n        }\n        activity.finish();\n        activity.startActivity(intent);\n        killCurrentProcess();\n    }\n\n    public static void restartApplication(@NonNull Activity activity, @NonNull CaocConfig config) {\n        Intent intent = new Intent(activity, config.getRestartActivityClass());\n        restartApplicationWithIntent(activity, intent, config);\n    }\n\n    /**\n     * Closes the app.\n     * If an event listener is provided, the close app event is invoked.\n     * Must only be used from your error activity.\n     *\n     * @param activity The current error activity. Must not be null.\n     * @param config   The config object as obtained by calling getConfigFromIntent.\n     */\n    public static void closeApplication(@NonNull Activity activity, @NonNull CaocConfig config) {\n        if (config.getEventListener() != null) {\n            config.getEventListener().onCloseAppFromErrorActivity();\n        }\n        activity.finish();\n        killCurrentProcess();\n    }\n\n    /// INTERNAL METHODS NOT TO BE USED BY THIRD PARTIES\n\n    /**\n     * INTERNAL method that returns the current configuration of the library.\n     * If you want to check the config, use CaocConfig.Builder.get();\n     *\n     * @return the current configuration\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    @NonNull\n    public static CaocConfig getConfig() {\n        return config;\n    }\n\n    /**\n     * INTERNAL method that sets the configuration of the library.\n     * You must not use this, use CaocConfig.Builder.apply()\n     *\n     * @param config the configuration to use\n     */\n    @RestrictTo(RestrictTo.Scope.LIBRARY)\n    public static void setConfig(@NonNull CaocConfig config) {\n        CustomActivityOnCrash.config = config;\n    }\n\n    /**\n     * INTERNAL method that checks if the stack trace that just crashed is conflictive. This is true in the following scenarios:\n     * - The application has crashed while initializing (handleBindApplication is in the stack)\n     * - The error activity has crashed (activityClass is in the stack)\n     *\n     * @param throwable     The throwable from which the stack trace will be checked\n     * @param activityClass The activity class to launch when the app crashes\n     * @return true if this stack trace is conflictive and the activity must not be launched, false otherwise\n     */\n    private static boolean isStackTraceLikelyConflictive(@NonNull Throwable throwable, @NonNull Class<? extends Activity> activityClass) {\n        do {\n            StackTraceElement[] stackTrace = throwable.getStackTrace();\n            for (StackTraceElement element : stackTrace) {\n                if ((element.getClassName().equals(\"android.app.ActivityThread\") && element.getMethodName().equals(\"handleBindApplication\")) || element.getClassName().equals(activityClass.getName())) {\n                    return true;\n                }\n            }\n        } while ((throwable = throwable.getCause()) != null);\n        return false;\n    }\n\n    /**\n     * INTERNAL method that returns the build date of the current APK as a string, or null if unable to determine it.\n     *\n     * @param context    A valid context. Must not be null.\n     * @param dateFormat DateFormat to use to convert from Date to String\n     * @return The formatted date, or \"Unknown\" if unable to determine it.\n     */\n    @Nullable\n    private static String getBuildDateAsString(@NonNull Context context, @NonNull DateFormat dateFormat) {\n        long buildDate;\n        try {\n            ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);\n            ZipFile zf = new ZipFile(ai.sourceDir);\n\n            //If this failed, try with the old zip method\n            ZipEntry ze = zf.getEntry(\"classes.dex\");\n            buildDate = ze.getTime();\n\n\n            zf.close();\n        } catch (Exception e) {\n            buildDate = 0;\n        }\n\n        if (buildDate > 312764400000L) {\n            return dateFormat.format(new Date(buildDate));\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * INTERNAL method that returns the version name of the current app, or null if unable to determine it.\n     *\n     * @param context A valid context. Must not be null.\n     * @return The version name, or \"Unknown if unable to determine it.\n     */\n    @NonNull\n    private static String getVersionName(Context context) {\n        try {\n            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);\n            return packageInfo.versionName;\n        } catch (Exception e) {\n            return \"Unknown\";\n        }\n    }\n\n    /**\n     * INTERNAL method that returns the device model name with correct capitalization.\n     * Taken from: http://stackoverflow.com/a/12707479/1254846\n     *\n     * @return The device model name (i.e., \"LGE Nexus 5\")\n     */\n    @NonNull\n    private static String getDeviceModelName() {\n        String manufacturer = Build.MANUFACTURER;\n        String model = Build.MODEL;\n        if (model.startsWith(manufacturer)) {\n            return capitalize(model);\n        } else {\n            return capitalize(manufacturer) + \" \" + model;\n        }\n    }\n\n    /**\n     * INTERNAL method that capitalizes the first character of a string\n     *\n     * @param s The string to capitalize\n     * @return The capitalized string\n     */\n    @NonNull\n    private static String capitalize(@Nullable String s) {\n        if (s == null || s.length() == 0) {\n            return \"\";\n        }\n        char first = s.charAt(0);\n        if (Character.isUpperCase(first)) {\n            return s;\n        } else {\n            return Character.toUpperCase(first) + s.substring(1);\n        }\n    }\n\n    /**\n     * INTERNAL method used to guess which activity must be called from the error activity to restart the app.\n     * It will first get activities from the AndroidManifest with intent filter <action android:name=\"cat.ereza.customactivityoncrash.RESTART\" />,\n     * if it cannot find them, then it will get the default launcher.\n     * If there is no default launcher, this returns null.\n     *\n     * @param context A valid context. Must not be null.\n     * @return The guessed restart activity class, or null if no suitable one is found\n     */\n    @Nullable\n    private static Class<? extends Activity> guessRestartActivityClass(@NonNull Context context) {\n        Class<? extends Activity> resolvedActivityClass;\n\n        //If action is defined, use that\n        resolvedActivityClass = getRestartActivityClassWithIntentFilter(context);\n\n        //Else, get the default launcher activity\n        if (resolvedActivityClass == null) {\n            resolvedActivityClass = getLauncherActivity(context);\n        }\n\n        return resolvedActivityClass;\n    }\n\n    /**\n     * INTERNAL method used to get the first activity with an intent-filter <action android:name=\"cat.ereza.customactivityoncrash.RESTART\" />,\n     * If there is no activity with that intent filter, this returns null.\n     *\n     * @param context A valid context. Must not be null.\n     * @return A valid activity class, or null if no suitable one is found\n     */\n    @SuppressWarnings(\"unchecked\")\n    @Nullable\n    private static Class<? extends Activity> getRestartActivityClassWithIntentFilter(@NonNull Context context) {\n        Intent searchedIntent = new Intent().setAction(INTENT_ACTION_RESTART_ACTIVITY).setPackage(context.getPackageName());\n        List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,\n                PackageManager.GET_RESOLVED_FILTER);\n\n        if (resolveInfos != null && resolveInfos.size() > 0) {\n            ResolveInfo resolveInfo = resolveInfos.get(0);\n            try {\n                return (Class<? extends Activity>) Class.forName(resolveInfo.activityInfo.name);\n            } catch (ClassNotFoundException e) {\n                //Should not happen, print it to the log!\n                Log.e(TAG, \"Failed when resolving the restart activity class via intent filter, stack trace follows!\", e);\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * INTERNAL method used to get the default launcher activity for the app.\n     * If there is no launchable activity, this returns null.\n     *\n     * @param context A valid context. Must not be null.\n     * @return A valid activity class, or null if no suitable one is found\n     */\n    @SuppressWarnings(\"unchecked\")\n    @Nullable\n    private static Class<? extends Activity> getLauncherActivity(@NonNull Context context) {\n        Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());\n        if (intent != null) {\n            try {\n                return (Class<? extends Activity>) Class.forName(intent.getComponent().getClassName());\n            } catch (ClassNotFoundException e) {\n                //Should not happen, print it to the log!\n                Log.e(TAG, \"Failed when resolving the restart activity class via getLaunchIntentForPackage, stack trace follows!\", e);\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * INTERNAL method used to guess which error activity must be called when the app crashes.\n     * It will first get activities from the AndroidManifest with intent filter <action android:name=\"cat.ereza.customactivityoncrash.ERROR\" />,\n     * if it cannot find them, then it will use the default error activity.\n     *\n     * @param context A valid context. Must not be null.\n     * @return The guessed error activity class, or the default error activity if not found\n     */\n    @NonNull\n    private static Class<? extends Activity> guessErrorActivityClass(@NonNull Context context) {\n        Class<? extends Activity> resolvedActivityClass;\n\n        //If action is defined, use that\n        resolvedActivityClass = getErrorActivityClassWithIntentFilter(context);\n\n        //Else, get the default error activity\n        if (resolvedActivityClass == null) {\n            resolvedActivityClass = DefaultErrorActivity.class;\n        }\n\n        return resolvedActivityClass;\n    }\n\n    /**\n     * INTERNAL method used to get the first activity with an intent-filter <action android:name=\"cat.ereza.customactivityoncrash.ERROR\" />,\n     * If there is no activity with that intent filter, this returns null.\n     *\n     * @param context A valid context. Must not be null.\n     * @return A valid activity class, or null if no suitable one is found\n     */\n    @SuppressWarnings(\"unchecked\")\n    @Nullable\n    private static Class<? extends Activity> getErrorActivityClassWithIntentFilter(@NonNull Context context) {\n        Intent searchedIntent = new Intent().setAction(INTENT_ACTION_ERROR_ACTIVITY).setPackage(context.getPackageName());\n        List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(searchedIntent,\n                PackageManager.GET_RESOLVED_FILTER);\n\n        if (resolveInfos != null && resolveInfos.size() > 0) {\n            ResolveInfo resolveInfo = resolveInfos.get(0);\n            try {\n                return (Class<? extends Activity>) Class.forName(resolveInfo.activityInfo.name);\n            } catch (ClassNotFoundException e) {\n                //Should not happen, print it to the log!\n                Log.e(TAG, \"Failed when resolving the error activity class via intent filter, stack trace follows!\", e);\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * INTERNAL method that kills the current process.\n     * It is used after restarting or killing the app.\n     */\n    private static void killCurrentProcess() {\n        android.os.Process.killProcess(android.os.Process.myPid());\n        System.exit(10);\n    }\n\n    /**\n     * INTERNAL method that stores the last crash timestamp\n     *\n     * @param timestamp The current timestamp.\n     */\n    @SuppressLint(\"ApplySharedPref\") //This must be done immediately since we are killing the app\n    private static void setLastCrashTimestamp(@NonNull Context context, long timestamp) {\n        context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).edit().putLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, timestamp).commit();\n    }\n\n    /**\n     * INTERNAL method that gets the last crash timestamp\n     *\n     * @return The last crash timestamp, or -1 if not set.\n     */\n    private static long getLastCrashTimestamp(@NonNull Context context) {\n        return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1);\n    }\n\n    /**\n     * INTERNAL method that tells if the app has crashed in the last seconds.\n     * This is used to avoid restart loops.\n     *\n     * @return true if the app has crashed in the last seconds, false otherwise.\n     */\n    private static boolean hasCrashedInTheLastSeconds(@NonNull Context context) {\n        long lastTimestamp = getLastCrashTimestamp(context);\n        long currentTimestamp = new Date().getTime();\n\n        return (lastTimestamp <= currentTimestamp && currentTimestamp - lastTimestamp < config.getMinTimeBetweenCrashesMs());\n    }\n\n    /**\n     * Interface to be called when events occur, so they can be reported\n     * by the app as, for example, Google Analytics events.\n     */\n    public interface EventListener extends Serializable {\n        void onLaunchErrorActivity();\n\n        void onRestartAppFromErrorActivity();\n\n        void onCloseAppFromErrorActivity();\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/crash/DefaultErrorActivity.java",
    "content": "/*\n * Copyright 2014-2017 Eduard Ereza Martínez\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n *\n * You may obtain a copy of the License at\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.goldze.mvvmhabit.crash;\n\nimport android.annotation.SuppressLint;\nimport android.app.AlertDialog;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.DialogInterface;\nimport android.content.res.TypedArray;\nimport android.os.Bundle;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.ImageView;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.core.content.res.ResourcesCompat;\nimport me.goldze.mvvmhabit.R;\n\n\npublic final class DefaultErrorActivity extends AppCompatActivity {\n\n    @SuppressLint(\"PrivateResource\")\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n\n        //This is needed to avoid a crash if the developer has not specified\n        //an app-level theme that extends Theme.AppCompat\n        TypedArray a = obtainStyledAttributes(R.styleable.AppCompatTheme);\n        if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {\n            setTheme(R.style.Theme_AppCompat_Light_DarkActionBar);\n        }\n        a.recycle();\n\n        setContentView(R.layout.customactivityoncrash_default_error_activity);\n\n        //Close/restart button logic:\n        //If a class if set, use restart.\n        //Else, use close and just finish the app.\n        //It is recommended that you follow this logic if implementing a custom error activity.\n        Button restartButton = (Button) findViewById(R.id.customactivityoncrash_error_activity_restart_button);\n\n        final CaocConfig config = CustomActivityOnCrash.getConfigFromIntent(getIntent());\n\n        if (config.isShowRestartButton() && config.getRestartActivityClass()!=null) {\n            restartButton.setText(R.string.customactivityoncrash_error_activity_restart_app);\n            restartButton.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    CustomActivityOnCrash.restartApplication(DefaultErrorActivity.this, config);\n                }\n            });\n        } else {\n            restartButton.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    CustomActivityOnCrash.closeApplication(DefaultErrorActivity.this, config);\n                }\n            });\n        }\n\n        Button moreInfoButton = (Button) findViewById(R.id.customactivityoncrash_error_activity_more_info_button);\n\n        if (config.isShowErrorDetails()) {\n            moreInfoButton.setOnClickListener(new View.OnClickListener() {\n                @Override\n                public void onClick(View v) {\n                    //We retrieve all the error data and show it\n\n                    AlertDialog dialog = new AlertDialog.Builder(DefaultErrorActivity.this)\n                            .setTitle(R.string.customactivityoncrash_error_activity_error_details_title)\n                            .setMessage(CustomActivityOnCrash.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent()))\n                            .setPositiveButton(R.string.customactivityoncrash_error_activity_error_details_close, null)\n                            .setNeutralButton(R.string.customactivityoncrash_error_activity_error_details_copy,\n                                    new DialogInterface.OnClickListener() {\n                                        @Override\n                                        public void onClick(DialogInterface dialog, int which) {\n                                            copyErrorToClipboard();\n                                            Toast.makeText(DefaultErrorActivity.this, R.string.customactivityoncrash_error_activity_error_details_copied, Toast.LENGTH_SHORT).show();\n                                        }\n                                    })\n                            .show();\n                    TextView textView = (TextView) dialog.findViewById(android.R.id.message);\n                    textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.customactivityoncrash_error_activity_error_details_text_size));\n                }\n            });\n        } else {\n            moreInfoButton.setVisibility(View.GONE);\n        }\n\n        Integer defaultErrorActivityDrawableId = config.getErrorDrawable();\n        ImageView errorImageView = ((ImageView) findViewById(R.id.customactivityoncrash_error_activity_image));\n\n        if (defaultErrorActivityDrawableId != null) {\n            errorImageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), defaultErrorActivityDrawableId, getTheme()));\n        }\n    }\n\n    private void copyErrorToClipboard() {\n        String errorInformation = CustomActivityOnCrash.getAllErrorDetailsFromIntent(DefaultErrorActivity.this, getIntent());\n\n        ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);\n        ClipData clip = ClipData.newPlainText(getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label), errorInformation);\n        clipboard.setPrimaryClip(clip);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/ApiDisposableObserver.java",
    "content": "package me.goldze.mvvmhabit.http;\n\nimport io.reactivex.observers.DisposableObserver;\nimport me.goldze.mvvmhabit.base.AppManager;\nimport me.goldze.mvvmhabit.utils.KLog;\nimport me.goldze.mvvmhabit.utils.ToastUtils;\nimport me.goldze.mvvmhabit.utils.Utils;\n\n/**\n * Created by goldze on 2017/5/10.\n * 统一的Code封装处理。该类仅供参考，实际业务逻辑, 根据需求来定义，\n */\n\npublic abstract class ApiDisposableObserver<T> extends DisposableObserver<T> {\n    public abstract void onResult(T t);\n\n    @Override\n    public void onComplete() {\n\n    }\n\n    @Override\n    public void onError(Throwable e) {\n        e.printStackTrace();\n        if (e instanceof ResponseThrowable) {\n            ResponseThrowable rError = (ResponseThrowable) e;\n            ToastUtils.showShort(rError.message);\n            return;\n        }\n        //其他全部甩锅网络异常\n        ToastUtils.showShort(\"网络异常\");\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        ToastUtils.showShort(\"http is start\");\n        // if  NetworkAvailable no !   must to call onCompleted\n        if (!NetworkUtil.isNetworkAvailable(Utils.getContext())) {\n            KLog.d(\"无网络，读取缓存数据\");\n            onComplete();\n        }\n    }\n\n    @Override\n    public void onNext(Object o) {\n        BaseResponse baseResponse = (BaseResponse) o;\n        switch (baseResponse.getCode()) {\n            case CodeRule.CODE_200:\n                //请求成功, 正确的操作方式\n                onResult((T) baseResponse.getResult());\n                break;\n            case CodeRule.CODE_220:\n                // 请求成功, 正确的操作方式, 并消息提示\n                onResult((T) baseResponse.getResult());\n                break;\n            case CodeRule.CODE_300:\n                //请求失败，不打印Message\n                KLog.e(\"请求失败\");\n                ToastUtils.showShort(\"错误代码:\", baseResponse.getCode());\n                break;\n            case CodeRule.CODE_330:\n                //请求失败，打印Message\n                ToastUtils.showShort(baseResponse.getMessage());\n                break;\n            case CodeRule.CODE_500:\n                //服务器内部异常\n                ToastUtils.showShort(\"错误代码:\", baseResponse.getCode());\n                break;\n            case CodeRule.CODE_503:\n                //参数为空\n                KLog.e(\"参数为空\");\n                break;\n            case CodeRule.CODE_502:\n                //没有数据\n                KLog.e(\"没有数据\");\n                break;\n            case CodeRule.CODE_510:\n                //无效的Token，提示跳入登录页\n                ToastUtils.showShort(\"token已过期，请重新登录\");\n                //关闭所有页面\n                AppManager.getAppManager().finishAllActivity();\n                //跳入登录界面\n                //*****该类仅供参考，实际业务Code, 根据需求来定义，******//\n                break;\n            case CodeRule.CODE_530:\n                ToastUtils.showShort(\"请先登录\");\n                break;\n            case CodeRule.CODE_551:\n                ToastUtils.showShort(\"错误代码:\", baseResponse.getCode());\n                break;\n            default:\n                ToastUtils.showShort(\"错误代码:\", baseResponse.getCode());\n                break;\n        }\n    }\n\n    public static final class CodeRule {\n        //请求成功, 正确的操作方式\n        static final int CODE_200 = 200;\n        //请求成功, 消息提示\n        static final int CODE_220 = 220;\n        //请求失败，不打印Message\n        static final int CODE_300 = 300;\n        //请求失败，打印Message\n        static final int CODE_330 = 330;\n        //服务器内部异常\n        static final int CODE_500 = 500;\n        //参数为空\n        static final int CODE_503 = 503;\n        //没有数据\n        static final int CODE_502 = 502;\n        //无效的Token\n        static final int CODE_510 = 510;\n        //未登录\n        static final int CODE_530 = 530;\n        //请求的操作异常终止：未知的页面类型\n        static final int CODE_551 = 551;\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/BaseResponse.java",
    "content": "package me.goldze.mvvmhabit.http;\n\n/**\n * Created by goldze on 2017/5/10.\n * 该类仅供参考，实际业务返回的固定字段, 根据需求来定义，\n */\npublic class BaseResponse<T> {\n    private int code;\n    private String message;\n    private T result;\n\n    public int getCode() {\n        return code;\n    }\n\n    public void setCode(int code) {\n        this.code = code;\n    }\n\n    public T getResult() {\n        return result;\n    }\n\n    public void setResult(T result) {\n        this.result = result;\n    }\n\n    public boolean isOk() {\n        return code == 0;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public void setMessage(String message) {\n        this.message = message;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/DownLoadManager.java",
    "content": "package me.goldze.mvvmhabit.http;\n\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.functions.Consumer;\nimport io.reactivex.schedulers.Schedulers;\nimport me.goldze.mvvmhabit.http.download.DownLoadSubscriber;\nimport me.goldze.mvvmhabit.http.download.ProgressCallBack;\nimport me.goldze.mvvmhabit.http.interceptor.ProgressInterceptor;\nimport okhttp3.OkHttpClient;\nimport okhttp3.ResponseBody;\nimport retrofit2.Retrofit;\nimport retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;\nimport retrofit2.http.GET;\nimport retrofit2.http.Streaming;\nimport retrofit2.http.Url;\n\n/**\n * Created by goldze on 2017/5/11.\n * 文件下载管理，封装一行代码实现下载\n */\n\npublic class DownLoadManager {\n    private static DownLoadManager instance;\n\n    private static Retrofit retrofit;\n\n    private DownLoadManager() {\n        buildNetWork();\n    }\n\n    /**\n     * 单例模式\n     *\n     * @return DownLoadManager\n     */\n    public static DownLoadManager getInstance() {\n        if (instance == null) {\n            instance = new DownLoadManager();\n        }\n        return instance;\n    }\n\n    //下载\n    public void load(String downUrl, final ProgressCallBack callBack) {\n        retrofit.create(ApiService.class)\n                .download(downUrl)\n                .subscribeOn(Schedulers.io())//请求网络 在调度者的io线程\n                .observeOn(Schedulers.io()) //指定线程保存文件\n                .doOnNext(new Consumer<ResponseBody>() {\n                    @Override\n                    public void accept(ResponseBody responseBody) throws Exception {\n                        callBack.saveFile(responseBody);\n                    }\n                })\n                .observeOn(AndroidSchedulers.mainThread()) //在主线程中更新ui\n                .subscribe(new DownLoadSubscriber<ResponseBody>(callBack));\n    }\n\n    private void buildNetWork() {\n        OkHttpClient okHttpClient = new OkHttpClient.Builder()\n                .addInterceptor(new ProgressInterceptor())\n                .connectTimeout(20, TimeUnit.SECONDS)\n                .build();\n\n        retrofit = new Retrofit.Builder()\n                .client(okHttpClient)\n                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())\n                .baseUrl(NetworkUtil.url)\n                .build();\n    }\n\n    private interface ApiService {\n        @Streaming\n        @GET\n        Observable<ResponseBody> download(@Url String url);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/ExceptionHandle.java",
    "content": "package me.goldze.mvvmhabit.http;\n\nimport com.google.gson.JsonParseException;\nimport com.google.gson.stream.MalformedJsonException;\n\nimport android.net.ParseException;\n\nimport org.apache.http.conn.ConnectTimeoutException;\nimport org.json.JSONException;\n\nimport java.net.ConnectException;\n\nimport retrofit2.HttpException;\n\n\n/**\n * Created by goldze on 2017/5/11.\n */\npublic class ExceptionHandle {\n\n    private static final int UNAUTHORIZED = 401;\n    private static final int FORBIDDEN = 403;\n    private static final int NOT_FOUND = 404;\n    private static final int REQUEST_TIMEOUT = 408;\n    private static final int INTERNAL_SERVER_ERROR = 500;\n    private static final int SERVICE_UNAVAILABLE = 503;\n\n    public static ResponseThrowable handleException(Throwable e) {\n        ResponseThrowable ex;\n        if (e instanceof HttpException) {\n            HttpException httpException = (HttpException) e;\n            ex = new ResponseThrowable(e, ERROR.HTTP_ERROR);\n            switch (httpException.code()) {\n                case UNAUTHORIZED:\n                    ex.message = \"操作未授权\";\n                    break;\n                case FORBIDDEN:\n                    ex.message = \"请求被拒绝\";\n                    break;\n                case NOT_FOUND:\n                    ex.message = \"资源不存在\";\n                    break;\n                case REQUEST_TIMEOUT:\n                    ex.message = \"服务器执行超时\";\n                    break;\n                case INTERNAL_SERVER_ERROR:\n                    ex.message = \"服务器内部错误\";\n                    break;\n                case SERVICE_UNAVAILABLE:\n                    ex.message = \"服务器不可用\";\n                    break;\n                default:\n                    ex.message = \"网络错误\";\n                    break;\n            }\n            return ex;\n        } else if (e instanceof JsonParseException\n                || e instanceof JSONException\n                || e instanceof ParseException || e instanceof MalformedJsonException) {\n            ex = new ResponseThrowable(e, ERROR.PARSE_ERROR);\n            ex.message = \"解析错误\";\n            return ex;\n        } else if (e instanceof ConnectException) {\n            ex = new ResponseThrowable(e, ERROR.NETWORD_ERROR);\n            ex.message = \"连接失败\";\n            return ex;\n        } else if (e instanceof javax.net.ssl.SSLException) {\n            ex = new ResponseThrowable(e, ERROR.SSL_ERROR);\n            ex.message = \"证书验证失败\";\n            return ex;\n        } else if (e instanceof ConnectTimeoutException) {\n            ex = new ResponseThrowable(e, ERROR.TIMEOUT_ERROR);\n            ex.message = \"连接超时\";\n            return ex;\n        } else if (e instanceof java.net.SocketTimeoutException) {\n            ex = new ResponseThrowable(e, ERROR.TIMEOUT_ERROR);\n            ex.message = \"连接超时\";\n            return ex;\n        } else if (e instanceof java.net.UnknownHostException) {\n            ex = new ResponseThrowable(e, ERROR.TIMEOUT_ERROR);\n            ex.message = \"主机地址未知\";\n            return ex;\n        } else {\n            ex = new ResponseThrowable(e, ERROR.UNKNOWN);\n            ex.message = \"未知错误\";\n            return ex;\n        }\n    }\n\n\n    /**\n     * 约定异常 这个具体规则需要与服务端或者领导商讨定义\n     */\n    class ERROR {\n        /**\n         * 未知错误\n         */\n        public static final int UNKNOWN = 1000;\n        /**\n         * 解析错误\n         */\n        public static final int PARSE_ERROR = 1001;\n        /**\n         * 网络错误\n         */\n        public static final int NETWORD_ERROR = 1002;\n        /**\n         * 协议出错\n         */\n        public static final int HTTP_ERROR = 1003;\n\n        /**\n         * 证书出错\n         */\n        public static final int SSL_ERROR = 1005;\n\n        /**\n         * 连接超时\n         */\n        public static final int TIMEOUT_ERROR = 1006;\n    }\n\n}\n\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/NetworkUtil.java",
    "content": "package me.goldze.mvvmhabit.http;\n\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\nimport android.telephony.TelephonyManager;\n\nimport java.io.IOException;\nimport java.net.HttpURLConnection;\nimport java.net.InetAddress;\nimport java.net.NetworkInterface;\nimport java.net.SocketException;\nimport java.net.URL;\nimport java.util.Enumeration;\n\npublic class NetworkUtil {\n    public static String url = \"http://www.baidu.com\";\n    public static int NET_CNNT_BAIDU_OK = 1; // NetworkAvailable\n    public static int NET_CNNT_BAIDU_TIMEOUT = 2; // no NetworkAvailable\n    public static int NET_NOT_PREPARE = 3; // Net no ready\n    public static int NET_ERROR = 4; //net error\n    private static int TIMEOUT = 3000; // TIMEOUT\n\n\n    /**\n     * check NetworkAvailable\n     * @param context\n     * @return\n     */\n    public static boolean isNetworkAvailable(Context context) {\n        ConnectivityManager manager = (ConnectivityManager) context.getApplicationContext().getSystemService(\n                Context.CONNECTIVITY_SERVICE);\n        if (null == manager)\n            return false;\n        NetworkInfo info = manager.getActiveNetworkInfo();\n        if (null == info || !info.isAvailable())\n            return false;\n        return true;\n    }\n\n    /**\n     * getLocalIpAddress\n     * @return\n     */\n    public static String getLocalIpAddress() {\n        String ret = \"\";\n        try {\n            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {\n                NetworkInterface intf = en.nextElement();\n                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {\n                    InetAddress inetAddress = enumIpAddr.nextElement();\n                    if (!inetAddress.isLoopbackAddress()) {\n                        ret = inetAddress.getHostAddress().toString();\n                    }\n                }\n            }\n        } catch (SocketException ex) {\n            ex.printStackTrace();\n        }\n        return ret;\n    }\n\n    /**\n     * 返回当前网络状态\n     *\n     * @param context\n     * @return\n     */\n    public static int getNetState(Context context) {\n        try {\n            ConnectivityManager connectivity = (ConnectivityManager) context\n                    .getSystemService(Context.CONNECTIVITY_SERVICE);\n            if (connectivity != null) {\n                NetworkInfo networkinfo = connectivity.getActiveNetworkInfo();\n                if (networkinfo != null) {\n                    if (networkinfo.isAvailable() && networkinfo.isConnected()) {\n                        if (!connectionNetwork())\n                            return NET_CNNT_BAIDU_TIMEOUT;\n                        else\n                            return NET_CNNT_BAIDU_OK;\n                    } else {\n                        return NET_NOT_PREPARE;\n                    }\n                }\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return NET_ERROR;\n    }\n\n    /**\n     *ping \"http://www.baidu.com\"\n     * @return\n     */\n    static private boolean connectionNetwork() {\n        boolean result = false;\n        HttpURLConnection httpUrl = null;\n        try {\n            httpUrl = (HttpURLConnection) new URL(url)\n                    .openConnection();\n            httpUrl.setConnectTimeout(TIMEOUT);\n            httpUrl.connect();\n            result = true;\n        } catch (IOException e) {\n        } finally {\n            if (null != httpUrl) {\n                httpUrl.disconnect();\n            }\n            httpUrl = null;\n        }\n        return result;\n    }\n\n    /**\n     * check is3G\n     * @param context\n     * @return boolean\n     */\n    public static boolean is3G(Context context) {\n        ConnectivityManager connectivityManager = (ConnectivityManager) context\n                .getSystemService(Context.CONNECTIVITY_SERVICE);\n        NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();\n        if (activeNetInfo != null\n                && activeNetInfo.getType() == ConnectivityManager.TYPE_MOBILE) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * isWifi\n     * @param context\n     * @return boolean\n     */\n    public static boolean isWifi(Context context) {\n        ConnectivityManager connectivityManager = (ConnectivityManager) context\n                .getSystemService(Context.CONNECTIVITY_SERVICE);\n        NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();\n        if (activeNetInfo != null\n                && activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * is2G\n     * @param context\n     * @return boolean\n     */\n    public static boolean is2G(Context context) {\n        ConnectivityManager connectivityManager = (ConnectivityManager) context\n                .getSystemService(Context.CONNECTIVITY_SERVICE);\n        NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();\n        if (activeNetInfo != null\n                && (activeNetInfo.getSubtype() == TelephonyManager.NETWORK_TYPE_EDGE\n                || activeNetInfo.getSubtype() == TelephonyManager.NETWORK_TYPE_GPRS || activeNetInfo\n                .getSubtype() == TelephonyManager.NETWORK_TYPE_CDMA)) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     *  is wifi on\n     */\n    public static boolean isWifiEnabled(Context context) {\n        ConnectivityManager mgrConn = (ConnectivityManager) context\n                .getSystemService(Context.CONNECTIVITY_SERVICE);\n        TelephonyManager mgrTel = (TelephonyManager) context\n                .getSystemService(Context.TELEPHONY_SERVICE);\n        return ((mgrConn.getActiveNetworkInfo() != null && mgrConn\n                .getActiveNetworkInfo().getState() == NetworkInfo.State.CONNECTED) || mgrTel\n                .getNetworkType() == TelephonyManager.NETWORK_TYPE_UMTS);\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/ResponseThrowable.java",
    "content": "package me.goldze.mvvmhabit.http;\n\n/**\n * Created by goldze on 2017/5/11.\n */\n\npublic class ResponseThrowable extends Exception {\n    public int code;\n    public String message;\n\n    public ResponseThrowable(Throwable throwable, int code) {\n        super(throwable);\n        this.code = code;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/cookie/CookieJarImpl.java",
    "content": "package me.goldze.mvvmhabit.http.cookie;\n\n\nimport java.util.List;\n\nimport me.goldze.mvvmhabit.http.cookie.store.CookieStore;\nimport okhttp3.Cookie;\nimport okhttp3.CookieJar;\nimport okhttp3.HttpUrl;\n\n/**\n * Created by goldze on 2017/5/13.\n */\npublic class CookieJarImpl implements CookieJar {\n\n    private CookieStore cookieStore;\n\n    public CookieJarImpl(CookieStore cookieStore) {\n        if (cookieStore == null) {\n            throw new IllegalArgumentException(\"cookieStore can not be null!\");\n        }\n        this.cookieStore = cookieStore;\n    }\n\n    @Override\n    public synchronized void saveFromResponse(HttpUrl url, List<Cookie> cookies) {\n        cookieStore.saveCookie(url, cookies);\n    }\n\n    @Override\n    public synchronized List<Cookie> loadForRequest(HttpUrl url) {\n        return cookieStore.loadCookie(url);\n    }\n\n    public CookieStore getCookieStore() {\n        return cookieStore;\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/cookie/store/CookieStore.java",
    "content": "package me.goldze.mvvmhabit.http.cookie.store;\n\nimport java.util.List;\n\nimport okhttp3.Cookie;\nimport okhttp3.HttpUrl;\n\n/**\n * Created by goldze on 2017/5/13.\n */\npublic interface CookieStore {\n\n    /** 保存url对应所有cookie */\n    void saveCookie(HttpUrl url, List<Cookie> cookie);\n\n    /** 保存url对应所有cookie */\n    void saveCookie(HttpUrl url, Cookie cookie);\n\n    /** 加载url所有的cookie */\n    List<Cookie> loadCookie(HttpUrl url);\n\n    /** 获取当前所有保存的cookie */\n    List<Cookie> getAllCookie();\n\n    /** 获取当前url对应的所有的cookie */\n    List<Cookie> getCookie(HttpUrl url);\n\n    /** 根据url和cookie移除对应的cookie */\n    boolean removeCookie(HttpUrl url, Cookie cookie);\n\n    /** 根据url移除所有的cookie */\n    boolean removeCookie(HttpUrl url);\n\n    /** 移除所有的cookie */\n    boolean removeAllCookie();\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/cookie/store/MemoryCookieStore.java",
    "content": "package me.goldze.mvvmhabit.http.cookie.store;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Set;\n\nimport okhttp3.Cookie;\nimport okhttp3.HttpUrl;\n\n/**\n * Created by goldze on 2017/5/13.\n */\npublic class MemoryCookieStore implements CookieStore {\n\n    private final HashMap<String, List<Cookie>> memoryCookies = new HashMap<>();\n\n    @Override\n    public synchronized void saveCookie(HttpUrl url, List<Cookie> cookies) {\n        List<Cookie> oldCookies = memoryCookies.get(url.host());\n        List<Cookie> needRemove = new ArrayList<>();\n        for (Cookie newCookie : cookies) {\n            for (Cookie oldCookie : oldCookies) {\n                if (newCookie.name().equals(oldCookie.name())) {\n                    needRemove.add(oldCookie);\n                }\n            }\n        }\n        oldCookies.removeAll(needRemove);\n        oldCookies.addAll(cookies);\n    }\n\n    @Override\n    public synchronized void saveCookie(HttpUrl url, Cookie cookie) {\n        List<Cookie> cookies = memoryCookies.get(url.host());\n        List<Cookie> needRemove = new ArrayList<>();\n        for (Cookie item : cookies) {\n            if (cookie.name().equals(item.name())) {\n                needRemove.add(item);\n            }\n        }\n        cookies.removeAll(needRemove);\n        cookies.add(cookie);\n    }\n\n    @Override\n    public synchronized List<Cookie> loadCookie(HttpUrl url) {\n        List<Cookie> cookies = memoryCookies.get(url.host());\n        if (cookies == null) {\n            cookies = new ArrayList<>();\n            memoryCookies.put(url.host(), cookies);\n        }\n        return cookies;\n    }\n\n    @Override\n    public synchronized List<Cookie> getAllCookie() {\n        List<Cookie> cookies = new ArrayList<>();\n        Set<String> httpUrls = memoryCookies.keySet();\n        for (String url : httpUrls) {\n            cookies.addAll(memoryCookies.get(url));\n        }\n        return cookies;\n    }\n\n    @Override\n    public List<Cookie> getCookie(HttpUrl url) {\n        List<Cookie> cookies = new ArrayList<>();\n        List<Cookie> urlCookies = memoryCookies.get(url.host());\n        if (urlCookies != null) cookies.addAll(urlCookies);\n        return cookies;\n    }\n\n    @Override\n    public synchronized boolean removeCookie(HttpUrl url, Cookie cookie) {\n        List<Cookie> cookies = memoryCookies.get(url.host());\n        return (cookie != null) && cookies.remove(cookie);\n    }\n\n    @Override\n    public synchronized boolean removeCookie(HttpUrl url) {\n        return memoryCookies.remove(url.host()) != null;\n    }\n\n    @Override\n    public synchronized boolean removeAllCookie() {\n        memoryCookies.clear();\n        return true;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/cookie/store/PersistentCookieStore.java",
    "content": "package me.goldze.mvvmhabit.http.cookie.store;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport okhttp3.Cookie;\nimport okhttp3.HttpUrl;\n\n/**\n * Created by goldze on 2017/5/13.\n */\npublic class PersistentCookieStore implements CookieStore {\n\n    private static final String LOG_TAG = \"PersistentCookieStore\";\n    private static final String COOKIE_PREFS = \"habit_cookie\";        //cookie使用prefs保存\n    private static final String COOKIE_NAME_PREFIX = \"cookie_\";          //cookie持久化的统一前缀\n\n    private final HashMap<String, ConcurrentHashMap<String, Cookie>> cookies;\n    private final SharedPreferences cookiePrefs;\n\n    public PersistentCookieStore(Context context) {\n        cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE);\n        cookies = new HashMap<>();\n\n        //将持久化的cookies缓存到内存中,数据结构为 Map<Url.host, Map<Cookie.name, Cookie>>\n        Map<String, ?> prefsMap = cookiePrefs.getAll();\n        for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {\n            if ((entry.getValue()) != null && !entry.getKey().startsWith(COOKIE_NAME_PREFIX)) {\n                //获取url对应的所有cookie的key,用\",\"分割\n                String[] cookieNames = TextUtils.split((String) entry.getValue(), \",\");\n                for (String name : cookieNames) {\n                    //根据对应cookie的Key,从xml中获取cookie的真实值\n                    String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);\n                    if (encodedCookie != null) {\n                        Cookie decodedCookie = decodeCookie(encodedCookie);\n                        if (decodedCookie != null) {\n                            if (!cookies.containsKey(entry.getKey())) cookies.put(entry.getKey(), new ConcurrentHashMap<String, Cookie>());\n                            cookies.get(entry.getKey()).put(name, decodedCookie);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private String getCookieToken(Cookie cookie) {\n        return cookie.name() + \"@\" + cookie.domain();\n    }\n\n    /** 当前cookie是否过期 */\n    private static boolean isCookieExpired(Cookie cookie) {\n        return cookie.expiresAt() < System.currentTimeMillis();\n    }\n\n    /** 根据当前url获取所有需要的cookie,只返回没有过期的cookie */\n    @Override\n    public List<Cookie> loadCookie(HttpUrl url) {\n        ArrayList<Cookie> ret = new ArrayList<>();\n        if (cookies.containsKey(url.host())) {\n            Collection<Cookie> urlCookies = cookies.get(url.host()).values();\n            for (Cookie cookie : urlCookies) {\n                if (isCookieExpired(cookie)) {\n                    removeCookie(url, cookie);\n                } else {\n                    ret.add(cookie);\n                }\n            }\n        }\n        return ret;\n    }\n\n    /** 将url的所有Cookie保存在本地 */\n    @Override\n    public void saveCookie(HttpUrl url, List<Cookie> urlCookies) {\n        if (!cookies.containsKey(url.host())) {\n            cookies.put(url.host(), new ConcurrentHashMap<String, Cookie>());\n        }\n        for (Cookie cookie : urlCookies) {\n            //当前cookie是否过期\n            if (isCookieExpired(cookie)) {\n                removeCookie(url, cookie);\n            } else {\n                saveCookie(url, cookie, getCookieToken(cookie));\n            }\n        }\n    }\n\n    @Override\n    public void saveCookie(HttpUrl url, Cookie cookie) {\n        if (!cookies.containsKey(url.host())) {\n            cookies.put(url.host(), new ConcurrentHashMap<String, Cookie>());\n        }\n        //当前cookie是否过期\n        if (isCookieExpired(cookie)) {\n            removeCookie(url, cookie);\n        } else {\n            saveCookie(url, cookie, getCookieToken(cookie));\n        }\n    }\n\n    /**\n     * 保存cookie，并将cookies持久化到本地,数据结构为\n     * Url.host -> Cookie1.name,Cookie2.name,Cookie3.name\n     * cookie_Cookie1.name -> CookieString\n     * cookie_Cookie2.name -> CookieString\n     */\n    private void saveCookie(HttpUrl url, Cookie cookie, String name) {\n        //内存缓存\n        cookies.get(url.host()).put(name, cookie);\n        //文件缓存\n        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();\n        prefsWriter.putString(url.host(), TextUtils.join(\",\", cookies.get(url.host()).keySet()));\n        prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new SerializableHttpCookie(cookie)));\n        prefsWriter.apply();\n    }\n\n    /** 根据url移除当前的cookie */\n    @Override\n    public boolean removeCookie(HttpUrl url, Cookie cookie) {\n        String name = getCookieToken(cookie);\n        if (cookies.containsKey(url.host()) && cookies.get(url.host()).containsKey(name)) {\n            //内存移除\n            cookies.get(url.host()).remove(name);\n            //文件移除\n            SharedPreferences.Editor prefsWriter = cookiePrefs.edit();\n            if (cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {\n                prefsWriter.remove(COOKIE_NAME_PREFIX + name);\n            }\n            prefsWriter.putString(url.host(), TextUtils.join(\",\", cookies.get(url.host()).keySet()));\n            prefsWriter.apply();\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public boolean removeCookie(HttpUrl url) {\n        if (cookies.containsKey(url.host())) {\n            //文件移除\n            Set<String> cookieNames = cookies.get(url.host()).keySet();\n            SharedPreferences.Editor prefsWriter = cookiePrefs.edit();\n            for (String cookieName : cookieNames) {\n                if (cookiePrefs.contains(COOKIE_NAME_PREFIX + cookieName)) {\n                    prefsWriter.remove(COOKIE_NAME_PREFIX + cookieName);\n                }\n            }\n            prefsWriter.remove(url.host()).apply();\n            //内存移除\n            cookies.remove(url.host());\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public boolean removeAllCookie() {\n        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();\n        prefsWriter.clear().apply();\n        cookies.clear();\n        return true;\n    }\n\n    /** 获取所有的cookie */\n    @Override\n    public List<Cookie> getAllCookie() {\n        List<Cookie> ret = new ArrayList<>();\n        for (String key : cookies.keySet())\n            ret.addAll(cookies.get(key).values());\n        return ret;\n    }\n\n    @Override\n    public List<Cookie> getCookie(HttpUrl url) {\n        List<Cookie> ret = new ArrayList<>();\n        Map<String, Cookie> mapCookie = cookies.get(url.host());\n        if (mapCookie != null) ret.addAll(mapCookie.values());\n        return ret;\n    }\n\n    /**\n     * cookies 序列化成 string\n     *\n     * @param cookie 要序列化的cookie\n     * @return 序列化之后的string\n     */\n    private String encodeCookie(SerializableHttpCookie cookie) {\n        if (cookie == null) return null;\n        ByteArrayOutputStream os = new ByteArrayOutputStream();\n        try {\n            ObjectOutputStream outputStream = new ObjectOutputStream(os);\n            outputStream.writeObject(cookie);\n        } catch (IOException e) {\n            Log.d(LOG_TAG, \"IOException in encodeCookie\", e);\n            return null;\n        }\n        return byteArrayToHexString(os.toByteArray());\n    }\n\n    /**\n     * 将字符串反序列化成cookies\n     *\n     * @param cookieString cookies string\n     * @return cookie object\n     */\n    private Cookie decodeCookie(String cookieString) {\n        byte[] bytes = hexStringToByteArray(cookieString);\n        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);\n        Cookie cookie = null;\n        try {\n            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);\n            cookie = ((SerializableHttpCookie) objectInputStream.readObject()).getCookie();\n        } catch (IOException e) {\n            Log.d(LOG_TAG, \"IOException in decodeCookie\", e);\n        } catch (ClassNotFoundException e) {\n            Log.d(LOG_TAG, \"ClassNotFoundException in decodeCookie\", e);\n        }\n        return cookie;\n    }\n\n    /**\n     * 二进制数组转十六进制字符串\n     *\n     * @param bytes byte array to be converted\n     * @return string containing hex values\n     */\n    private String byteArrayToHexString(byte[] bytes) {\n        StringBuilder sb = new StringBuilder(bytes.length * 2);\n        for (byte element : bytes) {\n            int v = element & 0xff;\n            if (v < 16) {\n                sb.append('0');\n            }\n            sb.append(Integer.toHexString(v));\n        }\n        return sb.toString().toUpperCase(Locale.US);\n    }\n\n    /**\n     * 十六进制字符串转二进制数组\n     *\n     * @param hexString string of hex-encoded values\n     * @return decoded byte array\n     */\n    private byte[] hexStringToByteArray(String hexString) {\n        int len = hexString.length();\n        byte[] data = new byte[len / 2];\n        for (int i = 0; i < len; i += 2) {\n            data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));\n        }\n        return data;\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/cookie/store/SerializableHttpCookie.java",
    "content": "package me.goldze.mvvmhabit.http.cookie.store;\n\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.io.Serializable;\n\nimport okhttp3.Cookie;\n\npublic class SerializableHttpCookie implements Serializable {\n    private static final long serialVersionUID = 6374381323722046732L;\n\n    private transient final Cookie cookie;\n    private transient Cookie clientCookie;\n\n    public SerializableHttpCookie(Cookie cookie) {\n        this.cookie = cookie;\n    }\n\n    public Cookie getCookie() {\n        Cookie bestCookie = cookie;\n        if (clientCookie != null) {\n            bestCookie = clientCookie;\n        }\n        return bestCookie;\n    }\n\n    private void writeObject(ObjectOutputStream out) throws IOException {\n        out.writeObject(cookie.name());\n        out.writeObject(cookie.value());\n        out.writeLong(cookie.expiresAt());\n        out.writeObject(cookie.domain());\n        out.writeObject(cookie.path());\n        out.writeBoolean(cookie.secure());\n        out.writeBoolean(cookie.httpOnly());\n        out.writeBoolean(cookie.hostOnly());\n        out.writeBoolean(cookie.persistent());\n    }\n\n    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {\n        String name = (String) in.readObject();\n        String value = (String) in.readObject();\n        long expiresAt = in.readLong();\n        String domain = (String) in.readObject();\n        String path = (String) in.readObject();\n        boolean secure = in.readBoolean();\n        boolean httpOnly = in.readBoolean();\n        boolean hostOnly = in.readBoolean();\n        boolean persistent = in.readBoolean();\n        Cookie.Builder builder = new Cookie.Builder();\n        builder = builder.name(name);\n        builder = builder.value(value);\n        builder = builder.expiresAt(expiresAt);\n        builder = hostOnly ? builder.hostOnlyDomain(domain) : builder.domain(domain);\n        builder = builder.path(path);\n        builder = secure ? builder.secure() : builder;\n        builder = httpOnly ? builder.httpOnly() : builder;\n        clientCookie = builder.build();\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/download/DownLoadStateBean.java",
    "content": "package me.goldze.mvvmhabit.http.download;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport java.io.Serializable;\n\n/**\n * Created by goldze on 2017/5/11.\n */\n\npublic class DownLoadStateBean implements Serializable, Parcelable {\n    long total; //  文件总大小\n    long bytesLoaded; //已加载文件的大小\n    String tag; // 多任务下载时的一个标记\n\n    public DownLoadStateBean(long total, long bytesLoaded) {\n        this.total = total;\n        this.bytesLoaded = bytesLoaded;\n    }\n\n    public DownLoadStateBean(long total, long bytesLoaded, String tag) {\n        this.total = total;\n        this.bytesLoaded = bytesLoaded;\n        this.tag = tag;\n    }\n\n    public long getTotal() {\n        return total;\n    }\n\n    public void setTotal(long total) {\n        this.total = total;\n    }\n\n    public long getBytesLoaded() {\n        return bytesLoaded;\n    }\n\n    public void setBytesLoaded(long bytesLoaded) {\n        this.bytesLoaded = bytesLoaded;\n    }\n\n    public String getTag() {\n        return tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeLong(this.total);\n        dest.writeLong(this.bytesLoaded);\n        dest.writeString(this.tag);\n    }\n\n    protected DownLoadStateBean(Parcel in) {\n        this.total = in.readLong();\n        this.bytesLoaded = in.readLong();\n        this.tag = in.readString();\n    }\n\n    public static final Creator<DownLoadStateBean> CREATOR = new Creator<DownLoadStateBean>() {\n        @Override\n        public DownLoadStateBean createFromParcel(Parcel source) {\n            return new DownLoadStateBean(source);\n        }\n\n        @Override\n        public DownLoadStateBean[] newArray(int size) {\n            return new DownLoadStateBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/download/DownLoadSubscriber.java",
    "content": "package me.goldze.mvvmhabit.http.download;\n\nimport io.reactivex.observers.DisposableObserver;\n\n/**\n * Created by goldze on 2017/5/11.\n */\n\npublic class DownLoadSubscriber<T> extends DisposableObserver<T> {\n    private ProgressCallBack fileCallBack;\n\n    public DownLoadSubscriber(ProgressCallBack fileCallBack) {\n        this.fileCallBack = fileCallBack;\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        if (fileCallBack != null)\n            fileCallBack.onStart();\n    }\n\n    @Override\n    public void onComplete() {\n        if (fileCallBack != null)\n            fileCallBack.onCompleted();\n    }\n\n    @Override\n    public void onError(Throwable e) {\n        if (fileCallBack != null)\n            fileCallBack.onError(e);\n    }\n\n    @Override\n    public void onNext(T t) {\n        if (fileCallBack != null)\n            fileCallBack.onSuccess(t);\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/download/ProgressCallBack.java",
    "content": "package me.goldze.mvvmhabit.http.download;\n\nimport android.util.Log;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.functions.Consumer;\nimport me.goldze.mvvmhabit.bus.RxBus;\nimport me.goldze.mvvmhabit.bus.RxSubscriptions;\nimport okhttp3.ResponseBody;\n\n/**\n * Created by goldze on 2017/9/26 0026.\n */\n\npublic abstract class ProgressCallBack<T> {\n\n    private String destFileDir; // 本地文件存放路径\n    private String destFileName; // 文件名\n    private Disposable mSubscription;\n\n    public ProgressCallBack(String destFileDir, String destFileName) {\n        this.destFileDir = destFileDir;\n        this.destFileName = destFileName;\n        subscribeLoadProgress();\n    }\n\n    public abstract void onSuccess(T t);\n\n    public abstract void progress(long progress, long total);\n\n    public void onStart() {\n    }\n\n    public void onCompleted() {\n    }\n\n    public abstract void onError(Throwable e);\n\n    public void saveFile(ResponseBody body) {\n        InputStream is = null;\n        byte[] buf = new byte[2048];\n        int len;\n        FileOutputStream fos = null;\n        try {\n            is = body.byteStream();\n            File dir = new File(destFileDir);\n            if (!dir.exists()) {\n                dir.mkdirs();\n            }\n            File file = new File(dir, destFileName);\n            fos = new FileOutputStream(file);\n            while ((len = is.read(buf)) != -1) {\n                fos.write(buf, 0, len);\n            }\n            fos.flush();\n            //onCompleted();\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n        } catch (IOException e) {\n            e.printStackTrace();\n        } finally {\n            try {\n                if (is != null) is.close();\n                if (fos != null) fos.close();\n                unsubscribe();\n            } catch (IOException e) {\n                Log.e(\"saveFile\", e.getMessage());\n            }\n        }\n    }\n\n    /**\n     * 订阅加载的进度条\n     */\n    public void subscribeLoadProgress() {\n        mSubscription = RxBus.getDefault().toObservable(DownLoadStateBean.class)\n                .observeOn(AndroidSchedulers.mainThread()) //回调到主线程更新UI\n                .subscribe(new Consumer<DownLoadStateBean>() {\n                    @Override\n                    public void accept(final DownLoadStateBean progressLoadBean) throws Exception {\n                        progress(progressLoadBean.getBytesLoaded(), progressLoadBean.getTotal());\n                    }\n                });\n        //将订阅者加入管理站\n        RxSubscriptions.add(mSubscription);\n    }\n\n    /**\n     * 取消订阅，防止内存泄漏\n     */\n    public void unsubscribe() {\n        RxSubscriptions.remove(mSubscription);\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/download/ProgressResponseBody.java",
    "content": "package me.goldze.mvvmhabit.http.download;\n\nimport java.io.IOException;\n\nimport me.goldze.mvvmhabit.bus.RxBus;\nimport okhttp3.MediaType;\nimport okhttp3.ResponseBody;\nimport okio.Buffer;\nimport okio.BufferedSource;\nimport okio.ForwardingSource;\nimport okio.Okio;\nimport okio.Source;\n\n/**\n * Created by goldze on 2017/5/11.\n */\n\npublic class ProgressResponseBody extends ResponseBody {\n    private ResponseBody responseBody;\n\n    private BufferedSource bufferedSource;\n    private String tag;\n\n    public ProgressResponseBody(ResponseBody responseBody) {\n        this.responseBody = responseBody;\n    }\n\n    public ProgressResponseBody(ResponseBody responseBody, String tag) {\n        this.responseBody = responseBody;\n        this.tag = tag;\n    }\n\n    @Override\n    public MediaType contentType() {\n        return responseBody.contentType();\n    }\n\n    @Override\n    public long contentLength() {\n        return responseBody.contentLength();\n    }\n\n    @Override\n    public BufferedSource source() {\n        if (bufferedSource == null) {\n            bufferedSource = Okio.buffer(source(responseBody.source()));\n        }\n        return bufferedSource;\n    }\n\n    private Source source(Source source) {\n        return new ForwardingSource(source) {\n            long bytesReaded = 0;\n\n            @Override\n            public long read(Buffer sink, long byteCount) throws IOException {\n                long bytesRead = super.read(sink, byteCount);\n                bytesReaded += bytesRead == -1 ? 0 : bytesRead;\n                //使用RxBus的方式，实时发送当前已读取(上传/下载)的字节数据\n                RxBus.getDefault().post(new DownLoadStateBean(contentLength(), bytesReaded, tag));\n                return bytesRead;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/BaseInterceptor.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor;\n\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.Set;\n\nimport okhttp3.Interceptor;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\n/**\n * Created by goldze on 2017/5/10.\n */\npublic class BaseInterceptor implements Interceptor {\n    private Map<String, String> headers;\n\n    public BaseInterceptor(Map<String, String> headers) {\n        this.headers = headers;\n    }\n\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Request.Builder builder = chain.request()\n                .newBuilder();\n        if (headers != null && headers.size() > 0) {\n            Set<String> keys = headers.keySet();\n            for (String headerKey : keys) {\n                builder.addHeader(headerKey, headers.get(headerKey)).build();\n            }\n        }\n        //请求信息\n        return chain.proceed(builder.build());\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/CacheInterceptor.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor;\n\nimport android.content.Context;\n\nimport java.io.IOException;\n\nimport me.goldze.mvvmhabit.http.NetworkUtil;\nimport okhttp3.CacheControl;\nimport okhttp3.Interceptor;\nimport okhttp3.Request;\nimport okhttp3.Response;\n\n/**\n * Created by goldze on 2017/5/10.\n * 无网络状态下智能读取缓存的拦截器\n */\npublic class CacheInterceptor implements Interceptor {\n\n    private Context context;\n\n    public CacheInterceptor(Context context) {\n        this.context = context;\n    }\n\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Request request = chain.request();\n        if (NetworkUtil.isNetworkAvailable(context)) {\n            Response response = chain.proceed(request);\n            // read from cache for 60 s\n            int maxAge = 60;\n            return response.newBuilder()\n                    .removeHeader(\"Pragma\")\n                    .removeHeader(\"Cache-Control\")\n                    .header(\"Cache-Control\", \"public, max-age=\" + maxAge)\n                    .build();\n        } else {\n            //读取缓存信息\n            request = request.newBuilder()\n                    .cacheControl(CacheControl.FORCE_CACHE)\n                    .build();\n            Response response = chain.proceed(request);\n            //set cache times is 3 days\n            int maxStale = 60 * 60 * 24 * 3;\n            return response.newBuilder()\n                    .removeHeader(\"Pragma\")\n                    .removeHeader(\"Cache-Control\")\n                    .header(\"Cache-Control\", \"public, only-if-cached, max-stale=\" + maxStale)\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/ProgressInterceptor.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor;\n\nimport java.io.IOException;\n\nimport me.goldze.mvvmhabit.http.download.ProgressResponseBody;\nimport okhttp3.Interceptor;\nimport okhttp3.Response;\n\n/**\n * Created by goldze on 2017/5/10.\n */\n\npublic class ProgressInterceptor implements Interceptor {\n\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Response originalResponse = chain.proceed(chain.request());\n        return originalResponse.newBuilder()\n                .body(new ProgressResponseBody(originalResponse.body()))\n                .build();\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/logging/I.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor.logging;\n\n\nimport java.util.logging.Level;\n\nimport okhttp3.internal.platform.Platform;\n\n/**\n * @author ihsan on 10/02/2017.\n */\nclass I {\n\n    protected I() {\n        throw new UnsupportedOperationException();\n    }\n\n    static void log(int type, String tag, String msg) {\n        java.util.logging.Logger logger = java.util.logging.Logger.getLogger(tag);\n        switch (type) {\n            case Platform.INFO:\n                logger.log(Level.INFO, msg);\n                break;\n            default:\n                logger.log(Level.WARNING, msg);\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/logging/Level.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor.logging;\n\n/**\n * @author ihsan on 21/02/2017.\n */\n\npublic enum Level {\n    /**\n     * No logs.\n     */\n    NONE,\n    /**\n     * <p>Example:\n     * <pre>{@code\n     *  - URL\n     *  - Method\n     *  - Headers\n     *  - Body\n     * }</pre>\n     */\n    BASIC,\n    /**\n     * <p>Example:\n     * <pre>{@code\n     *  - URL\n     *  - Method\n     *  - Headers\n     * }</pre>\n     */\n    HEADERS,\n    /**\n     * <p>Example:\n     * <pre>{@code\n     *  - URL\n     *  - Method\n     *  - Body\n     * }</pre>\n     */\n    BODY\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/logging/Logger.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor.logging;\n\nimport okhttp3.internal.platform.Platform;\n\n/**\n * @author ihsan on 11/07/2017.\n */\n@SuppressWarnings({\"WeakerAccess\", \"unused\"})\npublic interface Logger {\n    void log(int level, String tag, String msg);\n\n    Logger DEFAULT = new Logger() {\n        @Override\n        public void log(int level, String tag, String message) {\n            Platform.get().log(level, message, null);\n        }\n    };\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/logging/LoggingInterceptor.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor.logging;\n\nimport android.text.TextUtils;\n\nimport java.io.IOException;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\n\nimport okhttp3.Headers;\nimport okhttp3.Interceptor;\nimport okhttp3.MediaType;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport okhttp3.ResponseBody;\nimport okhttp3.internal.platform.Platform;\n\n/**\n * @author ihsan on 09/02/2017.\n */\n\npublic class LoggingInterceptor implements Interceptor {\n\n    private boolean isDebug;\n    private Builder builder;\n\n    private LoggingInterceptor(Builder builder) {\n        this.builder = builder;\n        this.isDebug = builder.isDebug;\n    }\n\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Request request = chain.request();\n        if (builder.getHeaders().size() > 0) {\n            Headers headers = request.headers();\n            Set<String> names = headers.names();\n            Iterator<String> iterator = names.iterator();\n            Request.Builder requestBuilder = request.newBuilder();\n            requestBuilder.headers(builder.getHeaders());\n            while (iterator.hasNext()) {\n                String name = iterator.next();\n                requestBuilder.addHeader(name, headers.get(name));\n            }\n            request = requestBuilder.build();\n        }\n\n        if (!isDebug || builder.getLevel() == Level.NONE) {\n            return chain.proceed(request);\n        }\n        RequestBody requestBody = request.body();\n\n        MediaType rContentType = null;\n        if (requestBody != null) {\n            rContentType = request.body().contentType();\n        }\n\n        String rSubtype = null;\n        if (rContentType != null) {\n            rSubtype = rContentType.subtype();\n        }\n\n        if (rSubtype != null && (rSubtype.contains(\"json\")\n                || rSubtype.contains(\"xml\")\n                || rSubtype.contains(\"plain\")\n                || rSubtype.contains(\"html\"))) {\n            Printer.printJsonRequest(builder, request);\n        } else {\n            Printer.printFileRequest(builder, request);\n        }\n\n        long st = System.nanoTime();\n        Response response = chain.proceed(request);\n\n        List<String> segmentList = request.url().encodedPathSegments();\n        long chainMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - st);\n        String header = response.headers().toString();\n        int code = response.code();\n        boolean isSuccessful = response.isSuccessful();\n        ResponseBody responseBody = response.body();\n        MediaType contentType = responseBody.contentType();\n\n        String subtype = null;\n        ResponseBody body;\n\n        if (contentType != null) {\n            subtype = contentType.subtype();\n        }\n\n        if (subtype != null && (subtype.contains(\"json\")\n                || subtype.contains(\"xml\")\n                || subtype.contains(\"plain\")\n                || subtype.contains(\"html\"))) {\n            String bodyString = responseBody.string();\n            String bodyJson = Printer.getJsonString(bodyString);\n            Printer.printJsonResponse(builder, chainMs, isSuccessful, code, header, bodyJson, segmentList);\n            body = ResponseBody.create(contentType, bodyString);\n        } else {\n            Printer.printFileResponse(builder, chainMs, isSuccessful, code, header, segmentList);\n            return response;\n        }\n        return response.newBuilder().body(body).build();\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static class Builder {\n\n        private static String TAG = \"LoggingI\";\n        private boolean isDebug;\n        private int type = Platform.INFO;\n        private String requestTag;\n        private String responseTag;\n        private Level level = Level.BASIC;\n        private Headers.Builder builder;\n        private Logger logger;\n\n        public Builder() {\n            builder = new Headers.Builder();\n        }\n\n        int getType() {\n            return type;\n        }\n\n        Level getLevel() {\n            return level;\n        }\n\n        Headers getHeaders() {\n            return builder.build();\n        }\n\n        String getTag(boolean isRequest) {\n            if (isRequest) {\n                return TextUtils.isEmpty(requestTag) ? TAG : requestTag;\n            } else {\n                return TextUtils.isEmpty(responseTag) ? TAG : responseTag;\n            }\n        }\n\n        Logger getLogger() {\n            return logger;\n        }\n\n        /**\n         * @param name  Filed\n         * @param value Value\n         * @return Builder\n         * Add a field with the specified value\n         */\n        public Builder addHeader(String name, String value) {\n            builder.set(name, value);\n            return this;\n        }\n\n        /**\n         * @param level set log level\n         * @return Builder\n         * @see Level\n         */\n        public Builder setLevel(Level level) {\n            this.level = level;\n            return this;\n        }\n\n        /**\n         * Set request and response each log tag\n         *\n         * @param tag general log tag\n         * @return Builder\n         */\n        public Builder tag(String tag) {\n            TAG = tag;\n            return this;\n        }\n\n        /**\n         * Set request log tag\n         *\n         * @param tag request log tag\n         * @return Builder\n         */\n        public Builder request(String tag) {\n            this.requestTag = tag;\n            return this;\n        }\n\n        /**\n         * Set response log tag\n         *\n         * @param tag response log tag\n         * @return Builder\n         */\n        public Builder response(String tag) {\n            this.responseTag = tag;\n            return this;\n        }\n\n        /**\n         * @param isDebug set can sending log output\n         * @return Builder\n         */\n        public Builder loggable(boolean isDebug) {\n            this.isDebug = isDebug;\n            return this;\n        }\n\n        /**\n         * @param type set sending log output type\n         * @return Builder\n         * @see Platform\n         */\n        public Builder log(int type) {\n            this.type = type;\n            return this;\n        }\n\n        /**\n         * @param logger manuel logging interface\n         * @return Builder\n         * @see Logger\n         */\n        public Builder logger(Logger logger) {\n            this.logger = logger;\n            return this;\n        }\n\n        public LoggingInterceptor build() {\n            return new LoggingInterceptor(this);\n        }\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/http/interceptor/logging/Printer.java",
    "content": "package me.goldze.mvvmhabit.http.interceptor.logging;\n\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.IOException;\nimport java.util.List;\n\nimport okhttp3.FormBody;\nimport okhttp3.Request;\nimport okio.Buffer;\n\n/**\n * @author ihsan on 09/02/2017.\n */\n\nclass Printer {\n\n    private static final int JSON_INDENT = 3;\n\n    private static final String LINE_SEPARATOR = System.getProperty(\"line.separator\");\n    private static final String DOUBLE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR;\n\n    private static final String[] OMITTED_RESPONSE = {LINE_SEPARATOR, \"Omitted response body\"};\n    private static final String[] OMITTED_REQUEST = {LINE_SEPARATOR, \"Omitted request body\"};\n\n    private static final String N = \"\\n\";\n    private static final String T = \"\\t\";\n    private static final String REQUEST_UP_LINE = \"┌────── Request ────────────────────────────────────────────────────────────────────────\";\n    private static final String END_LINE = \"└───────────────────────────────────────────────────────────────────────────────────────\";\n    private static final String RESPONSE_UP_LINE = \"┌────── Response ───────────────────────────────────────────────────────────────────────\";\n    private static final String BODY_TAG = \"Body:\";\n    private static final String URL_TAG = \"URL: \";\n    private static final String METHOD_TAG = \"Method: @\";\n    private static final String HEADERS_TAG = \"Headers:\";\n    private static final String STATUS_CODE_TAG = \"Status Code: \";\n    private static final String RECEIVED_TAG = \"Received in: \";\n    private static final String CORNER_UP = \"┌ \";\n    private static final String CORNER_BOTTOM = \"└ \";\n    private static final String CENTER_LINE = \"├ \";\n    private static final String DEFAULT_LINE = \"│ \";\n\n    protected Printer() {\n        throw new UnsupportedOperationException();\n    }\n\n    private static boolean isEmpty(String line) {\n        return TextUtils.isEmpty(line) || N.equals(line) || T.equals(line) || TextUtils.isEmpty(line.trim());\n    }\n\n    static void printJsonRequest(LoggingInterceptor.Builder builder, Request request) {\n        String requestBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(request);\n        String tag = builder.getTag(true);\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, REQUEST_UP_LINE);\n        logLines(builder.getType(), tag, new String[]{URL_TAG + request.url()}, builder.getLogger(), false);\n        logLines(builder.getType(), tag, getRequest(request, builder.getLevel()), builder.getLogger(), true);\n        if (request.body() instanceof FormBody) {\n            StringBuilder formBody = new StringBuilder();\n            FormBody body = (FormBody) request.body();\n            if (body != null && body.size() != 0) {\n                for (int i = 0; i < body.size(); i++) {\n                    formBody.append(body.encodedName(i) + \"=\" + body.encodedValue(i) + \"&\");\n                }\n                formBody.delete(formBody.length() - 1, formBody.length());\n                logLines(builder.getType(), tag, new String[]{formBody.toString()}, builder.getLogger(), true);\n            }\n        }\n        if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) {\n            logLines(builder.getType(), tag, requestBody.split(LINE_SEPARATOR), builder.getLogger(), true);\n        }\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, END_LINE);\n    }\n\n    static void printJsonResponse(LoggingInterceptor.Builder builder, long chainMs, boolean isSuccessful,\n                                  int code, String headers, String bodyString, List<String> segments) {\n        String responseBody = LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + getJsonString(bodyString);\n        String tag = builder.getTag(false);\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, RESPONSE_UP_LINE);\n\n        logLines(builder.getType(), tag, getResponse(headers, chainMs, code, isSuccessful,\n                builder.getLevel(), segments), builder.getLogger(), true);\n        if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) {\n            logLines(builder.getType(), tag, responseBody.split(LINE_SEPARATOR), builder.getLogger(), true);\n        }\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, END_LINE);\n    }\n\n    static void printFileRequest(LoggingInterceptor.Builder builder, Request request) {\n        String tag = builder.getTag(true);\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, REQUEST_UP_LINE);\n        logLines(builder.getType(), tag, new String[]{URL_TAG + request.url()}, builder.getLogger(), false);\n        logLines(builder.getType(), tag, getRequest(request, builder.getLevel()), builder.getLogger(), true);\n        if (request.body() instanceof FormBody) {\n            StringBuilder formBody = new StringBuilder();\n            FormBody body = (FormBody) request.body();\n            if (body != null && body.size() != 0) {\n                for (int i = 0; i < body.size(); i++) {\n                    formBody.append(body.encodedName(i) + \"=\" + body.encodedValue(i) + \"&\");\n                }\n                formBody.delete(formBody.length() - 1, formBody.length());\n                logLines(builder.getType(), tag, new String[]{formBody.toString()}, builder.getLogger(), true);\n            }\n        }\n        if (builder.getLevel() == Level.BASIC || builder.getLevel() == Level.BODY) {\n            logLines(builder.getType(), tag, OMITTED_REQUEST, builder.getLogger(), true);\n        }\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, END_LINE);\n    }\n\n    static void printFileResponse(LoggingInterceptor.Builder builder, long chainMs, boolean isSuccessful,\n                                  int code, String headers, List<String> segments) {\n        String tag = builder.getTag(false);\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, RESPONSE_UP_LINE);\n\n        logLines(builder.getType(), tag, getResponse(headers, chainMs, code, isSuccessful,\n                builder.getLevel(), segments), builder.getLogger(), true);\n        logLines(builder.getType(), tag, OMITTED_RESPONSE, builder.getLogger(), true);\n        if (builder.getLogger() == null)\n            I.log(builder.getType(), tag, END_LINE);\n    }\n\n    private static String[] getRequest(Request request, Level level) {\n        String message;\n        String header = request.headers().toString();\n        boolean loggableHeader = level == Level.HEADERS || level == Level.BASIC;\n        message = METHOD_TAG + request.method() + DOUBLE_SEPARATOR +\n                (isEmpty(header) ? \"\" : loggableHeader ? HEADERS_TAG + LINE_SEPARATOR + dotHeaders(header) : \"\");\n        return message.split(LINE_SEPARATOR);\n    }\n\n    private static String[] getResponse(String header, long tookMs, int code, boolean isSuccessful,\n                                        Level level, List<String> segments) {\n        String message;\n        boolean loggableHeader = level == Level.HEADERS || level == Level.BASIC;\n        String segmentString = slashSegments(segments);\n        message = ((!TextUtils.isEmpty(segmentString) ? segmentString + \" - \" : \"\") + \"is success : \"\n                + isSuccessful + \" - \" + RECEIVED_TAG + tookMs + \"ms\" + DOUBLE_SEPARATOR + STATUS_CODE_TAG +\n                code + DOUBLE_SEPARATOR + (isEmpty(header) ? \"\" : loggableHeader ? HEADERS_TAG + LINE_SEPARATOR +\n                dotHeaders(header) : \"\"));\n        return message.split(LINE_SEPARATOR);\n    }\n\n    private static String slashSegments(List<String> segments) {\n        StringBuilder segmentString = new StringBuilder();\n        for (String segment : segments) {\n            segmentString.append(\"/\").append(segment);\n        }\n        return segmentString.toString();\n    }\n\n    private static String dotHeaders(String header) {\n        String[] headers = header.split(LINE_SEPARATOR);\n        StringBuilder builder = new StringBuilder();\n        String tag = \"─ \";\n        if (headers.length > 1) {\n            for (int i = 0; i < headers.length; i++) {\n                if (i == 0) {\n                    tag = CORNER_UP;\n                } else if (i == headers.length - 1) {\n                    tag = CORNER_BOTTOM;\n                } else {\n                    tag = CENTER_LINE;\n                }\n                builder.append(tag).append(headers[i]).append(\"\\n\");\n            }\n        } else {\n            for (String item : headers) {\n                builder.append(tag).append(item).append(\"\\n\");\n            }\n        }\n        return builder.toString();\n    }\n\n    private static void logLines(int type, String tag, String[] lines, Logger logger, boolean withLineSize) {\n        for (String line : lines) {\n            int lineLength = line.length();\n            int MAX_LONG_SIZE = withLineSize ? 110 : lineLength;\n            for (int i = 0; i <= lineLength / MAX_LONG_SIZE; i++) {\n                int start = i * MAX_LONG_SIZE;\n                int end = (i + 1) * MAX_LONG_SIZE;\n                end = end > line.length() ? line.length() : end;\n                if (logger == null) {\n                    I.log(type, tag, DEFAULT_LINE + line.substring(start, end));\n                } else {\n                    logger.log(type, tag, line.substring(start, end));\n                }\n            }\n        }\n    }\n\n    private static String bodyToString(final Request request) {\n        try {\n            final Request copy = request.newBuilder().build();\n            final Buffer buffer = new Buffer();\n            if (copy.body() == null)\n                return \"\";\n            copy.body().writeTo(buffer);\n            return getJsonString(buffer.readUtf8());\n        } catch (final IOException e) {\n            return \"{\\\"err\\\": \\\"\" + e.getMessage() + \"\\\"}\";\n        }\n    }\n\n    static String getJsonString(String msg) {\n        String message;\n        try {\n            if (msg.startsWith(\"{\")) {\n                JSONObject jsonObject = new JSONObject(msg);\n                message = jsonObject.toString(JSON_INDENT);\n            } else if (msg.startsWith(\"[\")) {\n                JSONArray jsonArray = new JSONArray(msg);\n                message = jsonArray.toString(JSON_INDENT);\n            } else {\n                message = msg;\n            }\n        } catch (JSONException e) {\n            message = msg;\n        }\n        return message;\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/CloseUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport java.io.Closeable;\nimport java.io.IOException;\n\n/**\n * Created by goldze on 2017/5/14.\n * 关闭相关工具类\n */\npublic final class CloseUtils {\n\n    private CloseUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * 关闭IO\n     *\n     * @param closeables closeables\n     */\n    public static void closeIO(final Closeable... closeables) {\n        if (closeables == null) return;\n        for (Closeable closeable : closeables) {\n            if (closeable != null) {\n                try {\n                    closeable.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n\n    /**\n     * 安静关闭IO\n     *\n     * @param closeables closeables\n     */\n    public static void closeIOQuietly(final Closeable... closeables) {\n        if (closeables == null) return;\n        for (Closeable closeable : closeables) {\n            if (closeable != null) {\n                try {\n                    closeable.close();\n                } catch (IOException ignored) {\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/ConvertUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.annotation.SuppressLint;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.PixelFormat;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.view.View;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.UnsupportedEncodingException;\n\nimport me.goldze.mvvmhabit.utils.constant.MemoryConstants;\nimport me.goldze.mvvmhabit.utils.constant.TimeConstants;\n\n/**\n * Created by goldze on 2017/5/14.\n * 转换相关工具类\n */\npublic final class ConvertUtils {\n\n    private ConvertUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    private static final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};\n\n    /**\n     * byteArr转hexString\n     * <p>例如：</p>\n     * bytes2HexString(new byte[] { 0, (byte) 0xa8 }) returns 00A8\n     *\n     * @param bytes 字节数组\n     * @return 16进制大写字符串\n     */\n    public static String bytes2HexString(final byte[] bytes) {\n        if (bytes == null) return null;\n        int len = bytes.length;\n        if (len <= 0) return null;\n        char[] ret = new char[len << 1];\n        for (int i = 0, j = 0; i < len; i++) {\n            ret[j++] = hexDigits[bytes[i] >>> 4 & 0x0f];\n            ret[j++] = hexDigits[bytes[i] & 0x0f];\n        }\n        return new String(ret);\n    }\n\n    /**\n     * hexString转byteArr\n     * <p>例如：</p>\n     * hexString2Bytes(\"00A8\") returns { 0, (byte) 0xA8 }\n     *\n     * @param hexString 十六进制字符串\n     * @return 字节数组\n     */\n    public static byte[] hexString2Bytes(String hexString) {\n        if (isSpace(hexString)) return null;\n        int len = hexString.length();\n        if (len % 2 != 0) {\n            hexString = \"0\" + hexString;\n            len = len + 1;\n        }\n        char[] hexBytes = hexString.toUpperCase().toCharArray();\n        byte[] ret = new byte[len >> 1];\n        for (int i = 0; i < len; i += 2) {\n            ret[i >> 1] = (byte) (hex2Dec(hexBytes[i]) << 4 | hex2Dec(hexBytes[i + 1]));\n        }\n        return ret;\n    }\n\n    /**\n     * hexChar转int\n     *\n     * @param hexChar hex单个字节\n     * @return 0..15\n     */\n    private static int hex2Dec(final char hexChar) {\n        if (hexChar >= '0' && hexChar <= '9') {\n            return hexChar - '0';\n        } else if (hexChar >= 'A' && hexChar <= 'F') {\n            return hexChar - 'A' + 10;\n        } else {\n            throw new IllegalArgumentException();\n        }\n    }\n\n    /**\n     * charArr转byteArr\n     *\n     * @param chars 字符数组\n     * @return 字节数组\n     */\n    public static byte[] chars2Bytes(final char[] chars) {\n        if (chars == null || chars.length <= 0) return null;\n        int len = chars.length;\n        byte[] bytes = new byte[len];\n        for (int i = 0; i < len; i++) {\n            bytes[i] = (byte) (chars[i]);\n        }\n        return bytes;\n    }\n\n    /**\n     * byteArr转charArr\n     *\n     * @param bytes 字节数组\n     * @return 字符数组\n     */\n    public static char[] bytes2Chars(final byte[] bytes) {\n        if (bytes == null) return null;\n        int len = bytes.length;\n        if (len <= 0) return null;\n        char[] chars = new char[len];\n        for (int i = 0; i < len; i++) {\n            chars[i] = (char) (bytes[i] & 0xff);\n        }\n        return chars;\n    }\n\n    /**\n     * 以unit为单位的内存大小转字节数\n     *\n     * @param memorySize 大小\n     * @param unit 单位类型\n     * <ul>\n     * <li>{@link MemoryConstants#BYTE}: 字节</li>\n     * <li>{@link MemoryConstants#KB}  : 千字节</li>\n     * <li>{@link MemoryConstants#MB}  : 兆</li>\n     * <li>{@link MemoryConstants#GB}  : GB</li>\n     * </ul>\n     * @return 字节数\n     */\n    public static long memorySize2Byte(final long memorySize, @MemoryConstants.Unit final int unit) {\n        if (memorySize < 0) return -1;\n        return memorySize * unit;\n    }\n\n    /**\n     * 字节数转以unit为单位的内存大小\n     *\n     * @param byteNum 字节数\n     * @param unit 单位类型\n     * <ul>\n     * <li>{@link MemoryConstants#BYTE}: 字节</li>\n     * <li>{@link MemoryConstants#KB}  : 千字节</li>\n     * <li>{@link MemoryConstants#MB}  : 兆</li>\n     * <li>{@link MemoryConstants#GB}  : GB</li>\n     * </ul>\n     * @return 以unit为单位的size\n     */\n    public static double byte2MemorySize(final long byteNum, @MemoryConstants.Unit final int unit) {\n        if (byteNum < 0) return -1;\n        return (double) byteNum / unit;\n    }\n\n    /**\n     * 字节数转合适内存大小\n     * <p>保留3位小数</p>\n     *\n     * @param byteNum 字节数\n     * @return 合适内存大小\n     */\n    @SuppressLint(\"DefaultLocale\")\n    public static String byte2FitMemorySize(final long byteNum) {\n        if (byteNum < 0) {\n            return \"shouldn't be less than zero!\";\n        } else if (byteNum < MemoryConstants.KB) {\n            return String.format(\"%.3fB\", (double) byteNum + 0.0005);\n        } else if (byteNum < MemoryConstants.MB) {\n            return String.format(\"%.3fKB\", (double) byteNum / MemoryConstants.KB + 0.0005);\n        } else if (byteNum < MemoryConstants.GB) {\n            return String.format(\"%.3fMB\", (double) byteNum / MemoryConstants.MB + 0.0005);\n        } else {\n            return String.format(\"%.3fGB\", (double) byteNum / MemoryConstants.GB + 0.0005);\n        }\n    }\n\n    /**\n     * 以unit为单位的时间长度转毫秒时间戳\n     *\n     * @param timeSpan 毫秒时间戳\n     * @param unit 单位类型\n     * <ul>\n     * <li>{@link TimeConstants#MSEC}: 毫秒</li>\n     * <li>{@link TimeConstants#SEC }: 秒</li>\n     * <li>{@link TimeConstants#MIN }: 分</li>\n     * <li>{@link TimeConstants#HOUR}: 小时</li>\n     * <li>{@link TimeConstants#DAY }: 天</li>\n     * </ul>\n     * @return 毫秒时间戳\n     */\n    public static long timeSpan2Millis(final long timeSpan, @TimeConstants.Unit final int unit) {\n        return timeSpan * unit;\n    }\n\n    /**\n     * 毫秒时间戳转以unit为单位的时间长度\n     *\n     * @param millis 毫秒时间戳\n     * @param unit 单位类型\n     * <ul>\n     * <li>{@link TimeConstants#MSEC}: 毫秒</li>\n     * <li>{@link TimeConstants#SEC }: 秒</li>\n     * <li>{@link TimeConstants#MIN }: 分</li>\n     * <li>{@link TimeConstants#HOUR}: 小时</li>\n     * <li>{@link TimeConstants#DAY }: 天</li>\n     * </ul>\n     * @return 以unit为单位的时间长度\n     */\n    public static long millis2TimeSpan(final long millis, @TimeConstants.Unit final int unit) {\n        return millis / unit;\n    }\n\n    /**\n     * 毫秒时间戳转合适时间长度\n     *\n     * @param millis 毫秒时间戳\n     * <p>小于等于0，返回null</p>\n     * @param precision 精度\n     * <ul>\n     * <li>precision = 0，返回null</li>\n     * <li>precision = 1，返回天</li>\n     * <li>precision = 2，返回天和小时</li>\n     * <li>precision = 3，返回天、小时和分钟</li>\n     * <li>precision = 4，返回天、小时、分钟和秒</li>\n     * <li>precision &gt;= 5，返回天、小时、分钟、秒和毫秒</li>\n     * </ul>\n     * @return 合适时间长度\n     */\n    @SuppressLint(\"DefaultLocale\")\n    public static String millis2FitTimeSpan(long millis, int precision) {\n        if (millis <= 0 || precision <= 0) return null;\n        StringBuilder sb = new StringBuilder();\n        String[] units = {\"天\", \"小时\", \"分钟\", \"秒\", \"毫秒\"};\n        int[] unitLen = {86400000, 3600000, 60000, 1000, 1};\n        precision = Math.min(precision, 5);\n        for (int i = 0; i < precision; i++) {\n            if (millis >= unitLen[i]) {\n                long mode = millis / unitLen[i];\n                millis -= mode * unitLen[i];\n                sb.append(mode).append(units[i]);\n            }\n        }\n        return sb.toString();\n    }\n\n    /**\n     * bytes转bits\n     *\n     * @param bytes 字节数组\n     * @return bits\n     */\n    public static String bytes2Bits(final byte[] bytes) {\n        StringBuilder sb = new StringBuilder();\n        for (byte aByte : bytes) {\n            for (int j = 7; j >= 0; --j) {\n                sb.append(((aByte >> j) & 0x01) == 0 ? '0' : '1');\n            }\n        }\n        return sb.toString();\n    }\n\n    /**\n     * bits转bytes\n     *\n     * @param bits 二进制\n     * @return bytes\n     */\n    public static byte[] bits2Bytes(String bits) {\n        int lenMod = bits.length() % 8;\n        int byteLen = bits.length() / 8;\n        // 不是8的倍数前面补0\n        if (lenMod != 0) {\n            for (int i = lenMod; i < 8; i++) {\n                bits = \"0\" + bits;\n            }\n            byteLen++;\n        }\n        byte[] bytes = new byte[byteLen];\n        for (int i = 0; i < byteLen; ++i) {\n            for (int j = 0; j < 8; ++j) {\n                bytes[i] <<= 1;\n                bytes[i] |= bits.charAt(i * 8 + j) - '0';\n            }\n        }\n        return bytes;\n    }\n\n    /**\n     * inputStream转outputStream\n     *\n     * @param is 输入流\n     * @return outputStream子类\n     */\n    public static ByteArrayOutputStream input2OutputStream(final InputStream is) {\n        if (is == null) return null;\n        try {\n            ByteArrayOutputStream os = new ByteArrayOutputStream();\n            byte[] b = new byte[MemoryConstants.KB];\n            int len;\n            while ((len = is.read(b, 0, MemoryConstants.KB)) != -1) {\n                os.write(b, 0, len);\n            }\n            return os;\n        } catch (IOException e) {\n            e.printStackTrace();\n            return null;\n        } finally {\n            CloseUtils.closeIO(is);\n        }\n    }\n\n    /**\n     * outputStream转inputStream\n     *\n     * @param out 输出流\n     * @return inputStream子类\n     */\n    public ByteArrayInputStream output2InputStream(final OutputStream out) {\n        if (out == null) return null;\n        return new ByteArrayInputStream(((ByteArrayOutputStream) out).toByteArray());\n    }\n\n    /**\n     * inputStream转byteArr\n     *\n     * @param is 输入流\n     * @return 字节数组\n     */\n    public static byte[] inputStream2Bytes(final InputStream is) {\n        if (is == null) return null;\n        return input2OutputStream(is).toByteArray();\n    }\n\n    /**\n     * byteArr转inputStream\n     *\n     * @param bytes 字节数组\n     * @return 输入流\n     */\n    public static InputStream bytes2InputStream(final byte[] bytes) {\n        if (bytes == null || bytes.length <= 0) return null;\n        return new ByteArrayInputStream(bytes);\n    }\n\n    /**\n     * outputStream转byteArr\n     *\n     * @param out 输出流\n     * @return 字节数组\n     */\n    public static byte[] outputStream2Bytes(final OutputStream out) {\n        if (out == null) return null;\n        return ((ByteArrayOutputStream) out).toByteArray();\n    }\n\n    /**\n     * outputStream转byteArr\n     *\n     * @param bytes 字节数组\n     * @return 字节数组\n     */\n    public static OutputStream bytes2OutputStream(final byte[] bytes) {\n        if (bytes == null || bytes.length <= 0) return null;\n        ByteArrayOutputStream os = null;\n        try {\n            os = new ByteArrayOutputStream();\n            os.write(bytes);\n            return os;\n        } catch (IOException e) {\n            e.printStackTrace();\n            return null;\n        } finally {\n            CloseUtils.closeIO(os);\n        }\n    }\n\n    /**\n     * inputStream转string按编码\n     *\n     * @param is 输入流\n     * @param charsetName 编码格式\n     * @return 字符串\n     */\n    public static String inputStream2String(final InputStream is, final String charsetName) {\n        if (is == null || isSpace(charsetName)) return null;\n        try {\n            return new String(inputStream2Bytes(is), charsetName);\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    /**\n     * string转inputStream按编码\n     *\n     * @param string 字符串\n     * @param charsetName 编码格式\n     * @return 输入流\n     */\n    public static InputStream string2InputStream(final String string, final String charsetName) {\n        if (string == null || isSpace(charsetName)) return null;\n        try {\n            return new ByteArrayInputStream(string.getBytes(charsetName));\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    /**\n     * outputStream转string按编码\n     *\n     * @param out 输出流\n     * @param charsetName 编码格式\n     * @return 字符串\n     */\n    public static String outputStream2String(final OutputStream out, final String charsetName) {\n        if (out == null || isSpace(charsetName)) return null;\n        try {\n            return new String(outputStream2Bytes(out), charsetName);\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    /**\n     * string转outputStream按编码\n     *\n     * @param string 字符串\n     * @param charsetName 编码格式\n     * @return 输入流\n     */\n    public static OutputStream string2OutputStream(final String string, final String charsetName) {\n        if (string == null || isSpace(charsetName)) return null;\n        try {\n            return bytes2OutputStream(string.getBytes(charsetName));\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    /**\n     * bitmap转byteArr\n     *\n     * @param bitmap bitmap对象\n     * @param format 格式\n     * @return 字节数组\n     */\n    public static byte[] bitmap2Bytes(final Bitmap bitmap, final Bitmap.CompressFormat format) {\n        if (bitmap == null) return null;\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        bitmap.compress(format, 100, baos);\n        return baos.toByteArray();\n    }\n\n    /**\n     * byteArr转bitmap\n     *\n     * @param bytes 字节数组\n     * @return bitmap\n     */\n    public static Bitmap bytes2Bitmap(final byte[] bytes) {\n        return (bytes == null || bytes.length == 0) ? null : BitmapFactory.decodeByteArray(bytes, 0, bytes.length);\n    }\n\n    /**\n     * drawable转bitmap\n     *\n     * @param drawable drawable对象\n     * @return bitmap\n     */\n    public static Bitmap drawable2Bitmap(final Drawable drawable) {\n        if (drawable instanceof BitmapDrawable) {\n            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;\n            if (bitmapDrawable.getBitmap() != null) {\n                return bitmapDrawable.getBitmap();\n            }\n        }\n        Bitmap bitmap;\n        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {\n            bitmap = Bitmap.createBitmap(1, 1,\n                    drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);\n        } else {\n            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),\n                    drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);\n        }\n        Canvas canvas = new Canvas(bitmap);\n        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());\n        drawable.draw(canvas);\n        return bitmap;\n    }\n\n    /**\n     * bitmap转drawable\n     *\n     * @param bitmap bitmap对象\n     * @return drawable\n     */\n    public static Drawable bitmap2Drawable(final Bitmap bitmap) {\n        return bitmap == null ? null : new BitmapDrawable(Utils.getContext().getResources(), bitmap);\n    }\n\n    /**\n     * drawable转byteArr\n     *\n     * @param drawable drawable对象\n     * @param format 格式\n     * @return 字节数组\n     */\n    public static byte[] drawable2Bytes(final Drawable drawable, final Bitmap.CompressFormat format) {\n        return drawable == null ? null : bitmap2Bytes(drawable2Bitmap(drawable), format);\n    }\n\n    /**\n     * byteArr转drawable\n     *\n     * @param bytes 字节数组\n     * @return drawable\n     */\n    public static Drawable bytes2Drawable(final byte[] bytes) {\n        return bytes == null ? null : bitmap2Drawable(bytes2Bitmap(bytes));\n    }\n\n    /**\n     * view转Bitmap\n     *\n     * @param view 视图\n     * @return bitmap\n     */\n    public static Bitmap view2Bitmap(final View view) {\n        if (view == null) return null;\n        Bitmap ret = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);\n        Canvas canvas = new Canvas(ret);\n        Drawable bgDrawable = view.getBackground();\n        if (bgDrawable != null) {\n            bgDrawable.draw(canvas);\n        } else {\n            canvas.drawColor(Color.WHITE);\n        }\n        view.draw(canvas);\n        return ret;\n    }\n\n    /**\n     * dp转px\n     *\n     * @param dpValue dp值\n     * @return px值\n     */\n    public static int dp2px(final float dpValue) {\n        final float scale = Utils.getContext().getResources().getDisplayMetrics().density;\n        return (int) (dpValue * scale + 0.5f);\n    }\n\n    /**\n     * px转dp\n     *\n     * @param pxValue px值\n     * @return dp值\n     */\n    public static int px2dp(final float pxValue) {\n        final float scale = Utils.getContext().getResources().getDisplayMetrics().density;\n        return (int) (pxValue / scale + 0.5f);\n    }\n\n    /**\n     * sp转px\n     *\n     * @param spValue sp值\n     * @return px值\n     */\n    public static int sp2px(final float spValue) {\n        final float fontScale = Utils.getContext().getResources().getDisplayMetrics().scaledDensity;\n        return (int) (spValue * fontScale + 0.5f);\n    }\n\n    /**\n     * px转sp\n     *\n     * @param pxValue px值\n     * @return sp值\n     */\n    public static int px2sp(final float pxValue) {\n        final float fontScale = Utils.getContext().getResources().getDisplayMetrics().scaledDensity;\n        return (int) (pxValue / fontScale + 0.5f);\n    }\n\n    /**\n     * 判断字符串是否为null或全为空白字符\n     *\n     * @param s 待校验字符串\n     * @return {@code true}: null或全空白字符<br> {@code false}: 不为null且不全空白字符\n     */\n    private static boolean isSpace(final String s) {\n        if (s == null) return true;\n        for (int i = 0, len = s.length(); i < len; ++i) {\n            if (!Character.isWhitespace(s.charAt(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/ImageUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.app.Activity;\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.graphics.Bitmap;\nimport android.graphics.Bitmap.CompressFormat;\nimport android.graphics.Bitmap.Config;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.graphics.LinearGradient;\nimport android.graphics.Matrix;\nimport android.graphics.Paint;\nimport android.graphics.PixelFormat;\nimport android.graphics.PorterDuff.Mode;\nimport android.graphics.PorterDuffXfermode;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.graphics.Shader.TileMode;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.os.Environment;\nimport android.provider.MediaStore;\nimport android.provider.MediaStore.MediaColumns;\nimport android.text.TextUtils;\nimport android.util.DisplayMetrics;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.sql.Timestamp;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Locale;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableSource;\nimport io.reactivex.Observer;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.functions.Consumer;\nimport io.reactivex.functions.Function;\nimport io.reactivex.schedulers.Schedulers;\nimport me.goldze.mvvmhabit.utils.compression.Luban;\n\nimport static me.goldze.mvvmhabit.utils.Utils.getContext;\n\n/**\n * Created by goldze on 2017/7/17.\n * 图片相关工具类,包含图片压缩,图片缩放,图片裁剪等功能\n */\npublic class ImageUtils {\n\n    private static final float MAX_SIZE = 200;//接受的最大图片尺寸为200k，200k以上的图片压缩到200k一下\n\n    public final static String SDCARD_MNT = \"/mnt/sdcard\";\n    public final static String SDCARD = \"/sdcard\";\n\n    /** 请求相册 */\n    public static final int REQUEST_CODE_GETIMAGE_BYSDCARD = 0;\n    \n    public static final int REQUEST_CODE_GETIMAGE_BYSDCARD_info = 4;\n    \n    /** 请求相机 */\n    public static final int REQUEST_CODE_GETIMAGE_BYCAMERA = 1;\n    /** 请求裁剪 */\n    public static final int REQUEST_CODE_GETIMAGE_BYCROP = 2;\n    /** 从图片浏览界面发送动弹 */\n    public static final int REQUEST_CODE_GETIMAGE_IMAGEPAVER = 3;\n\n    /**\n     * 写图片文件 在Android系统中，文件保存在 /data/data/PACKAGE_NAME/files 目录下\n     * \n     * @throws IOException\n     */\n    public static void saveImage(Context context, String fileName, Bitmap bitmap)\n            throws IOException {\n        saveImage(context, fileName, bitmap, 100);\n    }\n\n    public static void saveImage(Context context, String fileName,\n                                 Bitmap bitmap, int quality) throws IOException {\n        if (bitmap == null || fileName == null || context == null)\n            return;\n\n        FileOutputStream fos = context.openFileOutput(fileName,\n                Context.MODE_PRIVATE);\n        ByteArrayOutputStream stream = new ByteArrayOutputStream();\n        bitmap.compress(CompressFormat.JPEG, quality, stream);\n        byte[] bytes = stream.toByteArray();\n        fos.write(bytes);\n        fos.close();\n    }\n\n    /**\n     * 写图片文件到SD卡\n     * \n     * @throws IOException\n     */\n    public static void saveImageToSD(Context ctx, String filePath,\n                                     Bitmap bitmap, int quality) throws IOException {\n        if (bitmap != null) {\n            File file = new File(filePath.substring(0,\n                    filePath.lastIndexOf(File.separator)));\n            if (!file.exists()) {\n                file.mkdirs();\n            }\n            BufferedOutputStream bos = new BufferedOutputStream(\n                    new FileOutputStream(filePath));\n            bitmap.compress(CompressFormat.JPEG, quality, bos);\n            bos.flush();\n            bos.close();\n            if (ctx != null) {\n                scanPhoto(ctx, filePath);\n            }\n        }\n    }\n\n    public static void saveBackgroundImage(Context ctx, String filePath,\n                                           Bitmap bitmap, int quality) throws IOException {\n        if (bitmap != null) {\n            File file = new File(filePath.substring(0,\n                    filePath.lastIndexOf(File.separator)));\n            if (!file.exists()) {\n                file.mkdirs();\n            }\n            BufferedOutputStream bos = new BufferedOutputStream(\n                    new FileOutputStream(filePath));\n            bitmap.compress(CompressFormat.PNG, quality, bos);\n            bos.flush();\n            bos.close();\n            if (ctx != null) {\n                scanPhoto(ctx, filePath);\n            }\n        }\n    }\n\n    /**\n     * 让Gallery上能马上看到该图片\n     */\n    private static void scanPhoto(Context ctx, String imgFileName) {\n        Intent mediaScanIntent = new Intent(\n                Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);\n        File file = new File(imgFileName);\n        Uri contentUri = Uri.fromFile(file);\n        mediaScanIntent.setData(contentUri);\n        ctx.sendBroadcast(mediaScanIntent);\n    }\n\n    /**\n     * 获取bitmap\n     * \n     * @param context\n     * @param fileName\n     * @return\n     */\n    public static Bitmap getBitmap(Context context, String fileName) {\n        FileInputStream fis = null;\n        Bitmap bitmap = null;\n        try {\n            fis = context.openFileInput(fileName);\n            bitmap = BitmapFactory.decodeStream(fis);\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n        } catch (OutOfMemoryError e) {\n            e.printStackTrace();\n        } finally {\n            try {\n                fis.close();\n            } catch (Exception e) {\n            }\n        }\n        return bitmap;\n    }\n\n    /**\n     * 获取bitmap\n     * \n     * @param filePath\n     * @return\n     */\n    public static Bitmap getBitmapByPath(String filePath) {\n        return getBitmapByPath(filePath, null);\n    }\n\n    public static Bitmap getBitmapByPath(String filePath,\n                                         BitmapFactory.Options opts) {\n        FileInputStream fis = null;\n        Bitmap bitmap = null;\n        try {\n            File file = new File(filePath);\n            fis = new FileInputStream(file);\n            bitmap = BitmapFactory.decodeStream(fis, null, opts);\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n        } catch (OutOfMemoryError e) {\n            e.printStackTrace();\n        } finally {\n            try {\n                fis.close();\n            } catch (Exception e) {\n            }\n        }\n        return bitmap;\n    }\n\n    /**\n     * 获取bitmap\n     * \n     * @param file\n     * @return\n     */\n    public static Bitmap getBitmapByFile(File file) {\n        FileInputStream fis = null;\n        Bitmap bitmap = null;\n        try {\n            fis = new FileInputStream(file);\n            bitmap = BitmapFactory.decodeStream(fis);\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n        } catch (OutOfMemoryError e) {\n            e.printStackTrace();\n        } finally {\n            try {\n                fis.close();\n            } catch (Exception e) {\n            }\n        }\n        return bitmap;\n    }\n\n    /**\n     * 使用当前时间戳拼接一个唯一的文件名\n     * \n     * @param\n     * @return\n     */\n    public static String getTempFileName() {\n        SimpleDateFormat format = new SimpleDateFormat(\"yyyy-MM-dd_HH-mm-ss_SS\");\n        String fileName = format.format(new Timestamp(System\n                .currentTimeMillis()));\n        return fileName;\n    }\n\n    /**\n     * 获取照相机使用的目录\n     *\n     * @return\n     */\n    public static String getCamerPath() {\n        return Environment.getExternalStorageDirectory() + File.separator\n                + \"FounderNews\" + File.separator;\n    }\n\n    /**\n     * 判断当前Url是否标准的content://样式，如果不是，则返回绝对路径\n     *\n     * @param\n     * @return\n     */\n    public static String getAbsolutePathFromNoStandardUri(Uri mUri) {\n        String filePath = null;\n\n        String mUriString = mUri.toString();\n        mUriString = Uri.decode(mUriString);\n\n        String pre1 = \"file://\" + SDCARD + File.separator;\n        String pre2 = \"file://\" + SDCARD_MNT + File.separator;\n\n        if (mUriString.startsWith(pre1)) {\n            filePath = Environment.getExternalStorageDirectory().getPath()\n                    + File.separator + mUriString.substring(pre1.length());\n        } else if (mUriString.startsWith(pre2)) {\n            filePath = Environment.getExternalStorageDirectory().getPath()\n                    + File.separator + mUriString.substring(pre2.length());\n        }\n        return filePath;\n    }\n\n    /**\n     * 通过uri获取文件的绝对路径\n     *\n     * @param uri\n     * @return\n     */\n    @SuppressWarnings(\"deprecation\")\n    public static String getAbsoluteImagePath(Activity context, Uri uri) {\n        String imagePath = \"\";\n        String[] proj = { MediaStore.Images.Media.DATA };\n        Cursor cursor = context.managedQuery(uri, proj, // Which columns to\n                                                        // return\n                null, // WHERE clause; which rows to return (all rows)\n                null, // WHERE clause selection arguments (none)\n                null); // Order-by clause (ascending by name)\n\n        if (cursor != null) {\n            int column_index = cursor\n                    .getColumnIndexOrThrow(MediaStore.Images.Media.DATA);\n            if (cursor.getCount() > 0 && cursor.moveToFirst()) {\n                imagePath = cursor.getString(column_index);\n            }\n        }\n\n        return imagePath;\n    }\n\n    /**\n     * 获取图片缩略图 只有Android2.1以上版本支持\n     *\n     * @param imgName\n     * @param kind\n     *            MediaStore.Images.Thumbnails.MICRO_KIND\n     * @return\n     */\n    @SuppressWarnings(\"deprecation\")\n    public static Bitmap loadImgThumbnail(Activity context, String imgName,\n                                          int kind) {\n        Bitmap bitmap = null;\n\n        String[] proj = { MediaStore.Images.Media._ID,\n                MediaStore.Images.Media.DISPLAY_NAME };\n\n        Cursor cursor = context.managedQuery(\n                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, proj,\n                MediaStore.Images.Media.DISPLAY_NAME + \"='\" + imgName + \"'\",\n                null, null);\n\n        if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {\n            ContentResolver crThumb = context.getContentResolver();\n            BitmapFactory.Options options = new BitmapFactory.Options();\n            options.inSampleSize = 1;\n            bitmap = MediaStore.Images.Thumbnails.getThumbnail(crThumb, cursor.getInt(0),\n                    kind, options);\n        }\n        return bitmap;\n    }\n\n    public static Bitmap loadImgThumbnail(String filePath, int w, int h) {\n        Bitmap bitmap = getBitmapByPath(filePath);\n        return zoomBitmap(bitmap, w, h);\n    }\n\n    /**\n     * 获取SD卡中最新图片路径\n     *\n     * @return\n     */\n    public static String getLatestImage(Activity context) {\n        String latestImage = null;\n        String[] items = { MediaStore.Images.Media._ID,\n                MediaStore.Images.Media.DATA };\n        Cursor cursor = context.managedQuery(\n                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, items, null,\n                null, MediaStore.Images.Media._ID + \" desc\");\n\n        if (cursor != null && cursor.getCount() > 0) {\n            cursor.moveToFirst();\n            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor\n                    .moveToNext()) {\n                latestImage = cursor.getString(1);\n                break;\n            }\n        }\n\n        return latestImage;\n    }\n\n    /**\n     * 计算缩放图片的宽高\n     *\n     * @param img_size\n     * @param square_size\n     * @return\n     */\n    public static int[] scaleImageSize(int[] img_size, int square_size) {\n        if (img_size[0] <= square_size && img_size[1] <= square_size)\n            return img_size;\n        double ratio = square_size\n                / (double) Math.max(img_size[0], img_size[1]);\n        return new int[] { (int) (img_size[0] * ratio),\n                (int) (img_size[1] * ratio) };\n    }\n\n    /**\n     * 创建缩略图\n     *\n     * @param context\n     * @param largeImagePath\n     *            原始大图路径\n     * @param thumbfilePath\n     *            输出缩略图路径\n     * @param square_size\n     *            输出图片宽度\n     * @param quality\n     *            输出图片质量\n     * @throws IOException\n     */\n    public static void createImageThumbnail(Context context,\n                                            String largeImagePath, String thumbfilePath, int square_size,\n                                            int quality) throws IOException {\n        BitmapFactory.Options opts = new BitmapFactory.Options();\n        opts.inSampleSize = 1;\n        // 原始图片bitmap\n        Bitmap cur_bitmap = getBitmapByPath(largeImagePath, opts);\n\n        if (cur_bitmap == null)\n            return;\n\n        // 原始图片的高宽\n        int[] cur_img_size = new int[] { cur_bitmap.getWidth(),\n                cur_bitmap.getHeight() };\n        // 计算原始图片缩放后的宽高\n        int[] new_img_size = scaleImageSize(cur_img_size, square_size);\n        // 生成缩放后的bitmap\n        Bitmap thb_bitmap = zoomBitmap(cur_bitmap, new_img_size[0],\n                new_img_size[1]);\n        // 生成缩放后的图片文件\n        saveImageToSD(null, thumbfilePath, thb_bitmap, quality);\n    }\n\n    /**\n     * 放大缩小图片\n     *\n     * @param bitmap\n     * @param w\n     * @param h\n     * @return\n     */\n    public static Bitmap zoomBitmap(Bitmap bitmap, int w, int h) {\n        Bitmap newbmp = null;\n        if (bitmap != null) {\n            int width = bitmap.getWidth();\n            int height = bitmap.getHeight();\n            Matrix matrix = new Matrix();\n            float scaleWidht = ((float) w / width);\n            float scaleHeight = ((float) h / height);\n            matrix.postScale(scaleWidht, scaleHeight);\n            newbmp = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix,\n                    true);\n        }\n        return newbmp;\n    }\n\n    public static Bitmap scaleBitmap(Bitmap bitmap) {\n        // 获取这个图片的宽和高\n        int width = bitmap.getWidth();\n        int height = bitmap.getHeight();\n        // 定义预转换成的图片的宽度和高度\n        int newWidth = 200;\n        int newHeight = 200;\n        // 计算缩放率，新尺寸除原始尺寸\n        float scaleWidth = ((float) newWidth) / width;\n        float scaleHeight = ((float) newHeight) / height;\n        // 创建操作图片用的matrix对象\n        Matrix matrix = new Matrix();\n        // 缩放图片动作\n        matrix.postScale(scaleWidth, scaleHeight);\n        // 旋转图片 动作\n        // matrix.postRotate(45);\n        // 创建新的图片\n        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height,\n                matrix, true);\n        return resizedBitmap;\n    }\n\n    /**\n     * (缩放)重绘图片\n     *\n     * @param context\n     *            Activity\n     * @param bitmap\n     * @return\n     */\n    public static Bitmap reDrawBitMap(Activity context, Bitmap bitmap) {\n        DisplayMetrics dm = new DisplayMetrics();\n        context.getWindowManager().getDefaultDisplay().getMetrics(dm);\n        int rHeight = dm.heightPixels;\n        int rWidth = dm.widthPixels;\n        // float rHeight=dm.heightPixels/dm.density+0.5f;\n        // float rWidth=dm.widthPixels/dm.density+0.5f;\n        // int height=bitmap.getScaledHeight(dm);\n        // int width = bitmap.getScaledWidth(dm);\n        int height = bitmap.getHeight();\n        int width = bitmap.getWidth();\n        float zoomScale;\n\n        /** 方式3 **/\n        if (width >= rWidth)\n            zoomScale = ((float) rWidth) / width;\n        else\n            zoomScale = 1.0f;\n        // 创建操作图片用的matrix对象\n        Matrix matrix = new Matrix();\n        // 缩放图片动作\n        matrix.postScale(zoomScale, zoomScale);\n        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,\n                bitmap.getWidth(), bitmap.getHeight(), matrix, true);\n        return resizedBitmap;\n    }\n\n    /**\n     * 将Drawable转化为Bitmap\n     *\n     * @param drawable\n     * @return\n     */\n    public static Bitmap drawableToBitmap(Drawable drawable) {\n        int width = drawable.getIntrinsicWidth();\n        int height = drawable.getIntrinsicHeight();\n        Bitmap bitmap = Bitmap.createBitmap(width, height, drawable\n                .getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8888\n                : Config.RGB_565);\n        Canvas canvas = new Canvas(bitmap);\n        drawable.setBounds(0, 0, width, height);\n        drawable.draw(canvas);\n        return bitmap;\n\n    }\n\n    /**\n     * 获得圆角图片的方法\n     *\n     * @param bitmap\n     * @param roundPx\n     *            一般设成14\n     * @return\n     */\n    public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, float roundPx) {\n\n        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),\n                bitmap.getHeight(), Config.ARGB_8888);\n        Canvas canvas = new Canvas(output);\n\n        final int color = 0xff424242;\n        final Paint paint = new Paint();\n        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());\n        final RectF rectF = new RectF(rect);\n\n        paint.setAntiAlias(true);\n        canvas.drawARGB(0, 0, 0, 0);\n        paint.setColor(color);\n        canvas.drawRoundRect(rectF, roundPx, roundPx, paint);\n\n        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));\n        canvas.drawBitmap(bitmap, rect, rect, paint);\n\n        return output;\n    }\n\n    /**\n     * 获得带倒影的图片方法\n     *\n     * @param bitmap\n     * @return\n     */\n    public static Bitmap createReflectionImageWithOrigin(Bitmap bitmap) {\n        final int reflectionGap = 4;\n        int width = bitmap.getWidth();\n        int height = bitmap.getHeight();\n\n        Matrix matrix = new Matrix();\n        matrix.preScale(1, -1);\n\n        Bitmap reflectionImage = Bitmap.createBitmap(bitmap, 0, height / 2,\n                width, height / 2, matrix, false);\n\n        Bitmap bitmapWithReflection = Bitmap.createBitmap(width,\n                (height + height / 2), Config.ARGB_8888);\n\n        Canvas canvas = new Canvas(bitmapWithReflection);\n        canvas.drawBitmap(bitmap, 0, 0, null);\n        Paint deafalutPaint = new Paint();\n        canvas.drawRect(0, height, width, height + reflectionGap, deafalutPaint);\n\n        canvas.drawBitmap(reflectionImage, 0, height + reflectionGap, null);\n\n        Paint paint = new Paint();\n        LinearGradient shader = new LinearGradient(0, bitmap.getHeight(), 0,\n                bitmapWithReflection.getHeight() + reflectionGap, 0x70ffffff,\n                0x00ffffff, TileMode.CLAMP);\n        paint.setShader(shader);\n        // Set the Transfer mode to be porter duff and destination in\n        paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));\n        // Draw a rectangle using the paint with our linear gradient\n        canvas.drawRect(0, height, width, bitmapWithReflection.getHeight()\n                + reflectionGap, paint);\n\n        return bitmapWithReflection;\n    }\n\n    /**\n     * 将bitmap转化为drawable\n     *\n     * @param bitmap\n     * @return\n     */\n    public static Drawable bitmapToDrawable(Bitmap bitmap) {\n        Drawable drawable = new BitmapDrawable(bitmap);\n        return drawable;\n    }\n\n    /**\n     * 获取图片类型\n     *\n     * @param file\n     * @return\n     */\n    public static String getImageType(File file) {\n        if (file == null || !file.exists()) {\n            return null;\n        }\n        InputStream in = null;\n        try {\n            in = new FileInputStream(file);\n            String type = getImageType(in);\n            return type;\n        } catch (IOException e) {\n            return null;\n        } finally {\n            try {\n                if (in != null) {\n                    in.close();\n                }\n            } catch (IOException e) {\n            }\n        }\n    }\n\n    /**\n     * 获取图片的类型信息\n     *\n     * @param in\n     * @return\n     * @see #getImageType(byte[])\n     */\n    public static String getImageType(InputStream in) {\n        if (in == null) {\n            return null;\n        }\n        try {\n            byte[] bytes = new byte[8];\n            in.read(bytes);\n            return getImageType(bytes);\n        } catch (IOException e) {\n            return null;\n        }\n    }\n\n    /**\n     * 获取图片的类型信息\n     *\n     * @param bytes\n     *            2~8 byte at beginning of the image file\n     * @return image mimetype or null if the file is not image\n     */\n    public static String getImageType(byte[] bytes) {\n        if (isJPEG(bytes)) {\n            return \"image/jpeg\";\n        }\n        if (isGIF(bytes)) {\n            return \"image/gif\";\n        }\n        if (isPNG(bytes)) {\n            return \"image/png\";\n        }\n        if (isBMP(bytes)) {\n            return \"application/x-bmp\";\n        }\n        return null;\n    }\n\n    private static boolean isJPEG(byte[] b) {\n        if (b.length < 2) {\n            return false;\n        }\n        return (b[0] == (byte) 0xFF) && (b[1] == (byte) 0xD8);\n    }\n\n    private static boolean isGIF(byte[] b) {\n        if (b.length < 6) {\n            return false;\n        }\n        return b[0] == 'G' && b[1] == 'I' && b[2] == 'F' && b[3] == '8'\n                && (b[4] == '7' || b[4] == '9') && b[5] == 'a';\n    }\n\n    private static boolean isPNG(byte[] b) {\n        if (b.length < 8) {\n            return false;\n        }\n        return (b[0] == (byte) 137 && b[1] == (byte) 80 && b[2] == (byte) 78\n                && b[3] == (byte) 71 && b[4] == (byte) 13 && b[5] == (byte) 10\n                && b[6] == (byte) 26 && b[7] == (byte) 10);\n    }\n\n    private static boolean isBMP(byte[] b) {\n        if (b.length < 2) {\n            return false;\n        }\n        return (b[0] == 0x42) && (b[1] == 0x4d);\n    }\n\n    /**\n     * 获取图片路径\n     *\n     * @param uri\n     * @param\n     */\n    public static String getImagePath(Uri uri, Activity context) {\n\n        String[] projection = { MediaColumns.DATA };\n        Cursor cursor = context.getContentResolver().query(uri, projection,\n                null, null, null);\n        if (cursor != null) {\n            cursor.moveToFirst();\n            int columIndex = cursor.getColumnIndexOrThrow(MediaColumns.DATA);\n            String ImagePath = cursor.getString(columIndex);\n            cursor.close();\n            return ImagePath;\n        }\n\n        return uri.toString();\n    }\n\n    static Bitmap bitmap = null;\n\n    public static Bitmap loadPicasaImageFromGalley(final Uri uri,\n                                                   final Activity context) {\n\n        String[] projection = { MediaColumns.DATA, MediaColumns.DISPLAY_NAME };\n        Cursor cursor = context.getContentResolver().query(uri, projection,\n                null, null, null);\n        if (cursor != null) {\n            cursor.moveToFirst();\n\n            int columIndex = cursor.getColumnIndex(MediaColumns.DISPLAY_NAME);\n            if (columIndex != -1) {\n                new Thread(new Runnable() {\n\n                    @Override\n                    public void run() {\n                        try {\n                            bitmap = MediaStore.Images.Media\n                                    .getBitmap(context.getContentResolver(),\n                                            uri);\n                        } catch (FileNotFoundException e) {\n                            e.printStackTrace();\n                        } catch (IOException e) {\n                            e.printStackTrace();\n                        }\n\n                    }\n                }).start();\n            }\n            cursor.close();\n            return bitmap;\n        } else\n            return null;\n    }\n\n    /******************************************************************/\n\n    /**\n     * 压缩Bitmap,同时使用两种策略压缩,先压缩宽高，再压缩质量\n     *\n     * @return 存储Bitmap的文件\n     * @throws IOException\n     */\n    public static File compressBitmap(String url, String storageDir, String prefix) throws IOException {\n        if (!TextUtils.isEmpty(url)) {\n            File img = new File(url);\n            Bitmap bitmap = revitionImageSize(img);\n            bitmap = compressImage(bitmap);\n            return convertToFile(bitmap, storageDir, prefix);\n        }\n        return null;\n    }\n\n\n    /**\n     * 使用矩阵缩放图片至期待的宽高\n     *\n     * @param source       被缩放的图片\n     * @param expectWidth  期待的宽\n     * @param expectHeight 期待的高\n     * @return 返回压缩后的图片\n     */\n    public static Bitmap zoomBitmap(Bitmap source, float expectWidth, float expectHeight) {\n        // 获取这个图片的宽和高\n        float width = source.getWidth();\n        float height = source.getHeight();\n        // 创建操作图片用的matrix对象\n        Matrix matrix = new Matrix();\n        //默认不缩放\n        float scaleWidth = 1;\n        float scaleHeight = 1;\n        // 计算宽高缩放率\n        if (expectWidth < width) {\n            scaleWidth = ((float) expectWidth) / width;\n        }\n        if (expectHeight < height) {\n            scaleHeight = ((float) expectHeight) / height;\n        }\n        // 缩放图片动作\n        matrix.postScale(scaleWidth, scaleHeight);\n        Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, (int) width,\n                (int) height, matrix, true);\n        return bitmap;\n    }\n\n    //压缩图片大小\n    public static Bitmap revitionImageSize(File file) throws IOException {\n        BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        BitmapFactory.decodeStream(in, null, options);\n        in.close();\n        int i = 1;\n        Bitmap bitmap = null;\n        while (true) {\n            if (((options.outWidth / i) <= 600)\n                    && ((options.outHeight / i) <= 600)) {\n                in = new BufferedInputStream(\n                        new FileInputStream(file));\n                options.inSampleSize = i;\n                options.inJustDecodeBounds = false;\n                bitmap = BitmapFactory.decodeStream(in, null, options);\n                break;\n            }\n            i += 1;\n        }\n        return bitmap;\n    }\n\n\n    //压缩图片质量\n    public static Bitmap compressImage(Bitmap image) {\n\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        image.compress(CompressFormat.JPEG, 100, baos);//质量压缩方法，这里100表示不压缩，把压缩后的数据存放到baos中\n        int offset = 100;\n        while (baos.toByteArray().length / 1024 > MAX_SIZE) {  //循环判断如果压缩后图片是否大于200kb,大于继续压缩\n\n            baos.reset();//重置baos即清空baos\n            image.compress(CompressFormat.JPEG, offset, baos);//这里压缩options%，把压缩后的数据存放到baos中\n            offset -= 10;//每次都减少10\n        }\n        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());\n        //把压缩后的数据baos存放到ByteArrayInputStream中\n        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片\n        return bitmap;\n    }\n\n    /**\n     * 将bitmap写入一个file中\n     *\n     * @return 保存bitmap的file对象\n     */\n    public static File convertToFile(Bitmap bitmap, String storageDir, String prefix) throws IOException {\n        File cacheDir = checkTargetCacheDir(storageDir);\n        //以时间戳生成一个临时文件名称\n        cacheDir = createFile(cacheDir, prefix, \".jpg\");\n        boolean created = false;//是否创建成功,默认没有创建\n        if (!cacheDir.exists()) created = cacheDir.createNewFile();\n        if (created)//将图片写入目标file,100表示不压缩,Note:png是默认忽略这个参数的\n            bitmap.compress(CompressFormat.PNG, 100, new FileOutputStream(cacheDir));\n        return cacheDir;\n    }\n\n    /**\n     * 检查目标缓存目录是否存在，如果存在则返回这个目录，如果不存在则新建这个目录\n     *\n     * @return\n     */\n    public static File checkTargetCacheDir(String storageDir) {\n\n        File file = null;\n        file = new File(storageDir);\n\n        if (!file.exists()) {\n            file.mkdirs();//创建目录\n        }\n\n        if (file != null && file.exists())\n            return file;//文件已经被成功创建\n        else {\n            return null;//即时经过以上检查，文件还是没有被准确的创建\n        }\n    }\n\n    /**\n     * 根据系统时间、前缀、后缀产生一个文件\n     */\n    public static File createFile(File folder, String prefix, String suffix) {\n        if (!folder.exists() || !folder.isDirectory()) folder.mkdirs();\n        SimpleDateFormat dateFormat = new SimpleDateFormat(\"yyyyMMdd_HHmmssSSS\", Locale.CHINA);\n        String filename = prefix + dateFormat.format(new Date(System.currentTimeMillis())) + suffix;\n        return new File(folder, filename);\n    }\n\n    /**\n     * android图片压缩工具\n     * 压缩多张图片 RxJava 方式\n     */\n    public static void compressWithRx(List<String> files, Observer observer) {\n\n        Luban.get(getContext())\n                .load(files)\n                .putGear(Luban.THIRD_GEAR)\n                .asListObservable()\n                .subscribeOn(Schedulers.computation())\n                .observeOn(AndroidSchedulers.mainThread())\n                .doOnError(new Consumer<Throwable>() {\n                    @Override\n                    public void accept(Throwable throwable) throws Exception {\n                        throwable.printStackTrace();\n                    }\n                })\n                .onErrorResumeNext(new Function<Throwable, ObservableSource<? extends File>>() {\n                    @Override\n                    public ObservableSource<? extends File> apply(Throwable throwable) throws Exception {\n                        return Observable.empty();\n                    }\n                })\n                .subscribe(observer);\n    }\n\n    /**\n     * android图片压缩工具\n     * 压缩单张图片 RxJava 方式\n     */\n    public static void compressWithRx(String url, Consumer consumer) {\n\n        Luban.get(getContext())\n                .load(url)\n                .putGear(Luban.THIRD_GEAR)\n                .asObservable()\n                .subscribeOn(Schedulers.computation())\n                .observeOn(AndroidSchedulers.mainThread())\n                .doOnError(new Consumer<Throwable>() {\n                    @Override\n                    public void accept(Throwable throwable) throws Exception {\n                        throwable.printStackTrace();\n                    }\n                })\n                .onErrorResumeNext(new Function<Throwable, ObservableSource<? extends File>>() {\n                    @Override\n                    public ObservableSource<? extends File> apply(Throwable throwable) throws Exception {\n                        return Observable.empty();\n                    }\n                })\n                .subscribe(consumer);\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/KLog.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\n\npublic class KLog {\n\n    private static boolean IS_SHOW_LOG = false;\n\n    private static final String DEFAULT_MESSAGE = \"execute\";\n    private static final String LINE_SEPARATOR = System.getProperty(\"line.separator\");\n    private static final int JSON_INDENT = 4;\n\n    private static final int V = 0x1;\n    private static final int D = 0x2;\n    private static final int I = 0x3;\n    private static final int W = 0x4;\n    private static final int E = 0x5;\n    private static final int A = 0x6;\n    private static final int JSON = 0x7;\n\n    public static void init(boolean isShowLog) {\n        IS_SHOW_LOG = isShowLog;\n    }\n\n    public static void v() {\n        printLog(V, null, DEFAULT_MESSAGE);\n    }\n\n    public static void v(Object msg) {\n        printLog(V, null, msg);\n    }\n\n    public static void v(String tag, String msg) {\n        printLog(V, tag, msg);\n    }\n\n    public static void d() {\n        printLog(D, null, DEFAULT_MESSAGE);\n    }\n\n    public static void d(Object msg) {\n        printLog(D, null, msg);\n    }\n\n    public static void d(String tag, Object msg) {\n        printLog(D, tag, msg);\n    }\n\n    public static void i() {\n        printLog(I, null, DEFAULT_MESSAGE);\n    }\n\n    public static void i(Object msg) {\n        printLog(I, null, msg);\n    }\n\n    public static void i(String tag, Object msg) {\n        printLog(I, tag, msg);\n    }\n\n    public static void w() {\n        printLog(W, null, DEFAULT_MESSAGE);\n    }\n\n    public static void w(Object msg) {\n        printLog(W, null, msg);\n    }\n\n    public static void w(String tag, Object msg) {\n        printLog(W, tag, msg);\n    }\n\n    public static void e() {\n        printLog(E, null, DEFAULT_MESSAGE);\n    }\n\n    public static void e(Object msg) {\n        printLog(E, null, msg);\n    }\n\n    public static void e(String tag, Object msg) {\n        printLog(E, tag, msg);\n    }\n\n    public static void a() {\n        printLog(A, null, DEFAULT_MESSAGE);\n    }\n\n    public static void a(Object msg) {\n        printLog(A, null, msg);\n    }\n\n    public static void a(String tag, Object msg) {\n        printLog(A, tag, msg);\n    }\n\n\n    public static void json(String jsonFormat) {\n        printLog(JSON, null, jsonFormat);\n    }\n\n    public static void json(String tag, String jsonFormat) {\n        printLog(JSON, tag, jsonFormat);\n    }\n\n\n    private static void printLog(int type, String tagStr, Object objectMsg) {\n        String msg;\n        if (!IS_SHOW_LOG) {\n            return;\n        }\n\n        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();\n\n        int index = 4;\n        String className = stackTrace[index].getFileName();\n        String methodName = stackTrace[index].getMethodName();\n        int lineNumber = stackTrace[index].getLineNumber();\n\n        String tag = (tagStr == null ? className : tagStr);\n        methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);\n\n        StringBuilder stringBuilder = new StringBuilder();\n        stringBuilder.append(\"[ (\").append(className).append(\":\").append(lineNumber).append(\")#\").append(methodName).append(\" ] \");\n\n        if (objectMsg == null) {\n            msg = \"Log with null Object\";\n        } else {\n            msg = objectMsg.toString();\n        }\n        if (msg != null && type != JSON) {\n            stringBuilder.append(msg);\n        }\n\n        String logStr = stringBuilder.toString();\n\n        switch (type) {\n            case V:\n                Log.v(tag, logStr);\n                break;\n            case D:\n                Log.d(tag, logStr);\n                break;\n            case I:\n                Log.i(tag, logStr);\n                break;\n            case W:\n                Log.w(tag, logStr);\n                break;\n            case E:\n                Log.e(tag, logStr);\n                break;\n            case A:\n                Log.wtf(tag, logStr);\n                break;\n            case JSON: {\n\n                if (TextUtils.isEmpty(msg)) {\n                    Log.d(tag, \"Empty or Null json content\");\n                    return;\n                }\n\n                String message = null;\n\n                try {\n                    if (msg.startsWith(\"{\")) {\n                        JSONObject jsonObject = new JSONObject(msg);\n                        message = jsonObject.toString(JSON_INDENT);\n                    } else if (msg.startsWith(\"[\")) {\n                        JSONArray jsonArray = new JSONArray(msg);\n                        message = jsonArray.toString(JSON_INDENT);\n                    }\n                } catch (JSONException e) {\n                    e(tag, e.getCause().getMessage() + \"\\n\" + msg);\n                    return;\n                }\n\n                printLine(tag, true);\n                message = logStr + LINE_SEPARATOR + message;\n                String[] lines = message.split(LINE_SEPARATOR);\n                StringBuilder jsonContent = new StringBuilder();\n                for (String line : lines) {\n                    jsonContent.append(\"║ \").append(line).append(LINE_SEPARATOR);\n                }\n                //Log.i(tag, jsonContent.toString());\n\n                if (jsonContent.toString().length() > 3200) {\n                    Log.w(tag, \"jsonContent.length = \" + jsonContent.toString().length());\n                    int chunkCount = jsonContent.toString().length() / 3200;\n                    for (int i = 0; i <= chunkCount; i++) {\n                        int max = 3200 * (i + 1);\n                        if (max >= jsonContent.toString().length()) {\n\n                            Log.w(tag, jsonContent.toString().substring(3200 * i));\n\n                        } else {\n\n                            Log.w(tag, jsonContent.toString().substring(3200 * i, max));\n\n                        }\n\n                    }\n\n                } else {\n                    Log.w(tag, jsonContent.toString());\n\n                }\n                printLine(tag, false);\n            }\n            break;\n        }\n\n    }\n\n    private static void printLine(String tag, boolean isTop) {\n        if (isTop) {\n            Log.w(tag, \"╔═══════════════════════════════════════════════════════════════════════════════════════\");\n        } else {\n            Log.w(tag, \"╚═══════════════════════════════════════════════════════════════════════════════════════\");\n        }\n    }\n\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/MaterialDialogUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.graphics.Color;\nimport android.text.InputType;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.View;\n\nimport com.afollestad.materialdialogs.DialogAction;\nimport com.afollestad.materialdialogs.GravityEnum;\nimport com.afollestad.materialdialogs.MaterialDialog;\nimport com.afollestad.materialdialogs.Theme;\n\nimport java.util.List;\n\nimport androidx.annotation.NonNull;\nimport me.goldze.mvvmhabit.R;\n\n\n/**\n * Created by goldze on 2017/5/10.\n */\n\npublic class MaterialDialogUtils {\n\n    public void showThemed(Context context, String\n            title, String content) {\n        new MaterialDialog.Builder(context)\n                .title(title)\n                .content(content)\n                .positiveText(\"agree\")\n                .negativeText(\"disagree\")\n                .positiveColorRes(R.color.white)\n                .negativeColorRes(R.color.white)\n                .titleGravity(GravityEnum.CENTER)\n                .titleColorRes(R.color.white)\n                .contentColorRes(android.R.color.white)\n                .backgroundColorRes(R.color.material_blue_grey_800)\n                .dividerColorRes(R.color.white)\n                .btnSelector(R.drawable.md_selector, DialogAction.POSITIVE)\n                .positiveColor(Color.WHITE)\n                .negativeColorAttr(android.R.attr.textColorSecondaryInverse)\n                .theme(Theme.DARK)\n                .autoDismiss(true)  //点击是否关闭对话框\n                .showListener(new DialogInterface.OnShowListener() {\n                    @Override\n                    public void onShow(DialogInterface dialog) {\n                        //dialog 出现\n                    }\n                })\n                .cancelListener(new DialogInterface.OnCancelListener() {\n                    @Override\n                    public void onCancel(DialogInterface dialog) {\n                        //dialog 消失（返回键）\n                    }\n                })\n                .dismissListener(new DialogInterface.OnDismissListener() {\n                    @Override\n                    public void onDismiss(DialogInterface dialog) {\n                        //dialog 消失\n                    }\n                })\n                .show();\n\n        //获取按钮并监听\n//        MDButton btn = materialDialog.getActionButton(DialogAction.NEGATIVE);\n//        btn.setOnClickListener(new View.OnClickListener() {\n//            @Override\n//            public void onClick(View v) {\n//\n//            }\n//        });\n    }\n\n    /***\n     * 获取一个耗时等待对话框\n     *\n     * @param horizontal\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showIndeterminateProgressDialog(Context context, String content, boolean horizontal) {\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(content)\n                .progress(true, 0)\n                .progressIndeterminateStyle(horizontal)\n                .canceledOnTouchOutside(false)\n                .backgroundColorRes(R.color.white)\n                .keyListener(new DialogInterface.OnKeyListener() {\n                    @Override\n                    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {\n                        if (event.getAction() == KeyEvent.ACTION_DOWN) {//如果是按下，则响应，否则，一次按下会响应两次\n                            if (keyCode == KeyEvent.KEYCODE_BACK) {\n                                //activity.onBackPressed();\n\n                            }\n                        }\n                        return false;//false允许按返回键取消对话框，true除了调用取消，其他情况下不会取消\n                    }\n                });\n        return builder;\n    }\n\n\n    /***\n     * 获取基本对话框\n     *\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showBasicDialog(final Context context, String\n            content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(content)\n                .positiveText(\"确定\")\n                .negativeText(\"取消\")\n//                .btnStackedGravity(GravityEnum.END)         //按钮排列位置\n//                .stackingBehavior(StackingBehavior.ALWAYS)  //按钮排列方式\n//                .iconRes(R.mipmap.ic_launcher)\n//                .limitIconToDefaultSize() // limits the displayed icon size to 48dp\n//                .onAny(new MaterialDialog.SingleButtonCallback() {\n//                    @Override\n//                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction\n//                            which) {\n//                    }\n//                })\n//                .checkBoxPromptRes(R.string.app_name, false, null)\n                ;\n\n        return builder;\n    }\n\n    /***\n     * 显示一个基础的对话框  只有内容没有标题\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showBasicDialogNoTitle(final Context context, String content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .content(content)\n                .positiveText(\"确定\")\n                .negativeText(\"取消\");\n\n        return builder;\n    }\n\n\n    /***\n     * 显示一个基础的对话框  带标题 带内容\n     * 没有取消按钮\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showBasicDialogNoCancel(final Context context, String\n            title, String content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(title)\n                .content(content)\n                .positiveText(\"确定\");\n\n        return builder;\n    }\n\n    /***\n     * 显示一个基础的对话框  带标题 带内容\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showBasicDialog(final Context context, String\n            title, String content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(title)\n                .content(content)\n                .positiveText(\"确定\")\n                .negativeText(\"取消\");\n\n        return builder;\n    }\n\n    /***\n     * 显示一个基础的对话框  带标题 带内容\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showBasicDialogPositive(final Context context, String\n            title, String content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(title)\n                .content(content)\n                .positiveText(\"复制\")\n                .negativeText(\"取消\");\n\n        return builder;\n    }\n\n    /***\n     * 选择图片等Item的对话框  带标题\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder getSelectDialog(Context context, String title, String[] arrays) {\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .items(arrays)\n                .itemsColor(0XFF456ea6)\n                .negativeText(\"取消\");\n        if (!TextUtils.isEmpty(title)) {\n            builder.title(title);\n        }\n        return builder;\n    }\n\n    /***\n     * 获取LIST对话框\n     *\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showBasicListDialog(final Context context, String title, List\n            content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(title)\n                .items(content)\n                .itemsCallback(new MaterialDialog.ListCallback() {\n                    @Override\n                    public void onSelection(MaterialDialog dialog, View itemView, int position, CharSequence text) {\n\n                    }\n                })\n                .negativeText(\"取消\")\n//                .checkBoxPromptRes(R.string.app_name, false, null)\n                ;\n\n        return builder;\n    }\n\n    /***\n     * 获取单选LIST对话框\n     *\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showSingleListDialog(final Context context, String title, List\n            content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(title)\n                .items(content)\n                .itemsCallbackSingleChoice(1, new MaterialDialog.ListCallbackSingleChoice() {\n                    @Override\n                    public boolean onSelection(MaterialDialog dialog, View itemView, int which,\n                                               CharSequence text) {\n\n\n                        return true; // allow selection\n                    }\n                })\n                .positiveText(\"选择\");\n\n        return builder;\n    }\n\n\n    /***\n     * 获取多选LIST对话框\n     *\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showMultiListDialog(final Context context, String title, List\n            content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(title)\n                .items(content)\n                .itemsCallbackMultiChoice(new Integer[]{1, 3}, new MaterialDialog\n                        .ListCallbackMultiChoice() {\n                    @Override\n                    public boolean onSelection(MaterialDialog dialog, Integer[] which, CharSequence[] text) {\n\n\n                        return true; // allow selection\n                    }\n                })\n                .onNeutral(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n                        dialog.clearSelectedIndices();\n                    }\n                })\n                .alwaysCallMultiChoiceCallback()\n                .positiveText(R.string.md_choose_label)\n                .autoDismiss(false)\n                .neutralText(\"clear\")\n                .itemsDisabledIndices(0, 1);\n\n        return builder;\n    }\n\n\n    /***\n     * 获取自定义对话框\n     *\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static void showCustomDialog(final Context context, String title, int\n            content) {\n\n        MaterialDialog dialog = new MaterialDialog.Builder(context)\n                .title(title)\n                .customView(content, true)\n                .positiveText(\"确定\")\n                .negativeText(android.R.string.cancel)\n                .onPositive(new MaterialDialog.SingleButtonCallback() {\n                    @Override\n                    public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {\n\n                    }\n                }).build();\n\n//        positiveAction = dialog.getActionButton(DialogAction.POSITIVE);\n//        //noinspection ConstantConditions\n//        passwordInput = (EditText) dialog.getCustomView().findViewById(R.id.password);\n//        passwordInput.addTextChangedListener(new TextWatcher() {\n//            @Override\n//            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n//            }\n//\n//            @Override\n//            public void onTextChanged(CharSequence s, int start, int before, int count) {\n//                positiveAction.setEnabled(s.toString().trim().length() > 0);\n//            }\n//\n//            @Override\n//            public void afterTextChanged(Editable s) {\n//            }\n//        });\n//\n//        // Toggling the show password CheckBox will mask or unmask the password input EditText\n//        CheckBox checkbox = (CheckBox) dialog.getCustomView().findViewById(R.id.showPassword);\n//        checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {\n//            @Override\n//            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {\n//                passwordInput.setInputType(!isChecked ? InputType.TYPE_TEXT_VARIATION_PASSWORD : InputType.TYPE_CLASS_TEXT);\n//                passwordInput.setTransformationMethod(!isChecked ? PasswordTransformationMethod.getInstance() : null);\n//            }\n//        });\n//\n//        int widgetColor = ThemeSingleton.get().widgetColor;\n//        MDTintHelper.setTint(checkbox,\n//                widgetColor == 0 ? ContextCompat.getColor(this, R.color.accent) : widgetColor);\n//\n//        MDTintHelper.setTint(passwordInput,\n//                widgetColor == 0 ? ContextCompat.getColor(this, R.color.accent) : widgetColor);\n//\n//        dialog.show();\n//        positiveAction.setEnabled(false); // disabled by default\n\n    }\n\n\n    /***\n     * 获取输入对话框\n     *\n     * @param\n     * @return MaterialDialog.Builder\n     */\n    public static MaterialDialog.Builder showInputDialog(final Context context, String title, String\n            content) {\n\n        MaterialDialog.Builder builder = new MaterialDialog.Builder(context)\n                .title(title)\n                .content(content)\n                .inputType(InputType.TYPE_CLASS_TEXT |\n                        InputType.TYPE_TEXT_VARIATION_PERSON_NAME |\n                        InputType.TYPE_TEXT_FLAG_CAP_WORDS)\n                .positiveText(\"确定\")\n                .negativeText(\"取消\")\n                .input(\"hint\", \"prefill\", true, new MaterialDialog.InputCallback() {\n                    @Override\n                    public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {\n\n                    }\n                });\n\n        return builder;\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/RegexUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport androidx.collection.SimpleArrayMap;\nimport me.goldze.mvvmhabit.utils.constant.RegexConstants;\n\n/**\n * Created by goldze on 2017/6/19.\n * 正则相关工具类\n */\npublic final class RegexUtils {\n\n    private final static SimpleArrayMap<String, String> CITY_MAP = new SimpleArrayMap<>();\n\n    private RegexUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    ///////////////////////////////////////////////////////////////////////////\n    // If u want more please visit http://toutiao.com/i6231678548520731137\n    ///////////////////////////////////////////////////////////////////////////\n\n    /**\n     * Return whether input matches regex of simple mobile.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isMobileSimple(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_MOBILE_SIMPLE, input);\n    }\n\n    /**\n     * Return whether input matches regex of exact mobile.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isMobileExact(final CharSequence input) {\n        return isMobileExact(input, null);\n    }\n\n    /**\n     * Return whether input matches regex of exact mobile.\n     *\n     * @param input       The input.\n     * @param newSegments The new segments of mobile number.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isMobileExact(final CharSequence input, List<String> newSegments) {\n        boolean match = isMatch(RegexConstants.REGEX_MOBILE_EXACT, input);\n        if (match) return true;\n        if (newSegments == null) return false;\n        if (input == null || input.length() != 11) return false;\n        String content = input.toString();\n        for (char c : content.toCharArray()) {\n            if (!Character.isDigit(c)) {\n                return false;\n            }\n        }\n        for (String newSegment : newSegments) {\n            if (content.startsWith(newSegment)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Return whether input matches regex of telephone number.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isTel(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_TEL, input);\n    }\n\n    /**\n     * Return whether input matches regex of id card number which length is 15.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isIDCard15(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_ID_CARD15, input);\n    }\n\n    /**\n     * Return whether input matches regex of id card number which length is 18.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isIDCard18(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_ID_CARD18, input);\n    }\n\n    /**\n     * Return whether input matches regex of exact id card number which length is 18.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isIDCard18Exact(final CharSequence input) {\n        if (isIDCard18(input)) {\n            int[] factor = new int[]{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};\n            char[] suffix = new char[]{'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};\n            if (CITY_MAP.isEmpty()) {\n                CITY_MAP.put(\"11\", \"北京\");\n                CITY_MAP.put(\"12\", \"天津\");\n                CITY_MAP.put(\"13\", \"河北\");\n                CITY_MAP.put(\"14\", \"山西\");\n                CITY_MAP.put(\"15\", \"内蒙古\");\n\n                CITY_MAP.put(\"21\", \"辽宁\");\n                CITY_MAP.put(\"22\", \"吉林\");\n                CITY_MAP.put(\"23\", \"黑龙江\");\n\n                CITY_MAP.put(\"31\", \"上海\");\n                CITY_MAP.put(\"32\", \"江苏\");\n                CITY_MAP.put(\"33\", \"浙江\");\n                CITY_MAP.put(\"34\", \"安徽\");\n                CITY_MAP.put(\"35\", \"福建\");\n                CITY_MAP.put(\"36\", \"江西\");\n                CITY_MAP.put(\"37\", \"山东\");\n\n                CITY_MAP.put(\"41\", \"河南\");\n                CITY_MAP.put(\"42\", \"湖北\");\n                CITY_MAP.put(\"43\", \"湖南\");\n                CITY_MAP.put(\"44\", \"广东\");\n                CITY_MAP.put(\"45\", \"广西\");\n                CITY_MAP.put(\"46\", \"海南\");\n\n                CITY_MAP.put(\"50\", \"重庆\");\n                CITY_MAP.put(\"51\", \"四川\");\n                CITY_MAP.put(\"52\", \"贵州\");\n                CITY_MAP.put(\"53\", \"云南\");\n                CITY_MAP.put(\"54\", \"西藏\");\n\n                CITY_MAP.put(\"61\", \"陕西\");\n                CITY_MAP.put(\"62\", \"甘肃\");\n                CITY_MAP.put(\"63\", \"青海\");\n                CITY_MAP.put(\"64\", \"宁夏\");\n                CITY_MAP.put(\"65\", \"新疆\");\n\n                CITY_MAP.put(\"71\", \"台湾老\");\n                CITY_MAP.put(\"81\", \"香港\");\n                CITY_MAP.put(\"82\", \"澳门\");\n                CITY_MAP.put(\"83\", \"台湾新\");\n                CITY_MAP.put(\"91\", \"国外\");\n            }\n            if (CITY_MAP.get(input.subSequence(0, 2).toString()) != null) {\n                int weightSum = 0;\n                for (int i = 0; i < 17; ++i) {\n                    weightSum += (input.charAt(i) - '0') * factor[i];\n                }\n                int idCardMod = weightSum % 11;\n                char idCardLast = input.charAt(17);\n                return idCardLast == suffix[idCardMod];\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Return whether input matches regex of email.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isEmail(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_EMAIL, input);\n    }\n\n    /**\n     * Return whether input matches regex of url.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isURL(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_URL, input);\n    }\n\n    /**\n     * Return whether input matches regex of Chinese character.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isZh(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_ZH, input);\n    }\n\n    /**\n     * Return whether input matches regex of username.\n     * <p>scope for \"a-z\", \"A-Z\", \"0-9\", \"_\", \"Chinese character\"</p>\n     * <p>can't end with \"_\"</p>\n     * <p>length is between 6 to 20</p>.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isUsername(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_USERNAME, input);\n    }\n\n    /**\n     * Return whether input matches regex of date which pattern is \"yyyy-MM-dd\".\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isDate(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_DATE, input);\n    }\n\n    /**\n     * Return whether input matches regex of ip address.\n     *\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isIP(final CharSequence input) {\n        return isMatch(RegexConstants.REGEX_IP, input);\n    }\n\n    /**\n     * Return whether input matches the regex.\n     *\n     * @param regex The regex.\n     * @param input The input.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isMatch(final String regex, final CharSequence input) {\n        return input != null && input.length() > 0 && Pattern.matches(regex, input);\n    }\n\n    /**\n     * Return the list of input matches the regex.\n     *\n     * @param regex The regex.\n     * @param input The input.\n     * @return the list of input matches the regex\n     */\n    public static List<String> getMatches(final String regex, final CharSequence input) {\n        if (input == null) return Collections.emptyList();\n        List<String> matches = new ArrayList<>();\n        Pattern pattern = Pattern.compile(regex);\n        Matcher matcher = pattern.matcher(input);\n        while (matcher.find()) {\n            matches.add(matcher.group());\n        }\n        return matches;\n    }\n\n    /**\n     * Splits input around matches of the regex.\n     *\n     * @param input The input.\n     * @param regex The regex.\n     * @return the array of strings computed by splitting input around matches of regex\n     */\n    public static String[] getSplits(final String input, final String regex) {\n        if (input == null) return new String[0];\n        return input.split(regex);\n    }\n\n    /**\n     * Replace the first subsequence of the input sequence that matches the\n     * regex with the given replacement string.\n     *\n     * @param input       The input.\n     * @param regex       The regex.\n     * @param replacement The replacement string.\n     * @return the string constructed by replacing the first matching\n     * subsequence by the replacement string, substituting captured\n     * subsequences as needed\n     */\n    public static String getReplaceFirst(final String input,\n                                         final String regex,\n                                         final String replacement) {\n        if (input == null) return \"\";\n        return Pattern.compile(regex).matcher(input).replaceFirst(replacement);\n    }\n\n    /**\n     * Replace every subsequence of the input sequence that matches the\n     * pattern with the given replacement string.\n     *\n     * @param input       The input.\n     * @param regex       The regex.\n     * @param replacement The replacement string.\n     * @return the string constructed by replacing each matching subsequence\n     * by the replacement string, substituting captured subsequences\n     * as needed\n     */\n    public static String getReplaceAll(final String input,\n                                       final String regex,\n                                       final String replacement) {\n        if (input == null) return \"\";\n        return Pattern.compile(regex).matcher(input).replaceAll(replacement);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/RxUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.content.Context;\n\nimport com.trello.rxlifecycle2.LifecycleProvider;\nimport com.trello.rxlifecycle2.LifecycleTransformer;\n\nimport androidx.fragment.app.Fragment;\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableSource;\nimport io.reactivex.ObservableTransformer;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.annotations.NonNull;\nimport io.reactivex.functions.Function;\nimport io.reactivex.schedulers.Schedulers;\nimport me.goldze.mvvmhabit.http.BaseResponse;\nimport me.goldze.mvvmhabit.http.ExceptionHandle;\n\n/**\n * Created by goldze on 2017/6/19.\n * 有关Rx的工具类\n */\npublic class RxUtils {\n    /**\n     * 生命周期绑定\n     *\n     * @param lifecycle Activity\n     */\n    public static <T> LifecycleTransformer<T> bindToLifecycle(@NonNull Context lifecycle) {\n        if (lifecycle instanceof LifecycleProvider) {\n            return ((LifecycleProvider) lifecycle).bindToLifecycle();\n        } else {\n            throw new IllegalArgumentException(\"context not the LifecycleProvider type\");\n        }\n    }\n\n    /**\n     * 生命周期绑定\n     *\n     * @param lifecycle Fragment\n     */\n    public static LifecycleTransformer bindToLifecycle(@NonNull Fragment lifecycle) {\n        if (lifecycle instanceof LifecycleProvider) {\n            return ((LifecycleProvider) lifecycle).bindToLifecycle();\n        } else {\n            throw new IllegalArgumentException(\"fragment not the LifecycleProvider type\");\n        }\n    }\n\n    /**\n     * 生命周期绑定\n     *\n     * @param lifecycle Fragment\n     */\n    public static LifecycleTransformer bindToLifecycle(@NonNull LifecycleProvider lifecycle) {\n        return lifecycle.bindToLifecycle();\n    }\n\n    /**\n     * 线程调度器\n     */\n    public static ObservableTransformer schedulersTransformer() {\n        return new ObservableTransformer() {\n            @Override\n            public ObservableSource apply(Observable upstream) {\n                return upstream.subscribeOn(Schedulers.io())\n                        .observeOn(AndroidSchedulers.mainThread());\n            }\n        };\n    }\n\n    public static ObservableTransformer exceptionTransformer() {\n\n        return new ObservableTransformer() {\n            @Override\n            public ObservableSource apply(Observable observable) {\n                return observable\n//                        .map(new HandleFuc<T>())  //这里可以取出BaseResponse中的Result\n                        .onErrorResumeNext(new HttpResponseFunc());\n            }\n        };\n    }\n\n    private static class HttpResponseFunc<T> implements Function<Throwable, Observable<T>> {\n        @Override\n        public Observable<T> apply(Throwable t) {\n            return Observable.error(ExceptionHandle.handleException(t));\n        }\n    }\n\n    private static class HandleFuc<T> implements Function<BaseResponse<T>, T> {\n        @Override\n        public T apply(BaseResponse<T> response) {\n            if (!response.isOk())\n                throw new RuntimeException(!\"\".equals(response.getCode() + \"\" + response.getMessage()) ? response.getMessage() : \"\");\n            return response.getResult();\n        }\n    }\n\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/SDCardUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.os.StatFs;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.InputStreamReader;\n/**\n * Created by goldze on 2017/5/14.\n * SD卡相关工具类\n */\npublic final class SDCardUtils {\n\n    private SDCardUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * 判断SD卡是否可用\n     *\n     * @return true : 可用<br>false : 不可用\n     */\n    public static boolean isSDCardEnable() {\n        return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());\n    }\n\n    /**\n     * 获取SD卡路径\n     * <p>先用shell，shell失败再普通方法获取，一般是/storage/emulated/0/</p>\n     *\n     * @return SD卡路径\n     */\n    public static String getSDCardPath() {\n        if (!isSDCardEnable()) return null;\n        String cmd = \"cat /proc/mounts\";\n        Runtime run = Runtime.getRuntime();\n        BufferedReader bufferedReader = null;\n        try {\n            Process p = run.exec(cmd);\n            bufferedReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(p.getInputStream())));\n            String lineStr;\n            while ((lineStr = bufferedReader.readLine()) != null) {\n                if (lineStr.contains(\"sdcard\") && lineStr.contains(\".android_secure\")) {\n                    String[] strArray = lineStr.split(\" \");\n                    if (strArray.length >= 5) {\n                        return strArray[1].replace(\"/.android_secure\", \"\") + File.separator;\n                    }\n                }\n                if (p.waitFor() != 0 && p.exitValue() == 1) {\n                    break;\n                }\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            CloseUtils.closeIO(bufferedReader);\n        }\n        return Environment.getExternalStorageDirectory().getPath() + File.separator;\n    }\n\n    /**\n     * 获取SD卡data路径\n     *\n     * @return SD卡data路径\n     */\n    public static String getDataPath() {\n        if (!isSDCardEnable()) return null;\n        return Environment.getExternalStorageDirectory().getPath() + File.separator + \"data\" + File.separator;\n    }\n\n    /**\n     * 获取SD卡剩余空间\n     *\n     * @return SD卡剩余空间\n     */\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)\n    public static String getFreeSpace() {\n        if (!isSDCardEnable()) return null;\n        StatFs stat = new StatFs(getSDCardPath());\n        long blockSize, availableBlocks;\n        availableBlocks = stat.getAvailableBlocksLong();\n        blockSize = stat.getBlockSizeLong();\n        return ConvertUtils.byte2FitMemorySize(availableBlocks * blockSize);\n    }\n\n    /**\n     * 获取SD卡信息\n     *\n     * @return SDCardInfo\n     */\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)\n    public static String getSDCardInfo() {\n        if (!isSDCardEnable()) return null;\n        SDCardInfo sd = new SDCardInfo();\n        sd.isExist = true;\n        StatFs sf = new StatFs(Environment.getExternalStorageDirectory().getPath());\n        sd.totalBlocks = sf.getBlockCountLong();\n        sd.blockByteSize = sf.getBlockSizeLong();\n        sd.availableBlocks = sf.getAvailableBlocksLong();\n        sd.availableBytes = sf.getAvailableBytes();\n        sd.freeBlocks = sf.getFreeBlocksLong();\n        sd.freeBytes = sf.getFreeBytes();\n        sd.totalBytes = sf.getTotalBytes();\n        return sd.toString();\n    }\n\n    public static class SDCardInfo {\n        boolean isExist;\n        long    totalBlocks;\n        long    freeBlocks;\n        long    availableBlocks;\n        long    blockByteSize;\n        long    totalBytes;\n        long    freeBytes;\n        long    availableBytes;\n\n        @Override\n        public String toString() {\n            return \"isExist=\" + isExist +\n                    \"\\ntotalBlocks=\" + totalBlocks +\n                    \"\\nfreeBlocks=\" + freeBlocks +\n                    \"\\navailableBlocks=\" + availableBlocks +\n                    \"\\nblockByteSize=\" + blockByteSize +\n                    \"\\ntotalBytes=\" + totalBytes +\n                    \"\\nfreeBytes=\" + freeBytes +\n                    \"\\navailableBytes=\" + availableBytes;\n        }\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/SPUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\nimport androidx.annotation.NonNull;\n\n/**\n * Created by goldze on 2017/5/14.\n * SharedPreferences工具类\n */\npublic final class SPUtils {\n\n    private static Map<String, SPUtils> sSPMap = new HashMap<>();\n    private SharedPreferences sp;\n\n    /**\n     * 获取SP实例\n     *\n     * @return {@link SPUtils}\n     */\n    public static SPUtils getInstance() {\n        return getInstance(\"\");\n    }\n\n    /**\n     * 获取SP实例\n     *\n     * @param spName sp名\n     * @return {@link SPUtils}\n     */\n    public static SPUtils getInstance(String spName) {\n        if (isSpace(spName)) spName = \"spUtils\";\n        SPUtils sp = sSPMap.get(spName);\n        if (sp == null) {\n            sp = new SPUtils(spName);\n            sSPMap.put(spName, sp);\n        }\n        return sp;\n    }\n\n    private SPUtils(final String spName) {\n        sp = Utils.getContext().getSharedPreferences(spName, Context.MODE_PRIVATE);\n    }\n\n    /**\n     * SP中写入String\n     *\n     * @param key 键\n     * @param value 值\n     */\n    public void put(@NonNull final String key, @NonNull final String value) {\n        sp.edit().putString(key, value).apply();\n    }\n\n    /**\n     * SP中读取String\n     *\n     * @param key 键\n     * @return 存在返回对应值，不存在返回默认值{@code \"\"}\n     */\n    public String getString(@NonNull final String key) {\n        return getString(key, \"\");\n    }\n\n    /**\n     * SP中读取String\n     *\n     * @param key 键\n     * @param defaultValue 默认值\n     * @return 存在返回对应值，不存在返回默认值{@code defaultValue}\n     */\n    public String getString(@NonNull final String key, @NonNull final String defaultValue) {\n        return sp.getString(key, defaultValue);\n    }\n\n    /**\n     * SP中写入int\n     *\n     * @param key 键\n     * @param value 值\n     */\n    public void put(@NonNull final String key, final int value) {\n        sp.edit().putInt(key, value).apply();\n    }\n\n    /**\n     * SP中读取int\n     *\n     * @param key 键\n     * @return 存在返回对应值，不存在返回默认值-1\n     */\n    public int getInt(@NonNull final String key) {\n        return getInt(key, -1);\n    }\n\n    /**\n     * SP中读取int\n     *\n     * @param key 键\n     * @param defaultValue 默认值\n     * @return 存在返回对应值，不存在返回默认值{@code defaultValue}\n     */\n    public int getInt(@NonNull final String key, final int defaultValue) {\n        return sp.getInt(key, defaultValue);\n    }\n\n    /**\n     * SP中写入long\n     *\n     * @param key 键\n     * @param value 值\n     */\n    public void put(@NonNull final String key, final long value) {\n        sp.edit().putLong(key, value).apply();\n    }\n\n    /**\n     * SP中读取long\n     *\n     * @param key 键\n     * @return 存在返回对应值，不存在返回默认值-1\n     */\n    public long getLong(@NonNull final String key) {\n        return getLong(key, -1L);\n    }\n\n    /**\n     * SP中读取long\n     *\n     * @param key 键\n     * @param defaultValue 默认值\n     * @return 存在返回对应值，不存在返回默认值{@code defaultValue}\n     */\n    public long getLong(@NonNull final String key, final long defaultValue) {\n        return sp.getLong(key, defaultValue);\n    }\n\n    /**\n     * SP中写入float\n     *\n     * @param key 键\n     * @param value 值\n     */\n    public void put(@NonNull final String key, final float value) {\n        sp.edit().putFloat(key, value).apply();\n    }\n\n    /**\n     * SP中读取float\n     *\n     * @param key 键\n     * @return 存在返回对应值，不存在返回默认值-1\n     */\n    public float getFloat(@NonNull final String key) {\n        return getFloat(key, -1f);\n    }\n\n    /**\n     * SP中读取float\n     *\n     * @param key 键\n     * @param defaultValue 默认值\n     * @return 存在返回对应值，不存在返回默认值{@code defaultValue}\n     */\n    public float getFloat(@NonNull final String key, final float defaultValue) {\n        return sp.getFloat(key, defaultValue);\n    }\n\n    /**\n     * SP中写入boolean\n     *\n     * @param key 键\n     * @param value 值\n     */\n    public void put(@NonNull final String key, final boolean value) {\n        sp.edit().putBoolean(key, value).apply();\n    }\n\n    /**\n     * SP中读取boolean\n     *\n     * @param key 键\n     * @return 存在返回对应值，不存在返回默认值{@code false}\n     */\n    public boolean getBoolean(@NonNull final String key) {\n        return getBoolean(key, false);\n    }\n\n    /**\n     * SP中读取boolean\n     *\n     * @param key 键\n     * @param defaultValue 默认值\n     * @return 存在返回对应值，不存在返回默认值{@code defaultValue}\n     */\n    public boolean getBoolean(@NonNull final String key, final boolean defaultValue) {\n        return sp.getBoolean(key, defaultValue);\n    }\n\n    /**\n     * SP中写入String集合\n     *\n     * @param key 键\n     * @param values 值\n     */\n    public void put(@NonNull final String key, @NonNull final Set<String> values) {\n        sp.edit().putStringSet(key, values).apply();\n    }\n\n    /**\n     * SP中读取StringSet\n     *\n     * @param key 键\n     * @return 存在返回对应值，不存在返回默认值{@code Collections.<String>emptySet()}\n     */\n    public Set<String> getStringSet(@NonNull final String key) {\n        return getStringSet(key, Collections.<String>emptySet());\n    }\n\n    /**\n     * SP中读取StringSet\n     *\n     * @param key 键\n     * @param defaultValue 默认值\n     * @return 存在返回对应值，不存在返回默认值{@code defaultValue}\n     */\n    public Set<String> getStringSet(@NonNull final String key, @NonNull final Set<String> defaultValue) {\n        return sp.getStringSet(key, defaultValue);\n    }\n\n    /**\n     * SP中获取所有键值对\n     *\n     * @return Map对象\n     */\n    public Map<String, ?> getAll() {\n        return sp.getAll();\n    }\n\n    /**\n     * SP中是否存在该key\n     *\n     * @param key 键\n     * @return {@code true}: 存在<br>{@code false}: 不存在\n     */\n    public boolean contains(@NonNull final String key) {\n        return sp.contains(key);\n    }\n\n    /**\n     * SP中移除该key\n     *\n     * @param key 键\n     */\n    public void remove(@NonNull final String key) {\n        sp.edit().remove(key).apply();\n    }\n\n    /**\n     * SP中清除所有数据\n     */\n    public void clear() {\n        sp.edit().clear().apply();\n    }\n\n    private static boolean isSpace(final String s) {\n        if (s == null) return true;\n        for (int i = 0, len = s.length(); i < len; ++i) {\n            if (!Character.isWhitespace(s.charAt(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/StringUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\n/**\n * Created by goldze on 2017/5/14.\n * 字符串相关工具类\n */\npublic final class StringUtils {\n\n    private StringUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * 判断字符串是否为null或长度为0\n     *\n     * @param s 待校验字符串\n     * @return {@code true}: 空<br> {@code false}: 不为空\n     */\n    public static boolean isEmpty(final CharSequence s) {\n        return s == null || s.length() == 0;\n    }\n\n    /**\n     * 判断字符串是否为null或全为空格\n     *\n     * @param s 待校验字符串\n     * @return {@code true}: null或全空格<br> {@code false}: 不为null且不全空格\n     */\n    public static boolean isTrimEmpty(final String s) {\n        return (s == null || s.trim().length() == 0);\n    }\n\n    /**\n     * 判断字符串是否为null或全为空白字符\n     *\n     * @param s 待校验字符串\n     * @return {@code true}: null或全空白字符<br> {@code false}: 不为null且不全空白字符\n     */\n    public static boolean isSpace(final String s) {\n        if (s == null) return true;\n        for (int i = 0, len = s.length(); i < len; ++i) {\n            if (!Character.isWhitespace(s.charAt(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * 判断两字符串是否相等\n     *\n     * @param a 待校验字符串a\n     * @param b 待校验字符串b\n     * @return {@code true}: 相等<br>{@code false}: 不相等\n     */\n    public static boolean equals(final CharSequence a, final CharSequence b) {\n        if (a == b) return true;\n        int length;\n        if (a != null && b != null && (length = a.length()) == b.length()) {\n            if (a instanceof String && b instanceof String) {\n                return a.equals(b);\n            } else {\n                for (int i = 0; i < length; i++) {\n                    if (a.charAt(i) != b.charAt(i)) return false;\n                }\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * 判断两字符串忽略大小写是否相等\n     *\n     * @param a 待校验字符串a\n     * @param b 待校验字符串b\n     * @return {@code true}: 相等<br>{@code false}: 不相等\n     */\n    public static boolean equalsIgnoreCase(final String a, final String b) {\n        return a == null ? b == null : a.equalsIgnoreCase(b);\n    }\n\n    /**\n     * null转为长度为0的字符串\n     *\n     * @param s 待转字符串\n     * @return s为null转为长度为0字符串，否则不改变\n     */\n    public static String null2Length0(final String s) {\n        return s == null ? \"\" : s;\n    }\n\n    /**\n     * 返回字符串长度\n     *\n     * @param s 字符串\n     * @return null返回0，其他返回自身长度\n     */\n    public static int length(final CharSequence s) {\n        return s == null ? 0 : s.length();\n    }\n\n    /**\n     * 首字母大写\n     *\n     * @param s 待转字符串\n     * @return 首字母大写字符串\n     */\n    public static String upperFirstLetter(final String s) {\n        if (isEmpty(s) || !Character.isLowerCase(s.charAt(0))) return s;\n        return String.valueOf((char) (s.charAt(0) - 32)) + s.substring(1);\n    }\n\n    /**\n     * 首字母小写\n     *\n     * @param s 待转字符串\n     * @return 首字母小写字符串\n     */\n    public static String lowerFirstLetter(final String s) {\n        if (isEmpty(s) || !Character.isUpperCase(s.charAt(0))) return s;\n        return String.valueOf((char) (s.charAt(0) + 32)) + s.substring(1);\n    }\n\n    /**\n     * 反转字符串\n     *\n     * @param s 待反转字符串\n     * @return 反转字符串\n     */\n    public static String reverse(final String s) {\n        int len = length(s);\n        if (len <= 1) return s;\n        int mid = len >> 1;\n        char[] chars = s.toCharArray();\n        char c;\n        for (int i = 0; i < mid; ++i) {\n            c = chars[i];\n            chars[i] = chars[len - i - 1];\n            chars[len - i - 1] = c;\n        }\n        return new String(chars);\n    }\n\n    /**\n     * 转化为半角字符\n     *\n     * @param s 待转字符串\n     * @return 半角字符串\n     */\n    public static String toDBC(final String s) {\n        if (isEmpty(s)) return s;\n        char[] chars = s.toCharArray();\n        for (int i = 0, len = chars.length; i < len; i++) {\n            if (chars[i] == 12288) {\n                chars[i] = ' ';\n            } else if (65281 <= chars[i] && chars[i] <= 65374) {\n                chars[i] = (char) (chars[i] - 65248);\n            } else {\n                chars[i] = chars[i];\n            }\n        }\n        return new String(chars);\n    }\n\n    /**\n     * 转化为全角字符\n     *\n     * @param s 待转字符串\n     * @return 全角字符串\n     */\n    public static String toSBC(final String s) {\n        if (isEmpty(s)) return s;\n        char[] chars = s.toCharArray();\n        for (int i = 0, len = chars.length; i < len; i++) {\n            if (chars[i] == ' ') {\n                chars[i] = (char) 12288;\n            } else if (33 <= chars[i] && chars[i] <= 126) {\n                chars[i] = (char) (chars[i] + 65248);\n            } else {\n                chars[i] = chars[i];\n            }\n        }\n        return new String(chars);\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/ToastUtils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.text.SpannableString;\nimport android.text.Spanned;\nimport android.text.style.ForegroundColorSpan;\nimport android.view.Gravity;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.Toast;\n\nimport java.lang.ref.WeakReference;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\n\n/**\n * Created by goldze on 2017/5/14.\n * 吐司工具类\n */\npublic final class ToastUtils {\n\n    private static final int DEFAULT_COLOR = 0x12000000;\n    private static Toast sToast;\n    private static int gravity         = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;\n    private static int xOffset         = 0;\n    private static int yOffset         = (int) (64 * Utils.getContext().getResources().getDisplayMetrics().density + 0.5);\n    private static int backgroundColor = DEFAULT_COLOR;\n    private static int bgResource      = -1;\n    private static int messageColor    = DEFAULT_COLOR;\n    private static WeakReference<View> sViewWeakReference;\n    private static Handler sHandler = new Handler(Looper.getMainLooper());\n\n    private ToastUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * 设置吐司位置\n     *\n     * @param gravity 位置\n     * @param xOffset x偏移\n     * @param yOffset y偏移\n     */\n    public static void setGravity(int gravity, int xOffset, int yOffset) {\n        ToastUtils.gravity = gravity;\n        ToastUtils.xOffset = xOffset;\n        ToastUtils.yOffset = yOffset;\n    }\n\n    /**\n     * 设置吐司view\n     *\n     * @param layoutId 视图\n     */\n    public static void setView(@LayoutRes int layoutId) {\n        LayoutInflater inflate = (LayoutInflater) Utils.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);\n        sViewWeakReference = new WeakReference<>(inflate.inflate(layoutId, null));\n    }\n\n    /**\n     * 设置吐司view\n     *\n     * @param view 视图\n     */\n    public static void setView(@Nullable View view) {\n        sViewWeakReference = view == null ? null : new WeakReference<>(view);\n    }\n\n    /**\n     * 获取吐司view\n     *\n     * @return view\n     */\n    public static View getView() {\n        if (sViewWeakReference != null) {\n            final View view = sViewWeakReference.get();\n            if (view != null) {\n                return view;\n            }\n        }\n        if (sToast != null) return sToast.getView();\n        return null;\n    }\n\n    /**\n     * 设置背景颜色\n     *\n     * @param backgroundColor 背景色\n     */\n    public static void setBackgroundColor(@ColorInt int backgroundColor) {\n        ToastUtils.backgroundColor = backgroundColor;\n    }\n\n    /**\n     * 设置背景资源\n     *\n     * @param bgResource 背景资源\n     */\n    public static void setBgResource(@DrawableRes int bgResource) {\n        ToastUtils.bgResource = bgResource;\n    }\n\n    /**\n     * 设置消息颜色\n     *\n     * @param messageColor 颜色\n     */\n    public static void setMessageColor(@ColorInt int messageColor) {\n        ToastUtils.messageColor = messageColor;\n    }\n\n    /**\n     * 安全地显示短时吐司\n     *\n     * @param text 文本\n     */\n    public static void showShortSafe(final CharSequence text) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(text, Toast.LENGTH_SHORT);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示短时吐司\n     *\n     * @param resId 资源Id\n     */\n    public static void showShortSafe(final @StringRes int resId) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(resId, Toast.LENGTH_SHORT);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示短时吐司\n     *\n     * @param resId 资源Id\n     * @param args  参数\n     */\n    public static void showShortSafe(final @StringRes int resId, final Object... args) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(resId, Toast.LENGTH_SHORT, args);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示短时吐司\n     *\n     * @param format 格式\n     * @param args   参数\n     */\n    public static void showShortSafe(final String format, final Object... args) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(format, Toast.LENGTH_SHORT, args);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示长时吐司\n     *\n     * @param text 文本\n     */\n    public static void showLongSafe(final CharSequence text) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(text, Toast.LENGTH_LONG);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示长时吐司\n     *\n     * @param resId 资源Id\n     */\n    public static void showLongSafe(final @StringRes int resId) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(resId, Toast.LENGTH_LONG);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示长时吐司\n     *\n     * @param resId 资源Id\n     * @param args  参数\n     */\n    public static void showLongSafe(final @StringRes int resId, final Object... args) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(resId, Toast.LENGTH_LONG, args);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示长时吐司\n     *\n     * @param format 格式\n     * @param args   参数\n     */\n    public static void showLongSafe(final String format, final Object... args) {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(format, Toast.LENGTH_LONG, args);\n            }\n        });\n    }\n\n    /**\n     * 显示短时吐司\n     *\n     * @param text 文本\n     */\n    public static void showShort(CharSequence text) {\n        show(text, Toast.LENGTH_SHORT);\n    }\n\n    /**\n     * 显示短时吐司\n     *\n     * @param resId 资源Id\n     */\n    public static void showShort(@StringRes int resId) {\n        show(resId, Toast.LENGTH_SHORT);\n    }\n\n    /**\n     * 显示短时吐司\n     *\n     * @param resId 资源Id\n     * @param args  参数\n     */\n    public static void showShort(@StringRes int resId, Object... args) {\n        show(resId, Toast.LENGTH_SHORT, args);\n    }\n\n    /**\n     * 显示短时吐司\n     *\n     * @param format 格式\n     * @param args   参数\n     */\n    public static void showShort(String format, Object... args) {\n        show(format, Toast.LENGTH_SHORT, args);\n    }\n\n    /**\n     * 显示长时吐司\n     *\n     * @param text 文本\n     */\n    public static void showLong(CharSequence text) {\n        show(text, Toast.LENGTH_LONG);\n    }\n\n    /**\n     * 显示长时吐司\n     *\n     * @param resId 资源Id\n     */\n    public static void showLong(@StringRes int resId) {\n        show(resId, Toast.LENGTH_LONG);\n    }\n\n    /**\n     * 显示长时吐司\n     *\n     * @param resId 资源Id\n     * @param args  参数\n     */\n    public static void showLong(@StringRes int resId, Object... args) {\n        show(resId, Toast.LENGTH_LONG, args);\n    }\n\n    /**\n     * 显示长时吐司\n     *\n     * @param format 格式\n     * @param args   参数\n     */\n    public static void showLong(String format, Object... args) {\n        show(format, Toast.LENGTH_LONG, args);\n    }\n\n    /**\n     * 安全地显示短时自定义吐司\n     */\n    public static void showCustomShortSafe() {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(\"\", Toast.LENGTH_SHORT);\n            }\n        });\n    }\n\n    /**\n     * 安全地显示长时自定义吐司\n     */\n    public static void showCustomLongSafe() {\n        sHandler.post(new Runnable() {\n            @Override\n            public void run() {\n                show(\"\", Toast.LENGTH_LONG);\n            }\n        });\n    }\n\n    /**\n     * 显示短时自定义吐司\n     */\n    public static void showCustomShort() {\n        show(\"\", Toast.LENGTH_SHORT);\n    }\n\n    /**\n     * 显示长时自定义吐司\n     */\n    public static void showCustomLong() {\n        show(\"\", Toast.LENGTH_LONG);\n    }\n\n    /**\n     * 显示吐司\n     *\n     * @param resId    资源Id\n     * @param duration 显示时长\n     */\n    private static void show(@StringRes int resId, int duration) {\n        show(Utils.getContext().getResources().getText(resId).toString(), duration);\n    }\n\n    /**\n     * 显示吐司\n     *\n     * @param resId    资源Id\n     * @param duration 显示时长\n     * @param args     参数\n     */\n    private static void show(@StringRes int resId, int duration, Object... args) {\n        show(String.format(Utils.getContext().getResources().getString(resId), args), duration);\n    }\n\n    /**\n     * 显示吐司\n     *\n     * @param format   格式\n     * @param duration 显示时长\n     * @param args     参数\n     */\n    private static void show(String format, int duration, Object... args) {\n        show(String.format(format, args), duration);\n    }\n\n    /**\n     * 显示吐司\n     *\n     * @param text     文本\n     * @param duration 显示时长\n     */\n    private static void show(CharSequence text, int duration) {\n        cancel();\n        boolean isCustom = false;\n        if (sViewWeakReference != null) {\n            final View view = sViewWeakReference.get();\n            if (view != null) {\n                sToast = new Toast(Utils.getContext());\n                sToast.setView(view);\n                sToast.setDuration(duration);\n                isCustom = true;\n            }\n        }\n        if (!isCustom) {\n            if (messageColor != DEFAULT_COLOR) {\n                SpannableString spannableString = new SpannableString(text);\n                ForegroundColorSpan colorSpan = new ForegroundColorSpan(messageColor);\n                spannableString.setSpan(colorSpan, 0, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n                sToast = Toast.makeText(Utils.getContext(), spannableString, duration);\n            } else {\n                sToast = Toast.makeText(Utils.getContext(), text, duration);\n            }\n        }\n        View view = sToast.getView();\n        if (bgResource != -1) {\n            view.setBackgroundResource(bgResource);\n        } else if (backgroundColor != DEFAULT_COLOR) {\n            view.setBackgroundColor(backgroundColor);\n        }\n        sToast.setGravity(gravity, xOffset, yOffset);\n        sToast.show();\n    }\n\n    /**\n     * 取消吐司显示\n     */\n    public static void cancel() {\n        if (sToast != null) {\n            sToast.cancel();\n            sToast = null;\n        }\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/Utils.java",
    "content": "package me.goldze.mvvmhabit.utils;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport androidx.annotation.NonNull;\n\n/**\n * Created by goldze on 2017/5/14.\n * 常用工具类\n */\npublic final class Utils {\n\n    @SuppressLint(\"StaticFieldLeak\")\n    private static Context context;\n\n    private Utils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * 初始化工具类\n     *\n     * @param context 上下文\n     */\n    public static void init(@NonNull final Context context) {\n        Utils.context = context.getApplicationContext();\n    }\n\n    /**\n     * 获取ApplicationContext\n     *\n     * @return ApplicationContext\n     */\n    public static Context getContext() {\n        if (context != null) {\n            return context;\n        }\n        throw new NullPointerException(\"should be initialized in application\");\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/compression/Luban.java",
    "content": "package me.goldze.mvvmhabit.utils.compression;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Matrix;\nimport android.media.ExifInterface;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport androidx.annotation.NonNull;\nimport io.reactivex.Observable;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.functions.Consumer;\nimport io.reactivex.functions.Function;\nimport io.reactivex.functions.Predicate;\nimport io.reactivex.schedulers.Schedulers;\n\n\npublic class Luban {\n\n    private static final int FIRST_GEAR = 1;\n    public static final int THIRD_GEAR = 3;\n\n    private static final String TAG = \"smartcity\";\n    private static String DEFAULT_DISK_CACHE_DIR = \"smartcity_disk_cache\";\n\n    private static volatile Luban INSTANCE;\n\n    private final File mCacheDir;\n\n    private OnCompressListener compressListener;\n    private String mFile;\n    private List<String> mListFile = new ArrayList<>();\n    private int gear = THIRD_GEAR;\n    private String filename;\n\n    private Luban(File cacheDir) {\n        mCacheDir = cacheDir;\n    }\n\n    /**\n     * Returns a directory with a default name in the private cache directory of the application to\n     * use to store\n     * retrieved media and thumbnails.\n     *\n     * @param context A context.\n     * @see #getPhotoCacheDir(Context, String)\n     */\n    private static synchronized File getPhotoCacheDir(Context context) {\n        return getPhotoCacheDir(context, Luban.DEFAULT_DISK_CACHE_DIR);\n    }\n\n    /**\n     * Returns a directory with the given name in the private cache directory of the application to\n     * use to store\n     * retrieved media and thumbnails.\n     *\n     * @param context   A context.\n     * @param cacheName The name of the subdirectory in which to store the cache.\n     * @see #getPhotoCacheDir(Context)\n     */\n    private static File getPhotoCacheDir(Context context, String cacheName) {\n        File cacheDir = context.getCacheDir();\n        //File cacheDir = ImageUtils.checkTargetCacheDir(FileConstant.IMAGE_COMPRESS);\n        if (cacheDir != null) {\n            File result = new File(cacheDir, cacheName);\n            if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) {\n                // File wasn't able to create a directory, or the result exists but not a directory\n                return null;\n            }\n\n            File noMedia = new File(cacheDir + \"/.nomedia\");\n            if (!noMedia.mkdirs() && (!noMedia.exists() || !noMedia.isDirectory())) {\n                return null;\n            }\n\n            return result;\n        }\n        if (Log.isLoggable(TAG, Log.ERROR)) {\n            Log.e(TAG, \"default disk cache dir is null\");\n        }\n        return null;\n    }\n\n    public static Luban get(Context context) {\n        if (INSTANCE == null) INSTANCE = new Luban(Luban.getPhotoCacheDir(context));\n        return INSTANCE;\n    }\n\n    public Luban launch() {\n        Preconditions.checkNotNull(mFile, \"the image file cannot be null, please call .load() before this method!\");\n\n        if (compressListener != null) compressListener.onStart();\n        if (gear == Luban.FIRST_GEAR)\n            Observable.just(mFile)\n                    .map(new Function<String, File>() {\n                        @Override\n                        public File apply(String s) throws Exception {\n                            File file = new File(s);\n                            return firstCompress(file);\n                        }\n                    })\n                    .subscribeOn(Schedulers.io())\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .doOnError(new Consumer<Throwable>() {\n                        @Override\n                        public void accept(Throwable throwable) throws Exception {\n                            if (compressListener != null) compressListener.onError(throwable);\n                        }\n                    })\n                    .onErrorResumeNext(Observable.<File>empty())\n                    .filter(new Predicate<File>() {\n                        @Override\n                        public boolean test(File file) throws Exception {\n                            return file != null;\n                        }\n                    })\n                    .subscribe(new Consumer<File>() {\n                        @Override\n                        public void accept(File file) throws Exception {\n                            if (compressListener != null) compressListener.onSuccess(file);\n                        }\n                    });\n        else if (gear == Luban.THIRD_GEAR)\n            Observable.just(mFile)\n                    .map(new Function<String, File>() {\n                        @Override\n                        public File apply(String s) throws Exception {\n                            File file = new File(s);\n                            return thirdCompress(file);\n                        }\n                    })\n                    .subscribeOn(Schedulers.io())\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .doOnError(new Consumer<Throwable>() {\n                        @Override\n                        public void accept(Throwable throwable) throws Exception {\n                            if (compressListener != null) compressListener.onError(throwable);\n                        }\n                    })\n                    .onErrorResumeNext(Observable.<File>empty())\n                    .filter(new Predicate<File>() {\n                        @Override\n                        public boolean test(File file) throws Exception {\n                            return file != null;\n                        }\n                    })\n                    .subscribe(new Consumer<File>() {\n                        @Override\n                        public void accept(File file) throws Exception {\n                            if (compressListener != null) compressListener.onSuccess(file);\n                        }\n                    });\n        return this;\n    }\n\n\n    public Luban load(String file) {\n        mFile = file;\n        return this;\n    }\n\n    public Luban load(List<String> listFile) {\n        mListFile = listFile;\n        return this;\n    }\n\n    public Luban setCompressListener(OnCompressListener listener) {\n        compressListener = listener;\n        return this;\n    }\n\n    public Luban putGear(int gear) {\n        this.gear = gear;\n        return this;\n    }\n\n    /**\n     * @deprecated\n     */\n    public Luban setFilename(String filename) {\n        this.filename = filename;\n        return this;\n    }\n\n    public Observable<File> asObservable() {\n        if (gear == FIRST_GEAR)\n            return Observable.just(mFile).map(new Function<String, File>() {\n                @Override\n                public File apply(String s) throws Exception {\n                    if (TextUtils.isEmpty(s) || s.contains(\"http\")) {\n                        return null;\n                    } else {\n                        File file = new File(s);\n                        if (file.exists()) {\n                            return firstCompress(file);\n                        } else {\n                            return null;\n                        }\n                    }\n                }\n            });\n        else if (gear == THIRD_GEAR)\n            return Observable.just(mFile).map(new Function<String, File>() {\n                @Override\n                public File apply(String s) throws Exception {\n                    if (TextUtils.isEmpty(s) || s.contains(\"http\")) {\n                        return null;\n                    } else {\n                        File file = new File(s);\n                        if (file.exists()) {\n                            return thirdCompress(file);\n                        } else {\n                            return null;\n                        }\n                    }\n                }\n            });\n        else return Observable.empty();\n    }\n\n    public Observable<File> asListObservable() {\n        if (gear == FIRST_GEAR)\n            return Observable.fromIterable(mListFile).map(new Function<String, File>() {\n                @Override\n                public File apply(String s) throws Exception {\n                    if (TextUtils.isEmpty(s)) {\n                        return null;\n                    } else {\n                        File file = new File(s);\n                        if (file.exists()) {\n                            return firstCompress(file);\n                        } else {\n                            return null;\n                        }\n                    }\n                }\n            });\n        else if (gear == THIRD_GEAR)\n            return Observable.fromIterable(mListFile).map(new Function<String, File>() {\n                @Override\n                public File apply(String s) throws Exception {\n                    if (TextUtils.isEmpty(s)) {\n                        return null;\n                    } else {\n                        File file = new File(s);\n                        if (file.exists()) {\n                            return thirdCompress(file);\n                        } else {\n                            return null;\n                        }\n                    }\n                }\n            });\n        else return Observable.empty();\n    }\n\n    private File thirdCompress(@NonNull File file) {\n        String thumb = mCacheDir.getAbsolutePath() + File.separator +\n                (TextUtils.isEmpty(filename) ? System.currentTimeMillis() : filename) + \".jpg\";\n\n        double size;\n        String filePath = file.getAbsolutePath();\n\n        int angle = getImageSpinAngle(filePath);\n        int width = getImageSize(filePath)[0];\n        int height = getImageSize(filePath)[1];\n        int thumbW = width % 2 == 1 ? width + 1 : width;\n        int thumbH = height % 2 == 1 ? height + 1 : height;\n\n        width = thumbW > thumbH ? thumbH : thumbW;\n        height = thumbW > thumbH ? thumbW : thumbH;\n\n        double scale = ((double) width / height);\n\n        if (scale <= 1 && scale > 0.5625) {\n            if (height < 1664) {\n                if (file.length() / 1024 < 150) return file;\n\n                size = (width * height) / Math.pow(1664, 2) * 150;\n                size = size < 60 ? 60 : size;\n            } else if (height >= 1664 && height < 4990) {\n                thumbW = width / 2;\n                thumbH = height / 2;\n                size = (thumbW * thumbH) / Math.pow(2495, 2) * 300;\n                size = size < 60 ? 60 : size;\n            } else if (height >= 4990 && height < 10240) {\n                thumbW = width / 4;\n                thumbH = height / 4;\n                size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;\n                size = size < 100 ? 100 : size;\n            } else {\n                int multiple = height / 1280 == 0 ? 1 : height / 1280;\n                thumbW = width / multiple;\n                thumbH = height / multiple;\n                size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;\n                size = size < 100 ? 100 : size;\n            }\n        } else if (scale <= 0.5625 && scale > 0.5) {\n            if (height < 1280 && file.length() / 1024 < 200) return file;\n\n            int multiple = height / 1280 == 0 ? 1 : height / 1280;\n            thumbW = width / multiple;\n            thumbH = height / multiple;\n            size = (thumbW * thumbH) / (1440.0 * 2560.0) * 400;\n            size = size < 100 ? 100 : size;\n        } else {\n            int multiple = (int) Math.ceil(height / (1280.0 / scale));\n            thumbW = width / multiple;\n            thumbH = height / multiple;\n            size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;\n            size = size < 100 ? 100 : size;\n        }\n\n        return compress(filePath, thumb, thumbW, thumbH, angle, (long) size);\n    }\n\n    private File firstCompress(@NonNull File file) {\n        int minSize = 60;\n        int longSide = 720;\n        int shortSide = 1280;\n\n        String filePath = file.getAbsolutePath();\n        String thumbFilePath = mCacheDir.getAbsolutePath() + File.separator +\n                (TextUtils.isEmpty(filename) ? System.currentTimeMillis() : filename) + \".jpg\";\n\n        long size = 0;\n        long maxSize = file.length() / 5;\n\n        int angle = getImageSpinAngle(filePath);\n        int[] imgSize = getImageSize(filePath);\n        int width = 0, height = 0;\n        if (imgSize[0] <= imgSize[1]) {\n            double scale = (double) imgSize[0] / (double) imgSize[1];\n            if (scale <= 1.0 && scale > 0.5625) {\n                width = imgSize[0] > shortSide ? shortSide : imgSize[0];\n                height = width * imgSize[1] / imgSize[0];\n                size = minSize;\n            } else if (scale <= 0.5625) {\n                height = imgSize[1] > longSide ? longSide : imgSize[1];\n                width = height * imgSize[0] / imgSize[1];\n                size = maxSize;\n            }\n        } else {\n            double scale = (double) imgSize[1] / (double) imgSize[0];\n            if (scale <= 1.0 && scale > 0.5625) {\n                height = imgSize[1] > shortSide ? shortSide : imgSize[1];\n                width = height * imgSize[0] / imgSize[1];\n                size = minSize;\n            } else if (scale <= 0.5625) {\n                width = imgSize[0] > longSide ? longSide : imgSize[0];\n                height = width * imgSize[1] / imgSize[0];\n                size = maxSize;\n            }\n        }\n\n        return compress(filePath, thumbFilePath, width, height, angle, size);\n    }\n\n    /**\n     * obtain the image's width and height\n     *\n     * @param imagePath the path of image\n     */\n    public int[] getImageSize(String imagePath) {\n        int[] res = new int[2];\n\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        options.inSampleSize = 1;\n        BitmapFactory.decodeFile(imagePath, options);\n\n        res[0] = options.outWidth;\n        res[1] = options.outHeight;\n\n        return res;\n    }\n\n    /**\n     * obtain the thumbnail that specify the size\n     *\n     * @param imagePath the target image path\n     * @param width     the width of thumbnail\n     * @param height    the height of thumbnail\n     * @return {@link Bitmap}\n     */\n    private Bitmap compress(String imagePath, int width, int height) {\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        BitmapFactory.decodeFile(imagePath, options);\n\n        int outH = options.outHeight;\n        int outW = options.outWidth;\n        int inSampleSize = 1;\n\n        if (outH > height || outW > width) {\n            int halfH = outH / 2;\n            int halfW = outW / 2;\n\n            while ((halfH / inSampleSize) > height && (halfW / inSampleSize) > width) {\n                inSampleSize *= 2;\n            }\n        }\n\n        options.inSampleSize = inSampleSize;\n\n        options.inJustDecodeBounds = false;\n\n        int heightRatio = (int) Math.ceil(options.outHeight / (float) height);\n        int widthRatio = (int) Math.ceil(options.outWidth / (float) width);\n\n        if (heightRatio > 1 || widthRatio > 1) {\n            if (heightRatio > widthRatio) {\n                options.inSampleSize = heightRatio;\n            } else {\n                options.inSampleSize = widthRatio;\n            }\n        }\n        options.inJustDecodeBounds = false;\n\n        return BitmapFactory.decodeFile(imagePath, options);\n    }\n\n    /**\n     * obtain the image rotation angle\n     *\n     * @param path path of target image\n     */\n    private int getImageSpinAngle(String path) {\n        int degree = 0;\n        try {\n            ExifInterface exifInterface = new ExifInterface(path);\n            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);\n            switch (orientation) {\n                case ExifInterface.ORIENTATION_ROTATE_90:\n                    degree = 90;\n                    break;\n                case ExifInterface.ORIENTATION_ROTATE_180:\n                    degree = 180;\n                    break;\n                case ExifInterface.ORIENTATION_ROTATE_270:\n                    degree = 270;\n                    break;\n            }\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return degree;\n    }\n\n    /**\n     * 指定参数压缩图片\n     * create the thumbnail with the true rotate angle\n     *\n     * @param largeImagePath the big image path\n     * @param thumbFilePath  the thumbnail path\n     * @param width          width of thumbnail\n     * @param height         height of thumbnail\n     * @param angle          rotation angle of thumbnail\n     * @param size           the file size of image\n     */\n    private File compress(String largeImagePath, String thumbFilePath, int width, int height, int angle, long size) {\n        Bitmap thbBitmap = compress(largeImagePath, width, height);\n\n        thbBitmap = rotatingImage(angle, thbBitmap);\n\n        return saveImage(thumbFilePath, thbBitmap, size);\n    }\n\n    /**\n     * 旋转图片\n     * rotate the image with specified angle\n     *\n     * @param angle  the angle will be rotating 旋转的角度\n     * @param bitmap target image               目标图片\n     */\n    private static Bitmap rotatingImage(int angle, Bitmap bitmap) {\n        //rotate image\n        Matrix matrix = new Matrix();\n        matrix.postRotate(angle);\n\n        //create a new image\n        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);\n    }\n\n    /**\n     * 保存图片到指定路径\n     * Save image with specified size\n     *\n     * @param filePath the image file save path 储存路径\n     * @param bitmap   the image what be save   目标图片\n     * @param size     the file size of image   期望大小\n     */\n    private File saveImage(String filePath, Bitmap bitmap, long size) {\n        Preconditions.checkNotNull(bitmap, TAG + \"bitmap cannot be null\");\n\n        File result = new File(filePath.substring(0, filePath.lastIndexOf(\"/\")));\n\n        if (!result.exists() && !result.mkdirs()) return null;\n\n        ByteArrayOutputStream stream = new ByteArrayOutputStream();\n        int options = 100;\n        bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);\n\n        while (stream.toByteArray().length / 1024 > size && options > 6) {\n            stream.reset();\n            options -= 6;\n            bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);\n        }\n\n        try {\n            FileOutputStream fos = new FileOutputStream(filePath);\n            fos.write(stream.toByteArray());\n            fos.flush();\n            fos.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n\n        return new File(filePath);\n    }\n}"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/compression/OnCompressListener.java",
    "content": "package me.goldze.mvvmhabit.utils.compression;\n\nimport java.io.File;\n\npublic interface OnCompressListener {\n\n    /**\n     * Fired when the compression is started, override to handle in your own code\n     */\n    void onStart();\n\n    /**\n     * Fired when a compression returns successfully, override to handle in your own code\n     */\n    void onSuccess(File file);\n\n    /**\n     * Fired when a compression fails to complete, override to handle in your own code\n     */\n    void onError(Throwable e);\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/compression/Preconditions.java",
    "content": "package me.goldze.mvvmhabit.utils.compression;\n\nimport androidx.annotation.Nullable;\n\nfinal class Preconditions {\n\n    /**\n     * Ensures that an object reference passed as a parameter to the calling method is not null.\n     *\n     * @param reference an object reference\n     * @return the non-null reference that was validated\n     * @throws NullPointerException if {@code reference} is null\n     */\n    static <T> T checkNotNull(T reference) {\n        if (reference == null) {\n            throw new NullPointerException();\n        }\n        return reference;\n    }\n\n    /**\n     * Ensures that an object reference passed as a parameter to the calling method is not null.\n     *\n     * @param reference    an object reference\n     * @param errorMessage the exception message to use if the check fails; will be converted to a\n     *                     string using {@link String#valueOf(Object)}\n     * @return the non-null reference that was validated\n     * @throws NullPointerException if {@code reference} is null\n     */\n    static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {\n        if (reference == null) {\n            throw new NullPointerException(String.valueOf(errorMessage));\n        }\n        return reference;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/constant/MemoryConstants.java",
    "content": "package me.goldze.mvvmhabit.utils.constant;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\nimport androidx.annotation.IntDef;\n\n/**\n * Created by goldze on 2017/5/14.\n * 存储相关常量\n */\npublic final class MemoryConstants {\n\n    /**\n     * Byte与Byte的倍数\n     */\n    public static final int BYTE = 1;\n    /**\n     * KB与Byte的倍数\n     */\n    public static final int KB   = 1024;\n    /**\n     * MB与Byte的倍数\n     */\n    public static final int MB   = 1048576;\n    /**\n     * GB与Byte的倍数\n     */\n    public static final int GB   = 1073741824;\n\n    @IntDef({BYTE, KB, MB, GB})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Unit {\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/constant/RegexConstants.java",
    "content": "package me.goldze.mvvmhabit.utils.constant;\n\n/**\n * Created by goldze on 2017/5/14.\n * 正则相关常量\n */\npublic final class RegexConstants {\n\n    /**\n     * 正则：手机号（简单）\n     */\n    public static final String REGEX_MOBILE_SIMPLE = \"^[1]\\\\d{10}$\";\n    /**\n     * 正则：手机号（精确）\n     * <p>移动：134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、178、182、183、184、187、188</p>\n     * <p>联通：130、131、132、145、155、156、175、176、185、186</p>\n     * <p>电信：133、153、173、177、180、181、189</p>\n     * <p>全球星：1349</p>\n     * <p>虚拟运营商：170</p>\n     */\n    public static final String REGEX_MOBILE_EXACT = \"^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|(147))\\\\d{8}$\";\n    /**\n     * 正则：电话号码\n     */\n    public static final String REGEX_TEL = \"^0\\\\d{2,3}[- ]?\\\\d{7,8}\";\n    /**\n     * 正则：身份证号码15位\n     */\n    public static final String REGEX_ID_CARD15 = \"^[1-9]\\\\d{7}((0\\\\d)|(1[0-2]))(([0|1|2]\\\\d)|3[0-1])\\\\d{3}$\";\n    /**\n     * 正则：身份证号码18位\n     */\n    public static final String REGEX_ID_CARD18 = \"^[1-9]\\\\d{5}[1-9]\\\\d{3}((0\\\\d)|(1[0-2]))(([0|1|2]\\\\d)|3[0-1])\\\\d{3}([0-9Xx])$\";\n    /**\n     * 正则：邮箱\n     */\n    public static final String REGEX_EMAIL = \"^\\\\w+([-+.]\\\\w+)*@\\\\w+([-.]\\\\w+)*\\\\.\\\\w+([-.]\\\\w+)*$\";\n    /**\n     * 正则：URL\n     */\n    public static final String REGEX_URL = \"[a-zA-z]+://[^\\\\s]*\";\n    /**\n     * 正则：汉字\n     */\n    public static final String REGEX_ZH = \"^[\\\\u4e00-\\\\u9fa5]+$\";\n    /**\n     * 正则：用户名，取值范围为a-z,A-Z,0-9,\"_\",汉字，不能以\"_\"结尾,用户名必须是6-20位\n     */\n    public static final String REGEX_USERNAME = \"^[\\\\w\\\\u4e00-\\\\u9fa5]{6,20}(?<!_)$\";\n    /**\n     * 正则：yyyy-MM-dd格式的日期校验，已考虑平闰年\n     */\n    public static final String REGEX_DATE = \"^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$\";\n    /**\n     * 正则：IP地址\n     */\n    public static final String REGEX_IP = \"((2[0-4]\\\\d|25[0-5]|[01]?\\\\d\\\\d?)\\\\.){3}(2[0-4]\\\\d|25[0-5]|[01]?\\\\d\\\\d?)\";\n\n    ///////////////////////////////////////////////////////////////////////////\n    // 以下摘自http://tool.oschina.net/regex\n    ///////////////////////////////////////////////////////////////////////////\n\n    /**\n     * 正则：双字节字符(包括汉字在内)\n     */\n    public static final String REGEX_DOUBLE_BYTE_CHAR = \"[^\\\\x00-\\\\xff]\";\n    /**\n     * 正则：空白行\n     */\n    public static final String REGEX_BLANK_LINE = \"\\\\n\\\\s*\\\\r\";\n    /**\n     * 正则：QQ号\n     */\n    public static final String REGEX_TENCENT_NUM = \"[1-9][0-9]{4,}\";\n    /**\n     * 正则：中国邮政编码\n     */\n    public static final String REGEX_ZIP_CODE = \"[1-9]\\\\d{5}(?!\\\\d)\";\n    /**\n     * 正则：正整数\n     */\n    public static final String REGEX_POSITIVE_INTEGER = \"^[1-9]\\\\d*$\";\n    /**\n     * 正则：负整数\n     */\n    public static final String REGEX_NEGATIVE_INTEGER = \"^-[1-9]\\\\d*$\";\n    /**\n     * 正则：整数\n     */\n    public static final String REGEX_INTEGER = \"^-?[1-9]\\\\d*$\";\n    /**\n     * 正则：非负整数(正整数 + 0)\n     */\n    public static final String REGEX_NOT_NEGATIVE_INTEGER = \"^[1-9]\\\\d*|0$\";\n    /**\n     * 正则：非正整数（负整数 + 0）\n     */\n    public static final String REGEX_NOT_POSITIVE_INTEGER = \"^-[1-9]\\\\d*|0$\";\n    /**\n     * 正则：正浮点数\n     */\n    public static final String REGEX_POSITIVE_FLOAT = \"^[1-9]\\\\d*\\\\.\\\\d*|0\\\\.\\\\d*[1-9]\\\\d*$\";\n    /**\n     * 正则：负浮点数\n     */\n    public static final String REGEX_NEGATIVE_FLOAT = \"^-[1-9]\\\\d*\\\\.\\\\d*|-0\\\\.\\\\d*[1-9]\\\\d*$\";\n\n    ///////////////////////////////////////////////////////////////////////////\n    // If u want more please visit http://toutiao.com/i6231678548520731137\n    ///////////////////////////////////////////////////////////////////////////\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/utils/constant/TimeConstants.java",
    "content": "package me.goldze.mvvmhabit.utils.constant;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\nimport androidx.annotation.IntDef;\n\n/**\n * Created by goldze on 2017/5/14.\n * 时间相关常量\n */\npublic final class TimeConstants {\n\n    /**\n     * 毫秒与毫秒的倍数\n     */\n    public static final int MSEC = 1;\n    /**\n     * 秒与毫秒的倍数\n     */\n    public static final int SEC = 1000;\n    /**\n     * 分与毫秒的倍数\n     */\n    public static final int MIN = 60000;\n    /**\n     * 时与毫秒的倍数\n     */\n    public static final int HOUR = 3600000;\n    /**\n     * 天与毫秒的倍数\n     */\n    public static final int DAY = 86400000;\n\n    @IntDef({MSEC, SEC, MIN, HOUR, DAY})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Unit {\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/java/me/goldze/mvvmhabit/widget/ControlDistributeLinearLayout.java",
    "content": "package me.goldze.mvvmhabit.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.widget.LinearLayout;\n\nimport me.goldze.mvvmhabit.R;\n\n/**\n * Created by goldze on 2017/3/16.\n * 控制事件分发的LinearLayout\n */\npublic class ControlDistributeLinearLayout extends LinearLayout {\n    //默认是不拦截事件,分发事件给子View\n    private boolean isDistributeEvent = false;\n\n    public ControlDistributeLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ControlDistributeLinearLayout);\n        isDistributeEvent = typedArray.getBoolean(R.styleable.ControlDistributeLinearLayout_distribute_event, false);\n    }\n\n    public ControlDistributeLinearLayout(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public ControlDistributeLinearLayout(Context context) {\n        this(context, null);\n    }\n\n    /**\n     * 重写事件分发方法,false 为分发 , true 为父控件自己消耗, 由外面传进来的参数决定\n     */\n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        return isDistributeEvent();\n    }\n\n    public boolean isDistributeEvent() {\n        return isDistributeEvent;\n    }\n\n    public void setDistributeEvent(boolean distributeEvent) {\n        isDistributeEvent = distributeEvent;\n    }\n}\n"
  },
  {
    "path": "mvvmhabit/src/main/res/layout/activity_container.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n</FrameLayout>"
  },
  {
    "path": "mvvmhabit/src/main/res/layout/customactivityoncrash_default_error_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        tools:ignore=\"UselessParent\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center\"\n            android:orientation=\"vertical\"\n            android:paddingBottom=\"@dimen/customactivityoncrash_activity_vertical_margin\"\n            android:paddingLeft=\"@dimen/customactivityoncrash_activity_horizontal_margin\"\n            android:paddingRight=\"@dimen/customactivityoncrash_activity_horizontal_margin\"\n            android:paddingTop=\"@dimen/customactivityoncrash_activity_vertical_margin\">\n\n            <ImageView\n                android:id=\"@+id/customactivityoncrash_error_activity_image\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:contentDescription=\"@null\"\n                android:src=\"@drawable/customactivityoncrash_error_image\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/customactivityoncrash_activity_vertical_margin\"\n                android:gravity=\"center\"\n                android:text=\"@string/customactivityoncrash_error_activity_error_occurred_explanation\"\n                android:textSize=\"18sp\"\n                android:textStyle=\"bold\" />\n\n            <Button\n                android:id=\"@+id/customactivityoncrash_error_activity_restart_button\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"@dimen/customactivityoncrash_activity_vertical_margin\"\n                android:text=\"@string/customactivityoncrash_error_activity_close_app\" />\n\n            <Button\n                android:id=\"@+id/customactivityoncrash_error_activity_more_info_button\"\n                style=\"?borderlessButtonStyle\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/customactivityoncrash_error_activity_error_details\"\n                android:textColor=\"?colorPrimary\" />\n        </LinearLayout>\n    </ScrollView>\n</RelativeLayout>"
  },
  {
    "path": "mvvmhabit/src/main/res/values/attrs.xml",
    "content": "<resources>\n\n    <!-- require boolean value to decide whether requestFocus for view. -->\n    <attr name=\"requestFocus\" format=\"boolean\" />\n    <!-- require ItemView {@link me.tatarka.bindingcollectionadapter.ItemView} or ItemViewSelector {{@link me.tatarka.bindingcollectionadapter.ItemViewSelector}.} -->\n    <attr name=\"itemView\" format=\"reference\" />\n    <!-- require List<ViewModel> bind to ItemView to presentation.-->\n    <attr name=\"items\" format=\"reference\" />\n    <!-- require a adapter which type of BindingRecyclerViewAdapter<T> to AdapterView-->\n    <attr name=\"adapter\" format=\"reference\" />\n\n    <attr name=\"onScrollChangeCommand\" format=\"reference\" />\n    <attr name=\"onScrollStateChangedCommand\" format=\"reference\" />\n    <attr name=\"url\" format=\"string\" />\n    <attr name=\"onTouchCommand\" format=\"reference\" />\n\n    <!-- require BindingCommand {@link com.kelin.mvvmlight.command.BindingCommand } to deal with view click event. -->\n    <attr name=\"onClickCommand\" format=\"reference\" />\n    <attr name=\"onLongClickCommand\" format=\"reference\" />\n    <!-- require BindingCommand<Boolean> {@link com.kelin.mvvmlight.command.BindingCommand } to deal with view focus change event.\n     BindingCommand would has params which means if view hasFocus.-->\n    <attr name=\"onFocusChangeCommand\" format=\"reference\" />\n    <attr name=\"isThrottleFirst\" format=\"boolean\" />\n    <attr name=\"currentView\" format=\"reference\" />\n    <attr name=\"isVisible\" format=\"boolean\" />\n    <!-- require boolean value to decide whether requestFocus for view. -->\n    <declare-styleable name=\"View\">\n        <!-- require BindingCommand {@link com.kelin.mvvmlight.command.BindingCommand } to deal with view click event. -->\n        <attr name=\"onClickCommand\" />\n        <attr name=\"onLongClickCommand\" />\n        <!-- require BindingCommand<Boolean> {@link com.kelin.mvvmlight.command.BindingCommand } to deal with view focus change event.\n         BindingCommand would has params which means if view hasFocus.-->\n        <attr name=\"onFocusChangeCommand\" />\n        <!-- require BindingCommand<MotionEvent> -->\n        <attr name=\"onTouchCommand\" />\n        <attr name=\"isThrottleFirst\" />\n        <attr name=\"currentView\" />\n\n    </declare-styleable>\n\n\n    <declare-styleable name=\"AdapterView\">\n        <!-- require ItemView {@link me.tatarka.bindingcollectionadapter.ItemView} or ItemViewSelector {{@link me.tatarka.bindingcollectionadapter.ItemViewSelector}.} -->\n        <attr name=\"itemView\" />\n        <!-- require List<ViewModel> bind to ItemView to presentation.-->\n        <attr name=\"items\" />\n        <!-- require a adapter which type of BindingRecyclerViewAdapter<T> to AdapterView-->\n        <attr name=\"adapter\" />\n        <attr name=\"dropDownItemView\" format=\"reference\" />\n        <attr name=\"itemIds\" format=\"reference\" />\n        <attr name=\"itemIsEnabled\" format=\"reference\" />\n        <!-- require BindingCommand<Integer> -->\n        <attr name=\"onScrollStateChangedCommand\" />\n        <!-- require BindingCommand<ListViewScrollDataWrapper> -->\n        <attr name=\"onScrollChangeCommand\" />\n        <!-- require BindingCommand<Integer> count of list items-->\n        <attr name=\"onLoadMoreCommand\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"TextView\">\n        <!--require BindingCommand<TextChangeDataWrapper> -->\n        <attr name=\"beforeTextChangedCommand\" format=\"reference\" />\n        <!--require BindingCommand<TextChangeDataWrapper> -->\n        <attr name=\"onTextChangedCommand\" format=\"reference\" />\n        <!--require BindingCommand<String> -->\n        <attr name=\"afterTextChangedCommand\" format=\"reference\" />\n        <attr name=\"textChanged\" format=\"reference\" />\n    </declare-styleable>\n\n\n    <declare-styleable name=\"ImageView\">\n        <!--  load bitmap from uri(string type) -->\n        <attr name=\"url\" />\n        <!--width for ResizeOptions (use Fresco to load bitmap). -->\n        <attr name=\"request_width\" format=\"integer\" />\n        <!--height for ResizeOptions (use Fresco to load bitmap). -->\n        <attr name=\"request_height\" format=\"integer\" />\n        <attr name=\"placeholderRes\" format=\"reference|color\" />\n        <!--  require BindingCommand<Bitmap> See {@link @link com.kelin.mvvmlight.command.BindingCommand} -->\n        <attr name=\"onSuccessCommand\" format=\"reference\" />\n        <!--require BindingCommand<CloseableReference<CloseableImage>> See {@link com.kelin.mvvmlight.command.BindingCommand} -->\n        <attr name=\"onFailureCommand\" format=\"reference\" />\n\n    </declare-styleable>\n\n\n    <declare-styleable name=\"ViewGroup\">\n        <!-- require ItemView {@link me.tatarka.bindingcollectionadapter.ItemView} or ItemViewSelector {{@link me.tatarka.bindingcollectionadapter.ItemViewSelector}.} -->\n        <attr name=\"itemView\" />\n        <!-- require List<ViewModel> bind to ItemView to presentation.-->\n        <attr name=\"observableList\" format=\"reference\" />\n\n    </declare-styleable>\n\n    <declare-styleable name=\"RecyclerView\" parent=\"AdapterView\">\n        <attr name=\"lineManager\" format=\"reference\" />\n        <attr name=\"itemBinding\" format=\"reference\" />\n        <attr name=\"layoutManager\" format=\"reference\" />\n        <attr name=\"itemAnimator\" format=\"reference\" />\n    </declare-styleable>\n    <declare-styleable name=\"RadioGroup\">\n        <attr name=\"onCheckedChangedCommand\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"Spinner\">\n        <attr name=\"itemDatas\" format=\"reference\" />\n        <attr name=\"valueReply\" format=\"string\" />\n        <attr name=\"resource\" format=\"integer\" />\n        <attr name=\"dropDownResource\" format=\"integer\" />\n        <attr name=\"onItemSelectedCommand\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"Switch\">\n        <attr name=\"onCheckedChangeCommand\" format=\"reference\" />\n        <attr name=\"switchState\" format=\"boolean\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"ListView\" parent=\"AdapterView\">\n        <!--require BindingCommand<Integer> integer mean to position where is clicked! -->\n        <attr name=\"onItemClickCommand\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"ViewPager\">\n        <!-- require ItemView {@link me.tatarka.bindingcollectionadapter.ItemView} or ItemViewSelector {{@link me.tatarka.bindingcollectionadapter.ItemViewSelector}.} -->\n        <attr name=\"itemView\" />\n        <!-- require List<ViewModel> bind to ItemView to presentation.-->\n        <attr name=\"items\" />\n        <!-- require a adapter which type of BindingRecyclerViewAdapter<T> to AdapterView-->\n        <attr name=\"adapter\" />\n        <!-- require PageTitles<T>-->\n        <attr name=\"pageTitles\" format=\"reference\" />\n        <!--require BindingCommand<ViewPagerDataWrapper> -->\n        <attr name=\"onPageScrolledCommand\" format=\"reference\" />\n        <!--require BindingCommand<Integer> -->\n        <attr name=\"onPageSelectedCommand\" format=\"reference\" />\n        <!--require BindingCommand<Integer> -->\n        <attr name=\"onPageScrollStateChangedCommand\" format=\"reference\" />\n\n    </declare-styleable>\n\n    <declare-styleable name=\"NestedScrollView\">\n        <!-- require BindingCommand<NestScrollDataWrapper> -->\n        <attr name=\"onScrollChangeCommand\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"SimpleDraweeView\">\n        <!-- require String to load Image\"-->\n        <attr name=\"url\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"ScrollView\">\n        <!-- require BindingCommand<ScrollDataWrapper> -->\n        <attr name=\"onScrollChangeCommand\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"SwipeRefreshLayout\">\n        <!-- require BindingCommand -->\n        <attr name=\"onRefreshCommand\" format=\"reference\" />\n        <attr name=\"refreshing\" format=\"boolean\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"WebView\">\n        <!-- require String render to html show in webview-->\n        <attr name=\"render\" format=\"string\" />\n    </declare-styleable>\n    <!-- 自定义控制事件分发的LinearLayout -->\n    <declare-styleable name=\"ControlDistributeLinearLayout\">\n        <attr name=\"distribute_event\" format=\"boolean\" />\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "mvvmhabit/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"white\">#FFFFFF</color>\n    <color name=\"black\">#000000</color>\n    <color name=\"gray\">#808080</color>\n    <color name=\"yellow\">#FFFF00</color>\n    <color name=\"green\">#008000</color>\n    <color name=\"blue\">#0000FF</color>\n    <color name=\"orange\">#FFA500</color>\n\n    <array name=\"in_colors_light\">\n        <item>#4B03A9F4</item>\n        <item>#3303A9F4</item>\n        <item>#1903A9F4</item>\n    </array>\n</resources>\n"
  },
  {
    "path": "mvvmhabit/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"customactivityoncrash_activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"customactivityoncrash_activity_vertical_margin\">16dp</dimen>\n\n    <dimen name=\"customactivityoncrash_error_activity_error_details_text_size\">12sp</dimen>\n</resources>\n"
  },
  {
    "path": "mvvmhabit/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">mvvmhabit</string>\n    <string name=\"customactivityoncrash_error_activity_error_occurred_explanation\">发生意外错误。\\n抱歉，给您带来不便。</string>\n    <string name=\"customactivityoncrash_error_activity_restart_app\">重新启动</string>\n    <string name=\"customactivityoncrash_error_activity_close_app\">关闭程序</string>\n    <string name=\"customactivityoncrash_error_activity_error_details\">错误日志</string>\n    <string name=\"customactivityoncrash_error_activity_error_details_title\">错误详情</string>\n    <string name=\"customactivityoncrash_error_activity_error_details_close\">关闭</string>\n    <string name=\"customactivityoncrash_error_activity_error_details_copy\">复制日志</string>\n    <string name=\"customactivityoncrash_error_activity_error_details_copied\">复制日志</string>\n    <string name=\"customactivityoncrash_error_activity_error_details_clipboard_label\">错误信息</string>\n</resources>\n"
  },
  {
    "path": "mvvmhabit/src/test/java/me/goldze/mvvmhabit/ExampleUnitTest.java",
    "content": "package me.goldze.mvvmhabit;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() throws Exception {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':mvvmhabit'\n"
  }
]