[
  {
    "path": ".gitignore",
    "content": "# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n#  Uncomment the following line in case you need and you don't have the release build type files in your app\n# release/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/gradle.xml\n.idea/assetWizardSettings.xml\n.idea/dictionaries\n.idea/libraries\n# Android Studio 3 in .gitignore file.\n.idea/caches\n.idea/modules.xml\n# Comment next line if keeping position of elements in Navigation Editor is relevant for you\n.idea/navEditor.xml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n.cxx/\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\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 [yyyy] [name of copyright owner]\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": "README.md",
    "content": "![](https://s2.loli.net/2023/09/14/POldYf3s7EQ9B6c.jpg)\n\n&nbsp;\n\n&nbsp;\n\n### [🌏 English README](https://github.com/KunMinX/MVI-Dispatcher/blob/main/README_EN.md)\n\n研发故事：[《解决 MVI 架构实战痛点》](https://juejin.cn/post/7134594010642907149)\n\n&nbsp;\n\n# 背景\n\n响应式编程便于单元测试，但其自身存在漏洞，MVI 即是来消除漏洞，\n\nMVI 有一定门槛，实现较繁琐，且存在性能等问题，难免同事撂挑子不干，一夜回到解放前，\n\n综合来说，MVI 适合与 Jetpack Compose 搭配实现 “现代化的开发模式”，\n\n反之如追求 “低成本、复用、稳定”，可通过遵循 “单一职责原则” 从源头把问题消除。\n\nMVI-Dispatcher 应运而生。\n\n&nbsp;\n\n&nbsp;\n\n|                          收藏或置顶                          |                           顺滑转场                           |                           删除笔记                           |\n| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |\n| ![](https://images.xiaozhuanlan.com/photo/2022/3555d17b46e04054154916d00f1214f8.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/d20a18e90cda8aa1f7d6977dca7b7135.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/5786c16f17612661b0b490dd40e78608.gif) |\n\n&nbsp;\n\n\n\n# 项目简介\n\n笔者长期专注 “业务架构” 模式，致力消除敏捷开发过程中 “不可预期问题”。\n\n在本案例中，我将为您展示，MVI-Dispatcher 是如何将原本 \"繁杂易出错\" 消息分发流程，通过 **寥寥几行代码** 轻而易举完成。\n\n```Groovy\nimplementation 'com.kunminx.arch:mvi-dispatch:7.6.0'\n\n//可选分支，简便安全完成 Config 读写\nimplementation 'com.kunminx.arch:keyvalue-dispatch:7.6.0'\n```\n\n&nbsp;\n\n一个完备的 “领域层” 消息分发组件，至少应满足以下几点：\n\n1.内含消息队列，可暂存 “发送过且未消费” 的消息，\n\n2.页面不可见时，队列暂存期间发来的消息，页面重新可见时，自动消费未消费的消息。\n\nMVI-Dispatcher 应运而生，\n\n&nbsp;\n\n此外，MVI-Dispatcher 改进和优化还包括：\n\n> 1.**可彻底消除 mutable 样板代码**，一行不必写\n>\n> 2.**可杜绝团队新手滥用** mutable.setValue( ) 于 Activity/Fragment\n>\n> 3.开发者只需关注 input、output 二处，**从唯一入口 input 发起请求，并于唯一出口 output 观察**\n>\n> 4.团队新手在不熟 LiveData、UnPeekLiveData、SharedFlow、mutable、MVI 情况下，仅根据 MVI-Dispatcher 简明易懂 input-output 设计亦可自动实现 “响应式” 开发\n>\n> 5.可无缝整合至 Jetpack MVVM 等模式项目\n\n&nbsp;\n\n![](https://s2.loli.net/2023/05/18/mn2zeTJdqrlNw6P.jpg)\n\n&nbsp;\n\nMVI-Dispatcher 以 “备忘录场景” 为例，提供完成一款 “记事本软件” 最少必要源码实现，\n\n故通过该示例，您还可获得内容包括：\n\n> 1.整洁代码风格 & 标准命名规范\n>\n> 2.对 “响应式编程” 知识点深入理解 & 正确使用\n>\n> 3.AndroidX 和 Material Design 全面使用\n>\n> 4.ConstraintLayout 约束布局使用\n>\n> 5.**十六进制复合状态管理最佳实践**\n>\n> 6.优秀用户体验 & 交互设计\n\n&nbsp;\n\n# Thanks to\n\n感谢小伙伴浓咖啡、苏旗的测试反馈\n\n[AndroidX](https://developer.android.google.cn/jetpack/androidx)\n\n[Jetpack](https://developer.android.google.cn/jetpack/)\n\n[SwipeDelMenuLayout](https://github.com/mcxtzhang/SwipeDelMenuLayout)\n\n项目中图标素材来自 [iconfinder](https://www.iconfinder.com/) 提供 **免费授权图片**。\n\n&nbsp;\n\n# Copyright\n\n本项目场景案例及 MVI-Dispatcher 框架，均属本人独立原创设计，本人对此享有最终解释权。\n\n任何个人或组织，未经与作者本人当面沟通许可，不得将本项目代码设计及本人对 \"响应式编程漏洞和 MVI\" 独家理解用于 \"**打包贩卖、出书、卖课**\" 等商业用途。\n\n如需引用借鉴 “本项目框架设计背景及思路” 写作发行，请注明**链接出处**。\n\n&nbsp;\n\n# License\n\n```\nCopyright 2019-present KunMinX\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "README_EN.md",
    "content": "**Development story**:\n\n[《Android: Solving the Pain Points of MVI Architecture in Practice》](https://blog.devgenius.io/android-solving-the-pain-points-of-mvi-architecture-in-practice-4971fa9ed9c0)\n\n&nbsp;\n\nReactive programming is conducive to unit testing, but it has its own flaws. MVI is designed to eliminate these flaws.\n\nMVI has a certain threshold and is more cumbersome to implement. It also has performance issues, which may cause some colleagues to give up and return to traditional methods.\n\nOverall, MVI is suitable for implementing a \"modern development model\" in combination with Jetpack Compose.\n\nOn the other hand, if you are pursuing \"low cost, reusability, and stability\", the problem can be solved from the source by following the \"single responsibility principle\".\n\nIn response to this, MVI-Dispatcher was born.\n\n&nbsp;\n\n|                      Collect or topped                       |                      Smooth transition                       |                         Delete notes                         |\n| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |\n| ![](https://images.xiaozhuanlan.com/photo/2022/3555d17b46e04054154916d00f1214f8.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/d20a18e90cda8aa1f7d6977dca7b7135.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/5786c16f17612661b0b490dd40e78608.gif) |\n\n&nbsp;\n\n# Project Description\n\nThe author has long focused on the \"business architecture\" pattern and is committed to eliminating unexpected issues in the agile development process.\n\nIn this case, I will show you how MVI-Dispatcher can easily accomplish the previously complicated and error-prone message distribution process with just a few lines of code.\n\n```Groovy\nimplementation 'com.kunminx.arch:mvi-dispatch:7.6.0'\n```\n\n&nbsp;\n\nA complete \"domain layer\" message distribution component should at least meet the following requirements:\n\n1. It contains a message queue that can store messages that have been sent but not consumed.\n2. When the page is not visible, any messages sent during the queue storage period will be automatically consumed when the page becomes visible again.\n\nMVI-Dispatcher was born to meet these needs.\n\n&nbsp;\n\nFurthermore, the improvements and optimizations of MVI-Dispatcher include:\n\n> 1. It can completely eliminate mutable boilerplate code, without writing a single line.\n> 2. It can prevent new team members from misusing mutable.setValue() in Activity/Fragment.\n> 3. Developers only need to focus on input and output. They inject events through the unique input entry point and observe them through the unique output exit point.\n> 4. New team members can automatically implement \"reactive\" development based on the concise and easy-to-understand input-output design of MVI-Dispatcher without being familiar with LiveData, UnPeekLiveData, SharedFlow, mutable, or MVI.\n> 5. It can be seamlessly integrated into Jetpack MVVM and other pattern projects.\n\n&nbsp;\n\n![](https://s2.loli.net/2023/05/18/JXHyColB2Knxmkq.jpg)\n\n&nbsp;\n\nMVI-Dispatcher provide the minimum necessary source code implementation to complete a notepad software.\n\nTherefore, through this example, you can also obtain content including:\n\n> 1.Clean code style & standard naming conventions\n>\n> 2.In-depth understanding of “Reactive programming” knowledge points & correct use\n>\n> 3.Full use of AndroidX and Material Design\n>\n> 4.ConstraintLayout Constraint Layout Best Practices\n>\n> 5.Best Practices for Hex Compound State Management\n>\n> 6.Excellent User Experience & Interaction Design\n\n\n&nbsp;\n\n# Thanks to\n\n[AndroidX](https://developer.android.google.cn/jetpack/androidx)\n\n[Jetpack](https://developer.android.google.cn/jetpack/)\n\n[SwipeDelMenuLayout](https://github.com/mcxtzhang/SwipeDelMenuLayout)\n\nThe icon material in the project comes from [iconfinder](https://www.iconfinder.com/) provided free licensed images.\n\n&nbsp;\n\n# Copyright\n\nThe scene cases and MVI dispatcher framework of this project are all my independent original designs, and I have the final right to interpret them.\n\nIf you need to quote and use the \"background and ideas of the framework design of this project\" for writing and publishing, please indicate the source of the link.\n\n&nbsp;\n\n# License\n\n```\nCopyright 2019-present KunMinX\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```"
  },
  {
    "path": "app/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion appTargetSdk\n    defaultConfig {\n        minSdkVersion 23\n        targetSdkVersion appTargetSdk\n        versionCode appVersionCode\n        versionName appVersionName\n        applicationId \"com.kunminx.purenote\"\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    buildFeatures {\n        dataBinding true\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \"libs\", include: [\"*.jar\", \"*.aar\"])\n\n    implementation project(\":architecture\")\n    implementation project(':mvi-dispatch')\n    implementation project(\":keyvalue-dispatch\")\n\n    testImplementation \"junit:junit:4.13.2\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n\n    implementation \"androidx.appcompat:appcompat:1.5.0\"\n    implementation \"androidx.constraintlayout:constraintlayout:2.1.4\"\n    implementation \"com.google.android.material:material:1.6.1\"\n    implementation 'androidx.recyclerview:recyclerview:1.2.1'\n    implementation \"androidx.navigation:navigation-runtime:2.5.1\"\n\n    implementation 'com.github.KunMinX:Smooth-Navigation:v4.0.0'\n    implementation 'com.github.KunMinX.Strict-DataBinding:strict_databinding:5.6.0'\n    implementation 'com.github.KunMinX.Strict-DataBinding:binding_state:5.6.0'\n    implementation 'com.github.KunMinX.Strict-DataBinding:binding_recyclerview:5.6.0'\n    implementation 'com.github.KunMinX.SealedClass4Java:sealed-annotation:1.4.0-beta'\n    annotationProcessor 'com.github.KunMinX.SealedClass4Java:sealed-compiler:1.4.0-beta'\n\n    implementation \"androidx.room:room-runtime:2.4.3\"\n    annotationProcessor \"androidx.room:room-compiler:2.4.3\"\n\n    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'\n    implementation 'io.reactivex.rxjava2:rxjava:2.2.21'\n\n    implementation \"com.google.code.gson:gson:2.9.1\"\n    implementation \"com.squareup.retrofit2:retrofit:2.9.0\"\n    implementation \"com.squareup.retrofit2:converter-gson:2.9.0\"\n    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'\n    implementation \"com.squareup.okhttp3:logging-interceptor:4.10.0\"\n    implementation \"com.squareup.okhttp3:okhttp:4.10.0\"\n}"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\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"
  },
  {
    "path": "app/src/androidTest/java/com/kunminx/purenote/ExampleInstrumentedTest.java",
    "content": "package com.kunminx.purenote;\n\nimport static org.junit.Assert.assertEquals;\n\nimport android.content.Context;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.platform.app.InstrumentationRegistry;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/**\n * Instrumented 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() {\n    // Context of the app under test.\n    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n    assertEquals(\"com.kunminx.purenote\", appContext.getPackageName());\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.kunminx.purenote\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <application\n        android:name=\".App\"\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.PureNote\">\n        <activity\n            android:name=\".ui.page.MainActivity\"\n            android:exported=\"true\">\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    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/App.java",
    "content": "package com.kunminx.purenote;\n\nimport android.app.Application;\n\nimport com.kunminx.architecture.utils.Utils;\n\n/**\n * Create by KunMinX at 2022/7/3\n */\npublic class App extends Application {\n  @Override\n  public void onCreate() {\n    super.onCreate();\n    Utils.init(this);\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/bean/Note.java",
    "content": "package com.kunminx.purenote.data.bean;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.ColumnInfo;\nimport androidx.room.Entity;\nimport androidx.room.Ignore;\nimport androidx.room.PrimaryKey;\n\nimport com.kunminx.architecture.utils.TimeUtils;\nimport com.kunminx.purenote.R;\n\n/**\n * Created by KunMinX on 2015/7/31.\n */\n@Entity\npublic class Note implements Parcelable {\n  public final static int TYPE_TOPPING = 0x0001;\n  public final static int TYPE_MARKED = 0x0002;\n\n  @PrimaryKey\n  @NonNull\n  private String id = \"\";\n\n  private String title = \"\";\n\n  private String content = \"\";\n\n  @ColumnInfo(name = \"create_time\")\n  private long createTime;\n\n  @ColumnInfo(name = \"modify_time\")\n  private long modifyTime;\n\n  private int type;\n\n  @Ignore\n  public String getCreateDate() {\n    return TimeUtils.getTime(createTime, TimeUtils.YYYY_MM_DD_HH_MM_SS);\n  }\n\n  @Ignore\n  public String getModifyDate() {\n    return TimeUtils.getTime(modifyTime, TimeUtils.YYYY_MM_DD_HH_MM_SS);\n  }\n\n  @Ignore\n  public boolean isMarked() {\n    return (type & TYPE_MARKED) != 0;\n  }\n\n  @Ignore\n  public boolean isTopping() {\n    return (type & TYPE_TOPPING) != 0;\n  }\n\n  @Ignore\n  public void toggleType(int param) {\n    if ((type & param) != 0) {\n      type = type & ~param;\n    } else {\n      type = type | param;\n    }\n  }\n\n  @Ignore\n  public int markIcon() {\n    return isMarked() ? R.drawable.icon_star : R.drawable.icon_star_board;\n  }\n\n  @Ignore\n  public Note() {\n  }\n\n  public Note(@NonNull String id, String title, String content, long createTime, long modifyTime, int type) {\n    this.id = id;\n    this.title = title;\n    this.content = content;\n    this.createTime = createTime;\n    this.modifyTime = modifyTime;\n    this.type = type;\n  }\n\n  @NonNull\n  public String getId() {\n    return id;\n  }\n  public String getTitle() {\n    return title;\n  }\n  public String getContent() {\n    return content;\n  }\n  public long getCreateTime() {\n    return createTime;\n  }\n  public long getModifyTime() {\n    return modifyTime;\n  }\n  public int getType() {\n    return type;\n  }\n\n  protected Note(Parcel in) {\n    id = in.readString();\n    title = in.readString();\n    content = in.readString();\n    createTime = in.readLong();\n    modifyTime = in.readLong();\n    type = in.readInt();\n  }\n\n  public static final Creator<Note> CREATOR = new Creator<Note>() {\n    @Override\n    public Note createFromParcel(Parcel in) {\n      return new Note(in);\n    }\n\n    @Override\n    public Note[] newArray(int size) {\n      return new Note[size];\n    }\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(id);\n    dest.writeString(title);\n    dest.writeString(content);\n    dest.writeLong(createTime);\n    dest.writeLong(modifyTime);\n    dest.writeInt(type);\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/bean/Weather.java",
    "content": "package com.kunminx.purenote.data.bean;\n\nimport java.util.List;\n/**\n * Create by KunMinX at 2022/8/24\n */\npublic class Weather {\n  private String status;\n  private String count;\n  private String info;\n  private String infocode;\n  private List<Live> lives;\n\n  public String getStatus() {\n    return status;\n  }\n  public String getCount() {\n    return count;\n  }\n  public String getInfo() {\n    return info;\n  }\n  public String getInfocode() {\n    return infocode;\n  }\n  public List<Live> getLives() {\n    return lives;\n  }\n\n  public static class Live {\n    private String city;\n    private String weather;\n    private String temperature;\n\n    public String getCity() {\n      return city;\n    }\n    public String getWeather() {\n      return weather;\n    }\n    public String getTemperature() {\n      return temperature;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/config/Key.java",
    "content": "package com.kunminx.purenote.data.config;\n\n/**\n * Create by KunMinX at 2022/8/15\n */\npublic class Key {\n  public final static String TEST_STRING = \"test_string\";\n  public final static String TEST_BOOLEAN = \"test_boolean\";\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/DataRepository.java",
    "content": "package com.kunminx.purenote.data.repo;\n\nimport android.annotation.SuppressLint;\n\nimport androidx.annotation.NonNull;\nimport androidx.room.Room;\n\nimport com.kunminx.architecture.data.response.AsyncTask;\nimport com.kunminx.architecture.data.response.DataResult;\nimport com.kunminx.architecture.data.response.ResponseStatus;\nimport com.kunminx.architecture.utils.Utils;\nimport com.kunminx.purenote.data.bean.Note;\nimport com.kunminx.purenote.data.bean.Weather;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\nimport okhttp3.OkHttpClient;\nimport okhttp3.logging.HttpLoggingInterceptor;\nimport retrofit2.Call;\nimport retrofit2.Callback;\nimport retrofit2.Response;\nimport retrofit2.Retrofit;\nimport retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;\nimport retrofit2.converter.gson.GsonConverterFactory;\n\n/**\n * Create by KunMinX at 2022/6/14\n */\npublic class DataRepository {\n\n  //TODO 天气服务使用高德 API_KEY，如有需要，请自行在 \"高德开放平台\" 获取和在 DataRepository 类填入\n\n  public final static String API_KEY = \"\";\n  public final static String BASE_URL = \"https://restapi.amap.com/v3/\";\n\n  private static final DataRepository instance = new DataRepository();\n  private static final String DATABASE_NAME = \"NOTE_DB.db\";\n  private final NoteDataBase mDataBase;\n  private final Retrofit mRetrofit;\n\n  {\n    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();\n    logging.setLevel(HttpLoggingInterceptor.Level.BODY);\n    OkHttpClient client = new OkHttpClient.Builder()\n            .connectTimeout(8, TimeUnit.SECONDS)\n            .readTimeout(8, TimeUnit.SECONDS)\n            .writeTimeout(8, TimeUnit.SECONDS)\n            .addInterceptor(logging)\n            .build();\n    mRetrofit = new Retrofit.Builder()\n            .baseUrl(BASE_URL)\n            .client(client)\n            .addConverterFactory(GsonConverterFactory.create())\n            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())\n            .build();\n  }\n\n  public static DataRepository getInstance() {\n    return instance;\n  }\n\n  private DataRepository() {\n    mDataBase = Room.databaseBuilder(Utils.getApp().getApplicationContext(),\n            NoteDataBase.class, DATABASE_NAME).build();\n  }\n\n  public Observable<List<Note>> getNotes() {\n    return AsyncTask.doIO(emitter -> emitter.onNext(mDataBase.noteDao().getNotes()));\n  }\n\n  public Observable<Boolean> insertNote(Note note) {\n    return AsyncTask.doIO(emitter -> {\n      mDataBase.noteDao().insertNote(note);\n      emitter.onNext(true);\n    });\n  }\n\n  public Observable<Boolean> updateNote(Note note) {\n    return AsyncTask.doIO(emitter -> {\n      mDataBase.noteDao().updateNote(note);\n      emitter.onNext(true);\n    });\n  }\n\n  public Observable<Boolean> deleteNote(Note note) {\n    return AsyncTask.doIO(emitter -> {\n      mDataBase.noteDao().deleteNote(note);\n      emitter.onNext(true);\n    });\n  }\n\n  @SuppressLint(\"CheckResult\")\n  public Observable<Weather> getWeatherInfo(String cityCode) {\n    WeatherService service = mRetrofit.create(WeatherService.class);\n    return service.getWeatherInfo(cityCode, API_KEY)\n            .subscribeOn(Schedulers.io())\n            .observeOn(AndroidSchedulers.mainThread());\n  }\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/NoteDao.java",
    "content": "package com.kunminx.purenote.data.repo;\n\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Insert;\nimport androidx.room.OnConflictStrategy;\nimport androidx.room.Query;\nimport androidx.room.Update;\n\nimport com.kunminx.purenote.data.bean.Note;\n\nimport java.util.List;\n\n/**\n * Create by KunMinX at 2022/6/14\n */\n@Dao\npublic interface NoteDao {\n\n  @Query(\"select * from note order by type & 0x0001 = 0x0001 desc, modify_time desc\")\n  List<Note> getNotes();\n\n  @Insert(onConflict = OnConflictStrategy.REPLACE)\n  void insertNote(Note note);\n\n  @Update()\n  void updateNote(Note note);\n\n  @Delete\n  void deleteNote(Note note);\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/NoteDataBase.java",
    "content": "package com.kunminx.purenote.data.repo;\n\nimport androidx.room.Database;\nimport androidx.room.RoomDatabase;\n\nimport com.kunminx.purenote.data.bean.Note;\n\n/**\n * Create by KunMinX at 2022/6/14\n */\n@Database(entities = {Note.class}, version = 1, exportSchema = false)\npublic abstract class NoteDataBase extends RoomDatabase {\n  public abstract NoteDao noteDao();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/WeatherService.java",
    "content": "package com.kunminx.purenote.data.repo;\n\nimport com.kunminx.purenote.data.bean.Weather;\n\nimport io.reactivex.Observable;\nimport retrofit2.Call;\nimport retrofit2.http.GET;\nimport retrofit2.http.Query;\n/**\n * Create by KunMinX at 2022/8/24\n */\npublic interface WeatherService {\n  @GET(\"weather/weatherInfo\")\n  Observable<Weather> getWeatherInfo(\n          @Query(\"city\") String city,\n          @Query(\"key\") String key\n  );\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_Api.java",
    "content": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.purenote.data.bean.Weather;\nimport com.kunminx.sealed.annotation.Param;\nimport com.kunminx.sealed.annotation.SealedClass;\n/**\n * TODO：可用于 Java 1.8 的 Sealed Class，使用方式见：\n * https://github.com/KunMinX/SealedClass4Java\n *\n * TODO tip 2：此 Intent 非传统意义上的 MVI intent，\n *  而是简化 reduce 和 action 后，拍平的 intent，\n *  它可以携带 param，经由 input 接口发送至 mvi-Dispatcher，\n *  可以 copy 和携带 result，经由 output 接口回推至表现层，\n *\n *  具体可参见《解决 MVI 实战痛点》解析\n *  https://juejin.cn/post/7134594010642907149\n *\n * Create by KunMinX at 2022/8/30\n */\n@SealedClass\npublic interface _Api {\n  void onLoading(boolean isLoading);\n  void getWeatherInfo(@Param String cityCode, Weather.Live live);\n  void onError(String errorInfo);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_ComplexIntent.java",
    "content": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.sealed.annotation.Param;\nimport com.kunminx.sealed.annotation.SealedClass;\n/**\n * TODO：可用于 Java 1.8 的 Sealed Class，使用方式见：\n * https://github.com/KunMinX/SealedClass4Java\n *\n * TODO tip 2：此 Intent 非传统意义上的 MVI intent，\n *  而是简化 reduce 和 action 后，拍平的 intent，\n *  它可以携带 param，经由 input 接口发送至 mvi-Dispatcher，\n *  可以 copy 和携带 result，经由 output 接口回推至表现层，\n *\n *  具体可参见《解决 MVI 实战痛点》解析\n *  https://juejin.cn/post/7134594010642907149\n *\n * Create by KunMinX at 2022/8/30\n */\n@SealedClass\npublic interface _ComplexIntent {\n  void test1(@Param int count, int count1);\n  void test2(@Param int count, int count1);\n  void test3(@Param int count, int count1);\n  void test4(@Param int count, int count1);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_Messages.java",
    "content": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.sealed.annotation.SealedClass;\n/**\n * TODO：可用于 Java 1.8 的 Sealed Class，使用方式见：\n * https://github.com/KunMinX/SealedClass4Java\n *\n * TODO tip 2：此 Intent 非传统意义上的 MVI intent，\n *  而是简化 reduce 和 action 后，拍平的 intent，\n *  它可以携带 param，经由 input 接口发送至 mvi-Dispatcher，\n *  可以 copy 和携带 result，经由 output 接口回推至表现层，\n *\n *  具体可参见《解决 MVI 实战痛点》解析\n *  https://juejin.cn/post/7134594010642907149\n *\n * Create by KunMinX at 2022/8/30\n */\n@SealedClass\npublic interface _Messages {\n  void refreshNoteList();\n  void finishActivity();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_NoteIntent.java",
    "content": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.purenote.data.bean.Note;\nimport com.kunminx.sealed.annotation.Param;\nimport com.kunminx.sealed.annotation.SealedClass;\n\nimport java.util.List;\n/**\n * TODO：可用于 Java 1.8 的 Sealed Class，使用方式见：\n * https://github.com/KunMinX/SealedClass4Java\n *\n * TODO tip 2：此 Intent 非传统意义上的 MVI intent，\n *  而是简化 reduce 和 action 后，拍平的 intent，\n *  它可以携带 param，经由 input 接口发送至 mvi-Dispatcher，\n *  可以 copy 和携带 result，经由 output 接口回推至表现层，\n *\n *  具体可参见《解决 MVI 实战痛点》解析\n *  https://juejin.cn/post/7134594010642907149\n *\n * Create by KunMinX at 2022/8/30\n */\n@SealedClass\npublic interface _NoteIntent {\n  void getNoteList(List<Note> notes);\n  void removeItem(@Param Note note, boolean isSuccess);\n  void updateItem(@Param Note note, boolean isSuccess);\n  void markItem(@Param Note note, boolean isSuccess);\n  void toppingItem(@Param Note note, boolean isSuccess);\n  void addItem(@Param Note note, boolean isSuccess);\n  void initItem(@Param Note note);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/message/PageMessenger.java",
    "content": "package com.kunminx.purenote.domain.message;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com.kunminx.purenote.domain.intent.Messages;\n\n/**\n * Create by KunMinX at 2022/6/14\n */\npublic class PageMessenger extends MviDispatcher<Messages> {\n\n  /**\n   * TODO tip 1：\n   *  此为领域层组件，接收发自页面消息，内部统一处理业务逻辑，并通过 sendResult 结果分发。\n   *  可为同业务不同页面复用。\n   *  ~\n   *  本组件通过封装，默使数据从 \"领域层\" 到 \"表现层\" 单向流动，\n   *  消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。\n   */\n  @Override\n  protected void onHandle(Messages intent) {\n    sendResult(intent);\n\n    // TODO：tip 2：除接收来自 Activity/Fragment 的事件，亦可从 Dispatcher 内部发送事件（作为副作用）：\n    //  ~\n    //  if (sent from within) {\n    //    Messages msg = new Messages(Messages.EVENT_SHOW_DIALOG);\n    //    sendResult(msg);\n    //  }\n\n  }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/request/ComplexRequester.java",
    "content": "package com.kunminx.purenote.domain.request;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com.kunminx.purenote.domain.intent.ComplexIntent;\n\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * TODO tip 1：让 UI 和业务分离，让数据总是从生产者流向消费者\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n * `\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n *\n * Create by KunMinX at 2022/7/5\n */\npublic class ComplexRequester extends MviDispatcher<ComplexIntent> {\n\n  private Disposable mDisposable;\n\n  /**\n   * TODO tip 1：可初始化配置队列长度，自动丢弃队首过时消息\n   */\n  @Override\n  protected int initQueueMaxLength() {\n    return 5;\n  }\n\n  /**\n   * TODO tip 2：\n   * 此为领域层组件，接收发自页面消息，内部统一处理业务逻辑，并通过 sendResult 结果分发。\n   * 可在页面中配置作用域，以实现单页面独享或多页面数据共享，\n   * `\n   * 本组件通过封装，默使数据从 \"领域层\" 到 \"表现层\" 单向流动，\n   * 消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。\n   */\n  @Override\n  protected void onHandle(ComplexIntent intent) {\n    switch (intent.id) {\n      case ComplexIntent.Test1.ID:\n\n        //TODO tip 3: 定长队列，随取随用，绝不丢失事件\n        //此处通过 RxJava 轮询模拟事件连发，可于 Logcat Debug 见输出\n\n        //通过判断 mDisposable，维持环境重建后还是同一 Rx 实例在回推数据，\n        //此 case 可用于验证 \"app 处于后台时，推送的数据会兜着，回到前台时，会回推，但此后环境重建也不会再回推，做到消费且只消费一次\"\n\n        if (mDisposable == null)\n          mDisposable = Observable.interval(1000, TimeUnit.MILLISECONDS)\n                  .subscribeOn(Schedulers.io())\n                  .observeOn(AndroidSchedulers.mainThread())\n                  .subscribe(aLong -> input(ComplexIntent.Test4(aLong.intValue())));\n        break;\n      case ComplexIntent.Test2.ID:\n        Observable.timer(200, TimeUnit.MILLISECONDS)\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(aLong -> sendResult(intent));\n        break;\n      case ComplexIntent.Test3.ID:\n        sendResult(intent);\n        break;\n      case ComplexIntent.Test4.ID:\n        ComplexIntent.Test4 test4 = (ComplexIntent.Test4) intent;\n        sendResult(test4.copy(test4.paramCount));\n        break;\n    }\n  }\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/request/NoteRequester.java",
    "content": "package com.kunminx.purenote.domain.request;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com.kunminx.purenote.data.repo.DataRepository;\nimport com.kunminx.purenote.domain.intent.NoteIntent;\n\n/**\n * TODO tip 1：让 UI 和业务分离，让数据总是从生产者流向消费者\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n * `\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n * <p>\n * Create by KunMinX at 2022/6/14\n */\npublic class NoteRequester extends MviDispatcher<NoteIntent> {\n\n  /**\n   * TODO tip 1：\n   * 此为领域层组件，接收发自页面消息，内部统一处理业务逻辑，并通过 sendResult 结果分发。\n   * 可在页面中配置作用域，以实现单页面独享或多页面数据共享，\n   * `\n   * 本组件通过封装，默使数据从 \"领域层\" 到 \"表现层\" 单向流动，\n   * 消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。\n   */\n  @Override\n  protected void onHandle(NoteIntent intent) {\n    DataRepository repo = DataRepository.getInstance();\n    switch (intent.id) {\n      case NoteIntent.InitItem.ID:\n        NoteIntent.InitItem initItem = (NoteIntent.InitItem) intent;\n        sendResult(initItem.copy());\n        break;\n      case NoteIntent.GetNoteList.ID:\n        NoteIntent.GetNoteList getNoteList = (NoteIntent.GetNoteList) intent;\n        repo.getNotes().subscribe(notes -> sendResult(getNoteList.copy(notes)));\n        break;\n      case NoteIntent.UpdateItem.ID:\n        NoteIntent.UpdateItem updateItem = (NoteIntent.UpdateItem) intent;\n        repo.updateNote(updateItem.paramNote).subscribe(it -> sendResult(updateItem.copy(it)));\n        break;\n      case NoteIntent.MarkItem.ID:\n        NoteIntent.MarkItem markItem = (NoteIntent.MarkItem) intent;\n        repo.updateNote(markItem.paramNote).subscribe(it -> sendResult(markItem.copy(it)));\n        break;\n      case NoteIntent.ToppingItem.ID:\n        NoteIntent.ToppingItem toppingItem = (NoteIntent.ToppingItem) intent;\n        repo.updateNote(toppingItem.paramNote).subscribe(it ->\n                repo.getNotes().subscribe(notes -> sendResult(NoteIntent.GetNoteList(notes))));\n        break;\n      case NoteIntent.AddItem.ID:\n        NoteIntent.AddItem addItem = (NoteIntent.AddItem) intent;\n        repo.insertNote(addItem.paramNote).subscribe(it -> sendResult(addItem.copy(it)));\n        break;\n      case NoteIntent.RemoveItem.ID:\n        NoteIntent.RemoveItem removeItem = (NoteIntent.RemoveItem) intent;\n        repo.deleteNote(removeItem.paramNote).subscribe(it -> sendResult(removeItem.copy(it)));\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/request/WeatherRequester.java",
    "content": "package com.kunminx.purenote.domain.request;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com.kunminx.purenote.data.bean.Weather;\nimport com.kunminx.purenote.data.repo.DataRepository;\nimport com.kunminx.purenote.domain.intent.Api;\n\nimport io.reactivex.Observer;\nimport io.reactivex.disposables.Disposable;\n\n/**\n * TODO tip 1：让 UI 和业务分离，让数据总是从生产者流向消费者\n * UI逻辑和业务逻辑，本质区别在于，前者是数据的消费者，后者是数据的生产者，\n * \"领域层组件\" 作为数据的生产者，职责应仅限于 \"请求调度 和 结果分发\"，\n * `\n * 换言之，\"领域层组件\" 中应当只关注数据的生成，而不关注数据的使用，\n * 改变 UI 状态的逻辑代码，只应在表现层页面中编写、在 Observer 回调中响应数据的变化，\n * 将来升级到 Jetpack Compose 更是如此，\n * <p>\n * Create by KunMinX at 2022/8/24\n */\npublic class WeatherRequester extends MviDispatcher<Api> {\n  public final static String CITY_CODE_BEIJING = \"110000\";\n\n  /**\n   * TODO tip 1：\n   * 此为领域层组件，接收发自页面消息，内部统一处理业务逻辑，并通过 sendResult 结果分发。\n   * 可在页面中配置作用域，以实现单页面独享或多页面数据共享，\n   * `\n   * 本组件通过封装，默使数据从 \"领域层\" 到 \"表现层\" 单向流动，\n   * 消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。\n   */\n  @Override\n  protected void onHandle(Api intent) {\n    DataRepository repo = DataRepository.getInstance();\n    switch (intent.id) {\n      case Api.OnLoading.ID:\n      case Api.OnError.ID:\n        sendResult(intent);\n        break;\n      case Api.GetWeatherInfo.ID:\n        Api.GetWeatherInfo getWeatherInfo = (Api.GetWeatherInfo) intent;\n        repo.getWeatherInfo(getWeatherInfo.paramCityCode).subscribe(new Observer<Weather>() {\n          @Override\n          public void onSubscribe(Disposable d) {\n            input(Api.OnLoading(true));\n          }\n          @Override\n          public void onNext(Weather weather) {\n            sendResult(getWeatherInfo.copy(weather.getLives().get(0)));\n          }\n          @Override\n          public void onError(Throwable e) {\n            input(Api.OnError(e.getMessage()));\n          }\n          @Override\n          public void onComplete() {\n            input(Api.OnLoading(false));\n          }\n        });\n        break;\n    }\n  }\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/adapter/NoteAdapter.java",
    "content": "package com.kunminx.purenote.ui.adapter;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunminx.architecture.ui.adapter.BaseBindingAdapter;\nimport com.kunminx.purenote.R;\nimport com.kunminx.purenote.data.bean.Note;\nimport com.kunminx.purenote.databinding.AdapterNoteListBinding;\n\nimport java.util.List;\n\n/**\n * Create by KunMinX at 2022/7/3\n */\npublic class NoteAdapter extends BaseBindingAdapter<Note, AdapterNoteListBinding> {\n\n  public NoteAdapter(List<Note> list) {\n    super(list);\n  }\n\n  @Override\n  protected int getLayoutResId(int viewType) {\n    return R.layout.adapter_note_list;\n  }\n\n  @Override\n  protected void onBindItem(AdapterNoteListBinding binding, Note note, RecyclerView.ViewHolder holder) {\n    binding.setNote(note);\n    int position = holder.getBindingAdapterPosition();\n    binding.cl.setOnClickListener(v -> {\n      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);\n    });\n    binding.btnMark.setOnClickListener(v -> {\n      note.toggleType(Note.TYPE_MARKED);\n      notifyItemChanged(position);\n      notifyItemRangeChanged(position, 1);\n      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);\n    });\n    binding.btnTopping.setOnClickListener(v -> {\n      note.toggleType(Note.TYPE_TOPPING);\n      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);\n    });\n    binding.btnDelete.setOnClickListener(v -> {\n      notifyItemRemoved(position);\n      getList().remove(position);\n      notifyItemRangeRemoved(position, getList().size() - position);\n      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);\n    });\n  }\n\n  @Override\n  public int getItemCount() {\n    return getList().size();\n  }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/EditorFragment.java",
    "content": "package com.kunminx.purenote.ui.page;\n\nimport android.os.Bundle;\nimport android.text.TextUtils;\n\nimport androidx.navigation.NavController;\n\nimport com.kunminx.architecture.ui.bind.ClickProxy;\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.architecture.utils.ToastUtils;\nimport com.kunminx.architecture.utils.Utils;\nimport com.kunminx.purenote.BR;\nimport com.kunminx.purenote.R;\nimport com.kunminx.purenote.data.bean.Note;\nimport com.kunminx.purenote.domain.intent.Messages;\nimport com.kunminx.purenote.domain.intent.NoteIntent;\nimport com.kunminx.purenote.domain.message.PageMessenger;\nimport com.kunminx.purenote.domain.request.NoteRequester;\n\nimport java.util.Objects;\nimport java.util.UUID;\n\n/**\n * Create by KunMinX at 2022/6/30\n */\npublic class EditorFragment extends BaseFragment {\n  private final static String NOTE = \"NOTE\";\n  private EditorStates mStates;\n  private NoteRequester mNoteRequester;\n  private PageMessenger mMessenger;\n  private ClickProxy mClickProxy;\n\n  public static void start(NavController controller, Note note) {\n    Bundle bundle = new Bundle();\n    bundle.putParcelable(NOTE, note);\n    controller.navigate(R.id.action_list_to_editor, bundle);\n  }\n\n  @Override\n  protected void initViewModel() {\n    mStates = getFragmentScopeViewModel(EditorStates.class);\n    mNoteRequester = getFragmentScopeViewModel(NoteRequester.class);\n    mMessenger = getApplicationScopeViewModel(PageMessenger.class);\n  }\n\n  @Override\n  protected DataBindingConfig getDataBindingConfig() {\n    return new DataBindingConfig(R.layout.fragment_editor, BR.state, mStates)\n            .addBindingParam(BR.click, mClickProxy = new ClickProxy());\n  }\n\n  /**\n   * TODO tip 1：\n   * 通过 PublishSubject 接收数据，并在唯一出口 output{ ... } 中响应数据的变化，\n   * 通过 BehaviorSubject 通知所绑定控件属性重新渲染，并为其兜住最后一次状态，\n   */\n  @Override\n  protected void onOutput() {\n    mNoteRequester.output(this, noteIntent -> {\n      if (Objects.equals(noteIntent.id, NoteIntent.InitItem.ID)) {\n        mStates.tempNote.set(((NoteIntent.InitItem) noteIntent).paramNote);\n        Note tempNote = Objects.requireNonNull(mStates.tempNote.get());\n        mStates.title.set(tempNote.getTitle());\n        mStates.content.set(tempNote.getContent());\n        if (TextUtils.isEmpty(tempNote.getId())) {\n          mStates.titleRequestFocus.set(true);\n        } else {\n          mStates.tip.set(getString(R.string.last_time_modify));\n          mStates.time.set(tempNote.getModifyDate());\n        }\n      } else if (Objects.equals(noteIntent.id, NoteIntent.AddItem.ID)) {\n        mMessenger.input(Messages.RefreshNoteList());\n        ToastUtils.showShortToast(getString(R.string.saved));\n        nav().navigateUp();\n      }\n    });\n  }\n\n  /**\n   * TODO tip 2：\n   * 通过唯一入口 input() 发消息至 \"可信源\"，由其内部统一处理业务逻辑和结果分发。\n   */\n  @Override\n  protected void onInput() {\n    mClickProxy.setOnClickListener(v -> {\n      if (v.getId() == R.id.btn_back) save();\n    });\n    if (getArguments() != null)\n      mNoteRequester.input(NoteIntent.InitItem(getArguments().getParcelable(NOTE)));\n  }\n\n  private void save() {\n    Note tempNote = Objects.requireNonNull(mStates.tempNote.get());\n    String title = mStates.title.get();\n    String content = mStates.content.get();\n    if (TextUtils.isEmpty(title + content) || tempNote.getTitle().equals(title) && tempNote.getContent().equals(content)) {\n      nav().navigateUp();\n      return;\n    }\n    Note note;\n    long time = System.currentTimeMillis();\n    if (TextUtils.isEmpty(tempNote.getId())) {\n      note = new Note(UUID.randomUUID().toString(), title, content, time, time, 0);\n    } else {\n      note = new Note(tempNote.getId(), title, content, tempNote.getCreateTime(), time, tempNote.getType());\n    }\n    mNoteRequester.input(NoteIntent.AddItem(note));\n  }\n\n  @Override\n  protected void onBackPressed() {\n    save();\n  }\n\n  /**\n   * TODO tip 3：\n   * 基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n   * 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n   * 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n   *\n   * 具体可参见《解决 MVI 实战痛点》解析\n   * https://juejin.cn/post/7134594010642907149\n   */\n  public static class EditorStates extends StateHolder {\n    public final State<Note> tempNote = new State<>(new Note());\n    public final State<String> title = new State<>(\"\");\n    public final State<String> content = new State<>(\"\");\n    public final State<String> tip = new State<>(Utils.getApp().getString(R.string.edit));\n    public final State<String> time = new State<>(Utils.getApp().getString(R.string.new_note));\n    public final State<Boolean> titleRequestFocus = new State<>(false);\n  }\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/ListFragment.java",
    "content": "package com.kunminx.purenote.ui.page;\n\nimport com.kunminx.architecture.domain.dispatch.GlobalConfigs;\nimport com.kunminx.architecture.ui.bind.ClickProxy;\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.purenote.BR;\nimport com.kunminx.purenote.R;\nimport com.kunminx.purenote.data.bean.Note;\nimport com.kunminx.purenote.data.bean.Weather;\nimport com.kunminx.purenote.data.config.Key;\nimport com.kunminx.purenote.domain.intent.Api;\nimport com.kunminx.purenote.domain.intent.Messages;\nimport com.kunminx.purenote.domain.intent.NoteIntent;\nimport com.kunminx.purenote.domain.message.PageMessenger;\nimport com.kunminx.purenote.domain.request.WeatherRequester;\nimport com.kunminx.purenote.domain.request.NoteRequester;\nimport com.kunminx.purenote.ui.adapter.NoteAdapter;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\n/**\n * Create by KunMinX at 2022/6/30\n */\npublic class ListFragment extends BaseFragment {\n  private ListStates mStates;\n  private NoteRequester mNoteRequester;\n  private WeatherRequester mWeatherRequester;\n  private PageMessenger mMessenger;\n  private NoteAdapter mAdapter;\n  private ClickProxy mClickProxy;\n\n  @Override\n  protected void initViewModel() {\n    mStates = getFragmentScopeViewModel(ListStates.class);\n    mNoteRequester = getFragmentScopeViewModel(NoteRequester.class);\n    mWeatherRequester = getFragmentScopeViewModel(WeatherRequester.class);\n    mMessenger = getApplicationScopeViewModel(PageMessenger.class);\n  }\n\n  @Override\n  protected DataBindingConfig getDataBindingConfig() {\n    return new DataBindingConfig(R.layout.fragment_list, BR.state, mStates)\n            .addBindingParam(BR.adapter, mAdapter = new NoteAdapter(mStates.list))\n            .addBindingParam(BR.click, mClickProxy = new ClickProxy());\n  }\n\n  /**\n   * TODO tip 1：\n   * 通过 PublishSubject 接收数据，并在唯一出口 output{ ... } 中响应数据的变化，\n   * 通过 BehaviorSubject 通知所绑定控件属性重新渲染，并为其兜住最后一次状态，\n   */\n  @Override\n  protected void onOutput() {\n    mMessenger.output(this, messages -> {\n      if (Objects.equals(messages.id, Messages.RefreshNoteList.ID)) {\n        mNoteRequester.input(NoteIntent.GetNoteList());\n      }\n    });\n\n    mNoteRequester.output(this, noteIntent -> {\n      switch (noteIntent.id) {\n        case NoteIntent.ToppingItem.ID:\n        case NoteIntent.GetNoteList.ID:\n          NoteIntent.GetNoteList getNoteList = (NoteIntent.GetNoteList) noteIntent;\n          mAdapter.refresh(getNoteList.resultNotes);\n          mStates.emptyViewShow.set(mStates.list.size() == 0);\n          break;\n        case NoteIntent.MarkItem.ID:\n        case NoteIntent.RemoveItem.ID:\n          break;\n      }\n    });\n\n    mWeatherRequester.output(this, api -> {\n      switch (api.id) {\n        case Api.OnLoading.ID:\n          mStates.loadingWeather.set(((Api.OnLoading) api).resultIsLoading);\n          break;\n        case Api.GetWeatherInfo.ID:\n          Api.GetWeatherInfo weatherInfo = (Api.GetWeatherInfo) api;\n          Weather.Live live = weatherInfo.resultLive;\n          if (live != null) mStates.weather.set(live.getWeather());\n          break;\n        case Api.OnError.ID:\n          break;\n      }\n    });\n\n    //TODO tip 3: 更新配置并刷新界面，是日常开发高频操作，\n    // 当别处通过 GlobalConfigs 为某配置 put 新值，此处响应并刷新 UI\n\n    GlobalConfigs.output(this, keyValueEvent -> {\n      switch (keyValueEvent.currentKey) {\n        case Key.TEST_STRING:\n          break;\n        case Key.TEST_BOOLEAN:\n          break;\n      }\n    });\n  }\n\n  /**\n   * TODO tip 2：\n   * 通过唯一入口 input() 发消息至 \"可信源\"，由其内部统一处理业务逻辑和结果分发。\n   */\n  @Override\n  protected void onInput() {\n    mAdapter.setOnItemClickListener((viewId, item, position) -> {\n      if (viewId == R.id.btn_mark) mNoteRequester.input(NoteIntent.MarkItem(item));\n      else if (viewId == R.id.btn_topping) mNoteRequester.input(NoteIntent.ToppingItem(item));\n      else if (viewId == R.id.btn_delete) mNoteRequester.input(NoteIntent.RemoveItem(item));\n      else if (viewId == R.id.cl) EditorFragment.start(nav(), item);\n    });\n    mClickProxy.setOnClickListener(view -> {\n      if (view.getId() == R.id.fab) EditorFragment.start(nav(), new Note());\n      else if (view.getId() == R.id.iv_logo) nav().navigate(R.id.action_list_to_setting);\n    });\n\n    //TODO 天气示例使用高德 API_KEY，如有需要，请自行在 \"高德开放平台\" 获取和在 DataRepository 类填入\n    //    if (TextUtils.isEmpty(mStates.weather.get())) {\n    //      mHttpRequester.input(Api.GetWeatherInfo(HttpRequester.CITY_CODE_BEIJING));\n    //    }\n\n    if (mStates.list.isEmpty()) mNoteRequester.input(NoteIntent.GetNoteList());\n  }\n\n  @Override\n  protected void onBackPressed() {\n    mMessenger.input(Messages.FinishActivity());\n  }\n\n  /**\n   * TODO tip 3：\n   * 基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n   * 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n   * 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n   *\n   * 具体可参见《解决 MVI 实战痛点》解析\n   * https://juejin.cn/post/7134594010642907149\n   */\n  public static class ListStates extends StateHolder {\n    public final List<Note> list = new ArrayList<>();\n    public final State<Boolean> emptyViewShow = new State<>(false);\n    public final State<Boolean> loadingWeather = new State<>(false);\n    public final State<String> weather = new State<>(\"\");\n  }\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/MainActivity.java",
    "content": "package com.kunminx.purenote.ui.page;\n\nimport android.util.Log;\n\nimport com.kunminx.architecture.ui.page.BaseActivity;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.purenote.BR;\nimport com.kunminx.purenote.R;\nimport com.kunminx.purenote.domain.intent.ComplexIntent;\nimport com.kunminx.purenote.domain.intent.Messages;\nimport com.kunminx.purenote.domain.message.PageMessenger;\nimport com.kunminx.purenote.domain.request.ComplexRequester;\n\nimport java.util.Objects;\n\npublic class MainActivity extends BaseActivity {\n  private MainAtyStates mStates;\n  private PageMessenger mMessenger;\n  private ComplexRequester mComplexRequester;\n\n  @Override\n  protected void initViewModel() {\n    mMessenger = getApplicationScopeViewModel(PageMessenger.class);\n    mComplexRequester = getActivityScopeViewModel(ComplexRequester.class);\n  }\n\n  @Override\n  protected DataBindingConfig getDataBindingConfig() {\n    return new DataBindingConfig(R.layout.activity_main, BR.state, mStates);\n  }\n\n  /**\n   * TODO tip 1：\n   * 通过 PublishSubject 接收数据，并在唯一出口 output{ ... } 中响应数据的变化，\n   * 通过 BehaviorSubject 通知所绑定控件属性重新渲染，并为其兜住最后一次状态，\n   */\n  @Override\n  protected void onOutput() {\n    mMessenger.output(this, messages -> {\n      if (Objects.equals(messages.id, Messages.FinishActivity.ID)) finish();\n    });\n\n    mComplexRequester.output(this, complexIntent -> {\n      switch (complexIntent.id) {\n        case ComplexIntent.Test1.ID:\n          Log.i(\"ComplexIntent\", \"---1\");\n          break;\n        case ComplexIntent.Test2.ID:\n          Log.i(\"ComplexIntent\", \"---2\");\n          break;\n        case ComplexIntent.Test3.ID:\n          Log.i(\"ComplexIntent\", \"---3\");\n          break;\n        case ComplexIntent.Test4.ID:\n          ComplexIntent.Test4 test4 = (ComplexIntent.Test4) complexIntent;\n          Log.i(\"ComplexIntent\", \"---4 \" + test4.resultCount1);\n          break;\n      }\n    });\n  }\n\n  /**\n   * TODO tip 2：\n   * 通过唯一入口 input() 发消息至 \"可信源\"，由其内部统一处理业务逻辑和结果分发。\n   *\n   * 此处展示通过 dispatcher.input 连续发送多事件而不被覆盖\n   */\n  @Override\n  protected void onInput() {\n    //TODO tip 3：Test1 可用于验证 \"app 处于后台时，推送的数据会兜着，回到前台时，会回推，但此后环境重建也不会再回推，做到消费且只消费一次\"\n    //在单独观察 Test1 能力时，可将 Test2、Test3 的测试注释\n\n    mComplexRequester.input(ComplexIntent.Test1(1));\n//    mComplexRequester.input(ComplexIntent.Test2(2));\n//    mComplexRequester.input(ComplexIntent.Test2(2));\n//    mComplexRequester.input(ComplexIntent.Test2(2));\n//    mComplexRequester.input(ComplexIntent.Test3(3));\n//    mComplexRequester.input(ComplexIntent.Test3(3));\n//    mComplexRequester.input(ComplexIntent.Test3(3));\n//    mComplexRequester.input(ComplexIntent.Test3(3));\n  }\n\n  public static class MainAtyStates extends StateHolder {\n  }\n}"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/SettingFragment.java",
    "content": "package com.kunminx.purenote.ui.page;\n\nimport com.kunminx.architecture.domain.dispatch.GlobalConfigs;\nimport com.kunminx.architecture.ui.bind.ClickProxy;\nimport com.kunminx.architecture.ui.page.BaseFragment;\nimport com.kunminx.architecture.ui.page.DataBindingConfig;\nimport com.kunminx.architecture.ui.page.StateHolder;\nimport com.kunminx.architecture.ui.state.State;\nimport com.kunminx.purenote.BR;\nimport com.kunminx.purenote.R;\nimport com.kunminx.purenote.data.config.Key;\n\n/**\n * Create by KunMinX at 2022/8/15\n */\npublic class SettingFragment extends BaseFragment {\n  private SettingStates mStates;\n  private ClickProxy mClickProxy;\n\n  @Override\n  protected void initViewModel() {\n    mStates = getFragmentScopeViewModel(SettingStates.class);\n  }\n\n  @Override\n  protected DataBindingConfig getDataBindingConfig() {\n    mStates.testString.set(GlobalConfigs.getString(Key.TEST_STRING));\n    mStates.testBoolean.set(GlobalConfigs.getBoolean(Key.TEST_BOOLEAN));\n    return new DataBindingConfig(R.layout.fragment_settings, BR.state, mStates)\n            .addBindingParam(BR.click, mClickProxy = new ClickProxy());\n  }\n\n  /**\n   * TODO tip 1：\n   * 通过唯一入口 input() 发消息至 \"可信源\"，由其内部统一处理业务逻辑和结果分发。\n   */\n  @Override\n  protected void onInput() {\n    mClickProxy.setOnClickListener(v -> {\n      if (v.getId() == R.id.btn_back) nav().navigateUp();\n      else if (v.getId() == R.id.btn_sure_1)\n        GlobalConfigs.put(Key.TEST_STRING, mStates.testString.get());\n      else if (v.getId() == R.id.sw_value_2)\n        GlobalConfigs.put(Key.TEST_BOOLEAN, mStates.testBoolean.get());\n    });\n  }\n\n  /**\n   * TODO tip 2：\n   * 基于单一职责原则，抽取 Jetpack ViewModel \"状态保存和恢复\" 的能力作为 StateHolder，\n   * 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject，用作所绑定控件的 \"可信数据源\"，\n   * 从而在收到来自 PublishSubject 的结果回推后，响应结果数据的变化，也即通知控件属性重新渲染，并为其兜住最后一次状态，\n   *\n   * 具体可参见《解决 MVI 实战痛点》解析\n   * https://juejin.cn/post/7134594010642907149\n   */\n  public static class SettingStates extends StateHolder {\n    public final State<String> testString = new State<>(\"\");\n    public final State<Boolean> testBoolean = new State<>(false);\n  }\n}\n"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"150\">\n    <translate\n        android:fromXDelta=\"10%p\"\n        android:interpolator=\"@android:anim/decelerate_interpolator\"\n        android:toXDelta=\"0\" />\n\n    <alpha\n        android:fromAlpha=\"0\"\n        android:toAlpha=\"1.0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromXDelta=\"0\"\n        android:interpolator=\"@android:anim/accelerate_interpolator\"\n        android:toXDelta=\"-10%p\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_pop_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromXDelta=\"-10%p\"\n        android:toXDelta=\"0\" />\n\n</set>"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_pop_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromXDelta=\"0\"\n        android:toXDelta=\"10%p\" />\n\n    <alpha\n        android:fromAlpha=\"1.0\"\n        android:toAlpha=\"0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_add.xml",
    "content": "<vector android:height=\"48dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"48dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_arrow_back.xml",
    "content": "<vector android:height=\"48dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"48dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.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=\"state\"\n            type=\"com.kunminx.purenote.ui.page.MainActivity.MainAtyStates\" />\n    </data>\n\n    <androidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/frag_container\"\n        android:name=\"androidx.navigation.fragment.NavHostFragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:defaultNavHost=\"true\"\n        app:navGraph=\"@navigation/nav_graph\" />\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/adapter_note_list.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    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <data>\n\n        <variable\n            name=\"note\"\n            type=\"com.kunminx.purenote.data.bean.Note\" />\n    </data>\n\n    <com.kunminx.architecture.ui.view.SwipeMenuLayout\n        android:id=\"@+id/swipe_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"80dp\"\n        android:layout_margin=\"4dp\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        app:ios=\"true\"\n        app:leftSwipe=\"true\"\n        app:swipeEnable=\"true\">\n\n        <androidx.constraintlayout.widget.ConstraintLayout\n            android:id=\"@+id/cl\"\n            clipToOutline=\"@{true}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"80dp\"\n            android:elevation=\"8dp\">\n\n            <androidx.appcompat.widget.AppCompatTextView\n                android:id=\"@+id/tv_title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"16dp\"\n                android:layout_marginTop=\"12dp\"\n                android:text=\"@{note.title}\"\n                android:textColor=\"@color/color_black\"\n                android:textSize=\"24sp\"\n                android:textStyle=\"bold\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/tv_time\"\n                app:layout_constraintTop_toTopOf=\"parent\"\n                tools:text=\"TEST 测试 123\" />\n\n            <androidx.appcompat.widget.AppCompatImageButton\n                android:id=\"@+id/btn_mark\"\n                imgRes=\"@{note.markIcon()}\"\n                android:layout_width=\"48dp\"\n                android:layout_height=\"48dp\"\n                android:layout_marginEnd=\"8dp\"\n                android:background=\"@color/transparent\"\n                android:padding=\"4dp\"\n                android:scaleType=\"centerInside\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintRight_toRightOf=\"parent\"\n                app:layout_constraintTop_toTopOf=\"parent\" />\n\n            <androidx.appcompat.widget.AppCompatTextView\n                android:id=\"@+id/tv_time\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"16dp\"\n                android:layout_marginTop=\"4dp\"\n                android:layout_marginBottom=\"4dp\"\n                android:text=\"@{note.modifyDate}\"\n                android:textColor=\"@color/blue\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toLeftOf=\"parent\"\n                app:layout_constraintTop_toBottomOf=\"@+id/tv_title\"\n                tools:text=\"2022-07-03\" />\n\n            <androidx.appcompat.widget.AppCompatTextView\n                android:id=\"@+id/tv_topped\"\n                visible=\"@{note.topping}\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"12dp\"\n                android:layout_marginTop=\"4dp\"\n                android:layout_marginBottom=\"4dp\"\n                android:text=\"@string/top\"\n                android:textColor=\"@color/blue\"\n                app:layout_constraintBottom_toBottomOf=\"parent\"\n                app:layout_constraintLeft_toRightOf=\"@+id/tv_time\"\n                app:layout_constraintTop_toBottomOf=\"@+id/tv_title\" />\n\n        </androidx.constraintlayout.widget.ConstraintLayout>\n\n        <FrameLayout\n            android:layout_width=\"80dp\"\n            android:layout_height=\"80dp\"\n            android:background=\"@color/blue\">\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/btn_topping\"\n                android:layout_width=\"28dp\"\n                android:layout_height=\"28dp\"\n                android:layout_gravity=\"center\"\n                android:src=\"@drawable/icon_pin\"\n                android:tint=\"@color/color_white\" />\n\n        </FrameLayout>\n\n        <FrameLayout\n            android:layout_width=\"80dp\"\n            android:layout_height=\"80dp\"\n            android:background=\"@color/pink\">\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/btn_delete\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:layout_gravity=\"center\"\n                android:src=\"@drawable/icon_delete\"\n                android:tint=\"@color/color_white\" />\n\n        </FrameLayout>\n\n    </com.kunminx.architecture.ui.view.SwipeMenuLayout>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_editor.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <data>\n\n        <variable\n            name=\"state\"\n            type=\"com.kunminx.purenote.ui.page.EditorFragment.EditorStates\" />\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.architecture.ui.bind.ClickProxy\" />\n    </data>\n\n    <androidx.appcompat.widget.LinearLayoutCompat\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <androidx.appcompat.widget.LinearLayoutCompat\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"89dp\"\n            android:background=\"@color/color_white\"\n            android:elevation=\"8dp\"\n            android:orientation=\"horizontal\"\n            android:paddingTop=\"25dp\">\n\n            <androidx.appcompat.widget.AppCompatImageButton\n                android:id=\"@+id/btn_back\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"56dp\"\n                android:layout_marginStart=\"12dp\"\n                android:background=\"@color/transparent\"\n                android:onClick=\"@{click.listener}\"\n                android:scaleType=\"centerInside\"\n                android:src=\"@drawable/ic_baseline_arrow_back\"\n                android:tint=\"@color/gray\" />\n\n            <androidx.appcompat.widget.LinearLayoutCompat\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\">\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_title\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"32dp\"\n                    android:gravity=\"center_vertical\"\n                    android:paddingStart=\"12dp\"\n                    android:paddingEnd=\"4dp\"\n                    android:text=\"@{state.tip}\"\n                    android:textColor=\"@color/gray\"\n                    android:textSize=\"24sp\" />\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_time\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"20dp\"\n                    android:gravity=\"center_vertical\"\n                    android:paddingStart=\"12dp\"\n                    android:paddingEnd=\"12dp\"\n                    android:text=\"@{state.time}\"\n                    android:textColor=\"@color/gray\"\n                    android:textSize=\"14sp\" />\n\n            </androidx.appcompat.widget.LinearLayoutCompat>\n\n        </androidx.appcompat.widget.LinearLayoutCompat>\n\n        <androidx.appcompat.widget.AppCompatEditText\n            android:id=\"@+id/et_title\"\n            requestFocus=\"@{state.titleRequestFocus}\"\n            showKeyboard=\"@{state.titleRequestFocus}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"56dp\"\n            android:background=\"@color/color_white\"\n            android:gravity=\"center_vertical\"\n            android:hint=\"@string/title\"\n            android:paddingStart=\"12dp\"\n            android:paddingEnd=\"12dp\"\n            android:singleLine=\"true\"\n            android:text=\"@={state.title}\"\n            android:textSize=\"24sp\" />\n\n        <androidx.appcompat.widget.AppCompatEditText\n            android:id=\"@+id/et_content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@color/color_white\"\n            android:gravity=\"top\"\n            android:hint=\"@string/content\"\n            android:padding=\"12dp\"\n            android:text=\"@={state.content}\"\n            android:textSize=\"16sp\" />\n\n    </androidx.appcompat.widget.LinearLayoutCompat>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_list.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    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <data>\n\n        <variable\n            name=\"state\"\n            type=\"com.kunminx.purenote.ui.page.ListFragment.ListStates\" />\n\n        <variable\n            name=\"adapter\"\n            type=\"androidx.recyclerview.widget.RecyclerView.Adapter\" />\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.architecture.ui.bind.ClickProxy\" />\n    </data>\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.appcompat.widget.LinearLayoutCompat\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"89dp\"\n            android:background=\"@color/color_white\"\n            android:elevation=\"8dp\"\n            android:orientation=\"horizontal\"\n            android:paddingTop=\"25dp\">\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/iv_logo\"\n                android:layout_width=\"48dp\"\n                android:layout_height=\"56dp\"\n                android:layout_marginStart=\"12dp\"\n                android:onClick=\"@{click.listener}\"\n                android:scaleType=\"centerInside\"\n                android:src=\"@drawable/icon_avatar\" />\n\n            <androidx.appcompat.widget.LinearLayoutCompat\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:orientation=\"vertical\">\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_title\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"32dp\"\n                    android:gravity=\"center_vertical\"\n                    android:onClick=\"@{click.listener}\"\n                    android:paddingStart=\"12dp\"\n                    android:paddingEnd=\"12dp\"\n                    android:text=\"@string/app_name\"\n                    android:textColor=\"@color/color_black\"\n                    android:textSize=\"24sp\" />\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_content\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"20dp\"\n                    android:gravity=\"center_vertical\"\n                    android:paddingStart=\"12dp\"\n                    android:paddingEnd=\"12dp\"\n                    android:text=\"@string/copyright\"\n                    android:textColor=\"@color/gray\"\n                    android:textSize=\"14sp\" />\n\n            </androidx.appcompat.widget.LinearLayoutCompat>\n\n            <FrameLayout\n                android:layout_width=\"80dp\"\n                android:layout_height=\"56dp\">\n\n                <ProgressBar\n                    android:id=\"@+id/progress\"\n                    visible=\"@{state.loadingWeather}\"\n                    android:layout_width=\"36dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\" />\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@{state.weather}\"\n                    android:textColor=\"@color/gray\"\n                    android:textSize=\"20sp\"\n                    tools:text=\"@string/weather\" />\n\n            </FrameLayout>\n\n            <androidx.appcompat.widget.AppCompatImageView\n                android:id=\"@+id/iv_search\"\n                android:layout_width=\"48dp\"\n                android:layout_height=\"56dp\"\n                android:layout_marginEnd=\"12dp\"\n                android:layout_weight=\"0\"\n                android:onClick=\"@{click.listener}\"\n                android:scaleType=\"centerInside\"\n                android:src=\"@drawable/icon_search\"\n                android:tint=\"@color/gray\" />\n\n        </androidx.appcompat.widget.LinearLayoutCompat>\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv\"\n            adapter=\"@{adapter}\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_below=\"@id/toolbar\"\n            android:background=\"@color/light_gray\"\n            app:layoutManager=\"androidx.recyclerview.widget.LinearLayoutManager\"\n            tools:itemCount=\"9\"\n            tools:listitem=\"@layout/adapter_note_list\" />\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/iv_empty\"\n            visible=\"@{state.emptyViewShow}\"\n            android:layout_width=\"300dp\"\n            android:layout_height=\"300dp\"\n            android:layout_centerInParent=\"true\"\n            android:src=\"@drawable/bg_empty\"\n            android:visibility=\"gone\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fab\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_alignParentBottom=\"true\"\n            android:layout_margin=\"16dp\"\n            android:contentDescription=\"@string/new_note\"\n            android:onClick=\"@{click.listener}\"\n            android:src=\"@drawable/ic_baseline_add\" />\n\n    </RelativeLayout>\n</layout>\n\n\n\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <data>\n\n        <variable\n            name=\"state\"\n            type=\"com.kunminx.purenote.ui.page.SettingFragment.SettingStates\" />\n\n        <variable\n            name=\"click\"\n            type=\"com.kunminx.architecture.ui.bind.ClickProxy\" />\n    </data>\n\n    <androidx.appcompat.widget.LinearLayoutCompat\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <androidx.appcompat.widget.LinearLayoutCompat\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"89dp\"\n            android:background=\"@color/color_white\"\n            android:elevation=\"8dp\"\n            android:orientation=\"horizontal\"\n            android:paddingTop=\"25dp\">\n\n            <androidx.appcompat.widget.AppCompatImageButton\n                android:id=\"@+id/btn_back\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"56dp\"\n                android:layout_marginStart=\"12dp\"\n                android:background=\"@color/transparent\"\n                android:onClick=\"@{click.listener}\"\n                android:scaleType=\"centerInside\"\n                android:src=\"@drawable/ic_baseline_arrow_back\"\n                android:tint=\"@color/gray\" />\n\n            <androidx.appcompat.widget.LinearLayoutCompat\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\">\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_title\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"32dp\"\n                    android:gravity=\"center_vertical\"\n                    android:paddingStart=\"12dp\"\n                    android:paddingEnd=\"4dp\"\n                    android:text=\"@string/setting\"\n                    android:textColor=\"@color/color_black\"\n                    android:textSize=\"24sp\" />\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_time\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"20dp\"\n                    android:gravity=\"center_vertical\"\n                    android:paddingStart=\"12dp\"\n                    android:paddingEnd=\"12dp\"\n                    android:text=\"@string/setting_summary\"\n                    android:textColor=\"@color/dark_gray\"\n                    android:textSize=\"14sp\" />\n\n            </androidx.appcompat.widget.LinearLayoutCompat>\n\n        </androidx.appcompat.widget.LinearLayoutCompat>\n\n        <androidx.appcompat.widget.LinearLayoutCompat\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@color/color_white\"\n            android:orientation=\"vertical\">\n\n            <RelativeLayout\n                android:id=\"@+id/rl_1\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"80dp\"\n                android:paddingStart=\"12dp\"\n                android:paddingEnd=\"12dp\">\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_key_1\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_centerVertical=\"true\"\n                    android:text=\"@string/string_test\"\n                    android:textColor=\"@color/color_black\"\n                    android:textSize=\"20sp\" />\n\n                <androidx.appcompat.widget.AppCompatEditText\n                    android:id=\"@+id/et_value_1\"\n                    android:layout_width=\"150dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_centerInParent=\"true\"\n                    android:hint=\"@string/enter_please\"\n                    android:singleLine=\"true\"\n                    android:text=\"@={state.testString}\" />\n\n                <Button\n                    android:id=\"@+id/btn_sure_1\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_alignParentEnd=\"true\"\n                    android:layout_centerVertical=\"true\"\n                    android:onClick=\"@{click.listener}\"\n                    android:text=\"@string/save\"\n                    tools:ignore=\"RelativeOverlap\" />\n\n            </RelativeLayout>\n\n            <RelativeLayout\n                android:id=\"@+id/rl_2\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"80dp\"\n                android:paddingStart=\"12dp\"\n                android:paddingEnd=\"12dp\">\n\n                <androidx.appcompat.widget.AppCompatTextView\n                    android:id=\"@+id/tv_key_2\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_centerVertical=\"true\"\n                    android:text=\"@string/boolean_test\"\n                    android:textColor=\"@color/color_black\"\n                    android:textSize=\"20sp\"\n                    tools:ignore=\"RelativeOverlap\" />\n\n                <androidx.appcompat.widget.SwitchCompat\n                    android:id=\"@+id/sw_value_2\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_alignParentEnd=\"true\"\n                    android:layout_centerVertical=\"true\"\n                    android:checked=\"@={state.testBoolean}\"\n                    android:onClick=\"@{click.listener}\" />\n\n            </RelativeLayout>\n\n        </androidx.appcompat.widget.LinearLayoutCompat>\n\n    </androidx.appcompat.widget.LinearLayoutCompat>\n</layout>\n"
  },
  {
    "path": "app/src/main/res/navigation/nav_graph.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/nav_graph\"\n    app:startDestination=\"@id/ListFragment\">\n\n    <fragment\n        android:id=\"@+id/ListFragment\"\n        android:name=\"com.kunminx.purenote.ui.page.ListFragment\"\n        tools:layout=\"@layout/fragment_list\">\n\n        <action\n            android:id=\"@+id/action_list_to_editor\"\n            app:destination=\"@id/EditorFragment\"\n            app:enterAnim=\"@anim/x_fragment_enter\"\n            app:exitAnim=\"@anim/x_fragment_exit\"\n            app:popEnterAnim=\"@anim/x_fragment_pop_enter\"\n            app:popExitAnim=\"@anim/x_fragment_pop_exit\" />\n        <action\n            android:id=\"@+id/action_list_to_setting\"\n            app:destination=\"@id/settingFragment\"\n            app:enterAnim=\"@anim/x_fragment_enter\"\n            app:exitAnim=\"@anim/x_fragment_exit\"\n            app:popEnterAnim=\"@anim/x_fragment_pop_enter\"\n            app:popExitAnim=\"@anim/x_fragment_pop_exit\" />\n    </fragment>\n\n    <fragment\n        android:id=\"@+id/EditorFragment\"\n        android:name=\"com.kunminx.purenote.ui.page.EditorFragment\"\n        tools:layout=\"@layout/fragment_editor\">\n\n    </fragment>\n    <fragment\n        android:id=\"@+id/settingFragment\"\n        android:name=\"com.kunminx.purenote.ui.page.SettingFragment\"\n        android:label=\"SettingFragment\"\n        tools:layout=\"@layout/fragment_settings\" />\n</navigation>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"color_black\">#FF000000</color>\n    <color name=\"color_white\">#FFFFFFFF</color>\n    <color name=\"transparent\">#00000000</color>\n\n    <color name=\"light_gray\">#F9FBFC</color>\n    <color name=\"blue\">#4C5DF2</color>\n    <color name=\"gray\">#cccccc</color>\n    <color name=\"dark_gray\">#666666</color>\n    <color name=\"pink\">#F70067</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <dimen name=\"fab_margin\">16dp</dimen>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">PureNote</string>\n    <string name=\"title\">标题</string>\n    <string name=\"content\">内容</string>\n    <string name=\"edit\">编辑</string>\n    <string name=\"note\">笔记</string>\n    <string name=\"new_note\">新建笔记</string>\n    <string name=\"saved\">已保存</string>\n    <string name=\"top\">置顶</string>\n    <string name=\"last_time_modify\">最后编辑于</string>\n    <string name=\"copyright\">Product by KunMinX with love</string>\n    <string name=\"setting\">设置</string>\n    <string name=\"setting_summary\">配置读写，持久化存储</string>\n    <string name=\"string_test\">String 读写</string>\n    <string name=\"save\">保存</string>\n    <string name=\"enter_please\">请输入</string>\n    <string name=\"boolean_test\">Boolean 读写</string>\n    <string name=\"weather\">天气</string>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<resources>\n\n    <style name=\"Theme.PureNote\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <item name=\"colorPrimary\">@color/purple_500</item>\n        <item name=\"colorPrimaryVariant\">@color/purple_700</item>\n        <item name=\"colorOnPrimary\">@color/color_white</item>\n        <item name=\"colorSecondary\">@color/blue</item>\n        <item name=\"colorSecondaryVariant\">@color/teal_700</item>\n        <item name=\"colorOnSecondary\">@color/color_white</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/test/java/com/kunminx/purenote/ExampleUnitTest.java",
    "content": "package com.kunminx.purenote;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\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() {\n    assertEquals(4, 2 + 2);\n  }\n}"
  },
  {
    "path": "architecture/.gitignore",
    "content": "/build"
  },
  {
    "path": "architecture/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion appTargetSdk\n    defaultConfig {\n        minSdkVersion 23\n        targetSdkVersion appTargetSdk\n        versionCode appVersionCode\n        versionName appVersionName\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    buildFeatures {\n        dataBinding true\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: \"libs\", include: [\"*.jar\", \"*.aar\"])\n\n    testImplementation \"junit:junit:4.13.2\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.3\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.4.0\"\n\n    implementation \"androidx.appcompat:appcompat:1.5.0\"\n    implementation \"androidx.navigation:navigation-runtime:2.5.1\"\n    implementation 'com.github.KunMinX:Smooth-Navigation:v4.0.0'\n    implementation 'com.github.KunMinX.Strict-DataBinding:strict_databinding:5.6.0'\n    implementation 'com.github.KunMinX.Strict-DataBinding:binding_state:5.6.0'\n    implementation 'androidx.recyclerview:recyclerview:1.2.1'\n    implementation project(\":mvi-dispatch\")\n\n    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'\n    implementation 'io.reactivex.rxjava2:rxjava:2.2.21'\n}"
  },
  {
    "path": "architecture/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "architecture/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\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"
  },
  {
    "path": "architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java",
    "content": "package com.kunminx.architecture;\n\nimport android.content.Context;\n\nimport androidx.test.platform.app.InstrumentationRegistry;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumented 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() {\n    // Context of the app under test.\n    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n    assertEquals(\"com.kunminx.architecture.test\", appContext.getPackageName());\n  }\n}"
  },
  {
    "path": "architecture/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.kunminx.architecture\">\n\n</manifest>"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/AsyncTask.java",
    "content": "package com.kunminx.architecture.data.response;\n\nimport android.annotation.SuppressLint;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableEmitter;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.annotations.NonNull;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * Create by KunMinX at 2022/6/14\n */\npublic class AsyncTask {\n\n  @SuppressLint(\"CheckResult\")\n  public static <T> Observable<T> doIO(Action<T> start) {\n    return Observable.create(start::onEmit)\n            .subscribeOn(Schedulers.io())\n            .observeOn(AndroidSchedulers.mainThread());\n  }\n\n  @SuppressLint(\"CheckResult\")\n  public static <T> Observable<T> doCalculate(Action<T> start) {\n    return Observable.create(start::onEmit)\n            .subscribeOn(Schedulers.computation())\n            .observeOn(AndroidSchedulers.mainThread());\n  }\n\n  public interface Action<T> {\n    void onEmit(ObservableEmitter<T> emitter);\n  }\n\n  public interface Observer<T> extends io.reactivex.Observer<T> {\n    default void onSubscribe(@NonNull Disposable d) {\n    }\n\n    void onNext(@NonNull T t);\n\n    default void onError(@NonNull Throwable e) {\n    }\n\n    default void onComplete() {\n    }\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java",
    "content": "/*\n *\n * Copyright 2018-present KunMinX\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 */\n\npackage com.kunminx.architecture.data.response;\n\n/**\n * Create by KunMinX at 2020/7/20\n */\npublic class DataResult<T> {\n\n  private final T mEntity;\n  private final ResponseStatus mResponseStatus;\n\n  public DataResult(T entity, ResponseStatus responseStatus) {\n    mEntity = entity;\n    mResponseStatus = responseStatus;\n  }\n\n  public DataResult(T entity) {\n    mEntity = entity;\n    mResponseStatus = new ResponseStatus();\n  }\n\n  public T getResult() {\n    return mEntity;\n  }\n\n  public ResponseStatus getResponseStatus() {\n    return mResponseStatus;\n  }\n\n  public interface Result<T> {\n    void onResult(DataResult<T> dataResult);\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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 com.kunminx.architecture.data.response;\n\n/**\n * Create by KunMinX at 19/10/11\n */\npublic class ResponseStatus {\n\n  private String responseCode = \"\";\n  private String msg = \"\";\n  private boolean success = true;\n  private Enum<ResultSource> source = ResultSource.NETWORK;\n\n  public ResponseStatus() {\n  }\n\n  public ResponseStatus(String msg) {\n    this.msg = msg;\n    this.success = false;\n  }\n\n  public ResponseStatus(String msg, String responseCode, boolean success) {\n    this.msg = msg;\n    this.responseCode = responseCode;\n    this.success = success;\n  }\n\n  public ResponseStatus(String responseCode, boolean success) {\n    this.responseCode = responseCode;\n    this.success = success;\n  }\n\n  public ResponseStatus(String responseCode, boolean success, Enum<ResultSource> source) {\n    this(responseCode, success);\n    this.source = source;\n  }\n\n  public String getResponseCode() {\n    return responseCode;\n  }\n\n  public String getMsg() {\n    return msg;\n  }\n\n  public boolean isSuccess() {\n    return success;\n  }\n\n  public Enum<ResultSource> getSource() {\n    return source;\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java",
    "content": "package com.kunminx.architecture.data.response;\n\n/**\n * Create by KunMinX at 2020/11/30\n */\npublic enum ResultSource {\n  NETWORK, DATABASE, LOCAL_FILE\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/adapter/BaseBindingAdapter.java",
    "content": "package com.kunminx.architecture.ui.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.LayoutRes;\nimport androidx.annotation.NonNull;\nimport androidx.databinding.DataBindingUtil;\nimport androidx.databinding.ViewDataBinding;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.List;\n/**\n * Create by KunMinX at 2022/8/20\n */\npublic abstract class BaseBindingAdapter<M, B extends ViewDataBinding>\n        extends RecyclerView.Adapter<BaseBindingAdapter.BaseBindingViewHolder> {\n\n  protected List<M> mList;\n  protected OnItemClickListener<M> mOnItemClickListener;\n  protected OnItemLongClickListener<M> mOnItemLongClickListener;\n\n  public BaseBindingAdapter(List<M> list) {\n    mList = list;\n  }\n\n  public void setOnItemClickListener(OnItemClickListener<M> onItemClickListener) {\n    mOnItemClickListener = onItemClickListener;\n  }\n\n  public void setOnItemLongClickListener(OnItemLongClickListener<M> onItemLongClickListener) {\n    mOnItemLongClickListener = onItemLongClickListener;\n  }\n\n  @Override\n  @NonNull\n  public BaseBindingAdapter.BaseBindingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n    B binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), this.getLayoutResId(viewType), parent, false);\n    BaseBindingViewHolder holder = new BaseBindingViewHolder(binding.getRoot());\n    holder.itemView.setOnClickListener(v -> {\n      if (mOnItemClickListener != null) {\n        int position = holder.getBindingAdapterPosition();\n        mOnItemClickListener.onItemClick(holder.itemView.getId(), mList.get(position), position);\n      }\n    });\n    holder.itemView.setOnLongClickListener(v -> {\n      if (mOnItemLongClickListener != null) {\n        int position = holder.getBindingAdapterPosition();\n        mOnItemLongClickListener.onItemLongClick(holder.itemView.getId(), mList.get(position), position);\n        return true;\n      }\n      return false;\n    });\n    return holder;\n  }\n\n  @Override\n  public void onBindViewHolder(BaseBindingAdapter.BaseBindingViewHolder holder, final int position) {\n    B binding = DataBindingUtil.getBinding(holder.itemView);\n    this.onBindItem(binding, mList.get(position), holder);\n    if (binding != null) binding.executePendingBindings();\n  }\n\n  protected abstract @LayoutRes\n  int getLayoutResId(int viewType);\n\n  protected abstract void onBindItem(B binding, M item, RecyclerView.ViewHolder holder);\n\n  public List<M> getList() {\n    return mList;\n  }\n\n  public void refresh(List<M> list) {\n    mList.clear();\n    mList.addAll(list);\n    notifyDataSetChanged();\n  }\n\n  public void append(List<M> list) {\n    mList.addAll(list);\n    notifyDataSetChanged();\n  }\n\n  public static class BaseBindingViewHolder extends RecyclerView.ViewHolder {\n    BaseBindingViewHolder(View itemView) {\n      super(itemView);\n    }\n  }\n\n  public interface OnItemClickListener<M> {\n    void onItemClick(int viewId, M item, int position);\n  }\n\n  public interface OnItemLongClickListener<M> {\n    void onItemLongClick(int viewId, M item, int position);\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/bind/ClickProxy.java",
    "content": "package com.kunminx.architecture.ui.bind;\n\nimport android.view.View;\n/**\n * Create by KunMinX at 2022/8/18\n */\npublic class ClickProxy implements View.OnClickListener {\n  public View.OnClickListener listener;\n\n  public void setOnClickListener(View.OnClickListener listener) {\n    this.listener = listener;\n  }\n  @Override\n  public void onClick(View view) {\n    listener.onClick(view);\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/bind/CommonBindingAdapter.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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 com.kunminx.architecture.ui.bind;\n\nimport android.content.Context;\nimport android.view.View;\nimport android.view.inputmethod.InputMethodManager;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.databinding.BindingAdapter;\n\nimport com.kunminx.architecture.utils.Utils;\n\n/**\n * Create by KunMinX at 19/9/18\n */\npublic class CommonBindingAdapter {\n  @BindingAdapter(value = {\"visible\"}, requireAll = false)\n  public static void visible(View view, boolean visible) {\n    if (visible && view.getVisibility() == View.GONE) {\n      view.setVisibility(View.VISIBLE);\n    } else if (!visible && view.getVisibility() == View.VISIBLE) {\n      view.setVisibility(View.GONE);\n    }\n  }\n\n  @BindingAdapter(value = {\"invisible\"}, requireAll = false)\n  public static void invisible(View view, boolean visible) {\n    if (visible && view.getVisibility() == View.INVISIBLE) {\n      view.setVisibility(View.VISIBLE);\n    } else if (!visible && view.getVisibility() == View.VISIBLE) {\n      view.setVisibility(View.INVISIBLE);\n    }\n  }\n\n  @BindingAdapter(value = {\"imgRes\"}, requireAll = false)\n  public static void setImageResource(ImageView imageView, int imgRes) {\n    imageView.setImageResource(imgRes);\n  }\n\n  @BindingAdapter(value = {\"textColor\"}, requireAll = false)\n  public static void setTextColor(TextView textView, int textColorRes) {\n    textView.setTextColor(textView.getContext().getColor(textColorRes));\n  }\n\n  @BindingAdapter(value = {\"selected\"}, requireAll = false)\n  public static void selected(View view, boolean select) {\n    view.setSelected(select);\n  }\n\n  @BindingAdapter(value = {\"clipToOutline\"}, requireAll = false)\n  public static void clipToOutline(View view, boolean clipToOutline) {\n    view.setClipToOutline(clipToOutline);\n  }\n\n  @BindingAdapter(value = {\"requestFocus\"}, requireAll = false)\n  public static void requestFocus(View view, boolean requestFocus) {\n    if (requestFocus) view.requestFocus();\n  }\n\n  @BindingAdapter(value = {\"showKeyboard\"}, requireAll = false)\n  public static void showKeyboard(View view, boolean showKeyboard) {\n    InputMethodManager imm = (InputMethodManager) Utils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);\n    imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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 com.kunminx.architecture.ui.page;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.ViewModel;\n\nimport com.kunminx.architecture.ui.scope.ViewModelScope;\nimport com.kunminx.architecture.utils.AdaptScreenUtils;\nimport com.kunminx.architecture.utils.Utils;\n\n/**\n * Create by KunMinX at 19/8/1\n */\npublic abstract class BaseActivity extends DataBindingActivity {\n\n  private static final int STATUS_BAR_TRANSPARENT_COLOR = 0x00000000;\n  private final ViewModelScope mViewModelScope = new ViewModelScope();\n\n  protected void onOutput() {\n  }\n\n  protected void onInput() {\n  }\n\n  @SuppressLint(\"SourceLockedOrientationActivity\")\n  @Override\n  protected void onCreate(@Nullable Bundle savedInstanceState) {\n    transparentStatusBar(this);\n    super.onCreate(savedInstanceState);\n    onOutput();\n    onInput();\n  }\n\n  public static void transparentStatusBar(@NonNull Activity activity) {\n    Window window = activity.getWindow();\n    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);\n    int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;\n    int vis = window.getDecorView().getSystemUiVisibility();\n    window.getDecorView().setSystemUiVisibility(option | vis);\n    window.setStatusBarColor(STATUS_BAR_TRANSPARENT_COLOR);\n  }\n\n  //TODO tip 2: Jetpack 通过 \"工厂模式\" 实现 ViewModel 作用域可控，\n  //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域，\n  //值得注意的是，通过不同作用域 Provider 获得 ViewModel 实例非同一个，\n  //故若 ViewModel 状态信息保留不符合预期，可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。\n\n  //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6257931840\n\n  protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {\n    return mViewModelScope.getActivityScopeViewModel(this, modelClass);\n  }\n\n  protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {\n    return mViewModelScope.getApplicationScopeViewModel(modelClass);\n  }\n\n  @Override\n  public Resources getResources() {\n    if (Utils.getApp().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {\n      return AdaptScreenUtils.adaptWidth(super.getResources(), 360);\n    } else {\n      return AdaptScreenUtils.adaptHeight(super.getResources(), 640);\n    }\n  }\n\n  protected void openUrlInBrowser(String url) {\n    Uri uri = Uri.parse(url);\n    Intent intent = new Intent(Intent.ACTION_VIEW, uri);\n    startActivity(intent);\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java",
    "content": "/*\n * Copyright 2018-present KunMinX\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 com.kunminx.architecture.ui.page;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.View;\n\nimport androidx.activity.OnBackPressedCallback;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.lifecycle.ViewModel;\nimport androidx.navigation.NavController;\nimport androidx.navigation.fragment.NavHostFragment;\n\nimport com.kunminx.architecture.ui.scope.ViewModelScope;\n\n/**\n * Create by KunMinX at 19/7/11\n */\npublic abstract class BaseFragment extends DataBindingFragment {\n\n  private final ViewModelScope mViewModelScope = new ViewModelScope();\n  protected AppCompatActivity mActivity;\n\n  protected void onOutput() {\n  }\n\n  protected void onInput() {\n  }\n\n  @Override\n  public void onAttach(@NonNull Context context) {\n    super.onAttach(context);\n    mActivity = (AppCompatActivity) context;\n  }\n\n  @Override\n  public void onCreate(@Nullable Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    addOnBackPressed();\n  }\n\n  @Override\n  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {\n    super.onViewCreated(view, savedInstanceState);\n    onOutput();\n    onInput();\n  }\n\n  //TODO tip 2: Jetpack 通过 \"工厂模式\" 实现 ViewModel 作用域可控，\n  //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域，\n  //值得注意的是，通过不同作用域 Provider 获得 ViewModel 实例非同一个，\n  //故若 ViewModel 状态信息保留不符合预期，可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。\n\n  //如这么说无体会，详见 https://xiaozhuanlan.com/topic/6257931840\n\n  protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Class<T> modelClass) {\n    return mViewModelScope.getFragmentScopeViewModel(this, modelClass);\n  }\n\n  protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {\n    return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass);\n  }\n\n  protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {\n    return mViewModelScope.getApplicationScopeViewModel(modelClass);\n  }\n\n  protected NavController nav() {\n    return NavHostFragment.findNavController(this);\n  }\n\n  protected void openUrlInBrowser(String url) {\n    Uri uri = Uri.parse(url);\n    Intent intent = new Intent(Intent.ACTION_VIEW, uri);\n    startActivity(intent);\n  }\n\n  protected Context getAppContext() {\n    return mActivity.getApplicationContext();\n  }\n\n  private void addOnBackPressed() {\n    requireActivity().getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {\n      @Override\n      public void handleOnBackPressed() {\n        onBackPressed();\n      }\n    });\n  }\n\n  protected void onBackPressed() {\n    nav().navigateUp();\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java",
    "content": "package com.kunminx.architecture.ui.page;\n\nimport androidx.lifecycle.ViewModel;\n\n/**\n * Create by KunMinX at 2022/8/11\n */\npublic class StateHolder extends ViewModel {\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/view/SwipeMenuLayout.java",
    "content": "package com.kunminx.architecture.ui.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ValueAnimator;\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.PointF;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.VelocityTracker;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.ViewGroup;\nimport android.view.animation.AccelerateInterpolator;\nimport android.view.animation.OvershootInterpolator;\n\nimport com.kunminx.architecture.R;\n\n/**\n * Created by zhangxutong .\n * Date: 16/04/24\n */\npublic class SwipeMenuLayout extends ViewGroup {\n  private static final String TAG = \"zxt/SwipeMenuLayout\";\n\n  private int mScaleTouchSlop;\n  private int mMaxVelocity;\n  private int mPointerId;\n  private int mRightMenuWidths;\n  private int mLimit;\n  private View mContentView;\n  private final PointF mLastP = new PointF();\n  private boolean isUnMoved = true;\n  private final PointF mFirstP = new PointF();\n  private boolean isUserSwiped;\n  @SuppressLint(\"StaticFieldLeak\")\n  private static SwipeMenuLayout mViewCache;\n  private static boolean isTouching;\n  private VelocityTracker mVelocityTracker;\n  private boolean isSwipeEnable;\n  private boolean isIos;\n  private boolean iosInterceptFlag;\n  private boolean isLeftSwipe;\n\n  public SwipeMenuLayout(Context context) {\n    this(context, null);\n  }\n\n  public SwipeMenuLayout(Context context, AttributeSet attrs) {\n    this(context, attrs, 0);\n  }\n\n  public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {\n    super(context, attrs, defStyleAttr);\n    init(context, attrs, defStyleAttr);\n  }\n\n  public boolean isSwipeEnable() {\n    return isSwipeEnable;\n  }\n\n  public void setSwipeEnable(boolean swipeEnable) {\n    isSwipeEnable = swipeEnable;\n  }\n\n  public boolean isIos() {\n    return isIos;\n  }\n\n  public SwipeMenuLayout setIos(boolean ios) {\n    isIos = ios;\n    return this;\n  }\n\n  public boolean isLeftSwipe() {\n    return isLeftSwipe;\n  }\n\n  public SwipeMenuLayout setLeftSwipe(boolean leftSwipe) {\n    isLeftSwipe = leftSwipe;\n    return this;\n  }\n\n  public static SwipeMenuLayout getViewCache() {\n    return mViewCache;\n  }\n\n  private void init(Context context, AttributeSet attrs, int defStyleAttr) {\n    mScaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();\n    mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();\n    isSwipeEnable = true;\n    isIos = true;\n    isLeftSwipe = true;\n    TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout, defStyleAttr, 0);\n    int count = ta.getIndexCount();\n    for (int i = 0; i < count; i++) {\n      int attr = ta.getIndex(i);\n      if (attr == R.styleable.SwipeMenuLayout_swipeEnable) {\n        isSwipeEnable = ta.getBoolean(attr, true);\n      } else if (attr == R.styleable.SwipeMenuLayout_ios) {\n        isIos = ta.getBoolean(attr, true);\n      } else if (attr == R.styleable.SwipeMenuLayout_leftSwipe) {\n        isLeftSwipe = ta.getBoolean(attr, true);\n      }\n    }\n    ta.recycle();\n  }\n\n  @Override\n  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n    super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    setClickable(true);\n    mRightMenuWidths = 0;\n    int height = 0;\n    int contentWidth = 0;\n    int childCount = getChildCount();\n    final boolean measureMatchParentChildren = MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;\n    boolean isNeedMeasureChildHeight = false;\n    for (int i = 0; i < childCount; i++) {\n      View childView = getChildAt(i);\n      childView.setClickable(true);\n      if (childView.getVisibility() != GONE) {\n        measureChild(childView, widthMeasureSpec, heightMeasureSpec);\n        final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();\n        height = Math.max(height, childView.getMeasuredHeight());\n        if (measureMatchParentChildren && lp.height == LayoutParams.MATCH_PARENT) {\n          isNeedMeasureChildHeight = true;\n        }\n        if (i > 0) {\n          mRightMenuWidths += childView.getMeasuredWidth();\n        } else {\n          mContentView = childView;\n          contentWidth = childView.getMeasuredWidth();\n        }\n      }\n    }\n    setMeasuredDimension(getPaddingLeft() + getPaddingRight() + contentWidth,\n            height + getPaddingTop() + getPaddingBottom());\n    mLimit = mRightMenuWidths * 4 / 10;\n    if (isNeedMeasureChildHeight) {\n      forceUniformHeight(childCount, widthMeasureSpec);\n    }\n  }\n\n  @Override\n  public LayoutParams generateLayoutParams(AttributeSet attrs) {\n    return new MarginLayoutParams(getContext(), attrs);\n  }\n\n  private void forceUniformHeight(int count, int widthMeasureSpec) {\n    int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),\n            MeasureSpec.EXACTLY);\n    for (int i = 0; i < count; ++i) {\n      final View child = getChildAt(i);\n      if (child.getVisibility() != GONE) {\n        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();\n        if (lp.height == LayoutParams.MATCH_PARENT) {\n          int oldWidth = lp.width;\n          lp.width = child.getMeasuredWidth();\n          measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0);\n          lp.width = oldWidth;\n        }\n      }\n    }\n  }\n\n  @Override\n  protected void onLayout(boolean changed, int l, int t, int r, int b) {\n    int childCount = getChildCount();\n    int left = getPaddingLeft();\n    int right = getPaddingLeft();\n    for (int i = 0; i < childCount; i++) {\n      View childView = getChildAt(i);\n      if (childView.getVisibility() != GONE) {\n        if (i == 0) {\n          childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());\n          left = left + childView.getMeasuredWidth();\n        } else {\n          if (isLeftSwipe) {\n            childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());\n            left = left + childView.getMeasuredWidth();\n          } else {\n            childView.layout(right - childView.getMeasuredWidth(), getPaddingTop(), right, getPaddingTop() + childView.getMeasuredHeight());\n            right = right - childView.getMeasuredWidth();\n          }\n        }\n      }\n    }\n  }\n\n  @Override\n  public boolean dispatchTouchEvent(MotionEvent ev) {\n    if (isSwipeEnable) {\n      acquireVelocityTracker(ev);\n      final VelocityTracker verTracker = mVelocityTracker;\n      switch (ev.getAction()) {\n        case MotionEvent.ACTION_DOWN:\n          isUserSwiped = false;\n          isUnMoved = true;\n          iosInterceptFlag = false;\n          if (isTouching) return false;\n          else isTouching = true;\n          mLastP.set(ev.getRawX(), ev.getRawY());\n          mFirstP.set(ev.getRawX(), ev.getRawY());\n          if (mViewCache != null) {\n            if (mViewCache != this) {\n              mViewCache.smoothClose();\n              iosInterceptFlag = isIos;\n            }\n            getParent().requestDisallowInterceptTouchEvent(true);\n          }\n          mPointerId = ev.getPointerId(0);\n          break;\n        case MotionEvent.ACTION_MOVE:\n          if (iosInterceptFlag) break;\n          float gap = mLastP.x - ev.getRawX();\n          if (Math.abs(gap) > 10 || Math.abs(getScrollX()) > 10) {\n            getParent().requestDisallowInterceptTouchEvent(true);\n          }\n          if (Math.abs(gap) > mScaleTouchSlop) isUnMoved = false;\n          scrollBy((int) (gap), 0);\n          if (isLeftSwipe) {\n            if (getScrollX() < 0) scrollTo(0, 0);\n            if (getScrollX() > mRightMenuWidths) scrollTo(mRightMenuWidths, 0);\n          } else {\n            if (getScrollX() < -mRightMenuWidths) scrollTo(-mRightMenuWidths, 0);\n            if (getScrollX() > 0) scrollTo(0, 0);\n          }\n          mLastP.set(ev.getRawX(), ev.getRawY());\n          break;\n        case MotionEvent.ACTION_UP:\n        case MotionEvent.ACTION_CANCEL:\n          if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) {\n            isUserSwiped = true;\n          }\n\n          if (!iosInterceptFlag) {\n            verTracker.computeCurrentVelocity(1000, mMaxVelocity);\n            final float velocityX = verTracker.getXVelocity(mPointerId);\n            if (Math.abs(velocityX) > 1000) {\n              if (velocityX < -1000) {\n                if (isLeftSwipe) smoothExpand();\n                else smoothClose();\n              } else {\n                if (isLeftSwipe) smoothClose();\n                else smoothExpand();\n              }\n            } else {\n              if (Math.abs(getScrollX()) > mLimit) smoothExpand();\n              else smoothClose();\n            }\n          }\n          releaseVelocityTracker();\n          isTouching = false;\n          break;\n        default:\n          break;\n      }\n    }\n    return super.dispatchTouchEvent(ev);\n  }\n\n  @Override\n  public boolean onInterceptTouchEvent(MotionEvent ev) {\n    if (isSwipeEnable) {\n      switch (ev.getAction()) {\n        case MotionEvent.ACTION_MOVE:\n          if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) return true;\n          break;\n        case MotionEvent.ACTION_UP:\n          if (isLeftSwipe) {\n            if (getScrollX() > mScaleTouchSlop) {\n              if (ev.getX() < getWidth() - getScrollX()) {\n                if (isUnMoved) smoothClose();\n                return true;\n              }\n            }\n          } else {\n            if (-getScrollX() > mScaleTouchSlop) {\n              if (ev.getX() > -getScrollX()) {\n                if (isUnMoved) smoothClose();\n                return true;\n              }\n            }\n          }\n          if (isUserSwiped) return true;\n          break;\n      }\n      if (iosInterceptFlag) {\n        return true;\n      }\n    }\n    return super.onInterceptTouchEvent(ev);\n  }\n\n  private ValueAnimator mExpandAnim, mCloseAnim;\n\n  private boolean isExpand;\n\n  public void smoothExpand() {\n    mViewCache = SwipeMenuLayout.this;\n    if (null != mContentView) {\n      mContentView.setLongClickable(false);\n    }\n\n    cancelAnim();\n    mExpandAnim = ValueAnimator.ofInt(getScrollX(), isLeftSwipe ? mRightMenuWidths : -mRightMenuWidths);\n    mExpandAnim.addUpdateListener(animation -> scrollTo((Integer) animation.getAnimatedValue(), 0));\n    mExpandAnim.setInterpolator(new OvershootInterpolator());\n    mExpandAnim.addListener(new AnimatorListenerAdapter() {\n      @Override\n      public void onAnimationEnd(Animator animation) {\n        isExpand = true;\n      }\n    });\n    mExpandAnim.setDuration(300).start();\n  }\n\n  private void cancelAnim() {\n    if (mCloseAnim != null && mCloseAnim.isRunning()) {\n      mCloseAnim.cancel();\n    }\n    if (mExpandAnim != null && mExpandAnim.isRunning()) {\n      mExpandAnim.cancel();\n    }\n  }\n\n  public void smoothClose() {\n    mViewCache = null;\n    if (null != mContentView) {\n      mContentView.setLongClickable(true);\n    }\n\n    cancelAnim();\n    mCloseAnim = ValueAnimator.ofInt(getScrollX(), 0);\n    mCloseAnim.addUpdateListener(animation -> scrollTo((Integer) animation.getAnimatedValue(), 0));\n    mCloseAnim.setInterpolator(new AccelerateInterpolator());\n    mCloseAnim.addListener(new AnimatorListenerAdapter() {\n      @Override\n      public void onAnimationEnd(Animator animation) {\n        isExpand = false;\n\n      }\n    });\n    mCloseAnim.setDuration(300).start();\n  }\n\n  private void acquireVelocityTracker(final MotionEvent event) {\n    if (null == mVelocityTracker) {\n      mVelocityTracker = VelocityTracker.obtain();\n    }\n    mVelocityTracker.addMovement(event);\n  }\n\n  private void releaseVelocityTracker() {\n    if (null != mVelocityTracker) {\n      mVelocityTracker.clear();\n      mVelocityTracker.recycle();\n      mVelocityTracker = null;\n    }\n  }\n\n  @Override\n  protected void onDetachedFromWindow() {\n    if (this == mViewCache) {\n      mViewCache.smoothClose();\n      mViewCache = null;\n    }\n    super.onDetachedFromWindow();\n  }\n\n  @Override\n  public boolean performLongClick() {\n    if (Math.abs(getScrollX()) > mScaleTouchSlop) {\n      return false;\n    }\n    return super.performLongClick();\n  }\n\n  public void quickClose() {\n    if (this == mViewCache) {\n      cancelAnim();\n      mViewCache.scrollTo(0, 0);\n      mViewCache = null;\n    }\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.content.res.Resources;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\n\nimport java.lang.reflect.Field;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/09/23\n *     desc  : AdaptScreenUtils\n * </pre>\n */\npublic final class AdaptScreenUtils {\n\n  private static boolean isInitMiui = false;\n  private static Field mTmpMetricsField;\n\n  public static Resources adaptWidth(Resources resources, int designWidth) {\n    DisplayMetrics dm = getDisplayMetrics(resources);\n    float newXdpi = dm.xdpi = (dm.widthPixels * 72f) / designWidth;\n    setAppDmXdpi(newXdpi);\n    return resources;\n  }\n\n  public static Resources adaptHeight(Resources resources, int designHeight) {\n    DisplayMetrics dm = getDisplayMetrics(resources);\n    float newXdpi = dm.xdpi = (dm.heightPixels * 72f) / designHeight;\n    setAppDmXdpi(newXdpi);\n    return resources;\n  }\n\n  private static void setAppDmXdpi(final float xdpi) {\n    Utils.getApp().getResources().getDisplayMetrics().xdpi = xdpi;\n  }\n\n  private static DisplayMetrics getDisplayMetrics(Resources resources) {\n    DisplayMetrics miuiDisplayMetrics = getMiuiTmpMetrics(resources);\n    if (miuiDisplayMetrics == null) {\n      return resources.getDisplayMetrics();\n    }\n    return miuiDisplayMetrics;\n  }\n\n  private static DisplayMetrics getMiuiTmpMetrics(Resources resources) {\n    if (!isInitMiui) {\n      DisplayMetrics ret = null;\n      String simpleName = resources.getClass().getSimpleName();\n      if (\"MiuiResources\".equals(simpleName) || \"XResources\".equals(simpleName)) {\n        try {\n          //noinspection JavaReflectionMemberAccess\n          mTmpMetricsField = Resources.class.getDeclaredField(\"mTmpMetrics\");\n          mTmpMetricsField.setAccessible(true);\n          ret = (DisplayMetrics) mTmpMetricsField.get(resources);\n        } catch (Exception e) {\n          Log.e(\"AdaptScreenUtils\", \"no field of mTmpMetrics in resources.\");\n        }\n      }\n      isInitMiui = true;\n      return ret;\n    }\n    if (mTmpMetricsField == null) {\n      return null;\n    }\n    try {\n      return (DisplayMetrics) mTmpMetricsField.get(resources);\n    } catch (Exception e) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/TimeUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\n\nimport java.sql.Date;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Objects;\n\n/**\n * Create by KunMinX at 2022/6/30\n */\npublic class TimeUtils {\n\n  public final static String YYYY_MM_DD_HH_MM_SS = \"yyyy-MM-dd HH:mm:ss\";\n  public final static String YY_MM_DD = \"yy-MM-dd\";\n  public final static String HH_MM_SS = \"HH:mm:ss\";\n\n  public static String getCurrentTime(String time_format) {\n    @SuppressLint(\"SimpleDateFormat\")\n    SimpleDateFormat formatter = new SimpleDateFormat(time_format);\n    Date curDate = new Date(System.currentTimeMillis());\n    return formatter.format(curDate);\n  }\n\n  public static String getTime(long time, String format) {\n    @SuppressLint(\"SimpleDateFormat\")\n    SimpleDateFormat formatter = new SimpleDateFormat(format);\n    Date curDate = new Date(time);\n    return formatter.format(curDate);\n  }\n\n  public static long getStringToDate(String dateString, String pattern) {\n    @SuppressLint(\"SimpleDateFormat\")\n    SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);\n    Date date = null;\n    try {\n      date = (Date) dateFormat.parse(dateString);\n    } catch (ParseException e) {\n      e.printStackTrace();\n    }\n    return Objects.requireNonNull(date).getTime();\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.widget.Toast;\n\n/**\n * Create by KunMinX at 2021/8/19\n */\npublic class ToastUtils {\n\n  public static void showLongToast(String text) {\n    Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_LONG).show();\n  }\n\n  public static void showShortToast(String text) {\n    Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_SHORT).show();\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/Utils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Application;\nimport android.content.Context;\n\nimport androidx.core.content.FileProvider;\n\nimport java.lang.reflect.InvocationTargetException;\n\n/**\n * <pre>\n *     blog  : http://blankj.com\n *     time  : 16/12/08\n *     desc  : utils about initialization\n * </pre>\n */\npublic final class Utils {\n\n  @SuppressLint(\"StaticFieldLeak\")\n  private static Application sApplication;\n\n  private Utils() {\n    throw new UnsupportedOperationException(\"u can't instantiate me...\");\n  }\n\n  public static void init(final Context context) {\n    if (context == null) {\n      init(getApplicationByReflect());\n      return;\n    }\n    init((Application) context.getApplicationContext());\n  }\n\n  public static void init(final Application app) {\n    if (sApplication == null) {\n      if (app == null) {\n        sApplication = getApplicationByReflect();\n      } else {\n        sApplication = app;\n      }\n    } else {\n      if (app != null && app.getClass() != sApplication.getClass()) {\n        sApplication = app;\n      }\n    }\n  }\n\n  public static Application getApp() {\n    if (sApplication != null) {\n      return sApplication;\n    }\n    Application app = getApplicationByReflect();\n    init(app);\n    return app;\n  }\n\n  private static Application getApplicationByReflect() {\n    try {\n      @SuppressLint(\"PrivateApi\")\n      Class<?> activityThread = Class.forName(\"android.app.ActivityThread\");\n      Object thread = activityThread.getMethod(\"currentActivityThread\").invoke(null);\n      Object app = activityThread.getMethod(\"getApplication\").invoke(thread);\n      if (app == null) {\n        throw new NullPointerException(\"u should init first\");\n      }\n      return (Application) app;\n    } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | InvocationTargetException e) {\n      e.printStackTrace();\n    }\n    throw new NullPointerException(\"u should init first\");\n  }\n\n  public static final class FileProvider4UtilCode extends FileProvider {\n    @Override\n    public boolean onCreate() {\n      Utils.init(getContext());\n      return true;\n    }\n  }\n}\n"
  },
  {
    "path": "architecture/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"SwipeMenuLayout\">\n        <attr name=\"swipeEnable\" format=\"boolean\"/>\n        <attr name=\"ios\" format=\"boolean\"/>\n        <attr name=\"leftSwipe\" format=\"boolean\"/>\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java",
    "content": "package com.kunminx.architecture;\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() {\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.\n\napply plugin: 'io.codearte.nexus-staging'\n\nbuildscript {\n\n    ext {\n        appTargetSdk = 33\n        appMinSdk = 15\n        appVersionCode = 7060000\n        appVersionName = \"7.6.0\"\n        lifecycleVersion = \"2.4.1\"\n        navigationVersion = \"2.4.2\"\n    }\n\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.1.3'\n        classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'\n        classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0'\n    }\n\n    tasks.withType(Javadoc) {\n        options.addStringOption('Xdoclint:none', '-quiet')\n        options.addStringOption('encoding', 'UTF-8')\n        options.addStringOption('charSet', 'UTF-8')\n        options.encoding = 'UTF-8'\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Thu Jun 30 19:13:32 CST 2022\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "android.enableJetifier=true\nandroid.injected.testOnly=false\nandroid.useAndroidX=true\norg.gradle.caching=true\norg.gradle.configureondemand=true\norg.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8\norg.gradle.parallel=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\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#      https://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\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\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\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\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='\"-Xmx64m\" \"-Xms64m\"'\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\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\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\" -a \"$nonstop\" = \"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 or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\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=`expr $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# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@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\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\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=\"-Xmx64m\" \"-Xms64m\"\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 execute\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 execute\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:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\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 %*\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": "keyvalue-dispatch/.gitignore",
    "content": "/build"
  },
  {
    "path": "keyvalue-dispatch/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\ngroup = 'com.github.KunMinX'\n\next {\n    PUBLISH_GROUP_ID = 'com.kunminx.arch'\n    PUBLISH_ARTIFACT_ID = 'keyvalue-dispatch'\n    PUBLISH_VERSION = appVersionName\n    VERSION_CODE = appVersionCode\n\n    ARTIFACT_DESCRIPTION = 'Jetpack Single Source of Truth Library for android'\n\n    POM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice'\n    POM_SCM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice/tree/master'\n    POM_SCM_CONNECTION='scm:git:github.com/KunMinX/Jetpack-MVI-Best-Practice.git'\n    POM_SCM_DEV_CONNECTION='scm:git:ssh://github.com/KunMinX/Jetpack-MVI-Best-Practice.git'\n\n    POM_DEVELOPER_ID='KunMinX'\n    POM_DEVELOPER_NAME='KunMinX'\n    POM_DEVELOPER_URL='https://github.com/KunMinX'\n    POM_DEVELOPER_EMAIL='kunminx@gmail.com'\n\n    LICENSE_NAME='The Apache Software License, Version 2.0'\n    LICENSE_URL='http://www.apache.org/licenses/LICENSE-2.0.txt'\n\n    uploadJavadocs = false\n    uploadSources = false\n}\n\napply from: \"${rootProject.projectDir}/publish-mavencentral.gradle\"\n\nandroid {\n    compileSdkVersion appTargetSdk\n    defaultConfig {\n        minSdkVersion appMinSdk\n        targetSdkVersion appTargetSdk\n        versionCode appVersionCode\n        versionName appVersionName\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation 'androidx.appcompat:appcompat:1.5.0'\n    testImplementation 'junit:junit:4.13.2'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.3'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'\n    implementation project(\":mvi-dispatch\")\n}"
  },
  {
    "path": "keyvalue-dispatch/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "keyvalue-dispatch/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\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"
  },
  {
    "path": "keyvalue-dispatch/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java",
    "content": "package com.kunminx.architecture;\n\nimport android.content.Context;\n\nimport androidx.test.platform.app.InstrumentationRegistry;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumented 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() {\n    // Context of the app under test.\n    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n    assertEquals(\"com.kunminx.keyvalue_dispatcher.test\", appContext.getPackageName());\n  }\n}"
  },
  {
    "path": "keyvalue-dispatch/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.kunminx.keyvalue_dispatch\">\n\n</manifest>"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/GlobalConfigs.java",
    "content": "package com.kunminx.architecture.domain.dispatch;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.fragment.app.Fragment;\nimport androidx.lifecycle.Observer;\n\nimport com.kunminx.architecture.domain.event.KeyValueMsg;\n\n/**\n * TODO tip: 此处基于 KeyValueDispatcher 提供全局配置的默认实现，\n *  开发者亦可继承 KeyValueDispatcher 或效仿本类实现局部或全局用 KeyValueStore\n *\n * Create by KunMinX at 2022/8/15\n */\npublic class GlobalConfigs {\n  private static final KeyValueDispatcher instance = new KeyValueDispatcher();\n\n  private GlobalConfigs() {\n  }\n\n  public static void output(@NonNull AppCompatActivity activity, @NonNull Observer<KeyValueMsg> observer) {\n    instance.output(activity, observer);\n  }\n\n  public static void output(@NonNull Fragment fragment, @NonNull Observer<KeyValueMsg> observer) {\n    instance.output(fragment, observer);\n  }\n\n  public static void put(String key, String value) {\n    instance.put(key, value);\n  }\n\n  public static void put(String key, Integer value) {\n    instance.put(key, value);\n  }\n\n  public static void put(String key, Long value) {\n    instance.put(key, value);\n  }\n\n  public static void put(String key, Float value) {\n    instance.put(key, value);\n  }\n\n  public static void put(String key, Boolean value) {\n    instance.put(key, value);\n  }\n\n  public static String getString(String key) {\n    return instance.getString(key);\n  }\n\n  public static Integer getInt(String key) {\n    return instance.getInt(key);\n  }\n\n  public static Long getLong(String key) {\n    return instance.getLong(key);\n  }\n\n  public static Float getFloat(String key) {\n    return instance.getFloat(key);\n  }\n\n  public static Boolean getBoolean(String key) {\n    return instance.getBoolean(key);\n  }\n}\n"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/KeyValueDispatcher.java",
    "content": "package com.kunminx.architecture.domain.dispatch;\n\nimport com.kunminx.architecture.domain.event.KeyValueMsg;\nimport com.kunminx.architecture.utils.SPUtils;\n\nimport java.util.HashMap;\n\n/**\n * TODO tip：通过内聚，消除 Key Value Getter Putter init 样板代码，\n *  开发者只需声明 Key 列表即可使用 get() put()，\n *  且顺带可从唯一出口 output 处响应配置变化完成 UI 刷新\n *\n * Create by KunMinX at 2022/8/15\n */\npublic class KeyValueDispatcher extends MviDispatcher<KeyValueMsg> {\n  private final HashMap<String, Object> keyValues = new HashMap<>();\n  private final SPUtils mSPUtils = SPUtils.getInstance(moduleName());\n\n  public String moduleName() {\n    return \"GlobalConfigs\";\n  }\n\n  @Override\n  protected void onHandle(KeyValueMsg intent) {\n    sendResult(intent);\n  }\n\n  public void put(String key, String value) {\n    keyValues.put(key, value);\n    mSPUtils.put(key, value);\n    input(new KeyValueMsg(key));\n  }\n\n  public void put(String key, Integer value) {\n    keyValues.put(key, value);\n    mSPUtils.put(key, value);\n    input(new KeyValueMsg(key));\n  }\n\n  public void put(String key, Long value) {\n    keyValues.put(key, value);\n    mSPUtils.put(key, value);\n    input(new KeyValueMsg(key));\n  }\n\n  public void put(String key, Float value) {\n    keyValues.put(key, value);\n    mSPUtils.put(key, value);\n    input(new KeyValueMsg(key));\n  }\n\n  public void put(String key, Boolean value) {\n    keyValues.put(key, value);\n    mSPUtils.put(key, value);\n    input(new KeyValueMsg(key));\n  }\n\n  public String getString(String key) {\n    if (keyValues.get(key) == null) {\n      keyValues.put(key, mSPUtils.getString(key, \"\"));\n    }\n    return (String) keyValues.get(key);\n  }\n\n  public Integer getInt(String key) {\n    if (keyValues.get(key) == null) {\n      keyValues.put(key, mSPUtils.getInt(key, 0));\n    }\n    return (Integer) keyValues.get(key);\n  }\n\n  public Long getLong(String key) {\n    if (keyValues.get(key) == null) {\n      keyValues.put(key, mSPUtils.getLong(key, 0));\n    }\n    return (Long) keyValues.get(key);\n  }\n\n  public Float getFloat(String key) {\n    if (keyValues.get(key) == null) {\n      keyValues.put(key, mSPUtils.getFloat(key, 0));\n    }\n    return (Float) keyValues.get(key);\n  }\n\n  public Boolean getBoolean(String key) {\n    if (keyValues.get(key) == null) {\n      keyValues.put(key, mSPUtils.getBoolean(key, false));\n    }\n    return (Boolean) keyValues.get(key);\n  }\n}\n"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/event/KeyValueMsg.java",
    "content": "package com.kunminx.architecture.domain.event;\n\n/**\n * Create by KunMinX at 2022/8/15\n */\npublic class KeyValueMsg {\n  public final String currentKey;\n\n  public KeyValueMsg(String currentKey) {\n    this.currentKey = currentKey;\n  }\n}\n"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/AppUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Application;\n\nimport java.lang.reflect.InvocationTargetException;\n\n/**\n * <pre>\n *     author:\n *                                      ___           ___           ___         ___\n *         _____                       /  /\\         /__/\\         /__/|       /  /\\\n *        /  /::\\                     /  /::\\        \\  \\:\\       |  |:|      /  /:/\n *       /  /:/\\:\\    ___     ___    /  /:/\\:\\        \\  \\:\\      |  |:|     /__/::\\\n *      /  /:/~/::\\  /__/\\   /  /\\  /  /:/~/::\\   _____\\__\\:\\   __|  |:|     \\__\\/\\:\\\n *     /__/:/ /:/\\:| \\  \\:\\ /  /:/ /__/:/ /:/\\:\\ /__/::::::::\\ /__/\\_|:|____    \\  \\:\\\n *     \\  \\:\\/:/~/:/  \\  \\:\\  /:/  \\  \\:\\/:/__\\/ \\  \\:\\~~\\~~\\/ \\  \\:\\/:::::/     \\__\\:\\\n *      \\  \\::/ /:/    \\  \\:\\/:/    \\  \\::/       \\  \\:\\  ~~~   \\  \\::/~~~~      /  /:/\n *       \\  \\:\\/:/      \\  \\::/      \\  \\:\\        \\  \\:\\        \\  \\:\\         /__/:/\n *        \\  \\::/        \\__\\/        \\  \\:\\        \\  \\:\\        \\  \\:\\        \\__\\/\n *         \\__\\/                       \\__\\/         \\__\\/         \\__\\/\n *     blog  : http://blankj.com\n *     time  : 16/12/08\n *     desc  : utils about initialization\n * </pre>\n */\npublic final class AppUtils {\n\n  @SuppressLint(\"StaticFieldLeak\")\n  private static Application sApplication;\n\n  private AppUtils() {\n    throw new UnsupportedOperationException(\"u can't instantiate me...\");\n  }\n\n  public static Application getApp() {\n    if (sApplication != null) {\n      return sApplication;\n    }\n    sApplication = getApplicationByReflect();\n    return sApplication;\n  }\n\n  private static Application getApplicationByReflect() {\n    try {\n      @SuppressLint(\"PrivateApi\")\n      Class<?> activityThread = Class.forName(\"android.app.ActivityThread\");\n      Object thread = activityThread.getMethod(\"currentActivityThread\").invoke(null);\n      Object app = activityThread.getMethod(\"getApplication\").invoke(thread);\n      if (app == null) {\n        throw new NullPointerException(\"u should init first\");\n      }\n      return (Application) app;\n    } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | InvocationTargetException e) {\n      e.printStackTrace();\n    }\n    throw new NullPointerException(\"u should init first\");\n  }\n}\n"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/SPUtils.java",
    "content": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.SharedPreferences;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/08/02\n *     desc  : utils about shared preference\n * </pre>\n */\n@SuppressLint(\"ApplySharedPref\")\npublic final class SPUtils {\n\n  private static final Map<String, SPUtils> SP_UTILS_MAP = new HashMap<>();\n  private final SharedPreferences sp;\n\n  private SPUtils(final String spName) {\n    sp = AppUtils.getApp().getSharedPreferences(spName, Context.MODE_PRIVATE);\n  }\n\n  private SPUtils(final String spName, final int mode) {\n    sp = AppUtils.getApp().getSharedPreferences(spName, mode);\n  }\n\n  /**\n   * Return the single {@link SPUtils} instance\n   *\n   * @return the single {@link SPUtils} instance\n   */\n  public static SPUtils getInstance() {\n    return getInstance(\"\", Context.MODE_PRIVATE);\n  }\n\n  /**\n   * Return the single {@link SPUtils} instance\n   *\n   * @param mode Operating mode.\n   * @return the single {@link SPUtils} instance\n   */\n  public static SPUtils getInstance(final int mode) {\n    return getInstance(\"\", mode);\n  }\n\n  /**\n   * Return the single {@link SPUtils} instance\n   *\n   * @param spName The name of sp.\n   * @return the single {@link SPUtils} instance\n   */\n  public static SPUtils getInstance(String spName) {\n    return getInstance(spName, Context.MODE_PRIVATE);\n  }\n\n  /**\n   * Return the single {@link SPUtils} instance\n   *\n   * @param spName The name of sp.\n   * @param mode   Operating mode.\n   * @return the single {@link SPUtils} instance\n   */\n  public static SPUtils getInstance(String spName, final int mode) {\n    if (isSpace(spName)) {\n      spName = \"spUtils\";\n    }\n    SPUtils spUtils = SP_UTILS_MAP.get(spName);\n    if (spUtils == null) {\n      synchronized (SPUtils.class) {\n        spUtils = SP_UTILS_MAP.get(spName);\n        if (spUtils == null) {\n          spUtils = new SPUtils(spName, mode);\n          SP_UTILS_MAP.put(spName, spUtils);\n        }\n      }\n    }\n    return spUtils;\n  }\n\n  private static boolean isSpace(final String s) {\n    if (s == null) {\n      return true;\n    }\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   * Put the string value in sp.\n   *\n   * @param key   The key of sp.\n   * @param value The value of sp.\n   */\n  public void put(@NonNull final String key, final String value) {\n    put(key, value, false);\n  }\n\n  /**\n   * Put the string value in sp.\n   *\n   * @param key      The key of sp.\n   * @param value    The value of sp.\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void put(@NonNull final String key, final String value, final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().putString(key, value).commit();\n    } else {\n      sp.edit().putString(key, value).apply();\n    }\n  }\n\n  /**\n   * Return the string value in sp.\n   *\n   * @param key The key of sp.\n   * @return the string value if sp exists or {@code \"\"} otherwise\n   */\n  public String getString(@NonNull final String key) {\n    return getString(key, \"\");\n  }\n\n  /**\n   * Return the string value in sp.\n   *\n   * @param key          The key of sp.\n   * @param defaultValue The default value if the sp doesn't exist.\n   * @return the string value if sp exists or {@code defaultValue} otherwise\n   */\n  public String getString(@NonNull final String key, final String defaultValue) {\n    return sp.getString(key, defaultValue);\n  }\n\n  /**\n   * Put the int value in sp.\n   *\n   * @param key   The key of sp.\n   * @param value The value of sp.\n   */\n  public void put(@NonNull final String key, final int value) {\n    put(key, value, false);\n  }\n\n  /**\n   * Put the int value in sp.\n   *\n   * @param key      The key of sp.\n   * @param value    The value of sp.\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void put(@NonNull final String key, final int value, final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().putInt(key, value).commit();\n    } else {\n      sp.edit().putInt(key, value).apply();\n    }\n  }\n\n  /**\n   * Return the int value in sp.\n   *\n   * @param key The key of sp.\n   * @return the int value if sp exists or {@code -1} otherwise\n   */\n  public int getInt(@NonNull final String key) {\n    return getInt(key, -1);\n  }\n\n  /**\n   * Return the int value in sp.\n   *\n   * @param key          The key of sp.\n   * @param defaultValue The default value if the sp doesn't exist.\n   * @return the int value if sp exists or {@code defaultValue} otherwise\n   */\n  public int getInt(@NonNull final String key, final int defaultValue) {\n    return sp.getInt(key, defaultValue);\n  }\n\n  /**\n   * Put the long value in sp.\n   *\n   * @param key   The key of sp.\n   * @param value The value of sp.\n   */\n  public void put(@NonNull final String key, final long value) {\n    put(key, value, false);\n  }\n\n  /**\n   * Put the long value in sp.\n   *\n   * @param key      The key of sp.\n   * @param value    The value of sp.\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void put(@NonNull final String key, final long value, final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().putLong(key, value).commit();\n    } else {\n      sp.edit().putLong(key, value).apply();\n    }\n  }\n\n  /**\n   * Return the long value in sp.\n   *\n   * @param key The key of sp.\n   * @return the long value if sp exists or {@code -1} otherwise\n   */\n  public long getLong(@NonNull final String key) {\n    return getLong(key, -1L);\n  }\n\n  /**\n   * Return the long value in sp.\n   *\n   * @param key          The key of sp.\n   * @param defaultValue The default value if the sp doesn't exist.\n   * @return the long value if sp exists or {@code defaultValue} otherwise\n   */\n  public long getLong(@NonNull final String key, final long defaultValue) {\n    return sp.getLong(key, defaultValue);\n  }\n\n  /**\n   * Put the float value in sp.\n   *\n   * @param key   The key of sp.\n   * @param value The value of sp.\n   */\n  public void put(@NonNull final String key, final float value) {\n    put(key, value, false);\n  }\n\n  /**\n   * Put the float value in sp.\n   *\n   * @param key      The key of sp.\n   * @param value    The value of sp.\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void put(@NonNull final String key, final float value, final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().putFloat(key, value).commit();\n    } else {\n      sp.edit().putFloat(key, value).apply();\n    }\n  }\n\n  /**\n   * Return the float value in sp.\n   *\n   * @param key The key of sp.\n   * @return the float value if sp exists or {@code -1f} otherwise\n   */\n  public float getFloat(@NonNull final String key) {\n    return getFloat(key, -1f);\n  }\n\n  /**\n   * Return the float value in sp.\n   *\n   * @param key          The key of sp.\n   * @param defaultValue The default value if the sp doesn't exist.\n   * @return the float value if sp exists or {@code defaultValue} otherwise\n   */\n  public float getFloat(@NonNull final String key, final float defaultValue) {\n    return sp.getFloat(key, defaultValue);\n  }\n\n  /**\n   * Put the boolean value in sp.\n   *\n   * @param key   The key of sp.\n   * @param value The value of sp.\n   */\n  public void put(@NonNull final String key, final boolean value) {\n    put(key, value, false);\n  }\n\n  /**\n   * Put the boolean value in sp.\n   *\n   * @param key      The key of sp.\n   * @param value    The value of sp.\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void put(@NonNull final String key, final boolean value, final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().putBoolean(key, value).commit();\n    } else {\n      sp.edit().putBoolean(key, value).apply();\n    }\n  }\n\n  /**\n   * Return the boolean value in sp.\n   *\n   * @param key The key of sp.\n   * @return the boolean value if sp exists or {@code false} otherwise\n   */\n  public boolean getBoolean(@NonNull final String key) {\n    return getBoolean(key, false);\n  }\n\n  /**\n   * Return the boolean value in sp.\n   *\n   * @param key          The key of sp.\n   * @param defaultValue The default value if the sp doesn't exist.\n   * @return the boolean value if sp exists or {@code defaultValue} otherwise\n   */\n  public boolean getBoolean(@NonNull final String key, final boolean defaultValue) {\n    return sp.getBoolean(key, defaultValue);\n  }\n\n  /**\n   * Put the set of string value in sp.\n   *\n   * @param key   The key of sp.\n   * @param value The value of sp.\n   */\n  public void put(@NonNull final String key, final Set<String> value) {\n    put(key, value, false);\n  }\n\n  /**\n   * Put the set of string value in sp.\n   *\n   * @param key      The key of sp.\n   * @param value    The value of sp.\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void put(@NonNull final String key,\n                  final Set<String> value,\n                  final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().putStringSet(key, value).commit();\n    } else {\n      sp.edit().putStringSet(key, value).apply();\n    }\n  }\n\n  /**\n   * Return the set of string value in sp.\n   *\n   * @param key The key of sp.\n   * @return the set of string value if sp exists\n   * or {@code Collections.<String>emptySet()} otherwise\n   */\n  public Set<String> getStringSet(@NonNull final String key) {\n    return getStringSet(key, Collections.<String>emptySet());\n  }\n\n  /**\n   * Return the set of string value in sp.\n   *\n   * @param key          The key of sp.\n   * @param defaultValue The default value if the sp doesn't exist.\n   * @return the set of string value if sp exists or {@code defaultValue} otherwise\n   */\n  public Set<String> getStringSet(@NonNull final String key,\n                                  final Set<String> defaultValue) {\n    return sp.getStringSet(key, defaultValue);\n  }\n\n  /**\n   * Return all values in sp.\n   *\n   * @return all values in sp\n   */\n  public Map<String, ?> getAll() {\n    return sp.getAll();\n  }\n\n  /**\n   * Return whether the sp contains the preference.\n   *\n   * @param key The key of sp.\n   * @return {@code true}: yes<br>{@code false}: no\n   */\n  public boolean contains(@NonNull final String key) {\n    return sp.contains(key);\n  }\n\n  /**\n   * Remove the preference in sp.\n   *\n   * @param key The key of sp.\n   */\n  public void remove(@NonNull final String key) {\n    remove(key, false);\n  }\n\n  /**\n   * Remove the preference in sp.\n   *\n   * @param key      The key of sp.\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void remove(@NonNull final String key, final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().remove(key).commit();\n    } else {\n      sp.edit().remove(key).apply();\n    }\n  }\n\n  /**\n   * Remove all preferences in sp.\n   */\n  public void clear() {\n    clear(false);\n  }\n\n  /**\n   * Remove all preferences in sp.\n   *\n   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},\n   *                 false to use {@link SharedPreferences.Editor#apply()}\n   */\n  public void clear(final boolean isCommit) {\n    if (isCommit) {\n      sp.edit().clear().commit();\n    } else {\n      sp.edit().clear().apply();\n    }\n  }\n}\n"
  },
  {
    "path": "keyvalue-dispatch/src/test/java/com/kunminx/architecture/ExampleUnitTest.java",
    "content": "package com.kunminx.architecture;\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() {\n    assertEquals(4, 2 + 2);\n  }\n}"
  },
  {
    "path": "mvi-dispatch/.gitignore",
    "content": "/build"
  },
  {
    "path": "mvi-dispatch/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\ngroup = 'com.github.KunMinX'\n\next {\n    PUBLISH_GROUP_ID = 'com.kunminx.arch'\n    PUBLISH_ARTIFACT_ID = 'mvi-dispatch'\n    PUBLISH_VERSION = appVersionName\n    VERSION_CODE = appVersionCode\n\n    ARTIFACT_DESCRIPTION = 'Jetpack Single Source of Truth Library for android'\n\n    POM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice'\n    POM_SCM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice/tree/master'\n    POM_SCM_CONNECTION='scm:git:github.com/KunMinX/Jetpack-MVI-Best-Practice.git'\n    POM_SCM_DEV_CONNECTION='scm:git:ssh://github.com/KunMinX/Jetpack-MVI-Best-Practice.git'\n\n    POM_DEVELOPER_ID='KunMinX'\n    POM_DEVELOPER_NAME='KunMinX'\n    POM_DEVELOPER_URL='https://github.com/KunMinX'\n    POM_DEVELOPER_EMAIL='kunminx@gmail.com'\n\n    LICENSE_NAME='The Apache Software License, Version 2.0'\n    LICENSE_URL='http://www.apache.org/licenses/LICENSE-2.0.txt'\n\n    uploadJavadocs = false\n    uploadSources = false\n}\n\napply from: \"${rootProject.projectDir}/publish-mavencentral.gradle\"\n\nandroid {\n    compileSdkVersion appTargetSdk\n    defaultConfig {\n        minSdkVersion appMinSdk\n        targetSdkVersion appTargetSdk\n        versionCode appVersionCode\n        versionName appVersionName\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles \"consumer-rules.pro\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation 'androidx.appcompat:appcompat:1.5.0'\n    testImplementation 'junit:junit:4.13.2'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.3'\n    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'\n}"
  },
  {
    "path": "mvi-dispatch/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "mvi-dispatch/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\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"
  },
  {
    "path": "mvi-dispatch/src/androidTest/java/com/kunminx/dispatch/ExampleInstrumentedTest.java",
    "content": "package com.kunminx.dispatch;\n\nimport android.content.Context;\n\nimport androidx.test.platform.app.InstrumentationRegistry;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumented 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() {\n    // Context of the app under test.\n    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n    assertEquals(\"com.kunminx.dispatch.test\", appContext.getPackageName());\n  }\n}"
  },
  {
    "path": "mvi-dispatch/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"com.kunminx.dispatch\">\n\n</manifest>"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/MviDispatcher.java",
    "content": "package com.kunminx.architecture.domain.dispatch;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.fragment.app.Fragment;\nimport androidx.lifecycle.DefaultLifecycleObserver;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.Observer;\nimport androidx.lifecycle.ViewModel;\n\nimport com.kunminx.architecture.domain.queue.FixedLengthList;\nimport com.kunminx.architecture.domain.result.OneTimeMessage;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * Create by KunMinX at 2022/7/3\n */\npublic class MviDispatcher<T> extends ViewModel implements DefaultLifecycleObserver {\n  private final static int DEFAULT_QUEUE_LENGTH = 10;\n  private final HashMap<Integer, LifecycleOwner> mOwner = new HashMap<>();\n  private final HashMap<Integer, LifecycleOwner> mFragmentOwner = new HashMap<>();\n  private final HashMap<Integer, Observer<T>> mObservers = new HashMap<>();\n  private final FixedLengthList<OneTimeMessage<T>> mResults = new FixedLengthList<>();\n\n  protected int initQueueMaxLength() {\n    return DEFAULT_QUEUE_LENGTH;\n  }\n\n  public final void output(@NonNull AppCompatActivity activity, @NonNull Observer<T> observer) {\n    activity.getLifecycle().addObserver(this);\n    Integer identityId = System.identityHashCode(activity);\n    outputTo(identityId, activity, observer);\n  }\n\n  public final void output(@NonNull Fragment fragment, @NonNull Observer<T> observer) {\n    fragment.getLifecycle().addObserver(this);\n    Integer identityId = System.identityHashCode(fragment);\n    this.mFragmentOwner.put(identityId, fragment);\n    outputTo(identityId, fragment.getViewLifecycleOwner(), observer);\n  }\n\n  private void outputTo(Integer identityId, LifecycleOwner owner, Observer<T> observer) {\n    this.mOwner.put(identityId, owner);\n    this.mObservers.put(identityId, observer);\n    for (OneTimeMessage<T> result : mResults) {\n      result.observe(owner, observer);\n    }\n  }\n\n  protected final void sendResult(@NonNull T intent) {\n    mResults.init(initQueueMaxLength(), mutableResult -> {\n      for (Map.Entry<Integer, Observer<T>> entry : mObservers.entrySet()) {\n        Observer<T> observer = entry.getValue();\n        mutableResult.removeObserver(observer);\n      }\n    });\n    boolean eventExist = false;\n    for (OneTimeMessage<T> result : mResults) {\n      int id1 = System.identityHashCode(result.get());\n      int id2 = System.identityHashCode(intent);\n      if (id1 == id2) {\n        eventExist = true;\n        break;\n      }\n    }\n    if (!eventExist) {\n      OneTimeMessage<T> result = new OneTimeMessage<>(intent);\n      for (Map.Entry<Integer, Observer<T>> entry : mObservers.entrySet()) {\n        Integer key = entry.getKey();\n        Observer<T> observer = entry.getValue();\n        LifecycleOwner owner = mOwner.get(key);\n        assert owner != null;\n        result.observe(owner, observer);\n      }\n      mResults.add(result);\n    }\n\n    OneTimeMessage<T> result = null;\n    for (OneTimeMessage<T> r : mResults) {\n      int id1 = System.identityHashCode(r.get());\n      int id2 = System.identityHashCode(intent);\n      if (id1 == id2) {\n        result = r;\n        break;\n      }\n    }\n    if (result != null) result.set(intent);\n  }\n\n  public final void input(T intent) {\n    onHandle(intent);\n  }\n\n  protected void onHandle(T intent) {\n  }\n\n  @Override\n  public void onDestroy(@NonNull LifecycleOwner owner) {\n    DefaultLifecycleObserver.super.onDestroy(owner);\n    boolean isFragment = owner instanceof Fragment;\n    for (Map.Entry<Integer, LifecycleOwner> entry : isFragment ? mFragmentOwner.entrySet() : mOwner.entrySet()) {\n      LifecycleOwner owner1 = entry.getValue();\n      if (owner1.equals(owner)) {\n        Integer key = entry.getKey();\n        mOwner.remove(key);\n        if (isFragment) mFragmentOwner.remove(key);\n        for (OneTimeMessage<T> mutableResult : mResults) {\n          mutableResult.removeObserver(Objects.requireNonNull(mObservers.get(key)));\n        }\n        mObservers.remove(key);\n        break;\n      }\n    }\n    if (mObservers.size() == 0) mResults.clear();\n  }\n}"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/domain/queue/FixedLengthList.java",
    "content": "package com.kunminx.architecture.domain.queue;\n\nimport java.util.LinkedList;\n\n/**\n * Create by KunMinX at 2022/7/5\n */\npublic class FixedLengthList<T> extends LinkedList<T> {\n  private int maxLength;\n  private boolean hasBeenInit;\n  private QueueCallback<T> queueCallback;\n\n  public final void init(int maxLength, QueueCallback<T> queueCallback) {\n    if (!hasBeenInit) {\n      this.maxLength = maxLength;\n      this.queueCallback = queueCallback;\n      hasBeenInit = true;\n    }\n  }\n\n  @Override\n  public boolean add(T t) {\n    if (size() + 1 > maxLength) {\n      T t1 = super.removeFirst();\n      if (queueCallback != null) queueCallback.onRemoveFirst(t1);\n    }\n    return super.add(t);\n  }\n\n  public interface QueueCallback<T> {\n    void onRemoveFirst(T t);\n  }\n}\n"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/OneTimeMessage.java",
    "content": "package com.kunminx.architecture.domain.result;\n\nimport static androidx.lifecycle.Lifecycle.State.DESTROYED;\nimport static androidx.lifecycle.Lifecycle.State.STARTED;\n\nimport androidx.annotation.MainThread;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.lifecycle.Lifecycle;\nimport androidx.lifecycle.LifecycleEventObserver;\nimport androidx.lifecycle.LifecycleOwner;\nimport androidx.lifecycle.Observer;\n\nimport java.util.Iterator;\nimport java.util.Map;\n\n/**\n * Create by KunMinX at 2022/8/16\n */\n\npublic class OneTimeMessage<T> {\n\n  private static final int START_VERSION = -1;\n  private static final Object NOT_SET = new Object();\n\n  private final SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>();\n  private int mActiveCount = 0;\n  private boolean mChangingActiveState;\n  private volatile Object mData;\n  private int mVersion;\n  private int mCurrentVersion = START_VERSION;\n\n  private boolean mDispatchingValue;\n  private boolean mDispatchInvalidated;\n\n  public OneTimeMessage(T value) {\n    mData = value;\n    mVersion = START_VERSION + 1;\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  private void considerNotify(ObserverWrapper observer) {\n    if (!observer.mActive) return;\n    if (!observer.shouldBeActive()) {\n      observer.activeStateChanged(false);\n      return;\n    }\n    if (observer.mLastVersion >= mVersion) return;\n    observer.mLastVersion = mVersion;\n    observer.mObserver.onChanged((T) mData);\n  }\n\n  void dispatchingValue(@Nullable ObserverWrapper initiator) {\n    if (mDispatchingValue) {\n      mDispatchInvalidated = true;\n      return;\n    }\n    mDispatchingValue = true;\n    do {\n      mDispatchInvalidated = false;\n      if (initiator != null) {\n        considerNotify(initiator);\n        initiator = null;\n      } else {\n        for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =\n             mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {\n          considerNotify(iterator.next().getValue());\n          if (mDispatchInvalidated) break;\n        }\n      }\n    } while (mDispatchInvalidated);\n    mDispatchingValue = false;\n  }\n\n  @MainThread\n  public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {\n    if (owner.getLifecycle().getCurrentState() == DESTROYED) return;\n    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);\n    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);\n    if (existing != null && !existing.isAttachedTo(owner))\n      throw new IllegalArgumentException(\"Cannot add the same observer with different lifecycles\");\n    if (existing != null) return;\n    mCurrentVersion = mVersion;\n    owner.getLifecycle().addObserver(wrapper);\n  }\n\n  @MainThread\n  public void removeObserver(@NonNull final Observer<? super T> observer) {\n    ObserverWrapper removed = mObservers.remove(observer);\n    if (removed == null) return;\n    removed.detachObserver();\n    removed.activeStateChanged(false);\n  }\n\n  @MainThread\n  public void set(T value) {\n    mVersion++;\n    mData = value;\n    dispatchingValue(null);\n  }\n\n  @SuppressWarnings(\"unchecked\")\n  @Nullable\n  public T get() {\n    Object data = mData;\n    if (data != NOT_SET) return (T) data;\n    return null;\n  }\n\n  @MainThread\n  void changeActiveCounter(int change) {\n    int previousActiveCount = mActiveCount;\n    mActiveCount += change;\n    if (mChangingActiveState) return;\n    mChangingActiveState = true;\n    try {\n      while (previousActiveCount != mActiveCount) previousActiveCount = mActiveCount;\n    } finally {\n      mChangingActiveState = false;\n    }\n  }\n\n  class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {\n    @NonNull\n    final LifecycleOwner mOwner;\n\n    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {\n      super(observer);\n      mOwner = owner;\n    }\n\n    @Override\n    boolean shouldBeActive() {\n      return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);\n    }\n\n    @Override\n    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {\n      Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();\n      if (currentState == DESTROYED) {\n        removeObserver(mObserver);\n        return;\n      }\n      Lifecycle.State prevState = null;\n      while (prevState != currentState) {\n        prevState = currentState;\n        activeStateChanged(shouldBeActive());\n        currentState = mOwner.getLifecycle().getCurrentState();\n      }\n    }\n\n    @Override\n    boolean isAttachedTo(LifecycleOwner owner) {\n      return mOwner == owner;\n    }\n\n    @Override\n    void detachObserver() {\n      mOwner.getLifecycle().removeObserver(this);\n    }\n  }\n\n  private abstract class ObserverWrapper {\n    final Observer<? super T> mObserver;\n    boolean mActive;\n    int mLastVersion = START_VERSION;\n\n    ObserverWrapper(Observer<? super T> observer) {\n      mObserver = observer;\n    }\n\n    abstract boolean shouldBeActive();\n\n    boolean isAttachedTo(LifecycleOwner owner) {\n      return false;\n    }\n\n    void detachObserver() {\n    }\n\n    void activeStateChanged(boolean newActive) {\n      if (newActive == mActive) return;\n      mActive = newActive;\n      changeActiveCounter(mActive ? 1 : -1);\n      if (mActive && mVersion > mCurrentVersion) dispatchingValue(this);\n    }\n  }\n}\n\n"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/SafeIterableMap.java",
    "content": "/*\n * Copyright 2018 The Android Open Source Project\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 com.kunminx.architecture.domain.result;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.RestrictTo;\n\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.WeakHashMap;\n\n/**\n * LinkedList, which pretends to be a map and supports modifications during iterations.\n * It is NOT thread safe.\n *\n * @param <K> Key type\n * @param <V> Value type\n * @hide\n */\n@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)\npublic class SafeIterableMap<K, V> implements Iterable<Map.Entry<K, V>> {\n\n  @SuppressWarnings(\"WeakerAccess\") /* synthetic access */\n          Entry<K, V> mStart;\n  private Entry<K, V> mEnd;\n  // using WeakHashMap over List<WeakReference>, so we don't have to manually remove\n  // WeakReferences that have null in them.\n  private WeakHashMap<SupportRemove<K, V>, Boolean> mIterators = new WeakHashMap<>();\n  private int mSize = 0;\n\n  protected Entry<K, V> get(K k) {\n    Entry<K, V> currentNode = mStart;\n    while (currentNode != null) {\n      if (currentNode.mKey.equals(k)) {\n        break;\n      }\n      currentNode = currentNode.mNext;\n    }\n    return currentNode;\n  }\n\n  /**\n   * If the specified key is not already associated\n   * with a value, associates it with the given value.\n   *\n   * @param key key with which the specified value is to be associated\n   * @param v   value to be associated with the specified key\n   * @return the previous value associated with the specified key,\n   * or {@code null} if there was no mapping for the key\n   */\n  public V putIfAbsent(@NonNull K key, @NonNull V v) {\n    Entry<K, V> entry = get(key);\n    if (entry != null) {\n      return entry.mValue;\n    }\n    put(key, v);\n    return null;\n  }\n\n  protected Entry<K, V> put(@NonNull K key, @NonNull V v) {\n    Entry<K, V> newEntry = new Entry<>(key, v);\n    mSize++;\n    if (mEnd == null) {\n      mStart = newEntry;\n      mEnd = mStart;\n      return newEntry;\n    }\n\n    mEnd.mNext = newEntry;\n    newEntry.mPrevious = mEnd;\n    mEnd = newEntry;\n    return newEntry;\n\n  }\n\n  /**\n   * Removes the mapping for a key from this map if it is present.\n   *\n   * @param key key whose mapping is to be removed from the map\n   * @return the previous value associated with the specified key,\n   * or {@code null} if there was no mapping for the key\n   */\n  public V remove(@NonNull K key) {\n    Entry<K, V> toRemove = get(key);\n    if (toRemove == null) {\n      return null;\n    }\n    mSize--;\n    if (!mIterators.isEmpty()) {\n      for (SupportRemove<K, V> iter : mIterators.keySet()) {\n        iter.supportRemove(toRemove);\n      }\n    }\n\n    if (toRemove.mPrevious != null) {\n      toRemove.mPrevious.mNext = toRemove.mNext;\n    } else {\n      mStart = toRemove.mNext;\n    }\n\n    if (toRemove.mNext != null) {\n      toRemove.mNext.mPrevious = toRemove.mPrevious;\n    } else {\n      mEnd = toRemove.mPrevious;\n    }\n\n    toRemove.mNext = null;\n    toRemove.mPrevious = null;\n    return toRemove.mValue;\n  }\n\n  /**\n   * @return the number of elements in this map\n   */\n  public int size() {\n    return mSize;\n  }\n\n  /**\n   * @return an ascending iterator, which doesn't include new elements added during an\n   * iteration.\n   */\n  @NonNull\n  @Override\n  public Iterator<Map.Entry<K, V>> iterator() {\n    ListIterator<K, V> iterator = new AscendingIterator<>(mStart, mEnd);\n    mIterators.put(iterator, false);\n    return iterator;\n  }\n\n  /**\n   * @return an descending iterator, which doesn't include new elements added during an\n   * iteration.\n   */\n  public Iterator<Map.Entry<K, V>> descendingIterator() {\n    DescendingIterator<K, V> iterator = new DescendingIterator<>(mEnd, mStart);\n    mIterators.put(iterator, false);\n    return iterator;\n  }\n\n  /**\n   * return an iterator with additions.\n   */\n  public IteratorWithAdditions iteratorWithAdditions() {\n    @SuppressWarnings(\"unchecked\")\n    IteratorWithAdditions iterator = new IteratorWithAdditions();\n    mIterators.put(iterator, false);\n    return iterator;\n  }\n\n  /**\n   * @return eldest added entry or null\n   */\n  public Map.Entry<K, V> eldest() {\n    return mStart;\n  }\n\n  /**\n   * @return newest added entry or null\n   */\n  public Map.Entry<K, V> newest() {\n    return mEnd;\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (obj == this) {\n      return true;\n    }\n    if (!(obj instanceof androidx.arch.core.internal.SafeIterableMap)) {\n      return false;\n    }\n    androidx.arch.core.internal.SafeIterableMap map = (androidx.arch.core.internal.SafeIterableMap) obj;\n    if (this.size() != map.size()) {\n      return false;\n    }\n    Iterator<Map.Entry<K, V>> iterator1 = iterator();\n    Iterator iterator2 = map.iterator();\n    while (iterator1.hasNext() && iterator2.hasNext()) {\n      Map.Entry<K, V> next1 = iterator1.next();\n      Object next2 = iterator2.next();\n      if ((next1 == null && next2 != null)\n              || (next1 != null && !next1.equals(next2))) {\n        return false;\n      }\n    }\n    return !iterator1.hasNext() && !iterator2.hasNext();\n  }\n\n  @Override\n  public int hashCode() {\n    int h = 0;\n    Iterator<Map.Entry<K, V>> i = iterator();\n    while (i.hasNext()) {\n      h += i.next().hashCode();\n    }\n    return h;\n  }\n\n  @Override\n  public String toString() {\n    StringBuilder builder = new StringBuilder();\n    builder.append(\"[\");\n    Iterator<Map.Entry<K, V>> iterator = iterator();\n    while (iterator.hasNext()) {\n      builder.append(iterator.next().toString());\n      if (iterator.hasNext()) {\n        builder.append(\", \");\n      }\n    }\n    builder.append(\"]\");\n    return builder.toString();\n  }\n\n  private abstract static class ListIterator<K, V> implements Iterator<Map.Entry<K, V>>,\n          SupportRemove<K, V> {\n    Entry<K, V> mExpectedEnd;\n    Entry<K, V> mNext;\n\n    ListIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {\n      this.mExpectedEnd = expectedEnd;\n      this.mNext = start;\n    }\n\n    @Override\n    public boolean hasNext() {\n      return mNext != null;\n    }\n\n    @SuppressWarnings(\"ReferenceEquality\")\n    @Override\n    public void supportRemove(@NonNull Entry<K, V> entry) {\n      if (mExpectedEnd == entry && entry == mNext) {\n        mNext = null;\n        mExpectedEnd = null;\n      }\n\n      if (mExpectedEnd == entry) {\n        mExpectedEnd = backward(mExpectedEnd);\n      }\n\n      if (mNext == entry) {\n        mNext = nextNode();\n      }\n    }\n\n    @SuppressWarnings(\"ReferenceEquality\")\n    private Entry<K, V> nextNode() {\n      if (mNext == mExpectedEnd || mExpectedEnd == null) {\n        return null;\n      }\n      return forward(mNext);\n    }\n\n    @Override\n    public Map.Entry<K, V> next() {\n      Map.Entry<K, V> result = mNext;\n      mNext = nextNode();\n      return result;\n    }\n\n    abstract Entry<K, V> forward(Entry<K, V> entry);\n\n    abstract Entry<K, V> backward(Entry<K, V> entry);\n  }\n\n  static class AscendingIterator<K, V> extends ListIterator<K, V> {\n    AscendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {\n      super(start, expectedEnd);\n    }\n\n    @Override\n    Entry<K, V> forward(Entry<K, V> entry) {\n      return entry.mNext;\n    }\n\n    @Override\n    Entry<K, V> backward(Entry<K, V> entry) {\n      return entry.mPrevious;\n    }\n  }\n\n  private static class DescendingIterator<K, V> extends ListIterator<K, V> {\n\n    DescendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {\n      super(start, expectedEnd);\n    }\n\n    @Override\n    Entry<K, V> forward(Entry<K, V> entry) {\n      return entry.mPrevious;\n    }\n\n    @Override\n    Entry<K, V> backward(Entry<K, V> entry) {\n      return entry.mNext;\n    }\n  }\n\n  private class IteratorWithAdditions implements Iterator<Map.Entry<K, V>>, SupportRemove<K, V> {\n    private Entry<K, V> mCurrent;\n    private boolean mBeforeStart = true;\n\n    IteratorWithAdditions() {\n    }\n\n    @SuppressWarnings(\"ReferenceEquality\")\n    @Override\n    public void supportRemove(@NonNull Entry<K, V> entry) {\n      if (entry == mCurrent) {\n        mCurrent = mCurrent.mPrevious;\n        mBeforeStart = mCurrent == null;\n      }\n    }\n\n    @Override\n    public boolean hasNext() {\n      if (mBeforeStart) {\n        return mStart != null;\n      }\n      return mCurrent != null && mCurrent.mNext != null;\n    }\n\n    @Override\n    public Map.Entry<K, V> next() {\n      if (mBeforeStart) {\n        mBeforeStart = false;\n        mCurrent = mStart;\n      } else {\n        mCurrent = mCurrent != null ? mCurrent.mNext : null;\n      }\n      return mCurrent;\n    }\n  }\n\n  interface SupportRemove<K, V> {\n    void supportRemove(@NonNull Entry<K, V> entry);\n  }\n\n  static class Entry<K, V> implements Map.Entry<K, V> {\n    @NonNull\n    final K mKey;\n    @NonNull\n    final V mValue;\n    Entry<K, V> mNext;\n    Entry<K, V> mPrevious;\n\n    Entry(@NonNull K key, @NonNull V value) {\n      mKey = key;\n      this.mValue = value;\n    }\n\n    @NonNull\n    @Override\n    public K getKey() {\n      return mKey;\n    }\n\n    @NonNull\n    @Override\n    public V getValue() {\n      return mValue;\n    }\n\n    @Override\n    public V setValue(V value) {\n      throw new UnsupportedOperationException(\"An entry modification is not supported\");\n    }\n\n    @Override\n    public String toString() {\n      return mKey + \"=\" + mValue;\n    }\n\n    @SuppressWarnings(\"ReferenceEquality\")\n    @Override\n    public boolean equals(Object obj) {\n      if (obj == this) {\n        return true;\n      }\n      if (!(obj instanceof Entry)) {\n        return false;\n      }\n      Entry entry = (Entry) obj;\n      return mKey.equals(entry.mKey) && mValue.equals(entry.mValue);\n    }\n\n    @Override\n    public int hashCode() {\n      return mKey.hashCode() ^ mValue.hashCode();\n    }\n  }\n}\n"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/ui/scope/ApplicationInstance.java",
    "content": "package com.kunminx.architecture.ui.scope;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModelStore;\nimport androidx.lifecycle.ViewModelStoreOwner;\n\n/**\n * Create by KunMinX at 2022/7/6\n */\npublic class ApplicationInstance implements ViewModelStoreOwner {\n  private final static ApplicationInstance sInstance = new ApplicationInstance();\n  private ViewModelStore mAppViewModelStore;\n\n  private ApplicationInstance() {\n  }\n\n  public static ApplicationInstance getInstance() {\n    return sInstance;\n  }\n\n  @NonNull\n  @Override\n  public ViewModelStore getViewModelStore() {\n    if (mAppViewModelStore == null) mAppViewModelStore = new ViewModelStore();\n    return mAppViewModelStore;\n  }\n}\n"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/ui/scope/ViewModelScope.java",
    "content": "package com.kunminx.architecture.ui.scope;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.fragment.app.Fragment;\nimport androidx.lifecycle.ViewModel;\nimport androidx.lifecycle.ViewModelProvider;\n\n/**\n * Create by KunMinX at 2022/7/6\n */\npublic class ViewModelScope {\n  private ViewModelProvider mFragmentProvider;\n  private ViewModelProvider mActivityProvider;\n  private ViewModelProvider mApplicationProvider;\n\n  public <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Fragment fragment, @NonNull Class<T> modelClass) {\n    if (mFragmentProvider == null) mFragmentProvider = new ViewModelProvider(fragment);\n    return mFragmentProvider.get(modelClass);\n  }\n\n  public <T extends ViewModel> T getActivityScopeViewModel(@NonNull AppCompatActivity activity, @NonNull Class<T> modelClass) {\n    if (mActivityProvider == null) mActivityProvider = new ViewModelProvider(activity);\n    return mActivityProvider.get(modelClass);\n  }\n\n  public <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {\n    if (mApplicationProvider == null)\n      mApplicationProvider = new ViewModelProvider(ApplicationInstance.getInstance());\n    return mApplicationProvider.get(modelClass);\n  }\n}\n"
  },
  {
    "path": "mvi-dispatch/src/test/java/com/kunminx/dispatch/ExampleUnitTest.java",
    "content": "package com.kunminx.dispatch;\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() {\n    assertEquals(4, 2 + 2);\n  }\n}"
  },
  {
    "path": "publish-mavencentral.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        from android.sourceSets.main.java.srcDirs\n    } else {\n        from sourceSets.main.java.srcDirs\n    }\n}\n\nartifacts {\n    if (uploadJavadocs) {\n        archives javadocJar\n    }\n    if (uploadSources) {\n        archives sourcesJar\n    }\n}\n\ngroup = PUBLISH_GROUP_ID\nversion = PUBLISH_VERSION\n\n// leave them empty to allow compilation\next[\"signing.keyId\"] = '' // GPG Id\next[\"signing.password\"] = '' // gpg key pass\next[\"signing.secretKeyRingFile\"] = '' // location of key :|\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    p.load(new FileInputStream(secretPropsFile))\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId group\n            artifactId PUBLISH_ARTIFACT_ID\n            version PUBLISH_VERSION\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n\n            pom {\n                name = PUBLISH_ARTIFACT_ID\n                description = ARTIFACT_DESCRIPTION\n                url = POM_URL\n                licenses {\n                    license {\n                        name = LICENSE_NAME\n                        url = LICENSE_URL\n                    }\n                }\n                developers {\n                    developer {\n                        id = POM_DEVELOPER_ID\n                        name = POM_DEVELOPER_NAME\n                        email = POM_DEVELOPER_EMAIL\n                    }\n                }\n                scm {\n                    connection = POM_SCM_CONNECTION\n                    developerConnection = POM_SCM_DEV_CONNECTION\n                    url = POM_SCM_URL\n                }\n                withXml {\n                    final dependenciesNode = asNode().appendNode('dependencies')\n\n                    ext.addDependency = { Dependency dep, String scope ->\n                        if (dep.group == null || dep.version == null || dep.name == null || dep.name == \"unspecified\")\n                            return // invalid dependencies should be ignored\n\n                        final dependencyNode = dependenciesNode.appendNode('dependency')\n                        dependencyNode.appendNode('artifactId', dep.name)\n\n                        if (dep.version == 'unspecified') {\n                            dependencyNode.appendNode('groupId', project.ext.pomGroupID)\n                            dependencyNode.appendNode('version', project.ext.pomVersion)\n                            System.println(\"${project.ext.pomGroupID} ${dep.name} ${project.ext.pomVersion}\")\n                        } else {\n                            dependencyNode.appendNode('groupId', dep.group)\n                            dependencyNode.appendNode('version', dep.version)\n                            System.println(\"${dep.group} ${dep.name} ${dep.version}\")\n                        }\n\n                        dependencyNode.appendNode('scope', scope)\n                        // Some dependencies may have types, such as aar, that should be mentioned in the POM file\n                        def artifactsList = dep.properties['artifacts']\n                        if (artifactsList != null && artifactsList.size() > 0) {\n                            final artifact = artifactsList[0]\n                            dependencyNode.appendNode('type', artifact.getType())\n                        }\n\n                        if (!dep.transitive) {\n                            // In case of non transitive dependency, all its dependencies should be force excluded from them POM file\n                            final exclusionNode = dependencyNode.appendNode('exclusions').appendNode('exclusion')\n                            exclusionNode.appendNode('groupId', '*')\n                            exclusionNode.appendNode('artifactId', '*')\n                        } else if (!dep.properties.excludeRules.empty) {\n                            // For transitive with exclusions, all exclude rules should be added to the POM file\n                            final exclusions = dependencyNode.appendNode('exclusions')\n                            dep.properties.excludeRules.each { ExcludeRule rule ->\n                                final exclusionNode = exclusions.appendNode('exclusion')\n                                exclusionNode.appendNode('groupId', rule.group ?: '*')\n                                exclusionNode.appendNode('artifactId', rule.module ?: '*')\n                            }\n                        }\n                    }\n\n                    configurations.api.getDependencies().each { dep -> addDependency(dep, \"compile\") }\n                    configurations.implementation.getDependencies().each { dep -> addDependency(dep, \"runtime\") }\n                }\n            }\n        }\n    }\n    repositories {\n        maven {\n            name = \"SonaType\"\n\n            def releasesRepoUrl = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            def snapshotsRepoUrl = \"https://s01.oss.sonatype.org/content/repositories/snapshots/\"\n            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl\n\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nnexusStaging {\n    packageGroup = PUBLISH_GROUP_ID\n    stagingProfileId = sonatypeStagingProfileId\n    username = ossrhUsername\n    password = ossrhPassword\n    serverUrl = \"https://s01.oss.sonatype.org/service/local/\"\n}\n\nsigning {\n    sign publishing.publications\n}\n\n\ntask assembleAndPublishLocally(dependsOn: ['assembleRelease']) {\n    finalizedBy('publishToMavenLocal')\n}\ntask assembleAndPublish(dependsOn: ['assembleRelease']) {\n    finalizedBy('publish')\n}\n\nconfigure(assembleAndPublishLocally) {\n    group = 'Publishing'\n    description = 'Publish the output locally'\n}\nconfigure(assembleAndPublish) {\n    group = 'Publishing'\n    description = 'Publish the output to mavenCentral'\n}\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = \"PureNote\"\ninclude ':app'\ninclude ':architecture'\ninclude ':mvi-dispatch'\ninclude ':keyvalue-dispatch'\n"
  }
]