Full Code of KunMinX/MVI-Dispatcher for AI

main 11bf442180be cached
103 files
190.9 KB
52.4k tokens
414 symbols
1 requests
Download .txt
Showing preview only (229K chars total). Download the full file or copy to clipboard to get everything.
Repository: KunMinX/MVI-Dispatcher
Branch: main
Commit: 11bf442180be
Files: 103
Total size: 190.9 KB

Directory structure:
gitextract_ntzdf0fx/

├── .gitignore
├── LICENSE
├── README.md
├── README_EN.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── purenote/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── purenote/
│       │   │               ├── App.java
│       │   │               ├── data/
│       │   │               │   ├── bean/
│       │   │               │   │   ├── Note.java
│       │   │               │   │   └── Weather.java
│       │   │               │   ├── config/
│       │   │               │   │   └── Key.java
│       │   │               │   └── repo/
│       │   │               │       ├── DataRepository.java
│       │   │               │       ├── NoteDao.java
│       │   │               │       ├── NoteDataBase.java
│       │   │               │       └── WeatherService.java
│       │   │               ├── domain/
│       │   │               │   ├── intent/
│       │   │               │   │   ├── _Api.java
│       │   │               │   │   ├── _ComplexIntent.java
│       │   │               │   │   ├── _Messages.java
│       │   │               │   │   └── _NoteIntent.java
│       │   │               │   ├── message/
│       │   │               │   │   └── PageMessenger.java
│       │   │               │   └── request/
│       │   │               │       ├── ComplexRequester.java
│       │   │               │       ├── NoteRequester.java
│       │   │               │       └── WeatherRequester.java
│       │   │               └── ui/
│       │   │                   ├── adapter/
│       │   │                   │   └── NoteAdapter.java
│       │   │                   └── page/
│       │   │                       ├── EditorFragment.java
│       │   │                       ├── ListFragment.java
│       │   │                       ├── MainActivity.java
│       │   │                       └── SettingFragment.java
│       │   └── res/
│       │       ├── anim/
│       │       │   ├── x_fragment_enter.xml
│       │       │   ├── x_fragment_exit.xml
│       │       │   ├── x_fragment_pop_enter.xml
│       │       │   └── x_fragment_pop_exit.xml
│       │       ├── drawable/
│       │       │   ├── ic_baseline_add.xml
│       │       │   └── ic_baseline_arrow_back.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── adapter_note_list.xml
│       │       │   ├── fragment_editor.xml
│       │       │   ├── fragment_list.xml
│       │       │   └── fragment_settings.xml
│       │       ├── navigation/
│       │       │   └── nav_graph.xml
│       │       └── values/
│       │           ├── colors.xml
│       │           ├── dimens.xml
│       │           ├── strings.xml
│       │           └── themes.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── purenote/
│                           └── ExampleUnitTest.java
├── architecture/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── architecture/
│       │   │               ├── data/
│       │   │               │   └── response/
│       │   │               │       ├── AsyncTask.java
│       │   │               │       ├── DataResult.java
│       │   │               │       ├── ResponseStatus.java
│       │   │               │       └── ResultSource.java
│       │   │               ├── ui/
│       │   │               │   ├── adapter/
│       │   │               │   │   └── BaseBindingAdapter.java
│       │   │               │   ├── bind/
│       │   │               │   │   ├── ClickProxy.java
│       │   │               │   │   └── CommonBindingAdapter.java
│       │   │               │   ├── page/
│       │   │               │   │   ├── BaseActivity.java
│       │   │               │   │   ├── BaseFragment.java
│       │   │               │   │   └── StateHolder.java
│       │   │               │   └── view/
│       │   │               │       └── SwipeMenuLayout.java
│       │   │               └── utils/
│       │   │                   ├── AdaptScreenUtils.java
│       │   │                   ├── TimeUtils.java
│       │   │                   ├── ToastUtils.java
│       │   │                   └── Utils.java
│       │   └── res/
│       │       └── values/
│       │           └── attrs.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── architecture/
│                           └── ExampleUnitTest.java
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── keyvalue-dispatch/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   ├── domain/
│       │                   │   ├── dispatch/
│       │                   │   │   ├── GlobalConfigs.java
│       │                   │   │   └── KeyValueDispatcher.java
│       │                   │   └── event/
│       │                   │       └── KeyValueMsg.java
│       │                   └── utils/
│       │                       ├── AppUtils.java
│       │                       └── SPUtils.java
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── architecture/
│                           └── ExampleUnitTest.java
├── mvi-dispatch/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── dispatch/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   ├── domain/
│       │                   │   ├── dispatch/
│       │                   │   │   └── MviDispatcher.java
│       │                   │   ├── queue/
│       │                   │   │   └── FixedLengthList.java
│       │                   │   └── result/
│       │                   │       ├── OneTimeMessage.java
│       │                   │       └── SafeIterableMap.java
│       │                   └── ui/
│       │                       └── scope/
│       │                           ├── ApplicationInstance.java
│       │                           └── ViewModelScope.java
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── dispatch/
│                           └── ExampleUnitTest.java
├── publish-mavencentral.gradle
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
#  Uncomment the following line in case you need and you don't have the release build type files in your app
# release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# Google Services (e.g. APIs or Firebase)
# google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

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

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

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


================================================
FILE: README.md
================================================
![](https://s2.loli.net/2023/09/14/POldYf3s7EQ9B6c.jpg)

 

 

### [🌏 English README](https://github.com/KunMinX/MVI-Dispatcher/blob/main/README_EN.md)

研发故事:[《解决 MVI 架构实战痛点》](https://juejin.cn/post/7134594010642907149)

 

# 背景

响应式编程便于单元测试,但其自身存在漏洞,MVI 即是来消除漏洞,

MVI 有一定门槛,实现较繁琐,且存在性能等问题,难免同事撂挑子不干,一夜回到解放前,

综合来说,MVI 适合与 Jetpack Compose 搭配实现 “现代化的开发模式”,

反之如追求 “低成本、复用、稳定”,可通过遵循 “单一职责原则” 从源头把问题消除。

MVI-Dispatcher 应运而生。

 

 

|                          收藏或置顶                          |                           顺滑转场                           |                           删除笔记                           |
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
| ![](https://images.xiaozhuanlan.com/photo/2022/3555d17b46e04054154916d00f1214f8.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/d20a18e90cda8aa1f7d6977dca7b7135.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/5786c16f17612661b0b490dd40e78608.gif) |

 



# 项目简介

笔者长期专注 “业务架构” 模式,致力消除敏捷开发过程中 “不可预期问题”。

在本案例中,我将为您展示,MVI-Dispatcher 是如何将原本 "繁杂易出错" 消息分发流程,通过 **寥寥几行代码** 轻而易举完成。

```Groovy
implementation 'com.kunminx.arch:mvi-dispatch:7.6.0'

//可选分支,简便安全完成 Config 读写
implementation 'com.kunminx.arch:keyvalue-dispatch:7.6.0'
```

 

一个完备的 “领域层” 消息分发组件,至少应满足以下几点:

1.内含消息队列,可暂存 “发送过且未消费” 的消息,

2.页面不可见时,队列暂存期间发来的消息,页面重新可见时,自动消费未消费的消息。

MVI-Dispatcher 应运而生,

 

此外,MVI-Dispatcher 改进和优化还包括:

> 1.**可彻底消除 mutable 样板代码**,一行不必写
>
> 2.**可杜绝团队新手滥用** mutable.setValue( ) 于 Activity/Fragment
>
> 3.开发者只需关注 input、output 二处,**从唯一入口 input 发起请求,并于唯一出口 output 观察**
>
> 4.团队新手在不熟 LiveData、UnPeekLiveData、SharedFlow、mutable、MVI 情况下,仅根据 MVI-Dispatcher 简明易懂 input-output 设计亦可自动实现 “响应式” 开发
>
> 5.可无缝整合至 Jetpack MVVM 等模式项目

 

![](https://s2.loli.net/2023/05/18/mn2zeTJdqrlNw6P.jpg)

 

MVI-Dispatcher 以 “备忘录场景” 为例,提供完成一款 “记事本软件” 最少必要源码实现,

故通过该示例,您还可获得内容包括:

> 1.整洁代码风格 & 标准命名规范
>
> 2.对 “响应式编程” 知识点深入理解 & 正确使用
>
> 3.AndroidX 和 Material Design 全面使用
>
> 4.ConstraintLayout 约束布局使用
>
> 5.**十六进制复合状态管理最佳实践**
>
> 6.优秀用户体验 & 交互设计

 

# Thanks to

感谢小伙伴浓咖啡、苏旗的测试反馈

[AndroidX](https://developer.android.google.cn/jetpack/androidx)

[Jetpack](https://developer.android.google.cn/jetpack/)

[SwipeDelMenuLayout](https://github.com/mcxtzhang/SwipeDelMenuLayout)

项目中图标素材来自 [iconfinder](https://www.iconfinder.com/) 提供 **免费授权图片**。

 

# Copyright

本项目场景案例及 MVI-Dispatcher 框架,均属本人独立原创设计,本人对此享有最终解释权。

任何个人或组织,未经与作者本人当面沟通许可,不得将本项目代码设计及本人对 "响应式编程漏洞和 MVI" 独家理解用于 "**打包贩卖、出书、卖课**" 等商业用途。

如需引用借鉴 “本项目框架设计背景及思路” 写作发行,请注明**链接出处**。

 

# License

```
Copyright 2019-present KunMinX

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

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

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

================================================
FILE: README_EN.md
================================================
**Development story**:

[《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)

 

Reactive programming is conducive to unit testing, but it has its own flaws. MVI is designed to eliminate these flaws.

MVI 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.

Overall, MVI is suitable for implementing a "modern development model" in combination with Jetpack Compose.

On 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".

In response to this, MVI-Dispatcher was born.

 

|                      Collect or topped                       |                      Smooth transition                       |                         Delete notes                         |
| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: |
| ![](https://images.xiaozhuanlan.com/photo/2022/3555d17b46e04054154916d00f1214f8.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/d20a18e90cda8aa1f7d6977dca7b7135.gif) | ![](https://images.xiaozhuanlan.com/photo/2022/5786c16f17612661b0b490dd40e78608.gif) |

 

# Project Description

The author has long focused on the "business architecture" pattern and is committed to eliminating unexpected issues in the agile development process.

In 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.

```Groovy
implementation 'com.kunminx.arch:mvi-dispatch:7.6.0'
```

 

A complete "domain layer" message distribution component should at least meet the following requirements:

1. It contains a message queue that can store messages that have been sent but not consumed.
2. When the page is not visible, any messages sent during the queue storage period will be automatically consumed when the page becomes visible again.

MVI-Dispatcher was born to meet these needs.

 

Furthermore, the improvements and optimizations of MVI-Dispatcher include:

> 1. It can completely eliminate mutable boilerplate code, without writing a single line.
> 2. It can prevent new team members from misusing mutable.setValue() in Activity/Fragment.
> 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.
> 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.
> 5. It can be seamlessly integrated into Jetpack MVVM and other pattern projects.

 

![](https://s2.loli.net/2023/05/18/JXHyColB2Knxmkq.jpg)

 

MVI-Dispatcher provide the minimum necessary source code implementation to complete a notepad software.

Therefore, through this example, you can also obtain content including:

> 1.Clean code style & standard naming conventions
>
> 2.In-depth understanding of “Reactive programming” knowledge points & correct use
>
> 3.Full use of AndroidX and Material Design
>
> 4.ConstraintLayout Constraint Layout Best Practices
>
> 5.Best Practices for Hex Compound State Management
>
> 6.Excellent User Experience & Interaction Design


 

# Thanks to

[AndroidX](https://developer.android.google.cn/jetpack/androidx)

[Jetpack](https://developer.android.google.cn/jetpack/)

[SwipeDelMenuLayout](https://github.com/mcxtzhang/SwipeDelMenuLayout)

The icon material in the project comes from [iconfinder](https://www.iconfinder.com/) provided free licensed images.

 

# Copyright

The scene cases and MVI dispatcher framework of this project are all my independent original designs, and I have the final right to interpret them.

If 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.

 

# License

```
Copyright 2019-present KunMinX

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

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

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

================================================
FILE: app/.gitignore
================================================
/build

================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'

android {
    compileSdkVersion appTargetSdk
    defaultConfig {
        minSdkVersion 23
        targetSdkVersion appTargetSdk
        versionCode appVersionCode
        versionName appVersionName
        applicationId "com.kunminx.purenote"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    buildFeatures {
        dataBinding true
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])

    implementation project(":architecture")
    implementation project(':mvi-dispatch')
    implementation project(":keyvalue-dispatch")

    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.3"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"

    implementation "androidx.appcompat:appcompat:1.5.0"
    implementation "androidx.constraintlayout:constraintlayout:2.1.4"
    implementation "com.google.android.material:material:1.6.1"
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation "androidx.navigation:navigation-runtime:2.5.1"

    implementation 'com.github.KunMinX:Smooth-Navigation:v4.0.0'
    implementation 'com.github.KunMinX.Strict-DataBinding:strict_databinding:5.6.0'
    implementation 'com.github.KunMinX.Strict-DataBinding:binding_state:5.6.0'
    implementation 'com.github.KunMinX.Strict-DataBinding:binding_recyclerview:5.6.0'
    implementation 'com.github.KunMinX.SealedClass4Java:sealed-annotation:1.4.0-beta'
    annotationProcessor 'com.github.KunMinX.SealedClass4Java:sealed-compiler:1.4.0-beta'

    implementation "androidx.room:room-runtime:2.4.3"
    annotationProcessor "androidx.room:room-compiler:2.4.3"

    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.21'

    implementation "com.google.code.gson:gson:2.9.1"
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
    implementation "com.squareup.okhttp3:logging-interceptor:4.10.0"
    implementation "com.squareup.okhttp3:okhttp:4.10.0"
}

================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: app/src/androidTest/java/com/kunminx/purenote/ExampleInstrumentedTest.java
================================================
package com.kunminx.purenote;

import static org.junit.Assert.assertEquals;

import android.content.Context;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
  @Test
  public void useAppContext() {
    // Context of the app under test.
    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
    assertEquals("com.kunminx.purenote", appContext.getPackageName());
  }
}

================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.kunminx.purenote">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:name=".App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.PureNote">
        <activity
            android:name=".ui.page.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

================================================
FILE: app/src/main/java/com/kunminx/purenote/App.java
================================================
package com.kunminx.purenote;

import android.app.Application;

import com.kunminx.architecture.utils.Utils;

/**
 * Create by KunMinX at 2022/7/3
 */
public class App extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    Utils.init(this);
  }
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/data/bean/Note.java
================================================
package com.kunminx.purenote.data.bean;

import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;

import com.kunminx.architecture.utils.TimeUtils;
import com.kunminx.purenote.R;

/**
 * Created by KunMinX on 2015/7/31.
 */
@Entity
public class Note implements Parcelable {
  public final static int TYPE_TOPPING = 0x0001;
  public final static int TYPE_MARKED = 0x0002;

  @PrimaryKey
  @NonNull
  private String id = "";

  private String title = "";

  private String content = "";

  @ColumnInfo(name = "create_time")
  private long createTime;

  @ColumnInfo(name = "modify_time")
  private long modifyTime;

  private int type;

  @Ignore
  public String getCreateDate() {
    return TimeUtils.getTime(createTime, TimeUtils.YYYY_MM_DD_HH_MM_SS);
  }

  @Ignore
  public String getModifyDate() {
    return TimeUtils.getTime(modifyTime, TimeUtils.YYYY_MM_DD_HH_MM_SS);
  }

  @Ignore
  public boolean isMarked() {
    return (type & TYPE_MARKED) != 0;
  }

  @Ignore
  public boolean isTopping() {
    return (type & TYPE_TOPPING) != 0;
  }

  @Ignore
  public void toggleType(int param) {
    if ((type & param) != 0) {
      type = type & ~param;
    } else {
      type = type | param;
    }
  }

  @Ignore
  public int markIcon() {
    return isMarked() ? R.drawable.icon_star : R.drawable.icon_star_board;
  }

  @Ignore
  public Note() {
  }

  public Note(@NonNull String id, String title, String content, long createTime, long modifyTime, int type) {
    this.id = id;
    this.title = title;
    this.content = content;
    this.createTime = createTime;
    this.modifyTime = modifyTime;
    this.type = type;
  }

  @NonNull
  public String getId() {
    return id;
  }
  public String getTitle() {
    return title;
  }
  public String getContent() {
    return content;
  }
  public long getCreateTime() {
    return createTime;
  }
  public long getModifyTime() {
    return modifyTime;
  }
  public int getType() {
    return type;
  }

  protected Note(Parcel in) {
    id = in.readString();
    title = in.readString();
    content = in.readString();
    createTime = in.readLong();
    modifyTime = in.readLong();
    type = in.readInt();
  }

  public static final Creator<Note> CREATOR = new Creator<Note>() {
    @Override
    public Note createFromParcel(Parcel in) {
      return new Note(in);
    }

    @Override
    public Note[] newArray(int size) {
      return new Note[size];
    }
  };

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(id);
    dest.writeString(title);
    dest.writeString(content);
    dest.writeLong(createTime);
    dest.writeLong(modifyTime);
    dest.writeInt(type);
  }
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/data/bean/Weather.java
================================================
package com.kunminx.purenote.data.bean;

import java.util.List;
/**
 * Create by KunMinX at 2022/8/24
 */
public class Weather {
  private String status;
  private String count;
  private String info;
  private String infocode;
  private List<Live> lives;

  public String getStatus() {
    return status;
  }
  public String getCount() {
    return count;
  }
  public String getInfo() {
    return info;
  }
  public String getInfocode() {
    return infocode;
  }
  public List<Live> getLives() {
    return lives;
  }

  public static class Live {
    private String city;
    private String weather;
    private String temperature;

    public String getCity() {
      return city;
    }
    public String getWeather() {
      return weather;
    }
    public String getTemperature() {
      return temperature;
    }
  }
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/data/config/Key.java
================================================
package com.kunminx.purenote.data.config;

/**
 * Create by KunMinX at 2022/8/15
 */
public class Key {
  public final static String TEST_STRING = "test_string";
  public final static String TEST_BOOLEAN = "test_boolean";
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/data/repo/DataRepository.java
================================================
package com.kunminx.purenote.data.repo;

import android.annotation.SuppressLint;

import androidx.annotation.NonNull;
import androidx.room.Room;

import com.kunminx.architecture.data.response.AsyncTask;
import com.kunminx.architecture.data.response.DataResult;
import com.kunminx.architecture.data.response.ResponseStatus;
import com.kunminx.architecture.utils.Utils;
import com.kunminx.purenote.data.bean.Note;
import com.kunminx.purenote.data.bean.Weather;

import java.util.List;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * Create by KunMinX at 2022/6/14
 */
public class DataRepository {

  //TODO 天气服务使用高德 API_KEY,如有需要,请自行在 "高德开放平台" 获取和在 DataRepository 类填入

  public final static String API_KEY = "";
  public final static String BASE_URL = "https://restapi.amap.com/v3/";

  private static final DataRepository instance = new DataRepository();
  private static final String DATABASE_NAME = "NOTE_DB.db";
  private final NoteDataBase mDataBase;
  private final Retrofit mRetrofit;

  {
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(HttpLoggingInterceptor.Level.BODY);
    OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(8, TimeUnit.SECONDS)
            .readTimeout(8, TimeUnit.SECONDS)
            .writeTimeout(8, TimeUnit.SECONDS)
            .addInterceptor(logging)
            .build();
    mRetrofit = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build();
  }

  public static DataRepository getInstance() {
    return instance;
  }

  private DataRepository() {
    mDataBase = Room.databaseBuilder(Utils.getApp().getApplicationContext(),
            NoteDataBase.class, DATABASE_NAME).build();
  }

  public Observable<List<Note>> getNotes() {
    return AsyncTask.doIO(emitter -> emitter.onNext(mDataBase.noteDao().getNotes()));
  }

  public Observable<Boolean> insertNote(Note note) {
    return AsyncTask.doIO(emitter -> {
      mDataBase.noteDao().insertNote(note);
      emitter.onNext(true);
    });
  }

  public Observable<Boolean> updateNote(Note note) {
    return AsyncTask.doIO(emitter -> {
      mDataBase.noteDao().updateNote(note);
      emitter.onNext(true);
    });
  }

  public Observable<Boolean> deleteNote(Note note) {
    return AsyncTask.doIO(emitter -> {
      mDataBase.noteDao().deleteNote(note);
      emitter.onNext(true);
    });
  }

  @SuppressLint("CheckResult")
  public Observable<Weather> getWeatherInfo(String cityCode) {
    WeatherService service = mRetrofit.create(WeatherService.class);
    return service.getWeatherInfo(cityCode, API_KEY)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread());
  }
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/data/repo/NoteDao.java
================================================
package com.kunminx.purenote.data.repo;

import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;

import com.kunminx.purenote.data.bean.Note;

import java.util.List;

/**
 * Create by KunMinX at 2022/6/14
 */
@Dao
public interface NoteDao {

  @Query("select * from note order by type & 0x0001 = 0x0001 desc, modify_time desc")
  List<Note> getNotes();

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertNote(Note note);

  @Update()
  void updateNote(Note note);

  @Delete
  void deleteNote(Note note);

}


================================================
FILE: app/src/main/java/com/kunminx/purenote/data/repo/NoteDataBase.java
================================================
package com.kunminx.purenote.data.repo;

import androidx.room.Database;
import androidx.room.RoomDatabase;

import com.kunminx.purenote.data.bean.Note;

/**
 * Create by KunMinX at 2022/6/14
 */
@Database(entities = {Note.class}, version = 1, exportSchema = false)
public abstract class NoteDataBase extends RoomDatabase {
  public abstract NoteDao noteDao();
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/data/repo/WeatherService.java
================================================
package com.kunminx.purenote.data.repo;

import com.kunminx.purenote.data.bean.Weather;

import io.reactivex.Observable;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
/**
 * Create by KunMinX at 2022/8/24
 */
public interface WeatherService {
  @GET("weather/weatherInfo")
  Observable<Weather> getWeatherInfo(
          @Query("city") String city,
          @Query("key") String key
  );
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_Api.java
================================================
package com.kunminx.purenote.domain.intent;

import com.kunminx.purenote.data.bean.Weather;
import com.kunminx.sealed.annotation.Param;
import com.kunminx.sealed.annotation.SealedClass;
/**
 * TODO:可用于 Java 1.8 的 Sealed Class,使用方式见:
 * https://github.com/KunMinX/SealedClass4Java
 *
 * TODO tip 2:此 Intent 非传统意义上的 MVI intent,
 *  而是简化 reduce 和 action 后,拍平的 intent,
 *  它可以携带 param,经由 input 接口发送至 mvi-Dispatcher,
 *  可以 copy 和携带 result,经由 output 接口回推至表现层,
 *
 *  具体可参见《解决 MVI 实战痛点》解析
 *  https://juejin.cn/post/7134594010642907149
 *
 * Create by KunMinX at 2022/8/30
 */
@SealedClass
public interface _Api {
  void onLoading(boolean isLoading);
  void getWeatherInfo(@Param String cityCode, Weather.Live live);
  void onError(String errorInfo);
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_ComplexIntent.java
================================================
package com.kunminx.purenote.domain.intent;

import com.kunminx.sealed.annotation.Param;
import com.kunminx.sealed.annotation.SealedClass;
/**
 * TODO:可用于 Java 1.8 的 Sealed Class,使用方式见:
 * https://github.com/KunMinX/SealedClass4Java
 *
 * TODO tip 2:此 Intent 非传统意义上的 MVI intent,
 *  而是简化 reduce 和 action 后,拍平的 intent,
 *  它可以携带 param,经由 input 接口发送至 mvi-Dispatcher,
 *  可以 copy 和携带 result,经由 output 接口回推至表现层,
 *
 *  具体可参见《解决 MVI 实战痛点》解析
 *  https://juejin.cn/post/7134594010642907149
 *
 * Create by KunMinX at 2022/8/30
 */
@SealedClass
public interface _ComplexIntent {
  void test1(@Param int count, int count1);
  void test2(@Param int count, int count1);
  void test3(@Param int count, int count1);
  void test4(@Param int count, int count1);
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_Messages.java
================================================
package com.kunminx.purenote.domain.intent;

import com.kunminx.sealed.annotation.SealedClass;
/**
 * TODO:可用于 Java 1.8 的 Sealed Class,使用方式见:
 * https://github.com/KunMinX/SealedClass4Java
 *
 * TODO tip 2:此 Intent 非传统意义上的 MVI intent,
 *  而是简化 reduce 和 action 后,拍平的 intent,
 *  它可以携带 param,经由 input 接口发送至 mvi-Dispatcher,
 *  可以 copy 和携带 result,经由 output 接口回推至表现层,
 *
 *  具体可参见《解决 MVI 实战痛点》解析
 *  https://juejin.cn/post/7134594010642907149
 *
 * Create by KunMinX at 2022/8/30
 */
@SealedClass
public interface _Messages {
  void refreshNoteList();
  void finishActivity();
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_NoteIntent.java
================================================
package com.kunminx.purenote.domain.intent;

import com.kunminx.purenote.data.bean.Note;
import com.kunminx.sealed.annotation.Param;
import com.kunminx.sealed.annotation.SealedClass;

import java.util.List;
/**
 * TODO:可用于 Java 1.8 的 Sealed Class,使用方式见:
 * https://github.com/KunMinX/SealedClass4Java
 *
 * TODO tip 2:此 Intent 非传统意义上的 MVI intent,
 *  而是简化 reduce 和 action 后,拍平的 intent,
 *  它可以携带 param,经由 input 接口发送至 mvi-Dispatcher,
 *  可以 copy 和携带 result,经由 output 接口回推至表现层,
 *
 *  具体可参见《解决 MVI 实战痛点》解析
 *  https://juejin.cn/post/7134594010642907149
 *
 * Create by KunMinX at 2022/8/30
 */
@SealedClass
public interface _NoteIntent {
  void getNoteList(List<Note> notes);
  void removeItem(@Param Note note, boolean isSuccess);
  void updateItem(@Param Note note, boolean isSuccess);
  void markItem(@Param Note note, boolean isSuccess);
  void toppingItem(@Param Note note, boolean isSuccess);
  void addItem(@Param Note note, boolean isSuccess);
  void initItem(@Param Note note);
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/message/PageMessenger.java
================================================
package com.kunminx.purenote.domain.message;

import com.kunminx.architecture.domain.dispatch.MviDispatcher;
import com.kunminx.purenote.domain.intent.Messages;

/**
 * Create by KunMinX at 2022/6/14
 */
public class PageMessenger extends MviDispatcher<Messages> {

  /**
   * TODO tip 1:
   *  此为领域层组件,接收发自页面消息,内部统一处理业务逻辑,并通过 sendResult 结果分发。
   *  可为同业务不同页面复用。
   *  ~
   *  本组件通过封装,默使数据从 "领域层" 到 "表现层" 单向流动,
   *  消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。
   */
  @Override
  protected void onHandle(Messages intent) {
    sendResult(intent);

    // TODO:tip 2:除接收来自 Activity/Fragment 的事件,亦可从 Dispatcher 内部发送事件(作为副作用):
    //  ~
    //  if (sent from within) {
    //    Messages msg = new Messages(Messages.EVENT_SHOW_DIALOG);
    //    sendResult(msg);
    //  }

  }
}



================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/request/ComplexRequester.java
================================================
package com.kunminx.purenote.domain.request;

import com.kunminx.architecture.domain.dispatch.MviDispatcher;
import com.kunminx.purenote.domain.intent.ComplexIntent;

import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

/**
 * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者
 * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
 * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
 * `
 * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用,
 * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化,
 * 将来升级到 Jetpack Compose 更是如此,
 *
 * Create by KunMinX at 2022/7/5
 */
public class ComplexRequester extends MviDispatcher<ComplexIntent> {

  private Disposable mDisposable;

  /**
   * TODO tip 1:可初始化配置队列长度,自动丢弃队首过时消息
   */
  @Override
  protected int initQueueMaxLength() {
    return 5;
  }

  /**
   * TODO tip 2:
   * 此为领域层组件,接收发自页面消息,内部统一处理业务逻辑,并通过 sendResult 结果分发。
   * 可在页面中配置作用域,以实现单页面独享或多页面数据共享,
   * `
   * 本组件通过封装,默使数据从 "领域层" 到 "表现层" 单向流动,
   * 消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。
   */
  @Override
  protected void onHandle(ComplexIntent intent) {
    switch (intent.id) {
      case ComplexIntent.Test1.ID:

        //TODO tip 3: 定长队列,随取随用,绝不丢失事件
        //此处通过 RxJava 轮询模拟事件连发,可于 Logcat Debug 见输出

        //通过判断 mDisposable,维持环境重建后还是同一 Rx 实例在回推数据,
        //此 case 可用于验证 "app 处于后台时,推送的数据会兜着,回到前台时,会回推,但此后环境重建也不会再回推,做到消费且只消费一次"

        if (mDisposable == null)
          mDisposable = Observable.interval(1000, TimeUnit.MILLISECONDS)
                  .subscribeOn(Schedulers.io())
                  .observeOn(AndroidSchedulers.mainThread())
                  .subscribe(aLong -> input(ComplexIntent.Test4(aLong.intValue())));
        break;
      case ComplexIntent.Test2.ID:
        Observable.timer(200, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(aLong -> sendResult(intent));
        break;
      case ComplexIntent.Test3.ID:
        sendResult(intent);
        break;
      case ComplexIntent.Test4.ID:
        ComplexIntent.Test4 test4 = (ComplexIntent.Test4) intent;
        sendResult(test4.copy(test4.paramCount));
        break;
    }
  }
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/request/NoteRequester.java
================================================
package com.kunminx.purenote.domain.request;

import com.kunminx.architecture.domain.dispatch.MviDispatcher;
import com.kunminx.purenote.data.repo.DataRepository;
import com.kunminx.purenote.domain.intent.NoteIntent;

/**
 * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者
 * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
 * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
 * `
 * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用,
 * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化,
 * 将来升级到 Jetpack Compose 更是如此,
 * <p>
 * Create by KunMinX at 2022/6/14
 */
public class NoteRequester extends MviDispatcher<NoteIntent> {

  /**
   * TODO tip 1:
   * 此为领域层组件,接收发自页面消息,内部统一处理业务逻辑,并通过 sendResult 结果分发。
   * 可在页面中配置作用域,以实现单页面独享或多页面数据共享,
   * `
   * 本组件通过封装,默使数据从 "领域层" 到 "表现层" 单向流动,
   * 消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。
   */
  @Override
  protected void onHandle(NoteIntent intent) {
    DataRepository repo = DataRepository.getInstance();
    switch (intent.id) {
      case NoteIntent.InitItem.ID:
        NoteIntent.InitItem initItem = (NoteIntent.InitItem) intent;
        sendResult(initItem.copy());
        break;
      case NoteIntent.GetNoteList.ID:
        NoteIntent.GetNoteList getNoteList = (NoteIntent.GetNoteList) intent;
        repo.getNotes().subscribe(notes -> sendResult(getNoteList.copy(notes)));
        break;
      case NoteIntent.UpdateItem.ID:
        NoteIntent.UpdateItem updateItem = (NoteIntent.UpdateItem) intent;
        repo.updateNote(updateItem.paramNote).subscribe(it -> sendResult(updateItem.copy(it)));
        break;
      case NoteIntent.MarkItem.ID:
        NoteIntent.MarkItem markItem = (NoteIntent.MarkItem) intent;
        repo.updateNote(markItem.paramNote).subscribe(it -> sendResult(markItem.copy(it)));
        break;
      case NoteIntent.ToppingItem.ID:
        NoteIntent.ToppingItem toppingItem = (NoteIntent.ToppingItem) intent;
        repo.updateNote(toppingItem.paramNote).subscribe(it ->
                repo.getNotes().subscribe(notes -> sendResult(NoteIntent.GetNoteList(notes))));
        break;
      case NoteIntent.AddItem.ID:
        NoteIntent.AddItem addItem = (NoteIntent.AddItem) intent;
        repo.insertNote(addItem.paramNote).subscribe(it -> sendResult(addItem.copy(it)));
        break;
      case NoteIntent.RemoveItem.ID:
        NoteIntent.RemoveItem removeItem = (NoteIntent.RemoveItem) intent;
        repo.deleteNote(removeItem.paramNote).subscribe(it -> sendResult(removeItem.copy(it)));
        break;
    }
  }
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/domain/request/WeatherRequester.java
================================================
package com.kunminx.purenote.domain.request;

import com.kunminx.architecture.domain.dispatch.MviDispatcher;
import com.kunminx.purenote.data.bean.Weather;
import com.kunminx.purenote.data.repo.DataRepository;
import com.kunminx.purenote.domain.intent.Api;

import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;

/**
 * TODO tip 1:让 UI 和业务分离,让数据总是从生产者流向消费者
 * UI逻辑和业务逻辑,本质区别在于,前者是数据的消费者,后者是数据的生产者,
 * "领域层组件" 作为数据的生产者,职责应仅限于 "请求调度 和 结果分发",
 * `
 * 换言之,"领域层组件" 中应当只关注数据的生成,而不关注数据的使用,
 * 改变 UI 状态的逻辑代码,只应在表现层页面中编写、在 Observer 回调中响应数据的变化,
 * 将来升级到 Jetpack Compose 更是如此,
 * <p>
 * Create by KunMinX at 2022/8/24
 */
public class WeatherRequester extends MviDispatcher<Api> {
  public final static String CITY_CODE_BEIJING = "110000";

  /**
   * TODO tip 1:
   * 此为领域层组件,接收发自页面消息,内部统一处理业务逻辑,并通过 sendResult 结果分发。
   * 可在页面中配置作用域,以实现单页面独享或多页面数据共享,
   * `
   * 本组件通过封装,默使数据从 "领域层" 到 "表现层" 单向流动,
   * 消除 “mutable 样板代码 + 连发事件覆盖 + mutable.setValue 误用滥用” 等高频痛点。
   */
  @Override
  protected void onHandle(Api intent) {
    DataRepository repo = DataRepository.getInstance();
    switch (intent.id) {
      case Api.OnLoading.ID:
      case Api.OnError.ID:
        sendResult(intent);
        break;
      case Api.GetWeatherInfo.ID:
        Api.GetWeatherInfo getWeatherInfo = (Api.GetWeatherInfo) intent;
        repo.getWeatherInfo(getWeatherInfo.paramCityCode).subscribe(new Observer<Weather>() {
          @Override
          public void onSubscribe(Disposable d) {
            input(Api.OnLoading(true));
          }
          @Override
          public void onNext(Weather weather) {
            sendResult(getWeatherInfo.copy(weather.getLives().get(0)));
          }
          @Override
          public void onError(Throwable e) {
            input(Api.OnError(e.getMessage()));
          }
          @Override
          public void onComplete() {
            input(Api.OnLoading(false));
          }
        });
        break;
    }
  }
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/ui/adapter/NoteAdapter.java
================================================
package com.kunminx.purenote.ui.adapter;

import androidx.recyclerview.widget.RecyclerView;

import com.kunminx.architecture.ui.adapter.BaseBindingAdapter;
import com.kunminx.purenote.R;
import com.kunminx.purenote.data.bean.Note;
import com.kunminx.purenote.databinding.AdapterNoteListBinding;

import java.util.List;

/**
 * Create by KunMinX at 2022/7/3
 */
public class NoteAdapter extends BaseBindingAdapter<Note, AdapterNoteListBinding> {

  public NoteAdapter(List<Note> list) {
    super(list);
  }

  @Override
  protected int getLayoutResId(int viewType) {
    return R.layout.adapter_note_list;
  }

  @Override
  protected void onBindItem(AdapterNoteListBinding binding, Note note, RecyclerView.ViewHolder holder) {
    binding.setNote(note);
    int position = holder.getBindingAdapterPosition();
    binding.cl.setOnClickListener(v -> {
      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);
    });
    binding.btnMark.setOnClickListener(v -> {
      note.toggleType(Note.TYPE_MARKED);
      notifyItemChanged(position);
      notifyItemRangeChanged(position, 1);
      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);
    });
    binding.btnTopping.setOnClickListener(v -> {
      note.toggleType(Note.TYPE_TOPPING);
      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);
    });
    binding.btnDelete.setOnClickListener(v -> {
      notifyItemRemoved(position);
      getList().remove(position);
      notifyItemRangeRemoved(position, getList().size() - position);
      if (mOnItemClickListener != null) mOnItemClickListener.onItemClick(v.getId(), note, position);
    });
  }

  @Override
  public int getItemCount() {
    return getList().size();
  }
}


================================================
FILE: app/src/main/java/com/kunminx/purenote/ui/page/EditorFragment.java
================================================
package com.kunminx.purenote.ui.page;

import android.os.Bundle;
import android.text.TextUtils;

import androidx.navigation.NavController;

import com.kunminx.architecture.ui.bind.ClickProxy;
import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.architecture.utils.ToastUtils;
import com.kunminx.architecture.utils.Utils;
import com.kunminx.purenote.BR;
import com.kunminx.purenote.R;
import com.kunminx.purenote.data.bean.Note;
import com.kunminx.purenote.domain.intent.Messages;
import com.kunminx.purenote.domain.intent.NoteIntent;
import com.kunminx.purenote.domain.message.PageMessenger;
import com.kunminx.purenote.domain.request.NoteRequester;

import java.util.Objects;
import java.util.UUID;

/**
 * Create by KunMinX at 2022/6/30
 */
public class EditorFragment extends BaseFragment {
  private final static String NOTE = "NOTE";
  private EditorStates mStates;
  private NoteRequester mNoteRequester;
  private PageMessenger mMessenger;
  private ClickProxy mClickProxy;

  public static void start(NavController controller, Note note) {
    Bundle bundle = new Bundle();
    bundle.putParcelable(NOTE, note);
    controller.navigate(R.id.action_list_to_editor, bundle);
  }

  @Override
  protected void initViewModel() {
    mStates = getFragmentScopeViewModel(EditorStates.class);
    mNoteRequester = getFragmentScopeViewModel(NoteRequester.class);
    mMessenger = getApplicationScopeViewModel(PageMessenger.class);
  }

  @Override
  protected DataBindingConfig getDataBindingConfig() {
    return new DataBindingConfig(R.layout.fragment_editor, BR.state, mStates)
            .addBindingParam(BR.click, mClickProxy = new ClickProxy());
  }

  /**
   * TODO tip 1:
   * 通过 PublishSubject 接收数据,并在唯一出口 output{ ... } 中响应数据的变化,
   * 通过 BehaviorSubject 通知所绑定控件属性重新渲染,并为其兜住最后一次状态,
   */
  @Override
  protected void onOutput() {
    mNoteRequester.output(this, noteIntent -> {
      if (Objects.equals(noteIntent.id, NoteIntent.InitItem.ID)) {
        mStates.tempNote.set(((NoteIntent.InitItem) noteIntent).paramNote);
        Note tempNote = Objects.requireNonNull(mStates.tempNote.get());
        mStates.title.set(tempNote.getTitle());
        mStates.content.set(tempNote.getContent());
        if (TextUtils.isEmpty(tempNote.getId())) {
          mStates.titleRequestFocus.set(true);
        } else {
          mStates.tip.set(getString(R.string.last_time_modify));
          mStates.time.set(tempNote.getModifyDate());
        }
      } else if (Objects.equals(noteIntent.id, NoteIntent.AddItem.ID)) {
        mMessenger.input(Messages.RefreshNoteList());
        ToastUtils.showShortToast(getString(R.string.saved));
        nav().navigateUp();
      }
    });
  }

  /**
   * TODO tip 2:
   * 通过唯一入口 input() 发消息至 "可信源",由其内部统一处理业务逻辑和结果分发。
   */
  @Override
  protected void onInput() {
    mClickProxy.setOnClickListener(v -> {
      if (v.getId() == R.id.btn_back) save();
    });
    if (getArguments() != null)
      mNoteRequester.input(NoteIntent.InitItem(getArguments().getParcelable(NOTE)));
  }

  private void save() {
    Note tempNote = Objects.requireNonNull(mStates.tempNote.get());
    String title = mStates.title.get();
    String content = mStates.content.get();
    if (TextUtils.isEmpty(title + content) || tempNote.getTitle().equals(title) && tempNote.getContent().equals(content)) {
      nav().navigateUp();
      return;
    }
    Note note;
    long time = System.currentTimeMillis();
    if (TextUtils.isEmpty(tempNote.getId())) {
      note = new Note(UUID.randomUUID().toString(), title, content, time, time, 0);
    } else {
      note = new Note(tempNote.getId(), title, content, tempNote.getCreateTime(), time, tempNote.getType());
    }
    mNoteRequester.input(NoteIntent.AddItem(note));
  }

  @Override
  protected void onBackPressed() {
    save();
  }

  /**
   * TODO tip 3:
   * 基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
   * 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
   * 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,
   *
   * 具体可参见《解决 MVI 实战痛点》解析
   * https://juejin.cn/post/7134594010642907149
   */
  public static class EditorStates extends StateHolder {
    public final State<Note> tempNote = new State<>(new Note());
    public final State<String> title = new State<>("");
    public final State<String> content = new State<>("");
    public final State<String> tip = new State<>(Utils.getApp().getString(R.string.edit));
    public final State<String> time = new State<>(Utils.getApp().getString(R.string.new_note));
    public final State<Boolean> titleRequestFocus = new State<>(false);
  }
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/ui/page/ListFragment.java
================================================
package com.kunminx.purenote.ui.page;

import com.kunminx.architecture.domain.dispatch.GlobalConfigs;
import com.kunminx.architecture.ui.bind.ClickProxy;
import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.purenote.BR;
import com.kunminx.purenote.R;
import com.kunminx.purenote.data.bean.Note;
import com.kunminx.purenote.data.bean.Weather;
import com.kunminx.purenote.data.config.Key;
import com.kunminx.purenote.domain.intent.Api;
import com.kunminx.purenote.domain.intent.Messages;
import com.kunminx.purenote.domain.intent.NoteIntent;
import com.kunminx.purenote.domain.message.PageMessenger;
import com.kunminx.purenote.domain.request.WeatherRequester;
import com.kunminx.purenote.domain.request.NoteRequester;
import com.kunminx.purenote.ui.adapter.NoteAdapter;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Create by KunMinX at 2022/6/30
 */
public class ListFragment extends BaseFragment {
  private ListStates mStates;
  private NoteRequester mNoteRequester;
  private WeatherRequester mWeatherRequester;
  private PageMessenger mMessenger;
  private NoteAdapter mAdapter;
  private ClickProxy mClickProxy;

  @Override
  protected void initViewModel() {
    mStates = getFragmentScopeViewModel(ListStates.class);
    mNoteRequester = getFragmentScopeViewModel(NoteRequester.class);
    mWeatherRequester = getFragmentScopeViewModel(WeatherRequester.class);
    mMessenger = getApplicationScopeViewModel(PageMessenger.class);
  }

  @Override
  protected DataBindingConfig getDataBindingConfig() {
    return new DataBindingConfig(R.layout.fragment_list, BR.state, mStates)
            .addBindingParam(BR.adapter, mAdapter = new NoteAdapter(mStates.list))
            .addBindingParam(BR.click, mClickProxy = new ClickProxy());
  }

  /**
   * TODO tip 1:
   * 通过 PublishSubject 接收数据,并在唯一出口 output{ ... } 中响应数据的变化,
   * 通过 BehaviorSubject 通知所绑定控件属性重新渲染,并为其兜住最后一次状态,
   */
  @Override
  protected void onOutput() {
    mMessenger.output(this, messages -> {
      if (Objects.equals(messages.id, Messages.RefreshNoteList.ID)) {
        mNoteRequester.input(NoteIntent.GetNoteList());
      }
    });

    mNoteRequester.output(this, noteIntent -> {
      switch (noteIntent.id) {
        case NoteIntent.ToppingItem.ID:
        case NoteIntent.GetNoteList.ID:
          NoteIntent.GetNoteList getNoteList = (NoteIntent.GetNoteList) noteIntent;
          mAdapter.refresh(getNoteList.resultNotes);
          mStates.emptyViewShow.set(mStates.list.size() == 0);
          break;
        case NoteIntent.MarkItem.ID:
        case NoteIntent.RemoveItem.ID:
          break;
      }
    });

    mWeatherRequester.output(this, api -> {
      switch (api.id) {
        case Api.OnLoading.ID:
          mStates.loadingWeather.set(((Api.OnLoading) api).resultIsLoading);
          break;
        case Api.GetWeatherInfo.ID:
          Api.GetWeatherInfo weatherInfo = (Api.GetWeatherInfo) api;
          Weather.Live live = weatherInfo.resultLive;
          if (live != null) mStates.weather.set(live.getWeather());
          break;
        case Api.OnError.ID:
          break;
      }
    });

    //TODO tip 3: 更新配置并刷新界面,是日常开发高频操作,
    // 当别处通过 GlobalConfigs 为某配置 put 新值,此处响应并刷新 UI

    GlobalConfigs.output(this, keyValueEvent -> {
      switch (keyValueEvent.currentKey) {
        case Key.TEST_STRING:
          break;
        case Key.TEST_BOOLEAN:
          break;
      }
    });
  }

  /**
   * TODO tip 2:
   * 通过唯一入口 input() 发消息至 "可信源",由其内部统一处理业务逻辑和结果分发。
   */
  @Override
  protected void onInput() {
    mAdapter.setOnItemClickListener((viewId, item, position) -> {
      if (viewId == R.id.btn_mark) mNoteRequester.input(NoteIntent.MarkItem(item));
      else if (viewId == R.id.btn_topping) mNoteRequester.input(NoteIntent.ToppingItem(item));
      else if (viewId == R.id.btn_delete) mNoteRequester.input(NoteIntent.RemoveItem(item));
      else if (viewId == R.id.cl) EditorFragment.start(nav(), item);
    });
    mClickProxy.setOnClickListener(view -> {
      if (view.getId() == R.id.fab) EditorFragment.start(nav(), new Note());
      else if (view.getId() == R.id.iv_logo) nav().navigate(R.id.action_list_to_setting);
    });

    //TODO 天气示例使用高德 API_KEY,如有需要,请自行在 "高德开放平台" 获取和在 DataRepository 类填入
    //    if (TextUtils.isEmpty(mStates.weather.get())) {
    //      mHttpRequester.input(Api.GetWeatherInfo(HttpRequester.CITY_CODE_BEIJING));
    //    }

    if (mStates.list.isEmpty()) mNoteRequester.input(NoteIntent.GetNoteList());
  }

  @Override
  protected void onBackPressed() {
    mMessenger.input(Messages.FinishActivity());
  }

  /**
   * TODO tip 3:
   * 基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
   * 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
   * 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,
   *
   * 具体可参见《解决 MVI 实战痛点》解析
   * https://juejin.cn/post/7134594010642907149
   */
  public static class ListStates extends StateHolder {
    public final List<Note> list = new ArrayList<>();
    public final State<Boolean> emptyViewShow = new State<>(false);
    public final State<Boolean> loadingWeather = new State<>(false);
    public final State<String> weather = new State<>("");
  }
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/ui/page/MainActivity.java
================================================
package com.kunminx.purenote.ui.page;

import android.util.Log;

import com.kunminx.architecture.ui.page.BaseActivity;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.purenote.BR;
import com.kunminx.purenote.R;
import com.kunminx.purenote.domain.intent.ComplexIntent;
import com.kunminx.purenote.domain.intent.Messages;
import com.kunminx.purenote.domain.message.PageMessenger;
import com.kunminx.purenote.domain.request.ComplexRequester;

import java.util.Objects;

public class MainActivity extends BaseActivity {
  private MainAtyStates mStates;
  private PageMessenger mMessenger;
  private ComplexRequester mComplexRequester;

  @Override
  protected void initViewModel() {
    mMessenger = getApplicationScopeViewModel(PageMessenger.class);
    mComplexRequester = getActivityScopeViewModel(ComplexRequester.class);
  }

  @Override
  protected DataBindingConfig getDataBindingConfig() {
    return new DataBindingConfig(R.layout.activity_main, BR.state, mStates);
  }

  /**
   * TODO tip 1:
   * 通过 PublishSubject 接收数据,并在唯一出口 output{ ... } 中响应数据的变化,
   * 通过 BehaviorSubject 通知所绑定控件属性重新渲染,并为其兜住最后一次状态,
   */
  @Override
  protected void onOutput() {
    mMessenger.output(this, messages -> {
      if (Objects.equals(messages.id, Messages.FinishActivity.ID)) finish();
    });

    mComplexRequester.output(this, complexIntent -> {
      switch (complexIntent.id) {
        case ComplexIntent.Test1.ID:
          Log.i("ComplexIntent", "---1");
          break;
        case ComplexIntent.Test2.ID:
          Log.i("ComplexIntent", "---2");
          break;
        case ComplexIntent.Test3.ID:
          Log.i("ComplexIntent", "---3");
          break;
        case ComplexIntent.Test4.ID:
          ComplexIntent.Test4 test4 = (ComplexIntent.Test4) complexIntent;
          Log.i("ComplexIntent", "---4 " + test4.resultCount1);
          break;
      }
    });
  }

  /**
   * TODO tip 2:
   * 通过唯一入口 input() 发消息至 "可信源",由其内部统一处理业务逻辑和结果分发。
   *
   * 此处展示通过 dispatcher.input 连续发送多事件而不被覆盖
   */
  @Override
  protected void onInput() {
    //TODO tip 3:Test1 可用于验证 "app 处于后台时,推送的数据会兜着,回到前台时,会回推,但此后环境重建也不会再回推,做到消费且只消费一次"
    //在单独观察 Test1 能力时,可将 Test2、Test3 的测试注释

    mComplexRequester.input(ComplexIntent.Test1(1));
//    mComplexRequester.input(ComplexIntent.Test2(2));
//    mComplexRequester.input(ComplexIntent.Test2(2));
//    mComplexRequester.input(ComplexIntent.Test2(2));
//    mComplexRequester.input(ComplexIntent.Test3(3));
//    mComplexRequester.input(ComplexIntent.Test3(3));
//    mComplexRequester.input(ComplexIntent.Test3(3));
//    mComplexRequester.input(ComplexIntent.Test3(3));
  }

  public static class MainAtyStates extends StateHolder {
  }
}

================================================
FILE: app/src/main/java/com/kunminx/purenote/ui/page/SettingFragment.java
================================================
package com.kunminx.purenote.ui.page;

import com.kunminx.architecture.domain.dispatch.GlobalConfigs;
import com.kunminx.architecture.ui.bind.ClickProxy;
import com.kunminx.architecture.ui.page.BaseFragment;
import com.kunminx.architecture.ui.page.DataBindingConfig;
import com.kunminx.architecture.ui.page.StateHolder;
import com.kunminx.architecture.ui.state.State;
import com.kunminx.purenote.BR;
import com.kunminx.purenote.R;
import com.kunminx.purenote.data.config.Key;

/**
 * Create by KunMinX at 2022/8/15
 */
public class SettingFragment extends BaseFragment {
  private SettingStates mStates;
  private ClickProxy mClickProxy;

  @Override
  protected void initViewModel() {
    mStates = getFragmentScopeViewModel(SettingStates.class);
  }

  @Override
  protected DataBindingConfig getDataBindingConfig() {
    mStates.testString.set(GlobalConfigs.getString(Key.TEST_STRING));
    mStates.testBoolean.set(GlobalConfigs.getBoolean(Key.TEST_BOOLEAN));
    return new DataBindingConfig(R.layout.fragment_settings, BR.state, mStates)
            .addBindingParam(BR.click, mClickProxy = new ClickProxy());
  }

  /**
   * TODO tip 1:
   * 通过唯一入口 input() 发消息至 "可信源",由其内部统一处理业务逻辑和结果分发。
   */
  @Override
  protected void onInput() {
    mClickProxy.setOnClickListener(v -> {
      if (v.getId() == R.id.btn_back) nav().navigateUp();
      else if (v.getId() == R.id.btn_sure_1)
        GlobalConfigs.put(Key.TEST_STRING, mStates.testString.get());
      else if (v.getId() == R.id.sw_value_2)
        GlobalConfigs.put(Key.TEST_BOOLEAN, mStates.testBoolean.get());
    });
  }

  /**
   * TODO tip 2:
   * 基于单一职责原则,抽取 Jetpack ViewModel "状态保存和恢复" 的能力作为 StateHolder,
   * 并使用 ObservableField 的改良版子类 State 来承担 BehaviorSubject,用作所绑定控件的 "可信数据源",
   * 从而在收到来自 PublishSubject 的结果回推后,响应结果数据的变化,也即通知控件属性重新渲染,并为其兜住最后一次状态,
   *
   * 具体可参见《解决 MVI 实战痛点》解析
   * https://juejin.cn/post/7134594010642907149
   */
  public static class SettingStates extends StateHolder {
    public final State<String> testString = new State<>("");
    public final State<Boolean> testBoolean = new State<>(false);
  }
}


================================================
FILE: app/src/main/res/anim/x_fragment_enter.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="150">
    <translate
        android:fromXDelta="10%p"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toXDelta="0" />

    <alpha
        android:fromAlpha="0"
        android:toAlpha="1.0" />
</set>

================================================
FILE: app/src/main/res/anim/x_fragment_exit.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200">
    <translate
        android:fromXDelta="0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toXDelta="-10%p" />
</set>

================================================
FILE: app/src/main/res/anim/x_fragment_pop_enter.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200">
    <translate
        android:fromXDelta="-10%p"
        android:toXDelta="0" />

</set>

================================================
FILE: app/src/main/res/anim/x_fragment_pop_exit.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200">
    <translate
        android:fromXDelta="0"
        android:toXDelta="10%p" />

    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="0" />
</set>

================================================
FILE: app/src/main/res/drawable/ic_baseline_add.xml
================================================
<vector android:height="48dp"
    android:tint="#FFFFFF"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="48dp"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_baseline_arrow_back.xml
================================================
<vector android:height="48dp"
    android:tint="#FFFFFF"
    android:viewportHeight="24"
    android:viewportWidth="24"
    android:width="48dp"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>


================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="state"
            type="com.kunminx.purenote.ui.page.MainActivity.MainAtyStates" />
    </data>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/frag_container"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
</layout>


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

    <data>

        <variable
            name="note"
            type="com.kunminx.purenote.data.bean.Note" />
    </data>

    <com.kunminx.architecture.ui.view.SwipeMenuLayout
        android:id="@+id/swipe_container"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:layout_margin="4dp"
        android:clickable="true"
        android:focusable="true"
        app:ios="true"
        app:leftSwipe="true"
        app:swipeEnable="true">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/cl"
            clipToOutline="@{true}"
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:elevation="8dp">

            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginTop="12dp"
                android:text="@{note.title}"
                android:textColor="@color/color_black"
                android:textSize="24sp"
                android:textStyle="bold"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tv_time"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="TEST 测试 123" />

            <androidx.appcompat.widget.AppCompatImageButton
                android:id="@+id/btn_mark"
                imgRes="@{note.markIcon()}"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_marginEnd="8dp"
                android:background="@color/transparent"
                android:padding="4dp"
                android:scaleType="centerInside"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tv_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginTop="4dp"
                android:layout_marginBottom="4dp"
                android:text="@{note.modifyDate}"
                android:textColor="@color/blue"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tv_title"
                tools:text="2022-07-03" />

            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/tv_topped"
                visible="@{note.topping}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:layout_marginTop="4dp"
                android:layout_marginBottom="4dp"
                android:text="@string/top"
                android:textColor="@color/blue"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toRightOf="@+id/tv_time"
                app:layout_constraintTop_toBottomOf="@+id/tv_title" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <FrameLayout
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:background="@color/blue">

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/btn_topping"
                android:layout_width="28dp"
                android:layout_height="28dp"
                android:layout_gravity="center"
                android:src="@drawable/icon_pin"
                android:tint="@color/color_white" />

        </FrameLayout>

        <FrameLayout
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:background="@color/pink">

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/btn_delete"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_gravity="center"
                android:src="@drawable/icon_delete"
                android:tint="@color/color_white" />

        </FrameLayout>

    </com.kunminx.architecture.ui.view.SwipeMenuLayout>
</layout>


================================================
FILE: app/src/main/res/layout/fragment_editor.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="state"
            type="com.kunminx.purenote.ui.page.EditorFragment.EditorStates" />

        <variable
            name="click"
            type="com.kunminx.architecture.ui.bind.ClickProxy" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="89dp"
            android:background="@color/color_white"
            android:elevation="8dp"
            android:orientation="horizontal"
            android:paddingTop="25dp">

            <androidx.appcompat.widget.AppCompatImageButton
                android:id="@+id/btn_back"
                android:layout_width="36dp"
                android:layout_height="56dp"
                android:layout_marginStart="12dp"
                android:background="@color/transparent"
                android:onClick="@{click.listener}"
                android:scaleType="centerInside"
                android:src="@drawable/ic_baseline_arrow_back"
                android:tint="@color/gray" />

            <androidx.appcompat.widget.LinearLayoutCompat
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_title"
                    android:layout_width="wrap_content"
                    android:layout_height="32dp"
                    android:gravity="center_vertical"
                    android:paddingStart="12dp"
                    android:paddingEnd="4dp"
                    android:text="@{state.tip}"
                    android:textColor="@color/gray"
                    android:textSize="24sp" />

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_time"
                    android:layout_width="match_parent"
                    android:layout_height="20dp"
                    android:gravity="center_vertical"
                    android:paddingStart="12dp"
                    android:paddingEnd="12dp"
                    android:text="@{state.time}"
                    android:textColor="@color/gray"
                    android:textSize="14sp" />

            </androidx.appcompat.widget.LinearLayoutCompat>

        </androidx.appcompat.widget.LinearLayoutCompat>

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/et_title"
            requestFocus="@{state.titleRequestFocus}"
            showKeyboard="@{state.titleRequestFocus}"
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:background="@color/color_white"
            android:gravity="center_vertical"
            android:hint="@string/title"
            android:paddingStart="12dp"
            android:paddingEnd="12dp"
            android:singleLine="true"
            android:text="@={state.title}"
            android:textSize="24sp" />

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/et_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/color_white"
            android:gravity="top"
            android:hint="@string/content"
            android:padding="12dp"
            android:text="@={state.content}"
            android:textSize="16sp" />

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>


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

    <data>

        <variable
            name="state"
            type="com.kunminx.purenote.ui.page.ListFragment.ListStates" />

        <variable
            name="adapter"
            type="androidx.recyclerview.widget.RecyclerView.Adapter" />

        <variable
            name="click"
            type="com.kunminx.architecture.ui.bind.ClickProxy" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="89dp"
            android:background="@color/color_white"
            android:elevation="8dp"
            android:orientation="horizontal"
            android:paddingTop="25dp">

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_logo"
                android:layout_width="48dp"
                android:layout_height="56dp"
                android:layout_marginStart="12dp"
                android:onClick="@{click.listener}"
                android:scaleType="centerInside"
                android:src="@drawable/icon_avatar" />

            <androidx.appcompat.widget.LinearLayoutCompat
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical">

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_title"
                    android:layout_width="match_parent"
                    android:layout_height="32dp"
                    android:gravity="center_vertical"
                    android:onClick="@{click.listener}"
                    android:paddingStart="12dp"
                    android:paddingEnd="12dp"
                    android:text="@string/app_name"
                    android:textColor="@color/color_black"
                    android:textSize="24sp" />

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_content"
                    android:layout_width="match_parent"
                    android:layout_height="20dp"
                    android:gravity="center_vertical"
                    android:paddingStart="12dp"
                    android:paddingEnd="12dp"
                    android:text="@string/copyright"
                    android:textColor="@color/gray"
                    android:textSize="14sp" />

            </androidx.appcompat.widget.LinearLayoutCompat>

            <FrameLayout
                android:layout_width="80dp"
                android:layout_height="56dp">

                <ProgressBar
                    android:id="@+id/progress"
                    visible="@{state.loadingWeather}"
                    android:layout_width="36dp"
                    android:layout_height="36dp"
                    android:layout_gravity="center" />

                <androidx.appcompat.widget.AppCompatTextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="@{state.weather}"
                    android:textColor="@color/gray"
                    android:textSize="20sp"
                    tools:text="@string/weather" />

            </FrameLayout>

            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_search"
                android:layout_width="48dp"
                android:layout_height="56dp"
                android:layout_marginEnd="12dp"
                android:layout_weight="0"
                android:onClick="@{click.listener}"
                android:scaleType="centerInside"
                android:src="@drawable/icon_search"
                android:tint="@color/gray" />

        </androidx.appcompat.widget.LinearLayoutCompat>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv"
            adapter="@{adapter}"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@id/toolbar"
            android:background="@color/light_gray"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            tools:itemCount="9"
            tools:listitem="@layout/adapter_note_list" />

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_empty"
            visible="@{state.emptyViewShow}"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:layout_centerInParent="true"
            android:src="@drawable/bg_empty"
            android:visibility="gone" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_alignParentBottom="true"
            android:layout_margin="16dp"
            android:contentDescription="@string/new_note"
            android:onClick="@{click.listener}"
            android:src="@drawable/ic_baseline_add" />

    </RelativeLayout>
</layout>





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

    <data>

        <variable
            name="state"
            type="com.kunminx.purenote.ui.page.SettingFragment.SettingStates" />

        <variable
            name="click"
            type="com.kunminx.architecture.ui.bind.ClickProxy" />
    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="89dp"
            android:background="@color/color_white"
            android:elevation="8dp"
            android:orientation="horizontal"
            android:paddingTop="25dp">

            <androidx.appcompat.widget.AppCompatImageButton
                android:id="@+id/btn_back"
                android:layout_width="36dp"
                android:layout_height="56dp"
                android:layout_marginStart="12dp"
                android:background="@color/transparent"
                android:onClick="@{click.listener}"
                android:scaleType="centerInside"
                android:src="@drawable/ic_baseline_arrow_back"
                android:tint="@color/gray" />

            <androidx.appcompat.widget.LinearLayoutCompat
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_title"
                    android:layout_width="wrap_content"
                    android:layout_height="32dp"
                    android:gravity="center_vertical"
                    android:paddingStart="12dp"
                    android:paddingEnd="4dp"
                    android:text="@string/setting"
                    android:textColor="@color/color_black"
                    android:textSize="24sp" />

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_time"
                    android:layout_width="match_parent"
                    android:layout_height="20dp"
                    android:gravity="center_vertical"
                    android:paddingStart="12dp"
                    android:paddingEnd="12dp"
                    android:text="@string/setting_summary"
                    android:textColor="@color/dark_gray"
                    android:textSize="14sp" />

            </androidx.appcompat.widget.LinearLayoutCompat>

        </androidx.appcompat.widget.LinearLayoutCompat>

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/color_white"
            android:orientation="vertical">

            <RelativeLayout
                android:id="@+id/rl_1"
                android:layout_width="match_parent"
                android:layout_height="80dp"
                android:paddingStart="12dp"
                android:paddingEnd="12dp">

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_key_1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:text="@string/string_test"
                    android:textColor="@color/color_black"
                    android:textSize="20sp" />

                <androidx.appcompat.widget.AppCompatEditText
                    android:id="@+id/et_value_1"
                    android:layout_width="150dp"
                    android:layout_height="wrap_content"
                    android:layout_centerInParent="true"
                    android:hint="@string/enter_please"
                    android:singleLine="true"
                    android:text="@={state.testString}" />

                <Button
                    android:id="@+id/btn_sure_1"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentEnd="true"
                    android:layout_centerVertical="true"
                    android:onClick="@{click.listener}"
                    android:text="@string/save"
                    tools:ignore="RelativeOverlap" />

            </RelativeLayout>

            <RelativeLayout
                android:id="@+id/rl_2"
                android:layout_width="match_parent"
                android:layout_height="80dp"
                android:paddingStart="12dp"
                android:paddingEnd="12dp">

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/tv_key_2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:text="@string/boolean_test"
                    android:textColor="@color/color_black"
                    android:textSize="20sp"
                    tools:ignore="RelativeOverlap" />

                <androidx.appcompat.widget.SwitchCompat
                    android:id="@+id/sw_value_2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentEnd="true"
                    android:layout_centerVertical="true"
                    android:checked="@={state.testBoolean}"
                    android:onClick="@{click.listener}" />

            </RelativeLayout>

        </androidx.appcompat.widget.LinearLayoutCompat>

    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>


================================================
FILE: app/src/main/res/navigation/nav_graph.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph"
    app:startDestination="@id/ListFragment">

    <fragment
        android:id="@+id/ListFragment"
        android:name="com.kunminx.purenote.ui.page.ListFragment"
        tools:layout="@layout/fragment_list">

        <action
            android:id="@+id/action_list_to_editor"
            app:destination="@id/EditorFragment"
            app:enterAnim="@anim/x_fragment_enter"
            app:exitAnim="@anim/x_fragment_exit"
            app:popEnterAnim="@anim/x_fragment_pop_enter"
            app:popExitAnim="@anim/x_fragment_pop_exit" />
        <action
            android:id="@+id/action_list_to_setting"
            app:destination="@id/settingFragment"
            app:enterAnim="@anim/x_fragment_enter"
            app:exitAnim="@anim/x_fragment_exit"
            app:popEnterAnim="@anim/x_fragment_pop_enter"
            app:popExitAnim="@anim/x_fragment_pop_exit" />
    </fragment>

    <fragment
        android:id="@+id/EditorFragment"
        android:name="com.kunminx.purenote.ui.page.EditorFragment"
        tools:layout="@layout/fragment_editor">

    </fragment>
    <fragment
        android:id="@+id/settingFragment"
        android:name="com.kunminx.purenote.ui.page.SettingFragment"
        android:label="SettingFragment"
        tools:layout="@layout/fragment_settings" />
</navigation>

================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="color_black">#FF000000</color>
    <color name="color_white">#FFFFFFFF</color>
    <color name="transparent">#00000000</color>

    <color name="light_gray">#F9FBFC</color>
    <color name="blue">#4C5DF2</color>
    <color name="gray">#cccccc</color>
    <color name="dark_gray">#666666</color>
    <color name="pink">#F70067</color>
</resources>

================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<resources>
    <dimen name="fab_margin">16dp</dimen>
</resources>

================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
    <string name="app_name">PureNote</string>
    <string name="title">标题</string>
    <string name="content">内容</string>
    <string name="edit">编辑</string>
    <string name="note">笔记</string>
    <string name="new_note">新建笔记</string>
    <string name="saved">已保存</string>
    <string name="top">置顶</string>
    <string name="last_time_modify">最后编辑于</string>
    <string name="copyright">Product by KunMinX with love</string>
    <string name="setting">设置</string>
    <string name="setting_summary">配置读写,持久化存储</string>
    <string name="string_test">String 读写</string>
    <string name="save">保存</string>
    <string name="enter_please">请输入</string>
    <string name="boolean_test">Boolean 读写</string>
    <string name="weather">天气</string>

</resources>

================================================
FILE: app/src/main/res/values/themes.xml
================================================
<resources>

    <style name="Theme.PureNote" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/color_white</item>
        <item name="colorSecondary">@color/blue</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/color_white</item>
    </style>
</resources>

================================================
FILE: app/src/test/java/com/kunminx/purenote/ExampleUnitTest.java
================================================
package com.kunminx.purenote;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
public class ExampleUnitTest {
  @Test
  public void addition_isCorrect() {
    assertEquals(4, 2 + 2);
  }
}

================================================
FILE: architecture/.gitignore
================================================
/build

================================================
FILE: architecture/build.gradle
================================================
apply plugin: 'com.android.library'

android {
    compileSdkVersion appTargetSdk
    defaultConfig {
        minSdkVersion 23
        targetSdkVersion appTargetSdk
        versionCode appVersionCode
        versionName appVersionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    buildFeatures {
        dataBinding true
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])

    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.3"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"

    implementation "androidx.appcompat:appcompat:1.5.0"
    implementation "androidx.navigation:navigation-runtime:2.5.1"
    implementation 'com.github.KunMinX:Smooth-Navigation:v4.0.0'
    implementation 'com.github.KunMinX.Strict-DataBinding:strict_databinding:5.6.0'
    implementation 'com.github.KunMinX.Strict-DataBinding:binding_state:5.6.0'
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    implementation project(":mvi-dispatch")

    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
}

================================================
FILE: architecture/consumer-rules.pro
================================================


================================================
FILE: architecture/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java
================================================
package com.kunminx.architecture;

import android.content.Context;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
  @Test
  public void useAppContext() {
    // Context of the app under test.
    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
    assertEquals("com.kunminx.architecture.test", appContext.getPackageName());
  }
}

================================================
FILE: architecture/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.kunminx.architecture">

</manifest>

================================================
FILE: architecture/src/main/java/com/kunminx/architecture/data/response/AsyncTask.java
================================================
package com.kunminx.architecture.data.response;

import android.annotation.SuppressLint;

import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

/**
 * Create by KunMinX at 2022/6/14
 */
public class AsyncTask {

  @SuppressLint("CheckResult")
  public static <T> Observable<T> doIO(Action<T> start) {
    return Observable.create(start::onEmit)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread());
  }

  @SuppressLint("CheckResult")
  public static <T> Observable<T> doCalculate(Action<T> start) {
    return Observable.create(start::onEmit)
            .subscribeOn(Schedulers.computation())
            .observeOn(AndroidSchedulers.mainThread());
  }

  public interface Action<T> {
    void onEmit(ObservableEmitter<T> emitter);
  }

  public interface Observer<T> extends io.reactivex.Observer<T> {
    default void onSubscribe(@NonNull Disposable d) {
    }

    void onNext(@NonNull T t);

    default void onError(@NonNull Throwable e) {
    }

    default void onComplete() {
    }
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java
================================================
/*
 *
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.kunminx.architecture.data.response;

/**
 * Create by KunMinX at 2020/7/20
 */
public class DataResult<T> {

  private final T mEntity;
  private final ResponseStatus mResponseStatus;

  public DataResult(T entity, ResponseStatus responseStatus) {
    mEntity = entity;
    mResponseStatus = responseStatus;
  }

  public DataResult(T entity) {
    mEntity = entity;
    mResponseStatus = new ResponseStatus();
  }

  public T getResult() {
    return mEntity;
  }

  public ResponseStatus getResponseStatus() {
    return mResponseStatus;
  }

  public interface Result<T> {
    void onResult(DataResult<T> dataResult);
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.architecture.data.response;

/**
 * Create by KunMinX at 19/10/11
 */
public class ResponseStatus {

  private String responseCode = "";
  private String msg = "";
  private boolean success = true;
  private Enum<ResultSource> source = ResultSource.NETWORK;

  public ResponseStatus() {
  }

  public ResponseStatus(String msg) {
    this.msg = msg;
    this.success = false;
  }

  public ResponseStatus(String msg, String responseCode, boolean success) {
    this.msg = msg;
    this.responseCode = responseCode;
    this.success = success;
  }

  public ResponseStatus(String responseCode, boolean success) {
    this.responseCode = responseCode;
    this.success = success;
  }

  public ResponseStatus(String responseCode, boolean success, Enum<ResultSource> source) {
    this(responseCode, success);
    this.source = source;
  }

  public String getResponseCode() {
    return responseCode;
  }

  public String getMsg() {
    return msg;
  }

  public boolean isSuccess() {
    return success;
  }

  public Enum<ResultSource> getSource() {
    return source;
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java
================================================
package com.kunminx.architecture.data.response;

/**
 * Create by KunMinX at 2020/11/30
 */
public enum ResultSource {
  NETWORK, DATABASE, LOCAL_FILE
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/ui/adapter/BaseBindingAdapter.java
================================================
package com.kunminx.architecture.ui.adapter;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;
/**
 * Create by KunMinX at 2022/8/20
 */
public abstract class BaseBindingAdapter<M, B extends ViewDataBinding>
        extends RecyclerView.Adapter<BaseBindingAdapter.BaseBindingViewHolder> {

  protected List<M> mList;
  protected OnItemClickListener<M> mOnItemClickListener;
  protected OnItemLongClickListener<M> mOnItemLongClickListener;

  public BaseBindingAdapter(List<M> list) {
    mList = list;
  }

  public void setOnItemClickListener(OnItemClickListener<M> onItemClickListener) {
    mOnItemClickListener = onItemClickListener;
  }

  public void setOnItemLongClickListener(OnItemLongClickListener<M> onItemLongClickListener) {
    mOnItemLongClickListener = onItemLongClickListener;
  }

  @Override
  @NonNull
  public BaseBindingAdapter.BaseBindingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    B binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), this.getLayoutResId(viewType), parent, false);
    BaseBindingViewHolder holder = new BaseBindingViewHolder(binding.getRoot());
    holder.itemView.setOnClickListener(v -> {
      if (mOnItemClickListener != null) {
        int position = holder.getBindingAdapterPosition();
        mOnItemClickListener.onItemClick(holder.itemView.getId(), mList.get(position), position);
      }
    });
    holder.itemView.setOnLongClickListener(v -> {
      if (mOnItemLongClickListener != null) {
        int position = holder.getBindingAdapterPosition();
        mOnItemLongClickListener.onItemLongClick(holder.itemView.getId(), mList.get(position), position);
        return true;
      }
      return false;
    });
    return holder;
  }

  @Override
  public void onBindViewHolder(BaseBindingAdapter.BaseBindingViewHolder holder, final int position) {
    B binding = DataBindingUtil.getBinding(holder.itemView);
    this.onBindItem(binding, mList.get(position), holder);
    if (binding != null) binding.executePendingBindings();
  }

  protected abstract @LayoutRes
  int getLayoutResId(int viewType);

  protected abstract void onBindItem(B binding, M item, RecyclerView.ViewHolder holder);

  public List<M> getList() {
    return mList;
  }

  public void refresh(List<M> list) {
    mList.clear();
    mList.addAll(list);
    notifyDataSetChanged();
  }

  public void append(List<M> list) {
    mList.addAll(list);
    notifyDataSetChanged();
  }

  public static class BaseBindingViewHolder extends RecyclerView.ViewHolder {
    BaseBindingViewHolder(View itemView) {
      super(itemView);
    }
  }

  public interface OnItemClickListener<M> {
    void onItemClick(int viewId, M item, int position);
  }

  public interface OnItemLongClickListener<M> {
    void onItemLongClick(int viewId, M item, int position);
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/ui/bind/ClickProxy.java
================================================
package com.kunminx.architecture.ui.bind;

import android.view.View;
/**
 * Create by KunMinX at 2022/8/18
 */
public class ClickProxy implements View.OnClickListener {
  public View.OnClickListener listener;

  public void setOnClickListener(View.OnClickListener listener) {
    this.listener = listener;
  }
  @Override
  public void onClick(View view) {
    listener.onClick(view);
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/ui/bind/CommonBindingAdapter.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.architecture.ui.bind;

import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.databinding.BindingAdapter;

import com.kunminx.architecture.utils.Utils;

/**
 * Create by KunMinX at 19/9/18
 */
public class CommonBindingAdapter {
  @BindingAdapter(value = {"visible"}, requireAll = false)
  public static void visible(View view, boolean visible) {
    if (visible && view.getVisibility() == View.GONE) {
      view.setVisibility(View.VISIBLE);
    } else if (!visible && view.getVisibility() == View.VISIBLE) {
      view.setVisibility(View.GONE);
    }
  }

  @BindingAdapter(value = {"invisible"}, requireAll = false)
  public static void invisible(View view, boolean visible) {
    if (visible && view.getVisibility() == View.INVISIBLE) {
      view.setVisibility(View.VISIBLE);
    } else if (!visible && view.getVisibility() == View.VISIBLE) {
      view.setVisibility(View.INVISIBLE);
    }
  }

  @BindingAdapter(value = {"imgRes"}, requireAll = false)
  public static void setImageResource(ImageView imageView, int imgRes) {
    imageView.setImageResource(imgRes);
  }

  @BindingAdapter(value = {"textColor"}, requireAll = false)
  public static void setTextColor(TextView textView, int textColorRes) {
    textView.setTextColor(textView.getContext().getColor(textColorRes));
  }

  @BindingAdapter(value = {"selected"}, requireAll = false)
  public static void selected(View view, boolean select) {
    view.setSelected(select);
  }

  @BindingAdapter(value = {"clipToOutline"}, requireAll = false)
  public static void clipToOutline(View view, boolean clipToOutline) {
    view.setClipToOutline(clipToOutline);
  }

  @BindingAdapter(value = {"requestFocus"}, requireAll = false)
  public static void requestFocus(View view, boolean requestFocus) {
    if (requestFocus) view.requestFocus();
  }

  @BindingAdapter(value = {"showKeyboard"}, requireAll = false)
  public static void showKeyboard(View view, boolean showKeyboard) {
    InputMethodManager imm = (InputMethodManager) Utils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.architecture.ui.page;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModel;

import com.kunminx.architecture.ui.scope.ViewModelScope;
import com.kunminx.architecture.utils.AdaptScreenUtils;
import com.kunminx.architecture.utils.Utils;

/**
 * Create by KunMinX at 19/8/1
 */
public abstract class BaseActivity extends DataBindingActivity {

  private static final int STATUS_BAR_TRANSPARENT_COLOR = 0x00000000;
  private final ViewModelScope mViewModelScope = new ViewModelScope();

  protected void onOutput() {
  }

  protected void onInput() {
  }

  @SuppressLint("SourceLockedOrientationActivity")
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    transparentStatusBar(this);
    super.onCreate(savedInstanceState);
    onOutput();
    onInput();
  }

  public static void transparentStatusBar(@NonNull Activity activity) {
    Window window = activity.getWindow();
    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
    int vis = window.getDecorView().getSystemUiVisibility();
    window.getDecorView().setSystemUiVisibility(option | vis);
    window.setStatusBarColor(STATUS_BAR_TRANSPARENT_COLOR);
  }

  //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控,
  //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域,
  //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个,
  //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。

  //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840

  protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {
    return mViewModelScope.getActivityScopeViewModel(this, modelClass);
  }

  protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {
    return mViewModelScope.getApplicationScopeViewModel(modelClass);
  }

  @Override
  public Resources getResources() {
    if (Utils.getApp().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
      return AdaptScreenUtils.adaptWidth(super.getResources(), 360);
    } else {
      return AdaptScreenUtils.adaptHeight(super.getResources(), 640);
    }
  }

  protected void openUrlInBrowser(String url) {
    Uri uri = Uri.parse(url);
    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    startActivity(intent);
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java
================================================
/*
 * Copyright 2018-present KunMinX
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.architecture.ui.page;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;

import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModel;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;

import com.kunminx.architecture.ui.scope.ViewModelScope;

/**
 * Create by KunMinX at 19/7/11
 */
public abstract class BaseFragment extends DataBindingFragment {

  private final ViewModelScope mViewModelScope = new ViewModelScope();
  protected AppCompatActivity mActivity;

  protected void onOutput() {
  }

  protected void onInput() {
  }

  @Override
  public void onAttach(@NonNull Context context) {
    super.onAttach(context);
    mActivity = (AppCompatActivity) context;
  }

  @Override
  public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addOnBackPressed();
  }

  @Override
  public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    onOutput();
    onInput();
  }

  //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控,
  //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域,
  //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个,
  //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。

  //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840

  protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Class<T> modelClass) {
    return mViewModelScope.getFragmentScopeViewModel(this, modelClass);
  }

  protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull Class<T> modelClass) {
    return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass);
  }

  protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNull Class<T> modelClass) {
    return mViewModelScope.getApplicationScopeViewModel(modelClass);
  }

  protected NavController nav() {
    return NavHostFragment.findNavController(this);
  }

  protected void openUrlInBrowser(String url) {
    Uri uri = Uri.parse(url);
    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    startActivity(intent);
  }

  protected Context getAppContext() {
    return mActivity.getApplicationContext();
  }

  private void addOnBackPressed() {
    requireActivity().getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
      @Override
      public void handleOnBackPressed() {
        onBackPressed();
      }
    });
  }

  protected void onBackPressed() {
    nav().navigateUp();
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java
================================================
package com.kunminx.architecture.ui.page;

import androidx.lifecycle.ViewModel;

/**
 * Create by KunMinX at 2022/8/11
 */
public class StateHolder extends ViewModel {
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/ui/view/SwipeMenuLayout.java
================================================
package com.kunminx.architecture.ui.view;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.OvershootInterpolator;

import com.kunminx.architecture.R;

/**
 * Created by zhangxutong .
 * Date: 16/04/24
 */
public class SwipeMenuLayout extends ViewGroup {
  private static final String TAG = "zxt/SwipeMenuLayout";

  private int mScaleTouchSlop;
  private int mMaxVelocity;
  private int mPointerId;
  private int mRightMenuWidths;
  private int mLimit;
  private View mContentView;
  private final PointF mLastP = new PointF();
  private boolean isUnMoved = true;
  private final PointF mFirstP = new PointF();
  private boolean isUserSwiped;
  @SuppressLint("StaticFieldLeak")
  private static SwipeMenuLayout mViewCache;
  private static boolean isTouching;
  private VelocityTracker mVelocityTracker;
  private boolean isSwipeEnable;
  private boolean isIos;
  private boolean iosInterceptFlag;
  private boolean isLeftSwipe;

  public SwipeMenuLayout(Context context) {
    this(context, null);
  }

  public SwipeMenuLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs, defStyleAttr);
  }

  public boolean isSwipeEnable() {
    return isSwipeEnable;
  }

  public void setSwipeEnable(boolean swipeEnable) {
    isSwipeEnable = swipeEnable;
  }

  public boolean isIos() {
    return isIos;
  }

  public SwipeMenuLayout setIos(boolean ios) {
    isIos = ios;
    return this;
  }

  public boolean isLeftSwipe() {
    return isLeftSwipe;
  }

  public SwipeMenuLayout setLeftSwipe(boolean leftSwipe) {
    isLeftSwipe = leftSwipe;
    return this;
  }

  public static SwipeMenuLayout getViewCache() {
    return mViewCache;
  }

  private void init(Context context, AttributeSet attrs, int defStyleAttr) {
    mScaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
    isSwipeEnable = true;
    isIos = true;
    isLeftSwipe = true;
    TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout, defStyleAttr, 0);
    int count = ta.getIndexCount();
    for (int i = 0; i < count; i++) {
      int attr = ta.getIndex(i);
      if (attr == R.styleable.SwipeMenuLayout_swipeEnable) {
        isSwipeEnable = ta.getBoolean(attr, true);
      } else if (attr == R.styleable.SwipeMenuLayout_ios) {
        isIos = ta.getBoolean(attr, true);
      } else if (attr == R.styleable.SwipeMenuLayout_leftSwipe) {
        isLeftSwipe = ta.getBoolean(attr, true);
      }
    }
    ta.recycle();
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setClickable(true);
    mRightMenuWidths = 0;
    int height = 0;
    int contentWidth = 0;
    int childCount = getChildCount();
    final boolean measureMatchParentChildren = MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    boolean isNeedMeasureChildHeight = false;
    for (int i = 0; i < childCount; i++) {
      View childView = getChildAt(i);
      childView.setClickable(true);
      if (childView.getVisibility() != GONE) {
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
        height = Math.max(height, childView.getMeasuredHeight());
        if (measureMatchParentChildren && lp.height == LayoutParams.MATCH_PARENT) {
          isNeedMeasureChildHeight = true;
        }
        if (i > 0) {
          mRightMenuWidths += childView.getMeasuredWidth();
        } else {
          mContentView = childView;
          contentWidth = childView.getMeasuredWidth();
        }
      }
    }
    setMeasuredDimension(getPaddingLeft() + getPaddingRight() + contentWidth,
            height + getPaddingTop() + getPaddingBottom());
    mLimit = mRightMenuWidths * 4 / 10;
    if (isNeedMeasureChildHeight) {
      forceUniformHeight(childCount, widthMeasureSpec);
    }
  }

  @Override
  public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
  }

  private void forceUniformHeight(int count, int widthMeasureSpec) {
    int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),
            MeasureSpec.EXACTLY);
    for (int i = 0; i < count; ++i) {
      final View child = getChildAt(i);
      if (child.getVisibility() != GONE) {
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        if (lp.height == LayoutParams.MATCH_PARENT) {
          int oldWidth = lp.width;
          lp.width = child.getMeasuredWidth();
          measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0);
          lp.width = oldWidth;
        }
      }
    }
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    int left = getPaddingLeft();
    int right = getPaddingLeft();
    for (int i = 0; i < childCount; i++) {
      View childView = getChildAt(i);
      if (childView.getVisibility() != GONE) {
        if (i == 0) {
          childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
          left = left + childView.getMeasuredWidth();
        } else {
          if (isLeftSwipe) {
            childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
            left = left + childView.getMeasuredWidth();
          } else {
            childView.layout(right - childView.getMeasuredWidth(), getPaddingTop(), right, getPaddingTop() + childView.getMeasuredHeight());
            right = right - childView.getMeasuredWidth();
          }
        }
      }
    }
  }

  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    if (isSwipeEnable) {
      acquireVelocityTracker(ev);
      final VelocityTracker verTracker = mVelocityTracker;
      switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
          isUserSwiped = false;
          isUnMoved = true;
          iosInterceptFlag = false;
          if (isTouching) return false;
          else isTouching = true;
          mLastP.set(ev.getRawX(), ev.getRawY());
          mFirstP.set(ev.getRawX(), ev.getRawY());
          if (mViewCache != null) {
            if (mViewCache != this) {
              mViewCache.smoothClose();
              iosInterceptFlag = isIos;
            }
            getParent().requestDisallowInterceptTouchEvent(true);
          }
          mPointerId = ev.getPointerId(0);
          break;
        case MotionEvent.ACTION_MOVE:
          if (iosInterceptFlag) break;
          float gap = mLastP.x - ev.getRawX();
          if (Math.abs(gap) > 10 || Math.abs(getScrollX()) > 10) {
            getParent().requestDisallowInterceptTouchEvent(true);
          }
          if (Math.abs(gap) > mScaleTouchSlop) isUnMoved = false;
          scrollBy((int) (gap), 0);
          if (isLeftSwipe) {
            if (getScrollX() < 0) scrollTo(0, 0);
            if (getScrollX() > mRightMenuWidths) scrollTo(mRightMenuWidths, 0);
          } else {
            if (getScrollX() < -mRightMenuWidths) scrollTo(-mRightMenuWidths, 0);
            if (getScrollX() > 0) scrollTo(0, 0);
          }
          mLastP.set(ev.getRawX(), ev.getRawY());
          break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
          if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) {
            isUserSwiped = true;
          }

          if (!iosInterceptFlag) {
            verTracker.computeCurrentVelocity(1000, mMaxVelocity);
            final float velocityX = verTracker.getXVelocity(mPointerId);
            if (Math.abs(velocityX) > 1000) {
              if (velocityX < -1000) {
                if (isLeftSwipe) smoothExpand();
                else smoothClose();
              } else {
                if (isLeftSwipe) smoothClose();
                else smoothExpand();
              }
            } else {
              if (Math.abs(getScrollX()) > mLimit) smoothExpand();
              else smoothClose();
            }
          }
          releaseVelocityTracker();
          isTouching = false;
          break;
        default:
          break;
      }
    }
    return super.dispatchTouchEvent(ev);
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (isSwipeEnable) {
      switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
          if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) return true;
          break;
        case MotionEvent.ACTION_UP:
          if (isLeftSwipe) {
            if (getScrollX() > mScaleTouchSlop) {
              if (ev.getX() < getWidth() - getScrollX()) {
                if (isUnMoved) smoothClose();
                return true;
              }
            }
          } else {
            if (-getScrollX() > mScaleTouchSlop) {
              if (ev.getX() > -getScrollX()) {
                if (isUnMoved) smoothClose();
                return true;
              }
            }
          }
          if (isUserSwiped) return true;
          break;
      }
      if (iosInterceptFlag) {
        return true;
      }
    }
    return super.onInterceptTouchEvent(ev);
  }

  private ValueAnimator mExpandAnim, mCloseAnim;

  private boolean isExpand;

  public void smoothExpand() {
    mViewCache = SwipeMenuLayout.this;
    if (null != mContentView) {
      mContentView.setLongClickable(false);
    }

    cancelAnim();
    mExpandAnim = ValueAnimator.ofInt(getScrollX(), isLeftSwipe ? mRightMenuWidths : -mRightMenuWidths);
    mExpandAnim.addUpdateListener(animation -> scrollTo((Integer) animation.getAnimatedValue(), 0));
    mExpandAnim.setInterpolator(new OvershootInterpolator());
    mExpandAnim.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationEnd(Animator animation) {
        isExpand = true;
      }
    });
    mExpandAnim.setDuration(300).start();
  }

  private void cancelAnim() {
    if (mCloseAnim != null && mCloseAnim.isRunning()) {
      mCloseAnim.cancel();
    }
    if (mExpandAnim != null && mExpandAnim.isRunning()) {
      mExpandAnim.cancel();
    }
  }

  public void smoothClose() {
    mViewCache = null;
    if (null != mContentView) {
      mContentView.setLongClickable(true);
    }

    cancelAnim();
    mCloseAnim = ValueAnimator.ofInt(getScrollX(), 0);
    mCloseAnim.addUpdateListener(animation -> scrollTo((Integer) animation.getAnimatedValue(), 0));
    mCloseAnim.setInterpolator(new AccelerateInterpolator());
    mCloseAnim.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationEnd(Animator animation) {
        isExpand = false;

      }
    });
    mCloseAnim.setDuration(300).start();
  }

  private void acquireVelocityTracker(final MotionEvent event) {
    if (null == mVelocityTracker) {
      mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);
  }

  private void releaseVelocityTracker() {
    if (null != mVelocityTracker) {
      mVelocityTracker.clear();
      mVelocityTracker.recycle();
      mVelocityTracker = null;
    }
  }

  @Override
  protected void onDetachedFromWindow() {
    if (this == mViewCache) {
      mViewCache.smoothClose();
      mViewCache = null;
    }
    super.onDetachedFromWindow();
  }

  @Override
  public boolean performLongClick() {
    if (Math.abs(getScrollX()) > mScaleTouchSlop) {
      return false;
    }
    return super.performLongClick();
  }

  public void quickClose() {
    if (this == mViewCache) {
      cancelAnim();
      mViewCache.scrollTo(0, 0);
      mViewCache = null;
    }
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java
================================================
package com.kunminx.architecture.utils;

import android.content.res.Resources;
import android.util.DisplayMetrics;
import android.util.Log;

import java.lang.reflect.Field;

/**
 * <pre>
 *     author: Blankj
 *     blog  : http://blankj.com
 *     time  : 2016/09/23
 *     desc  : AdaptScreenUtils
 * </pre>
 */
public final class AdaptScreenUtils {

  private static boolean isInitMiui = false;
  private static Field mTmpMetricsField;

  public static Resources adaptWidth(Resources resources, int designWidth) {
    DisplayMetrics dm = getDisplayMetrics(resources);
    float newXdpi = dm.xdpi = (dm.widthPixels * 72f) / designWidth;
    setAppDmXdpi(newXdpi);
    return resources;
  }

  public static Resources adaptHeight(Resources resources, int designHeight) {
    DisplayMetrics dm = getDisplayMetrics(resources);
    float newXdpi = dm.xdpi = (dm.heightPixels * 72f) / designHeight;
    setAppDmXdpi(newXdpi);
    return resources;
  }

  private static void setAppDmXdpi(final float xdpi) {
    Utils.getApp().getResources().getDisplayMetrics().xdpi = xdpi;
  }

  private static DisplayMetrics getDisplayMetrics(Resources resources) {
    DisplayMetrics miuiDisplayMetrics = getMiuiTmpMetrics(resources);
    if (miuiDisplayMetrics == null) {
      return resources.getDisplayMetrics();
    }
    return miuiDisplayMetrics;
  }

  private static DisplayMetrics getMiuiTmpMetrics(Resources resources) {
    if (!isInitMiui) {
      DisplayMetrics ret = null;
      String simpleName = resources.getClass().getSimpleName();
      if ("MiuiResources".equals(simpleName) || "XResources".equals(simpleName)) {
        try {
          //noinspection JavaReflectionMemberAccess
          mTmpMetricsField = Resources.class.getDeclaredField("mTmpMetrics");
          mTmpMetricsField.setAccessible(true);
          ret = (DisplayMetrics) mTmpMetricsField.get(resources);
        } catch (Exception e) {
          Log.e("AdaptScreenUtils", "no field of mTmpMetrics in resources.");
        }
      }
      isInitMiui = true;
      return ret;
    }
    if (mTmpMetricsField == null) {
      return null;
    }
    try {
      return (DisplayMetrics) mTmpMetricsField.get(resources);
    } catch (Exception e) {
      return null;
    }
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/utils/TimeUtils.java
================================================
package com.kunminx.architecture.utils;

import android.annotation.SuppressLint;

import java.sql.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Objects;

/**
 * Create by KunMinX at 2022/6/30
 */
public class TimeUtils {

  public final static String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
  public final static String YY_MM_DD = "yy-MM-dd";
  public final static String HH_MM_SS = "HH:mm:ss";

  public static String getCurrentTime(String time_format) {
    @SuppressLint("SimpleDateFormat")
    SimpleDateFormat formatter = new SimpleDateFormat(time_format);
    Date curDate = new Date(System.currentTimeMillis());
    return formatter.format(curDate);
  }

  public static String getTime(long time, String format) {
    @SuppressLint("SimpleDateFormat")
    SimpleDateFormat formatter = new SimpleDateFormat(format);
    Date curDate = new Date(time);
    return formatter.format(curDate);
  }

  public static long getStringToDate(String dateString, String pattern) {
    @SuppressLint("SimpleDateFormat")
    SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
    Date date = null;
    try {
      date = (Date) dateFormat.parse(dateString);
    } catch (ParseException e) {
      e.printStackTrace();
    }
    return Objects.requireNonNull(date).getTime();
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java
================================================
package com.kunminx.architecture.utils;

import android.widget.Toast;

/**
 * Create by KunMinX at 2021/8/19
 */
public class ToastUtils {

  public static void showLongToast(String text) {
    Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_LONG).show();
  }

  public static void showShortToast(String text) {
    Toast.makeText(Utils.getApp().getApplicationContext(), text, Toast.LENGTH_SHORT).show();
  }
}


================================================
FILE: architecture/src/main/java/com/kunminx/architecture/utils/Utils.java
================================================
package com.kunminx.architecture.utils;

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;

import androidx.core.content.FileProvider;

import java.lang.reflect.InvocationTargetException;

/**
 * <pre>
 *     blog  : http://blankj.com
 *     time  : 16/12/08
 *     desc  : utils about initialization
 * </pre>
 */
public final class Utils {

  @SuppressLint("StaticFieldLeak")
  private static Application sApplication;

  private Utils() {
    throw new UnsupportedOperationException("u can't instantiate me...");
  }

  public static void init(final Context context) {
    if (context == null) {
      init(getApplicationByReflect());
      return;
    }
    init((Application) context.getApplicationContext());
  }

  public static void init(final Application app) {
    if (sApplication == null) {
      if (app == null) {
        sApplication = getApplicationByReflect();
      } else {
        sApplication = app;
      }
    } else {
      if (app != null && app.getClass() != sApplication.getClass()) {
        sApplication = app;
      }
    }
  }

  public static Application getApp() {
    if (sApplication != null) {
      return sApplication;
    }
    Application app = getApplicationByReflect();
    init(app);
    return app;
  }

  private static Application getApplicationByReflect() {
    try {
      @SuppressLint("PrivateApi")
      Class<?> activityThread = Class.forName("android.app.ActivityThread");
      Object thread = activityThread.getMethod("currentActivityThread").invoke(null);
      Object app = activityThread.getMethod("getApplication").invoke(thread);
      if (app == null) {
        throw new NullPointerException("u should init first");
      }
      return (Application) app;
    } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | InvocationTargetException e) {
      e.printStackTrace();
    }
    throw new NullPointerException("u should init first");
  }

  public static final class FileProvider4UtilCode extends FileProvider {
    @Override
    public boolean onCreate() {
      Utils.init(getContext());
      return true;
    }
  }
}


================================================
FILE: architecture/src/main/res/values/attrs.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SwipeMenuLayout">
        <attr name="swipeEnable" format="boolean"/>
        <attr name="ios" format="boolean"/>
        <attr name="leftSwipe" format="boolean"/>
    </declare-styleable>
</resources>

================================================
FILE: architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java
================================================
package com.kunminx.architecture;

import org.junit.Test;

import static org.junit.Assert.*;

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
public class ExampleUnitTest {
  @Test
  public void addition_isCorrect() {
    assertEquals(4, 2 + 2);
  }
}

================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.

apply plugin: 'io.codearte.nexus-staging'

buildscript {

    ext {
        appTargetSdk = 33
        appMinSdk = 15
        appVersionCode = 7060000
        appVersionName = "7.6.0"
        lifecycleVersion = "2.4.1"
        navigationVersion = "2.4.2"
    }

    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.3'
        classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
        classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0'
    }

    tasks.withType(Javadoc) {
        options.addStringOption('Xdoclint:none', '-quiet')
        options.addStringOption('encoding', 'UTF-8')
        options.addStringOption('charSet', 'UTF-8')
        options.encoding = 'UTF-8'
    }
}

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

task clean(type: Delete) {
    delete rootProject.buildDir
}


================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Thu Jun 30 19:13:32 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME


================================================
FILE: gradle.properties
================================================
android.enableJetifier=true
android.injected.testOnly=false
android.useAndroidX=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8
org.gradle.parallel=true

================================================
FILE: gradlew
================================================
#!/usr/bin/env sh

#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn () {
    echo "$*"
}

die () {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
  NONSTOP* )
    nonstop=true
    ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`

    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=`expr $i + 1`
    done
    case $i in
        0) set -- ;;
        1) set -- "$args0" ;;
        2) set -- "$args0" "$args1" ;;
        3) set -- "$args0" "$args1" "$args2" ;;
        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Escape application args
save () {
    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
    echo " "
}
APP_ARGS=`save "$@"`

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: keyvalue-dispatch/.gitignore
================================================
/build

================================================
FILE: keyvalue-dispatch/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
group = 'com.github.KunMinX'

ext {
    PUBLISH_GROUP_ID = 'com.kunminx.arch'
    PUBLISH_ARTIFACT_ID = 'keyvalue-dispatch'
    PUBLISH_VERSION = appVersionName
    VERSION_CODE = appVersionCode

    ARTIFACT_DESCRIPTION = 'Jetpack Single Source of Truth Library for android'

    POM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice'
    POM_SCM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice/tree/master'
    POM_SCM_CONNECTION='scm:git:github.com/KunMinX/Jetpack-MVI-Best-Practice.git'
    POM_SCM_DEV_CONNECTION='scm:git:ssh://github.com/KunMinX/Jetpack-MVI-Best-Practice.git'

    POM_DEVELOPER_ID='KunMinX'
    POM_DEVELOPER_NAME='KunMinX'
    POM_DEVELOPER_URL='https://github.com/KunMinX'
    POM_DEVELOPER_EMAIL='kunminx@gmail.com'

    LICENSE_NAME='The Apache Software License, Version 2.0'
    LICENSE_URL='http://www.apache.org/licenses/LICENSE-2.0.txt'

    uploadJavadocs = false
    uploadSources = false
}

apply from: "${rootProject.projectDir}/publish-mavencentral.gradle"

android {
    compileSdkVersion appTargetSdk
    defaultConfig {
        minSdkVersion appMinSdk
        targetSdkVersion appTargetSdk
        versionCode appVersionCode
        versionName appVersionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    implementation project(":mvi-dispatch")
}

================================================
FILE: keyvalue-dispatch/consumer-rules.pro
================================================


================================================
FILE: keyvalue-dispatch/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: keyvalue-dispatch/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java
================================================
package com.kunminx.architecture;

import android.content.Context;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
  @Test
  public void useAppContext() {
    // Context of the app under test.
    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
    assertEquals("com.kunminx.keyvalue_dispatcher.test", appContext.getPackageName());
  }
}

================================================
FILE: keyvalue-dispatch/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.kunminx.keyvalue_dispatch">

</manifest>

================================================
FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/GlobalConfigs.java
================================================
package com.kunminx.architecture.domain.dispatch;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;

import com.kunminx.architecture.domain.event.KeyValueMsg;

/**
 * TODO tip: 此处基于 KeyValueDispatcher 提供全局配置的默认实现,
 *  开发者亦可继承 KeyValueDispatcher 或效仿本类实现局部或全局用 KeyValueStore
 *
 * Create by KunMinX at 2022/8/15
 */
public class GlobalConfigs {
  private static final KeyValueDispatcher instance = new KeyValueDispatcher();

  private GlobalConfigs() {
  }

  public static void output(@NonNull AppCompatActivity activity, @NonNull Observer<KeyValueMsg> observer) {
    instance.output(activity, observer);
  }

  public static void output(@NonNull Fragment fragment, @NonNull Observer<KeyValueMsg> observer) {
    instance.output(fragment, observer);
  }

  public static void put(String key, String value) {
    instance.put(key, value);
  }

  public static void put(String key, Integer value) {
    instance.put(key, value);
  }

  public static void put(String key, Long value) {
    instance.put(key, value);
  }

  public static void put(String key, Float value) {
    instance.put(key, value);
  }

  public static void put(String key, Boolean value) {
    instance.put(key, value);
  }

  public static String getString(String key) {
    return instance.getString(key);
  }

  public static Integer getInt(String key) {
    return instance.getInt(key);
  }

  public static Long getLong(String key) {
    return instance.getLong(key);
  }

  public static Float getFloat(String key) {
    return instance.getFloat(key);
  }

  public static Boolean getBoolean(String key) {
    return instance.getBoolean(key);
  }
}


================================================
FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/KeyValueDispatcher.java
================================================
package com.kunminx.architecture.domain.dispatch;

import com.kunminx.architecture.domain.event.KeyValueMsg;
import com.kunminx.architecture.utils.SPUtils;

import java.util.HashMap;

/**
 * TODO tip:通过内聚,消除 Key Value Getter Putter init 样板代码,
 *  开发者只需声明 Key 列表即可使用 get() put(),
 *  且顺带可从唯一出口 output 处响应配置变化完成 UI 刷新
 *
 * Create by KunMinX at 2022/8/15
 */
public class KeyValueDispatcher extends MviDispatcher<KeyValueMsg> {
  private final HashMap<String, Object> keyValues = new HashMap<>();
  private final SPUtils mSPUtils = SPUtils.getInstance(moduleName());

  public String moduleName() {
    return "GlobalConfigs";
  }

  @Override
  protected void onHandle(KeyValueMsg intent) {
    sendResult(intent);
  }

  public void put(String key, String value) {
    keyValues.put(key, value);
    mSPUtils.put(key, value);
    input(new KeyValueMsg(key));
  }

  public void put(String key, Integer value) {
    keyValues.put(key, value);
    mSPUtils.put(key, value);
    input(new KeyValueMsg(key));
  }

  public void put(String key, Long value) {
    keyValues.put(key, value);
    mSPUtils.put(key, value);
    input(new KeyValueMsg(key));
  }

  public void put(String key, Float value) {
    keyValues.put(key, value);
    mSPUtils.put(key, value);
    input(new KeyValueMsg(key));
  }

  public void put(String key, Boolean value) {
    keyValues.put(key, value);
    mSPUtils.put(key, value);
    input(new KeyValueMsg(key));
  }

  public String getString(String key) {
    if (keyValues.get(key) == null) {
      keyValues.put(key, mSPUtils.getString(key, ""));
    }
    return (String) keyValues.get(key);
  }

  public Integer getInt(String key) {
    if (keyValues.get(key) == null) {
      keyValues.put(key, mSPUtils.getInt(key, 0));
    }
    return (Integer) keyValues.get(key);
  }

  public Long getLong(String key) {
    if (keyValues.get(key) == null) {
      keyValues.put(key, mSPUtils.getLong(key, 0));
    }
    return (Long) keyValues.get(key);
  }

  public Float getFloat(String key) {
    if (keyValues.get(key) == null) {
      keyValues.put(key, mSPUtils.getFloat(key, 0));
    }
    return (Float) keyValues.get(key);
  }

  public Boolean getBoolean(String key) {
    if (keyValues.get(key) == null) {
      keyValues.put(key, mSPUtils.getBoolean(key, false));
    }
    return (Boolean) keyValues.get(key);
  }
}


================================================
FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/event/KeyValueMsg.java
================================================
package com.kunminx.architecture.domain.event;

/**
 * Create by KunMinX at 2022/8/15
 */
public class KeyValueMsg {
  public final String currentKey;

  public KeyValueMsg(String currentKey) {
    this.currentKey = currentKey;
  }
}


================================================
FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/AppUtils.java
================================================
package com.kunminx.architecture.utils;

import android.annotation.SuppressLint;
import android.app.Application;

import java.lang.reflect.InvocationTargetException;

/**
 * <pre>
 *     author:
 *                                      ___           ___           ___         ___
 *         _____                       /  /\         /__/\         /__/|       /  /\
 *        /  /::\                     /  /::\        \  \:\       |  |:|      /  /:/
 *       /  /:/\:\    ___     ___    /  /:/\:\        \  \:\      |  |:|     /__/::\
 *      /  /:/~/::\  /__/\   /  /\  /  /:/~/::\   _____\__\:\   __|  |:|     \__\/\:\
 *     /__/:/ /:/\:| \  \:\ /  /:/ /__/:/ /:/\:\ /__/::::::::\ /__/\_|:|____    \  \:\
 *     \  \:\/:/~/:/  \  \:\  /:/  \  \:\/:/__\/ \  \:\~~\~~\/ \  \:\/:::::/     \__\:\
 *      \  \::/ /:/    \  \:\/:/    \  \::/       \  \:\  ~~~   \  \::/~~~~      /  /:/
 *       \  \:\/:/      \  \::/      \  \:\        \  \:\        \  \:\         /__/:/
 *        \  \::/        \__\/        \  \:\        \  \:\        \  \:\        \__\/
 *         \__\/                       \__\/         \__\/         \__\/
 *     blog  : http://blankj.com
 *     time  : 16/12/08
 *     desc  : utils about initialization
 * </pre>
 */
public final class AppUtils {

  @SuppressLint("StaticFieldLeak")
  private static Application sApplication;

  private AppUtils() {
    throw new UnsupportedOperationException("u can't instantiate me...");
  }

  public static Application getApp() {
    if (sApplication != null) {
      return sApplication;
    }
    sApplication = getApplicationByReflect();
    return sApplication;
  }

  private static Application getApplicationByReflect() {
    try {
      @SuppressLint("PrivateApi")
      Class<?> activityThread = Class.forName("android.app.ActivityThread");
      Object thread = activityThread.getMethod("currentActivityThread").invoke(null);
      Object app = activityThread.getMethod("getApplication").invoke(thread);
      if (app == null) {
        throw new NullPointerException("u should init first");
      }
      return (Application) app;
    } catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException | InvocationTargetException e) {
      e.printStackTrace();
    }
    throw new NullPointerException("u should init first");
  }
}


================================================
FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/SPUtils.java
================================================
package com.kunminx.architecture.utils;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.annotation.NonNull;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * <pre>
 *     author: Blankj
 *     blog  : http://blankj.com
 *     time  : 2016/08/02
 *     desc  : utils about shared preference
 * </pre>
 */
@SuppressLint("ApplySharedPref")
public final class SPUtils {

  private static final Map<String, SPUtils> SP_UTILS_MAP = new HashMap<>();
  private final SharedPreferences sp;

  private SPUtils(final String spName) {
    sp = AppUtils.getApp().getSharedPreferences(spName, Context.MODE_PRIVATE);
  }

  private SPUtils(final String spName, final int mode) {
    sp = AppUtils.getApp().getSharedPreferences(spName, mode);
  }

  /**
   * Return the single {@link SPUtils} instance
   *
   * @return the single {@link SPUtils} instance
   */
  public static SPUtils getInstance() {
    return getInstance("", Context.MODE_PRIVATE);
  }

  /**
   * Return the single {@link SPUtils} instance
   *
   * @param mode Operating mode.
   * @return the single {@link SPUtils} instance
   */
  public static SPUtils getInstance(final int mode) {
    return getInstance("", mode);
  }

  /**
   * Return the single {@link SPUtils} instance
   *
   * @param spName The name of sp.
   * @return the single {@link SPUtils} instance
   */
  public static SPUtils getInstance(String spName) {
    return getInstance(spName, Context.MODE_PRIVATE);
  }

  /**
   * Return the single {@link SPUtils} instance
   *
   * @param spName The name of sp.
   * @param mode   Operating mode.
   * @return the single {@link SPUtils} instance
   */
  public static SPUtils getInstance(String spName, final int mode) {
    if (isSpace(spName)) {
      spName = "spUtils";
    }
    SPUtils spUtils = SP_UTILS_MAP.get(spName);
    if (spUtils == null) {
      synchronized (SPUtils.class) {
        spUtils = SP_UTILS_MAP.get(spName);
        if (spUtils == null) {
          spUtils = new SPUtils(spName, mode);
          SP_UTILS_MAP.put(spName, spUtils);
        }
      }
    }
    return spUtils;
  }

  private static boolean isSpace(final String s) {
    if (s == null) {
      return true;
    }
    for (int i = 0, len = s.length(); i < len; ++i) {
      if (!Character.isWhitespace(s.charAt(i))) {
        return false;
      }
    }
    return true;
  }

  /**
   * Put the string value in sp.
   *
   * @param key   The key of sp.
   * @param value The value of sp.
   */
  public void put(@NonNull final String key, final String value) {
    put(key, value, false);
  }

  /**
   * Put the string value in sp.
   *
   * @param key      The key of sp.
   * @param value    The value of sp.
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void put(@NonNull final String key, final String value, final boolean isCommit) {
    if (isCommit) {
      sp.edit().putString(key, value).commit();
    } else {
      sp.edit().putString(key, value).apply();
    }
  }

  /**
   * Return the string value in sp.
   *
   * @param key The key of sp.
   * @return the string value if sp exists or {@code ""} otherwise
   */
  public String getString(@NonNull final String key) {
    return getString(key, "");
  }

  /**
   * Return the string value in sp.
   *
   * @param key          The key of sp.
   * @param defaultValue The default value if the sp doesn't exist.
   * @return the string value if sp exists or {@code defaultValue} otherwise
   */
  public String getString(@NonNull final String key, final String defaultValue) {
    return sp.getString(key, defaultValue);
  }

  /**
   * Put the int value in sp.
   *
   * @param key   The key of sp.
   * @param value The value of sp.
   */
  public void put(@NonNull final String key, final int value) {
    put(key, value, false);
  }

  /**
   * Put the int value in sp.
   *
   * @param key      The key of sp.
   * @param value    The value of sp.
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void put(@NonNull final String key, final int value, final boolean isCommit) {
    if (isCommit) {
      sp.edit().putInt(key, value).commit();
    } else {
      sp.edit().putInt(key, value).apply();
    }
  }

  /**
   * Return the int value in sp.
   *
   * @param key The key of sp.
   * @return the int value if sp exists or {@code -1} otherwise
   */
  public int getInt(@NonNull final String key) {
    return getInt(key, -1);
  }

  /**
   * Return the int value in sp.
   *
   * @param key          The key of sp.
   * @param defaultValue The default value if the sp doesn't exist.
   * @return the int value if sp exists or {@code defaultValue} otherwise
   */
  public int getInt(@NonNull final String key, final int defaultValue) {
    return sp.getInt(key, defaultValue);
  }

  /**
   * Put the long value in sp.
   *
   * @param key   The key of sp.
   * @param value The value of sp.
   */
  public void put(@NonNull final String key, final long value) {
    put(key, value, false);
  }

  /**
   * Put the long value in sp.
   *
   * @param key      The key of sp.
   * @param value    The value of sp.
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void put(@NonNull final String key, final long value, final boolean isCommit) {
    if (isCommit) {
      sp.edit().putLong(key, value).commit();
    } else {
      sp.edit().putLong(key, value).apply();
    }
  }

  /**
   * Return the long value in sp.
   *
   * @param key The key of sp.
   * @return the long value if sp exists or {@code -1} otherwise
   */
  public long getLong(@NonNull final String key) {
    return getLong(key, -1L);
  }

  /**
   * Return the long value in sp.
   *
   * @param key          The key of sp.
   * @param defaultValue The default value if the sp doesn't exist.
   * @return the long value if sp exists or {@code defaultValue} otherwise
   */
  public long getLong(@NonNull final String key, final long defaultValue) {
    return sp.getLong(key, defaultValue);
  }

  /**
   * Put the float value in sp.
   *
   * @param key   The key of sp.
   * @param value The value of sp.
   */
  public void put(@NonNull final String key, final float value) {
    put(key, value, false);
  }

  /**
   * Put the float value in sp.
   *
   * @param key      The key of sp.
   * @param value    The value of sp.
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void put(@NonNull final String key, final float value, final boolean isCommit) {
    if (isCommit) {
      sp.edit().putFloat(key, value).commit();
    } else {
      sp.edit().putFloat(key, value).apply();
    }
  }

  /**
   * Return the float value in sp.
   *
   * @param key The key of sp.
   * @return the float value if sp exists or {@code -1f} otherwise
   */
  public float getFloat(@NonNull final String key) {
    return getFloat(key, -1f);
  }

  /**
   * Return the float value in sp.
   *
   * @param key          The key of sp.
   * @param defaultValue The default value if the sp doesn't exist.
   * @return the float value if sp exists or {@code defaultValue} otherwise
   */
  public float getFloat(@NonNull final String key, final float defaultValue) {
    return sp.getFloat(key, defaultValue);
  }

  /**
   * Put the boolean value in sp.
   *
   * @param key   The key of sp.
   * @param value The value of sp.
   */
  public void put(@NonNull final String key, final boolean value) {
    put(key, value, false);
  }

  /**
   * Put the boolean value in sp.
   *
   * @param key      The key of sp.
   * @param value    The value of sp.
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void put(@NonNull final String key, final boolean value, final boolean isCommit) {
    if (isCommit) {
      sp.edit().putBoolean(key, value).commit();
    } else {
      sp.edit().putBoolean(key, value).apply();
    }
  }

  /**
   * Return the boolean value in sp.
   *
   * @param key The key of sp.
   * @return the boolean value if sp exists or {@code false} otherwise
   */
  public boolean getBoolean(@NonNull final String key) {
    return getBoolean(key, false);
  }

  /**
   * Return the boolean value in sp.
   *
   * @param key          The key of sp.
   * @param defaultValue The default value if the sp doesn't exist.
   * @return the boolean value if sp exists or {@code defaultValue} otherwise
   */
  public boolean getBoolean(@NonNull final String key, final boolean defaultValue) {
    return sp.getBoolean(key, defaultValue);
  }

  /**
   * Put the set of string value in sp.
   *
   * @param key   The key of sp.
   * @param value The value of sp.
   */
  public void put(@NonNull final String key, final Set<String> value) {
    put(key, value, false);
  }

  /**
   * Put the set of string value in sp.
   *
   * @param key      The key of sp.
   * @param value    The value of sp.
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void put(@NonNull final String key,
                  final Set<String> value,
                  final boolean isCommit) {
    if (isCommit) {
      sp.edit().putStringSet(key, value).commit();
    } else {
      sp.edit().putStringSet(key, value).apply();
    }
  }

  /**
   * Return the set of string value in sp.
   *
   * @param key The key of sp.
   * @return the set of string value if sp exists
   * or {@code Collections.<String>emptySet()} otherwise
   */
  public Set<String> getStringSet(@NonNull final String key) {
    return getStringSet(key, Collections.<String>emptySet());
  }

  /**
   * Return the set of string value in sp.
   *
   * @param key          The key of sp.
   * @param defaultValue The default value if the sp doesn't exist.
   * @return the set of string value if sp exists or {@code defaultValue} otherwise
   */
  public Set<String> getStringSet(@NonNull final String key,
                                  final Set<String> defaultValue) {
    return sp.getStringSet(key, defaultValue);
  }

  /**
   * Return all values in sp.
   *
   * @return all values in sp
   */
  public Map<String, ?> getAll() {
    return sp.getAll();
  }

  /**
   * Return whether the sp contains the preference.
   *
   * @param key The key of sp.
   * @return {@code true}: yes<br>{@code false}: no
   */
  public boolean contains(@NonNull final String key) {
    return sp.contains(key);
  }

  /**
   * Remove the preference in sp.
   *
   * @param key The key of sp.
   */
  public void remove(@NonNull final String key) {
    remove(key, false);
  }

  /**
   * Remove the preference in sp.
   *
   * @param key      The key of sp.
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void remove(@NonNull final String key, final boolean isCommit) {
    if (isCommit) {
      sp.edit().remove(key).commit();
    } else {
      sp.edit().remove(key).apply();
    }
  }

  /**
   * Remove all preferences in sp.
   */
  public void clear() {
    clear(false);
  }

  /**
   * Remove all preferences in sp.
   *
   * @param isCommit True to use {@link SharedPreferences.Editor#commit()},
   *                 false to use {@link SharedPreferences.Editor#apply()}
   */
  public void clear(final boolean isCommit) {
    if (isCommit) {
      sp.edit().clear().commit();
    } else {
      sp.edit().clear().apply();
    }
  }
}


================================================
FILE: keyvalue-dispatch/src/test/java/com/kunminx/architecture/ExampleUnitTest.java
================================================
package com.kunminx.architecture;

import org.junit.Test;

import static org.junit.Assert.*;

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
public class ExampleUnitTest {
  @Test
  public void addition_isCorrect() {
    assertEquals(4, 2 + 2);
  }
}

================================================
FILE: mvi-dispatch/.gitignore
================================================
/build

================================================
FILE: mvi-dispatch/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
group = 'com.github.KunMinX'

ext {
    PUBLISH_GROUP_ID = 'com.kunminx.arch'
    PUBLISH_ARTIFACT_ID = 'mvi-dispatch'
    PUBLISH_VERSION = appVersionName
    VERSION_CODE = appVersionCode

    ARTIFACT_DESCRIPTION = 'Jetpack Single Source of Truth Library for android'

    POM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice'
    POM_SCM_URL='https://github.com/KunMinX/Jetpack-MVI-Best-Practice/tree/master'
    POM_SCM_CONNECTION='scm:git:github.com/KunMinX/Jetpack-MVI-Best-Practice.git'
    POM_SCM_DEV_CONNECTION='scm:git:ssh://github.com/KunMinX/Jetpack-MVI-Best-Practice.git'

    POM_DEVELOPER_ID='KunMinX'
    POM_DEVELOPER_NAME='KunMinX'
    POM_DEVELOPER_URL='https://github.com/KunMinX'
    POM_DEVELOPER_EMAIL='kunminx@gmail.com'

    LICENSE_NAME='The Apache Software License, Version 2.0'
    LICENSE_URL='http://www.apache.org/licenses/LICENSE-2.0.txt'

    uploadJavadocs = false
    uploadSources = false
}

apply from: "${rootProject.projectDir}/publish-mavencentral.gradle"

android {
    compileSdkVersion appTargetSdk
    defaultConfig {
        minSdkVersion appMinSdk
        targetSdkVersion appTargetSdk
        versionCode appVersionCode
        versionName appVersionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.5.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

================================================
FILE: mvi-dispatch/consumer-rules.pro
================================================


================================================
FILE: mvi-dispatch/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: mvi-dispatch/src/androidTest/java/com/kunminx/dispatch/ExampleInstrumentedTest.java
================================================
package com.kunminx.dispatch;

import android.content.Context;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
  @Test
  public void useAppContext() {
    // Context of the app under test.
    Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
    assertEquals("com.kunminx.dispatch.test", appContext.getPackageName());
  }
}

================================================
FILE: mvi-dispatch/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.kunminx.dispatch">

</manifest>

================================================
FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/MviDispatcher.java
================================================
package com.kunminx.architecture.domain.dispatch;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;

import com.kunminx.architecture.domain.queue.FixedLengthList;
import com.kunminx.architecture.domain.result.OneTimeMessage;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Create by KunMinX at 2022/7/3
 */
public class MviDispatcher<T> extends ViewModel implements DefaultLifecycleObserver {
  private final static int DEFAULT_QUEUE_LENGTH = 10;
  private final HashMap<Integer, LifecycleOwner> mOwner = new HashMap<>();
  private final HashMap<Integer, LifecycleOwner> mFragmentOwner = new HashMap<>();
  private final HashMap<Integer, Observer<T>> mObservers = new HashMap<>();
  private final FixedLengthList<OneTimeMessage<T>> mResults = new FixedLengthList<>();

  protected int initQueueMaxLength() {
    return DEFAULT_QUEUE_LENGTH;
  }

  public final void output(@NonNull AppCompatActivity activity, @NonNull Observer<T> observer) {
    activity.getLifecycle().addObserver(this);
    Integer identityId = System.identityHashCode(activity);
    outputTo(identityId, activity, observer);
  }

  public final void output(@NonNull Fragment fragment, @NonNull Observer<T> observer) {
    fragment.getLifecycle().addObserver(this);
    Integer identityId = System.identityHashCode(fragment);
    this.mFragmentOwner.put(identityId, fragment);
    outputTo(identityId, fragment.getViewLifecycleOwner(), observer);
  }

  private void outputTo(Integer identityId, LifecycleOwner owner, Observer<T> observer) {
    this.mOwner.put(identityId, owner);
    this.mObservers.put(identityId, observer);
    for (OneTimeMessage<T> result : mResults) {
      result.observe(owner, observer);
    }
  }

  protected final void sendResult(@NonNull T intent) {
    mResults.init(initQueueMaxLength(), mutableResult -> {
      for (Map.Entry<Integer, Observer<T>> entry : mObservers.entrySet()) {
        Observer<T> observer = entry.getValue();
        mutableResult.removeObserver(observer);
      }
    });
    boolean eventExist = false;
    for (OneTimeMessage<T> result : mResults) {
      int id1 = System.identityHashCode(result.get());
      int id2 = System.identityHashCode(intent);
      if (id1 == id2) {
        eventExist = true;
        break;
      }
    }
    if (!eventExist) {
      OneTimeMessage<T> result = new OneTimeMessage<>(intent);
      for (Map.Entry<Integer, Observer<T>> entry : mObservers.entrySet()) {
        Integer key = entry.getKey();
        Observer<T> observer = entry.getValue();
        LifecycleOwner owner = mOwner.get(key);
        assert owner != null;
        result.observe(owner, observer);
      }
      mResults.add(result);
    }

    OneTimeMessage<T> result = null;
    for (OneTimeMessage<T> r : mResults) {
      int id1 = System.identityHashCode(r.get());
      int id2 = System.identityHashCode(intent);
      if (id1 == id2) {
        result = r;
        break;
      }
    }
    if (result != null) result.set(intent);
  }

  public final void input(T intent) {
    onHandle(intent);
  }

  protected void onHandle(T intent) {
  }

  @Override
  public void onDestroy(@NonNull LifecycleOwner owner) {
    DefaultLifecycleObserver.super.onDestroy(owner);
    boolean isFragment = owner instanceof Fragment;
    for (Map.Entry<Integer, LifecycleOwner> entry : isFragment ? mFragmentOwner.entrySet() : mOwner.entrySet()) {
      LifecycleOwner owner1 = entry.getValue();
      if (owner1.equals(owner)) {
        Integer key = entry.getKey();
        mOwner.remove(key);
        if (isFragment) mFragmentOwner.remove(key);
        for (OneTimeMessage<T> mutableResult : mResults) {
          mutableResult.removeObserver(Objects.requireNonNull(mObservers.get(key)));
        }
        mObservers.remove(key);
        break;
      }
    }
    if (mObservers.size() == 0) mResults.clear();
  }
}

================================================
FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/queue/FixedLengthList.java
================================================
package com.kunminx.architecture.domain.queue;

import java.util.LinkedList;

/**
 * Create by KunMinX at 2022/7/5
 */
public class FixedLengthList<T> extends LinkedList<T> {
  private int maxLength;
  private boolean hasBeenInit;
  private QueueCallback<T> queueCallback;

  public final void init(int maxLength, QueueCallback<T> queueCallback) {
    if (!hasBeenInit) {
      this.maxLength = maxLength;
      this.queueCallback = queueCallback;
      hasBeenInit = true;
    }
  }

  @Override
  public boolean add(T t) {
    if (size() + 1 > maxLength) {
      T t1 = super.removeFirst();
      if (queueCallback != null) queueCallback.onRemoveFirst(t1);
    }
    return super.add(t);
  }

  public interface QueueCallback<T> {
    void onRemoveFirst(T t);
  }
}


================================================
FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/OneTimeMessage.java
================================================
package com.kunminx.architecture.domain.result;

import static androidx.lifecycle.Lifecycle.State.DESTROYED;
import static androidx.lifecycle.Lifecycle.State.STARTED;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;

import java.util.Iterator;
import java.util.Map;

/**
 * Create by KunMinX at 2022/8/16
 */

public class OneTimeMessage<T> {

  private static final int START_VERSION = -1;
  private static final Object NOT_SET = new Object();

  private final SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>();
  private int mActiveCount = 0;
  private boolean mChangingActiveState;
  private volatile Object mData;
  private int mVersion;
  private int mCurrentVersion = START_VERSION;

  private boolean mDispatchingValue;
  private boolean mDispatchInvalidated;

  public OneTimeMessage(T value) {
    mData = value;
    mVersion = START_VERSION + 1;
  }

  @SuppressWarnings("unchecked")
  private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) return;
    if (!observer.shouldBeActive()) {
      observer.activeStateChanged(false);
      return;
    }
    if (observer.mLastVersion >= mVersion) return;
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
  }

  void dispatchingValue(@Nullable ObserverWrapper initiator) {
    if (mDispatchingValue) {
      mDispatchInvalidated = true;
      return;
    }
    mDispatchingValue = true;
    do {
      mDispatchInvalidated = false;
      if (initiator != null) {
        considerNotify(initiator);
        initiator = null;
      } else {
        for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
             mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
          considerNotify(iterator.next().getValue());
          if (mDispatchInvalidated) break;
        }
      }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
  }

  @MainThread
  public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    if (owner.getLifecycle().getCurrentState() == DESTROYED) return;
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing != null && !existing.isAttachedTo(owner))
      throw new IllegalArgumentException("Cannot add the same observer with different lifecycles");
    if (existing != null) return;
    mCurrentVersion = mVersion;
    owner.getLifecycle().addObserver(wrapper);
  }

  @MainThread
  public void removeObserver(@NonNull final Observer<? super T> observer) {
    ObserverWrapper removed = mObservers.remove(observer);
    if (removed == null) return;
    removed.detachObserver();
    removed.activeStateChanged(false);
  }

  @MainThread
  public void set(T value) {
    mVersion++;
    mData = value;
    dispatchingValue(null);
  }

  @SuppressWarnings("unchecked")
  @Nullable
  public T get() {
    Object data = mData;
    if (data != NOT_SET) return (T) data;
    return null;
  }

  @MainThread
  void changeActiveCounter(int change) {
    int previousActiveCount = mActiveCount;
    mActiveCount += change;
    if (mChangingActiveState) return;
    mChangingActiveState = true;
    try {
      while (previousActiveCount != mActiveCount) previousActiveCount = mActiveCount;
    } finally {
      mChangingActiveState = false;
    }
  }

  class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
      super(observer);
      mOwner = owner;
    }

    @Override
    boolean shouldBeActive() {
      return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    @Override
    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
      Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
      if (currentState == DESTROYED) {
        removeObserver(mObserver);
        return;
      }
      Lifecycle.State prevState = null;
      while (prevState != currentState) {
        prevState = currentState;
        activeStateChanged(shouldBeActive());
        currentState = mOwner.getLifecycle().getCurrentState();
      }
    }

    @Override
    boolean isAttachedTo(LifecycleOwner owner) {
      return mOwner == owner;
    }

    @Override
    void detachObserver() {
      mOwner.getLifecycle().removeObserver(this);
    }
  }

  private abstract class ObserverWrapper {
    final Observer<? super T> mObserver;
    boolean mActive;
    int mLastVersion = START_VERSION;

    ObserverWrapper(Observer<? super T> observer) {
      mObserver = observer;
    }

    abstract boolean shouldBeActive();

    boolean isAttachedTo(LifecycleOwner owner) {
      return false;
    }

    void detachObserver() {
    }

    void activeStateChanged(boolean newActive) {
      if (newActive == mActive) return;
      mActive = newActive;
      changeActiveCounter(mActive ? 1 : -1);
      if (mActive && mVersion > mCurrentVersion) dispatchingValue(this);
    }
  }
}



================================================
FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/SafeIterableMap.java
================================================
/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kunminx.architecture.domain.result;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;

import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * LinkedList, which pretends to be a map and supports modifications during iterations.
 * It is NOT thread safe.
 *
 * @param <K> Key type
 * @param <V> Value type
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class SafeIterableMap<K, V> implements Iterable<Map.Entry<K, V>> {

  @SuppressWarnings("WeakerAccess") /* synthetic access */
          Entry<K, V> mStart;
  private Entry<K, V> mEnd;
  // using WeakHashMap over List<WeakReference>, so we don't have to manually remove
  // WeakReferences that have null in them.
  private WeakHashMap<SupportRemove<K, V>, Boolean> mIterators = new WeakHashMap<>();
  private int mSize = 0;

  protected Entry<K, V> get(K k) {
    Entry<K, V> currentNode = mStart;
    while (currentNode != null) {
      if (currentNode.mKey.equals(k)) {
        break;
      }
      currentNode = currentNode.mNext;
    }
    return currentNode;
  }

  /**
   * If the specified key is not already associated
   * with a value, associates it with the given value.
   *
   * @param key key with which the specified value is to be associated
   * @param v   value to be associated with the specified key
   * @return the previous value associated with the specified key,
   * or {@code null} if there was no mapping for the key
   */
  public V putIfAbsent(@NonNull K key, @NonNull V v) {
    Entry<K, V> entry = get(key);
    if (entry != null) {
      return entry.mValue;
    }
    put(key, v);
    return null;
  }

  protected Entry<K, V> put(@NonNull K key, @NonNull V v) {
    Entry<K, V> newEntry = new Entry<>(key, v);
    mSize++;
    if (mEnd == null) {
      mStart = newEntry;
      mEnd = mStart;
      return newEntry;
    }

    mEnd.mNext = newEntry;
    newEntry.mPrevious = mEnd;
    mEnd = newEntry;
    return newEntry;

  }

  /**
   * Removes the mapping for a key from this map if it is present.
   *
   * @param key key whose mapping is to be removed from the map
   * @return the previous value associated with the specified key,
   * or {@code null} if there was no mapping for the key
   */
  public V remove(@NonNull K key) {
    Entry<K, V> toRemove = get(key);
    if (toRemove == null) {
      return null;
    }
    mSize--;
    if (!mIterators.isEmpty()) {
      for (SupportRemove<K, V> iter : mIterators.keySet()) {
        iter.supportRemove(toRemove);
      }
    }

    if (toRemove.mPrevious != null) {
      toRemove.mPrevious.mNext = toRemove.mNext;
    } else {
      mStart = toRemove.mNext;
    }

    if (toRemove.mNext != null) {
      toRemove.mNext.mPrevious = toRemove.mPrevious;
    } else {
      mEnd = toRemove.mPrevious;
    }

    toRemove.mNext = null;
    toRemove.mPrevious = null;
    return toRemove.mValue;
  }

  /**
   * @return the number of elements in this map
   */
  public int size() {
    return mSize;
  }

  /**
   * @return an ascending iterator, which doesn't include new elements added during an
   * iteration.
   */
  @NonNull
  @Override
  public Iterator<Map.Entry<K, V>> iterator() {
    ListIterator<K, V> iterator = new AscendingIterator<>(mStart, mEnd);
    mIterators.put(iterator, false);
    return iterator;
  }

  /**
   * @return an descending iterator, which doesn't include new elements added during an
   * iteration.
   */
  public Iterator<Map.Entry<K, V>> descendingIterator() {
    DescendingIterator<K, V> iterator = new DescendingIterator<>(mEnd, mStart);
    mIterators.put(iterator, false);
    return iterator;
  }

  /**
   * return an iterator with additions.
   */
  public IteratorWithAdditions iteratorWithAdditions() {
    @SuppressWarnings("unchecked")
    IteratorWithAdditions iterator = new IteratorWithAdditions();
    mIterators.put(iterator, false);
    return iterator;
  }

  /**
   * @return eldest added entry or null
   */
  public Map.Entry<K, V> eldest() {
    return mStart;
  }

  /**
   * @return newest added entry or null
   */
  public Map.Entry<K, V> newest() {
    return mEnd;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    }
    if (!(obj instanceof androidx.arch.core.internal.SafeIterableMap)) {
      return false;
    }
    androidx.arch.core.internal.SafeIterableMap map = (androidx.arch.core.internal.SafeIterableMap) obj;
    if (this.size() != map.size()) {
      return false;
    }
    Iterator<Map.Entry<K, V>> iterator1 = iterator();
    Iterator iterator2 = map.iterator();
    while (iterator1.hasNext() && iterator2.hasNext()) {
      Map.Entry<K, V> next1 = iterator1.next();
      Object next2 = iterator2.next();
      if ((next1 == null && next2 != null)
              || (next1 != null && !next1.equals(next2))) {
        return false;
      }
    }
    return !iterator1.hasNext() && !iterator2.hasNext();
  }

  @Override
  public int hashCode() {
    int h = 0;
    Iterator<Map.Entry<K, V>> i = iterator();
    while (i.hasNext()) {
      h += i.next().hashCode();
    }
    return h;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("[");
    Iterator<Map.Entry<K, V>> iterator = iterator();
    while (iterator.hasNext()) {
      builder.append(iterator.next().toString());
      if (iterator.hasNext()) {
        builder.append(", ");
      }
    }
    builder.append("]");
    return builder.toString();
  }

  private abstract static class ListIterator<K, V> implements Iterator<Map.Entry<K, V>>,
          SupportRemove<K, V> {
    Entry<K, V> mExpectedEnd;
    Entry<K, V> mNext;

    ListIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
      this.mExpectedEnd = expectedEnd;
      this.mNext = start;
    }

    @Override
    public boolean hasNext() {
      return mNext != null;
    }

    @SuppressWarnings("ReferenceEquality")
    @Override
    public void supportRemove(@NonNull Entry<K, V> entry) {
      if (mExpectedEnd == entry && entry == mNext) {
        mNext = null;
        mExpectedEnd = null;
      }

      if (mExpectedEnd == entry) {
        mExpectedEnd = backward(mExpectedEnd);
      }

      if (mNext == entry) {
        mNext = nextNode();
      }
    }

    @SuppressWarnings("ReferenceEquality")
    private Entry<K, V> nextNode() {
      if (mNext == mExpectedEnd || mExpectedEnd == null) {
        return null;
      }
      return forward(mNext);
    }

    @Override
    public Map.Entry<K, V> next() {
      Map.Entry<K, V> result = mNext;
      mNext = nextNode();
      return result;
    }

    abstract Entry<K, V> forward(Entry<K, V> entry);

    abstract Entry<K, V> backward(Entry<K, V> entry);
  }

  static class AscendingIterator<K, V> extends ListIterator<K, V> {
    AscendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
      super(start, expectedEnd);
    }

    @Override
    Entry<K, V> forward(Entry<K, V> entry) {
      return entry.mNext;
    }

    @Override
    Entry<K, V> backward(Entry<K, V> entry) {
      return entry.mPrevious;
    }
  }

  private static class DescendingIterator<K, V> extends ListIterator<K, V> {

    DescendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
      super(start, expectedEnd);
    }

    @Override
    Entry<K, V> forward(Entry<K, V> entry) {
      return entry.mPrevious;
    }

    @Override
    Entry<K, V> backward(Entry<K, V> entry) {
      return entry.mNext;
    }
  }

  private class IteratorWithAdditions implements Iterator<Map.Entry<K, V>>, SupportRemove<K, V> {
    private Entry<K, V> mCurrent;
    private boolean mBeforeStart = true;

    IteratorWithAdditions() {
    }

    @SuppressWarnings("ReferenceEquality")
    @Override
    public void supportRemove(@NonNul
Download .txt
gitextract_ntzdf0fx/

├── .gitignore
├── LICENSE
├── README.md
├── README_EN.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── purenote/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── purenote/
│       │   │               ├── App.java
│       │   │               ├── data/
│       │   │               │   ├── bean/
│       │   │               │   │   ├── Note.java
│       │   │               │   │   └── Weather.java
│       │   │               │   ├── config/
│       │   │               │   │   └── Key.java
│       │   │               │   └── repo/
│       │   │               │       ├── DataRepository.java
│       │   │               │       ├── NoteDao.java
│       │   │               │       ├── NoteDataBase.java
│       │   │               │       └── WeatherService.java
│       │   │               ├── domain/
│       │   │               │   ├── intent/
│       │   │               │   │   ├── _Api.java
│       │   │               │   │   ├── _ComplexIntent.java
│       │   │               │   │   ├── _Messages.java
│       │   │               │   │   └── _NoteIntent.java
│       │   │               │   ├── message/
│       │   │               │   │   └── PageMessenger.java
│       │   │               │   └── request/
│       │   │               │       ├── ComplexRequester.java
│       │   │               │       ├── NoteRequester.java
│       │   │               │       └── WeatherRequester.java
│       │   │               └── ui/
│       │   │                   ├── adapter/
│       │   │                   │   └── NoteAdapter.java
│       │   │                   └── page/
│       │   │                       ├── EditorFragment.java
│       │   │                       ├── ListFragment.java
│       │   │                       ├── MainActivity.java
│       │   │                       └── SettingFragment.java
│       │   └── res/
│       │       ├── anim/
│       │       │   ├── x_fragment_enter.xml
│       │       │   ├── x_fragment_exit.xml
│       │       │   ├── x_fragment_pop_enter.xml
│       │       │   └── x_fragment_pop_exit.xml
│       │       ├── drawable/
│       │       │   ├── ic_baseline_add.xml
│       │       │   └── ic_baseline_arrow_back.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── adapter_note_list.xml
│       │       │   ├── fragment_editor.xml
│       │       │   ├── fragment_list.xml
│       │       │   └── fragment_settings.xml
│       │       ├── navigation/
│       │       │   └── nav_graph.xml
│       │       └── values/
│       │           ├── colors.xml
│       │           ├── dimens.xml
│       │           ├── strings.xml
│       │           └── themes.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── purenote/
│                           └── ExampleUnitTest.java
├── architecture/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── kunminx/
│       │   │           └── architecture/
│       │   │               ├── data/
│       │   │               │   └── response/
│       │   │               │       ├── AsyncTask.java
│       │   │               │       ├── DataResult.java
│       │   │               │       ├── ResponseStatus.java
│       │   │               │       └── ResultSource.java
│       │   │               ├── ui/
│       │   │               │   ├── adapter/
│       │   │               │   │   └── BaseBindingAdapter.java
│       │   │               │   ├── bind/
│       │   │               │   │   ├── ClickProxy.java
│       │   │               │   │   └── CommonBindingAdapter.java
│       │   │               │   ├── page/
│       │   │               │   │   ├── BaseActivity.java
│       │   │               │   │   ├── BaseFragment.java
│       │   │               │   │   └── StateHolder.java
│       │   │               │   └── view/
│       │   │               │       └── SwipeMenuLayout.java
│       │   │               └── utils/
│       │   │                   ├── AdaptScreenUtils.java
│       │   │                   ├── TimeUtils.java
│       │   │                   ├── ToastUtils.java
│       │   │                   └── Utils.java
│       │   └── res/
│       │       └── values/
│       │           └── attrs.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── architecture/
│                           └── ExampleUnitTest.java
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── keyvalue-dispatch/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   ├── domain/
│       │                   │   ├── dispatch/
│       │                   │   │   ├── GlobalConfigs.java
│       │                   │   │   └── KeyValueDispatcher.java
│       │                   │   └── event/
│       │                   │       └── KeyValueMsg.java
│       │                   └── utils/
│       │                       ├── AppUtils.java
│       │                       └── SPUtils.java
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── architecture/
│                           └── ExampleUnitTest.java
├── mvi-dispatch/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── dispatch/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   └── java/
│       │       └── com/
│       │           └── kunminx/
│       │               └── architecture/
│       │                   ├── domain/
│       │                   │   ├── dispatch/
│       │                   │   │   └── MviDispatcher.java
│       │                   │   ├── queue/
│       │                   │   │   └── FixedLengthList.java
│       │                   │   └── result/
│       │                   │       ├── OneTimeMessage.java
│       │                   │       └── SafeIterableMap.java
│       │                   └── ui/
│       │                       └── scope/
│       │                           ├── ApplicationInstance.java
│       │                           └── ViewModelScope.java
│       └── test/
│           └── java/
│               └── com/
│                   └── kunminx/
│                       └── dispatch/
│                           └── ExampleUnitTest.java
├── publish-mavencentral.gradle
└── settings.gradle
Download .txt
SYMBOL INDEX (414 symbols across 55 files)

FILE: app/src/androidTest/java/com/kunminx/purenote/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 18) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 20) | @Test

FILE: app/src/main/java/com/kunminx/purenote/App.java
  class App (line 10) | public class App extends Application {
    method onCreate (line 11) | @Override

FILE: app/src/main/java/com/kunminx/purenote/data/bean/Note.java
  class Note (line 18) | @Entity
    method getCreateDate (line 39) | @Ignore
    method getModifyDate (line 44) | @Ignore
    method isMarked (line 49) | @Ignore
    method isTopping (line 54) | @Ignore
    method toggleType (line 59) | @Ignore
    method markIcon (line 68) | @Ignore
    method Note (line 73) | @Ignore
    method Note (line 77) | public Note(@NonNull String id, String title, String content, long cre...
    method getId (line 86) | @NonNull
    method getTitle (line 90) | public String getTitle() {
    method getContent (line 93) | public String getContent() {
    method getCreateTime (line 96) | public long getCreateTime() {
    method getModifyTime (line 99) | public long getModifyTime() {
    method getType (line 102) | public int getType() {
    method Note (line 106) | protected Note(Parcel in) {
    method createFromParcel (line 116) | @Override
    method newArray (line 121) | @Override
    method describeContents (line 127) | @Override
    method writeToParcel (line 132) | @Override

FILE: app/src/main/java/com/kunminx/purenote/data/bean/Weather.java
  class Weather (line 7) | public class Weather {
    method getStatus (line 14) | public String getStatus() {
    method getCount (line 17) | public String getCount() {
    method getInfo (line 20) | public String getInfo() {
    method getInfocode (line 23) | public String getInfocode() {
    method getLives (line 26) | public List<Live> getLives() {
    class Live (line 30) | public static class Live {
      method getCity (line 35) | public String getCity() {
      method getWeather (line 38) | public String getWeather() {
      method getTemperature (line 41) | public String getTemperature() {

FILE: app/src/main/java/com/kunminx/purenote/data/config/Key.java
  class Key (line 6) | public class Key {

FILE: app/src/main/java/com/kunminx/purenote/data/repo/DataRepository.java
  class DataRepository (line 33) | public class DataRepository {
    method getInstance (line 62) | public static DataRepository getInstance() {
    method DataRepository (line 66) | private DataRepository() {
    method getNotes (line 71) | public Observable<List<Note>> getNotes() {
    method insertNote (line 75) | public Observable<Boolean> insertNote(Note note) {
    method updateNote (line 82) | public Observable<Boolean> updateNote(Note note) {
    method deleteNote (line 89) | public Observable<Boolean> deleteNote(Note note) {
    method getWeatherInfo (line 96) | @SuppressLint("CheckResult")

FILE: app/src/main/java/com/kunminx/purenote/data/repo/NoteDao.java
  type NoteDao (line 17) | @Dao
    method getNotes (line 20) | @Query("select * from note order by type & 0x0001 = 0x0001 desc, modif...
    method insertNote (line 23) | @Insert(onConflict = OnConflictStrategy.REPLACE)
    method updateNote (line 26) | @Update()
    method deleteNote (line 29) | @Delete

FILE: app/src/main/java/com/kunminx/purenote/data/repo/NoteDataBase.java
  class NoteDataBase (line 11) | @Database(entities = {Note.class}, version = 1, exportSchema = false)
    method noteDao (line 13) | public abstract NoteDao noteDao();

FILE: app/src/main/java/com/kunminx/purenote/data/repo/WeatherService.java
  type WeatherService (line 12) | public interface WeatherService {
    method getWeatherInfo (line 13) | @GET("weather/weatherInfo")

FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_Api.java
  type _Api (line 20) | @SealedClass
    method onLoading (line 22) | void onLoading(boolean isLoading);
    method getWeatherInfo (line 23) | void getWeatherInfo(@Param String cityCode, Weather.Live live);
    method onError (line 24) | void onError(String errorInfo);

FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_ComplexIntent.java
  type _ComplexIntent (line 19) | @SealedClass
    method test1 (line 21) | void test1(@Param int count, int count1);
    method test2 (line 22) | void test2(@Param int count, int count1);
    method test3 (line 23) | void test3(@Param int count, int count1);
    method test4 (line 24) | void test4(@Param int count, int count1);

FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_Messages.java
  type _Messages (line 18) | @SealedClass
    method refreshNoteList (line 20) | void refreshNoteList();
    method finishActivity (line 21) | void finishActivity();

FILE: app/src/main/java/com/kunminx/purenote/domain/intent/_NoteIntent.java
  type _NoteIntent (line 22) | @SealedClass
    method getNoteList (line 24) | void getNoteList(List<Note> notes);
    method removeItem (line 25) | void removeItem(@Param Note note, boolean isSuccess);
    method updateItem (line 26) | void updateItem(@Param Note note, boolean isSuccess);
    method markItem (line 27) | void markItem(@Param Note note, boolean isSuccess);
    method toppingItem (line 28) | void toppingItem(@Param Note note, boolean isSuccess);
    method addItem (line 29) | void addItem(@Param Note note, boolean isSuccess);
    method initItem (line 30) | void initItem(@Param Note note);

FILE: app/src/main/java/com/kunminx/purenote/domain/message/PageMessenger.java
  class PageMessenger (line 9) | public class PageMessenger extends MviDispatcher<Messages> {
    method onHandle (line 19) | @Override

FILE: app/src/main/java/com/kunminx/purenote/domain/request/ComplexRequester.java
  class ComplexRequester (line 24) | public class ComplexRequester extends MviDispatcher<ComplexIntent> {
    method initQueueMaxLength (line 31) | @Override
    method onHandle (line 44) | @Override

FILE: app/src/main/java/com/kunminx/purenote/domain/request/NoteRequester.java
  class NoteRequester (line 18) | public class NoteRequester extends MviDispatcher<NoteIntent> {
    method onHandle (line 28) | @Override

FILE: app/src/main/java/com/kunminx/purenote/domain/request/WeatherRequester.java
  class WeatherRequester (line 22) | public class WeatherRequester extends MviDispatcher<Api> {
    method onHandle (line 33) | @Override

FILE: app/src/main/java/com/kunminx/purenote/ui/adapter/NoteAdapter.java
  class NoteAdapter (line 15) | public class NoteAdapter extends BaseBindingAdapter<Note, AdapterNoteLis...
    method NoteAdapter (line 17) | public NoteAdapter(List<Note> list) {
    method getLayoutResId (line 21) | @Override
    method onBindItem (line 26) | @Override
    method getItemCount (line 51) | @Override

FILE: app/src/main/java/com/kunminx/purenote/ui/page/EditorFragment.java
  class EditorFragment (line 29) | public class EditorFragment extends BaseFragment {
    method start (line 36) | public static void start(NavController controller, Note note) {
    method initViewModel (line 42) | @Override
    method getDataBindingConfig (line 49) | @Override
    method onOutput (line 60) | @Override
    method onInput (line 86) | @Override
    method save (line 95) | private void save() {
    method onBackPressed (line 113) | @Override
    class EditorStates (line 127) | public static class EditorStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/purenote/ui/page/ListFragment.java
  class ListFragment (line 29) | public class ListFragment extends BaseFragment {
    method initViewModel (line 37) | @Override
    method getDataBindingConfig (line 45) | @Override
    method onOutput (line 57) | @Override
    method onInput (line 111) | @Override
    method onBackPressed (line 132) | @Override
    class ListStates (line 146) | public static class ListStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/purenote/ui/page/MainActivity.java
  class MainActivity (line 17) | public class MainActivity extends BaseActivity {
    method initViewModel (line 22) | @Override
    method getDataBindingConfig (line 28) | @Override
    method onOutput (line 38) | @Override
    method onInput (line 69) | @Override
    class MainAtyStates (line 84) | public static class MainAtyStates extends StateHolder {

FILE: app/src/main/java/com/kunminx/purenote/ui/page/SettingFragment.java
  class SettingFragment (line 16) | public class SettingFragment extends BaseFragment {
    method initViewModel (line 20) | @Override
    method getDataBindingConfig (line 25) | @Override
    method onInput (line 37) | @Override
    class SettingStates (line 57) | public static class SettingStates extends StateHolder {

FILE: app/src/test/java/com/kunminx/purenote/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 13) | @Test

FILE: architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 18) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 20) | @Test

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/AsyncTask.java
  class AsyncTask (line 16) | public class AsyncTask {
    method doIO (line 18) | @SuppressLint("CheckResult")
    method doCalculate (line 25) | @SuppressLint("CheckResult")
    type Action (line 32) | public interface Action<T> {
      method onEmit (line 33) | void onEmit(ObservableEmitter<T> emitter);
    type Observer (line 36) | public interface Observer<T> extends io.reactivex.Observer<T> {
      method onSubscribe (line 37) | default void onSubscribe(@NonNull Disposable d) {
      method onNext (line 40) | void onNext(@NonNull T t);
      method onError (line 42) | default void onError(@NonNull Throwable e) {
      method onComplete (line 45) | default void onComplete() {

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java
  class DataResult (line 24) | public class DataResult<T> {
    method DataResult (line 29) | public DataResult(T entity, ResponseStatus responseStatus) {
    method DataResult (line 34) | public DataResult(T entity) {
    method getResult (line 39) | public T getResult() {
    method getResponseStatus (line 43) | public ResponseStatus getResponseStatus() {
    type Result (line 47) | public interface Result<T> {
      method onResult (line 48) | void onResult(DataResult<T> dataResult);

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java
  class ResponseStatus (line 22) | public class ResponseStatus {
    method ResponseStatus (line 29) | public ResponseStatus() {
    method ResponseStatus (line 32) | public ResponseStatus(String msg) {
    method ResponseStatus (line 37) | public ResponseStatus(String msg, String responseCode, boolean success) {
    method ResponseStatus (line 43) | public ResponseStatus(String responseCode, boolean success) {
    method ResponseStatus (line 48) | public ResponseStatus(String responseCode, boolean success, Enum<Resul...
    method getResponseCode (line 53) | public String getResponseCode() {
    method getMsg (line 57) | public String getMsg() {
    method isSuccess (line 61) | public boolean isSuccess() {
    method getSource (line 65) | public Enum<ResultSource> getSource() {

FILE: architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java
  type ResultSource (line 6) | public enum ResultSource {

FILE: architecture/src/main/java/com/kunminx/architecture/ui/adapter/BaseBindingAdapter.java
  class BaseBindingAdapter (line 17) | public abstract class BaseBindingAdapter<M, B extends ViewDataBinding>
    method BaseBindingAdapter (line 24) | public BaseBindingAdapter(List<M> list) {
    method setOnItemClickListener (line 28) | public void setOnItemClickListener(OnItemClickListener<M> onItemClickL...
    method setOnItemLongClickListener (line 32) | public void setOnItemLongClickListener(OnItemLongClickListener<M> onIt...
    method onCreateViewHolder (line 36) | @Override
    method onBindViewHolder (line 58) | @Override
    method getLayoutResId (line 65) | protected abstract @LayoutRes
    method onBindItem (line 68) | protected abstract void onBindItem(B binding, M item, RecyclerView.Vie...
    method getList (line 70) | public List<M> getList() {
    method refresh (line 74) | public void refresh(List<M> list) {
    method append (line 80) | public void append(List<M> list) {
    class BaseBindingViewHolder (line 85) | public static class BaseBindingViewHolder extends RecyclerView.ViewHol...
      method BaseBindingViewHolder (line 86) | BaseBindingViewHolder(View itemView) {
    type OnItemClickListener (line 91) | public interface OnItemClickListener<M> {
      method onItemClick (line 92) | void onItemClick(int viewId, M item, int position);
    type OnItemLongClickListener (line 95) | public interface OnItemLongClickListener<M> {
      method onItemLongClick (line 96) | void onItemLongClick(int viewId, M item, int position);

FILE: architecture/src/main/java/com/kunminx/architecture/ui/bind/ClickProxy.java
  class ClickProxy (line 7) | public class ClickProxy implements View.OnClickListener {
    method setOnClickListener (line 10) | public void setOnClickListener(View.OnClickListener listener) {
    method onClick (line 13) | @Override

FILE: architecture/src/main/java/com/kunminx/architecture/ui/bind/CommonBindingAdapter.java
  class CommonBindingAdapter (line 32) | public class CommonBindingAdapter {
    method visible (line 33) | @BindingAdapter(value = {"visible"}, requireAll = false)
    method invisible (line 42) | @BindingAdapter(value = {"invisible"}, requireAll = false)
    method setImageResource (line 51) | @BindingAdapter(value = {"imgRes"}, requireAll = false)
    method setTextColor (line 56) | @BindingAdapter(value = {"textColor"}, requireAll = false)
    method selected (line 61) | @BindingAdapter(value = {"selected"}, requireAll = false)
    method clipToOutline (line 66) | @BindingAdapter(value = {"clipToOutline"}, requireAll = false)
    method requestFocus (line 71) | @BindingAdapter(value = {"requestFocus"}, requireAll = false)
    method showKeyboard (line 76) | @BindingAdapter(value = {"showKeyboard"}, requireAll = false)

FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java
  class BaseActivity (line 41) | public abstract class BaseActivity extends DataBindingActivity {
    method onOutput (line 46) | protected void onOutput() {
    method onInput (line 49) | protected void onInput() {
    method onCreate (line 52) | @SuppressLint("SourceLockedOrientationActivity")
    method transparentStatusBar (line 61) | public static void transparentStatusBar(@NonNull Activity activity) {
    method getActivityScopeViewModel (line 77) | protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull C...
    method getApplicationScopeViewModel (line 81) | protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNul...
    method getResources (line 85) | @Override
    method openUrlInBrowser (line 94) | protected void openUrlInBrowser(String url) {

FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java
  class BaseFragment (line 38) | public abstract class BaseFragment extends DataBindingFragment {
    method onOutput (line 43) | protected void onOutput() {
    method onInput (line 46) | protected void onInput() {
    method onAttach (line 49) | @Override
    method onCreate (line 55) | @Override
    method onViewCreated (line 61) | @Override
    method getFragmentScopeViewModel (line 75) | protected <T extends ViewModel> T getFragmentScopeViewModel(@NonNull C...
    method getActivityScopeViewModel (line 79) | protected <T extends ViewModel> T getActivityScopeViewModel(@NonNull C...
    method getApplicationScopeViewModel (line 83) | protected <T extends ViewModel> T getApplicationScopeViewModel(@NonNul...
    method nav (line 87) | protected NavController nav() {
    method openUrlInBrowser (line 91) | protected void openUrlInBrowser(String url) {
    method getAppContext (line 97) | protected Context getAppContext() {
    method addOnBackPressed (line 101) | private void addOnBackPressed() {
    method onBackPressed (line 110) | protected void onBackPressed() {

FILE: architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java
  class StateHolder (line 8) | public class StateHolder extends ViewModel {

FILE: architecture/src/main/java/com/kunminx/architecture/ui/view/SwipeMenuLayout.java
  class SwipeMenuLayout (line 25) | public class SwipeMenuLayout extends ViewGroup {
    method SwipeMenuLayout (line 47) | public SwipeMenuLayout(Context context) {
    method SwipeMenuLayout (line 51) | public SwipeMenuLayout(Context context, AttributeSet attrs) {
    method SwipeMenuLayout (line 55) | public SwipeMenuLayout(Context context, AttributeSet attrs, int defSty...
    method isSwipeEnable (line 60) | public boolean isSwipeEnable() {
    method setSwipeEnable (line 64) | public void setSwipeEnable(boolean swipeEnable) {
    method isIos (line 68) | public boolean isIos() {
    method setIos (line 72) | public SwipeMenuLayout setIos(boolean ios) {
    method isLeftSwipe (line 77) | public boolean isLeftSwipe() {
    method setLeftSwipe (line 81) | public SwipeMenuLayout setLeftSwipe(boolean leftSwipe) {
    method getViewCache (line 86) | public static SwipeMenuLayout getViewCache() {
    method init (line 90) | private void init(Context context, AttributeSet attrs, int defStyleAtt...
    method onMeasure (line 111) | @Override
    method generateLayoutParams (line 147) | @Override
    method forceUniformHeight (line 152) | private void forceUniformHeight(int count, int widthMeasureSpec) {
    method onLayout (line 169) | @Override
    method dispatchTouchEvent (line 193) | @Override
    method onInterceptTouchEvent (line 265) | @Override
    method smoothExpand (line 302) | public void smoothExpand() {
    method cancelAnim (line 321) | private void cancelAnim() {
    method smoothClose (line 330) | public void smoothClose() {
    method acquireVelocityTracker (line 350) | private void acquireVelocityTracker(final MotionEvent event) {
    method releaseVelocityTracker (line 357) | private void releaseVelocityTracker() {
    method onDetachedFromWindow (line 365) | @Override
    method performLongClick (line 374) | @Override
    method quickClose (line 382) | public void quickClose() {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java
  class AdaptScreenUtils (line 17) | public final class AdaptScreenUtils {
    method adaptWidth (line 22) | public static Resources adaptWidth(Resources resources, int designWidt...
    method adaptHeight (line 29) | public static Resources adaptHeight(Resources resources, int designHei...
    method setAppDmXdpi (line 36) | private static void setAppDmXdpi(final float xdpi) {
    method getDisplayMetrics (line 40) | private static DisplayMetrics getDisplayMetrics(Resources resources) {
    method getMiuiTmpMetrics (line 48) | private static DisplayMetrics getMiuiTmpMetrics(Resources resources) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/TimeUtils.java
  class TimeUtils (line 13) | public class TimeUtils {
    method getCurrentTime (line 19) | public static String getCurrentTime(String time_format) {
    method getTime (line 26) | public static String getTime(long time, String format) {
    method getStringToDate (line 33) | public static long getStringToDate(String dateString, String pattern) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java
  class ToastUtils (line 8) | public class ToastUtils {
    method showLongToast (line 10) | public static void showLongToast(String text) {
    method showShortToast (line 14) | public static void showShortToast(String text) {

FILE: architecture/src/main/java/com/kunminx/architecture/utils/Utils.java
  class Utils (line 18) | public final class Utils {
    method Utils (line 23) | private Utils() {
    method init (line 27) | public static void init(final Context context) {
    method init (line 35) | public static void init(final Application app) {
    method getApp (line 49) | public static Application getApp() {
    method getApplicationByReflect (line 58) | private static Application getApplicationByReflect() {
    class FileProvider4UtilCode (line 74) | public static final class FileProvider4UtilCode extends FileProvider {
      method onCreate (line 75) | @Override

FILE: architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 13) | @Test

FILE: keyvalue-dispatch/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 18) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 20) | @Test

FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/GlobalConfigs.java
  class GlobalConfigs (line 16) | public class GlobalConfigs {
    method GlobalConfigs (line 19) | private GlobalConfigs() {
    method output (line 22) | public static void output(@NonNull AppCompatActivity activity, @NonNul...
    method output (line 26) | public static void output(@NonNull Fragment fragment, @NonNull Observe...
    method put (line 30) | public static void put(String key, String value) {
    method put (line 34) | public static void put(String key, Integer value) {
    method put (line 38) | public static void put(String key, Long value) {
    method put (line 42) | public static void put(String key, Float value) {
    method put (line 46) | public static void put(String key, Boolean value) {
    method getString (line 50) | public static String getString(String key) {
    method getInt (line 54) | public static Integer getInt(String key) {
    method getLong (line 58) | public static Long getLong(String key) {
    method getFloat (line 62) | public static Float getFloat(String key) {
    method getBoolean (line 66) | public static Boolean getBoolean(String key) {

FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/KeyValueDispatcher.java
  class KeyValueDispatcher (line 15) | public class KeyValueDispatcher extends MviDispatcher<KeyValueMsg> {
    method moduleName (line 19) | public String moduleName() {
    method onHandle (line 23) | @Override
    method put (line 28) | public void put(String key, String value) {
    method put (line 34) | public void put(String key, Integer value) {
    method put (line 40) | public void put(String key, Long value) {
    method put (line 46) | public void put(String key, Float value) {
    method put (line 52) | public void put(String key, Boolean value) {
    method getString (line 58) | public String getString(String key) {
    method getInt (line 65) | public Integer getInt(String key) {
    method getLong (line 72) | public Long getLong(String key) {
    method getFloat (line 79) | public Float getFloat(String key) {
    method getBoolean (line 86) | public Boolean getBoolean(String key) {

FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/event/KeyValueMsg.java
  class KeyValueMsg (line 6) | public class KeyValueMsg {
    method KeyValueMsg (line 9) | public KeyValueMsg(String currentKey) {

FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/AppUtils.java
  class AppUtils (line 27) | public final class AppUtils {
    method AppUtils (line 32) | private AppUtils() {
    method getApp (line 36) | public static Application getApp() {
    method getApplicationByReflect (line 44) | private static Application getApplicationByReflect() {

FILE: keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/SPUtils.java
  class SPUtils (line 22) | @SuppressLint("ApplySharedPref")
    method SPUtils (line 28) | private SPUtils(final String spName) {
    method SPUtils (line 32) | private SPUtils(final String spName, final int mode) {
    method getInstance (line 41) | public static SPUtils getInstance() {
    method getInstance (line 51) | public static SPUtils getInstance(final int mode) {
    method getInstance (line 61) | public static SPUtils getInstance(String spName) {
    method getInstance (line 72) | public static SPUtils getInstance(String spName, final int mode) {
    method isSpace (line 89) | private static boolean isSpace(final String s) {
    method put (line 107) | public void put(@NonNull final String key, final String value) {
    method put (line 119) | public void put(@NonNull final String key, final String value, final b...
    method getString (line 133) | public String getString(@NonNull final String key) {
    method getString (line 144) | public String getString(@NonNull final String key, final String defaul...
    method put (line 154) | public void put(@NonNull final String key, final int value) {
    method put (line 166) | public void put(@NonNull final String key, final int value, final bool...
    method getInt (line 180) | public int getInt(@NonNull final String key) {
    method getInt (line 191) | public int getInt(@NonNull final String key, final int defaultValue) {
    method put (line 201) | public void put(@NonNull final String key, final long value) {
    method put (line 213) | public void put(@NonNull final String key, final long value, final boo...
    method getLong (line 227) | public long getLong(@NonNull final String key) {
    method getLong (line 238) | public long getLong(@NonNull final String key, final long defaultValue) {
    method put (line 248) | public void put(@NonNull final String key, final float value) {
    method put (line 260) | public void put(@NonNull final String key, final float value, final bo...
    method getFloat (line 274) | public float getFloat(@NonNull final String key) {
    method getFloat (line 285) | public float getFloat(@NonNull final String key, final float defaultVa...
    method put (line 295) | public void put(@NonNull final String key, final boolean value) {
    method put (line 307) | public void put(@NonNull final String key, final boolean value, final ...
    method getBoolean (line 321) | public boolean getBoolean(@NonNull final String key) {
    method getBoolean (line 332) | public boolean getBoolean(@NonNull final String key, final boolean def...
    method put (line 342) | public void put(@NonNull final String key, final Set<String> value) {
    method put (line 354) | public void put(@NonNull final String key,
    method getStringSet (line 371) | public Set<String> getStringSet(@NonNull final String key) {
    method getStringSet (line 382) | public Set<String> getStringSet(@NonNull final String key,
    method getAll (line 392) | public Map<String, ?> getAll() {
    method contains (line 402) | public boolean contains(@NonNull final String key) {
    method remove (line 411) | public void remove(@NonNull final String key) {
    method remove (line 422) | public void remove(@NonNull final String key, final boolean isCommit) {
    method clear (line 433) | public void clear() {
    method clear (line 443) | public void clear(final boolean isCommit) {

FILE: keyvalue-dispatch/src/test/java/com/kunminx/architecture/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 13) | @Test

FILE: mvi-dispatch/src/androidTest/java/com/kunminx/dispatch/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 18) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 20) | @Test

FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/MviDispatcher.java
  class MviDispatcher (line 21) | public class MviDispatcher<T> extends ViewModel implements DefaultLifecy...
    method initQueueMaxLength (line 28) | protected int initQueueMaxLength() {
    method output (line 32) | public final void output(@NonNull AppCompatActivity activity, @NonNull...
    method output (line 38) | public final void output(@NonNull Fragment fragment, @NonNull Observer...
    method outputTo (line 45) | private void outputTo(Integer identityId, LifecycleOwner owner, Observ...
    method sendResult (line 53) | protected final void sendResult(@NonNull T intent) {
    method input (line 93) | public final void input(T intent) {
    method onHandle (line 97) | protected void onHandle(T intent) {
    method onDestroy (line 100) | @Override

FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/queue/FixedLengthList.java
  class FixedLengthList (line 8) | public class FixedLengthList<T> extends LinkedList<T> {
    method init (line 13) | public final void init(int maxLength, QueueCallback<T> queueCallback) {
    method add (line 21) | @Override
    type QueueCallback (line 30) | public interface QueueCallback<T> {
      method onRemoveFirst (line 31) | void onRemoveFirst(T t);

FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/OneTimeMessage.java
  class OneTimeMessage (line 21) | public class OneTimeMessage<T> {
    method OneTimeMessage (line 36) | public OneTimeMessage(T value) {
    method considerNotify (line 41) | @SuppressWarnings("unchecked")
    method dispatchingValue (line 53) | void dispatchingValue(@Nullable ObserverWrapper initiator) {
    method observe (line 75) | @MainThread
    method removeObserver (line 87) | @MainThread
    method set (line 95) | @MainThread
    method get (line 102) | @SuppressWarnings("unchecked")
    method changeActiveCounter (line 110) | @MainThread
    class LifecycleBoundObserver (line 123) | class LifecycleBoundObserver extends ObserverWrapper implements Lifecy...
      method LifecycleBoundObserver (line 127) | LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? sup...
      method shouldBeActive (line 132) | @Override
      method onStateChanged (line 137) | @Override
      method isAttachedTo (line 152) | @Override
      method detachObserver (line 157) | @Override
    class ObserverWrapper (line 163) | private abstract class ObserverWrapper {
      method ObserverWrapper (line 168) | ObserverWrapper(Observer<? super T> observer) {
      method shouldBeActive (line 172) | abstract boolean shouldBeActive();
      method isAttachedTo (line 174) | boolean isAttachedTo(LifecycleOwner owner) {
      method detachObserver (line 178) | void detachObserver() {
      method activeStateChanged (line 181) | void activeStateChanged(boolean newActive) {

FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/SafeIterableMap.java
  class SafeIterableMap (line 34) | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    method get (line 45) | protected Entry<K, V> get(K k) {
    method putIfAbsent (line 65) | public V putIfAbsent(@NonNull K key, @NonNull V v) {
    method put (line 74) | protected Entry<K, V> put(@NonNull K key, @NonNull V v) {
    method remove (line 97) | public V remove(@NonNull K key) {
    method size (line 129) | public int size() {
    method iterator (line 137) | @NonNull
    method descendingIterator (line 149) | public Iterator<Map.Entry<K, V>> descendingIterator() {
    method iteratorWithAdditions (line 158) | public IteratorWithAdditions iteratorWithAdditions() {
    method eldest (line 168) | public Map.Entry<K, V> eldest() {
    method newest (line 175) | public Map.Entry<K, V> newest() {
    method equals (line 179) | @Override
    method hashCode (line 204) | @Override
    method toString (line 214) | @Override
    class ListIterator (line 229) | private abstract static class ListIterator<K, V> implements Iterator<M...
      method ListIterator (line 234) | ListIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
      method hasNext (line 239) | @Override
      method supportRemove (line 244) | @SuppressWarnings("ReferenceEquality")
      method nextNode (line 261) | @SuppressWarnings("ReferenceEquality")
      method next (line 269) | @Override
      method forward (line 276) | abstract Entry<K, V> forward(Entry<K, V> entry);
      method backward (line 278) | abstract Entry<K, V> backward(Entry<K, V> entry);
    class AscendingIterator (line 281) | static class AscendingIterator<K, V> extends ListIterator<K, V> {
      method AscendingIterator (line 282) | AscendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
      method forward (line 286) | @Override
      method backward (line 291) | @Override
    class DescendingIterator (line 297) | private static class DescendingIterator<K, V> extends ListIterator<K, ...
      method DescendingIterator (line 299) | DescendingIterator(Entry<K, V> start, Entry<K, V> expectedEnd) {
      method forward (line 303) | @Override
      method backward (line 308) | @Override
    class IteratorWithAdditions (line 314) | private class IteratorWithAdditions implements Iterator<Map.Entry<K, V...
      method IteratorWithAdditions (line 318) | IteratorWithAdditions() {
      method supportRemove (line 321) | @SuppressWarnings("ReferenceEquality")
      method hasNext (line 330) | @Override
      method next (line 338) | @Override
    type SupportRemove (line 350) | interface SupportRemove<K, V> {
      method supportRemove (line 351) | void supportRemove(@NonNull Entry<K, V> entry);
    class Entry (line 354) | static class Entry<K, V> implements Map.Entry<K, V> {
      method Entry (line 362) | Entry(@NonNull K key, @NonNull V value) {
      method getKey (line 367) | @NonNull
      method getValue (line 373) | @NonNull
      method setValue (line 379) | @Override
      method toString (line 384) | @Override
      method equals (line 389) | @SuppressWarnings("ReferenceEquality")
      method hashCode (line 402) | @Override

FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/ui/scope/ApplicationInstance.java
  class ApplicationInstance (line 10) | public class ApplicationInstance implements ViewModelStoreOwner {
    method ApplicationInstance (line 14) | private ApplicationInstance() {
    method getInstance (line 17) | public static ApplicationInstance getInstance() {
    method getViewModelStore (line 21) | @NonNull

FILE: mvi-dispatch/src/main/java/com/kunminx/architecture/ui/scope/ViewModelScope.java
  class ViewModelScope (line 12) | public class ViewModelScope {
    method getFragmentScopeViewModel (line 17) | public <T extends ViewModel> T getFragmentScopeViewModel(@NonNull Frag...
    method getActivityScopeViewModel (line 22) | public <T extends ViewModel> T getActivityScopeViewModel(@NonNull AppC...
    method getApplicationScopeViewModel (line 27) | public <T extends ViewModel> T getApplicationScopeViewModel(@NonNull C...

FILE: mvi-dispatch/src/test/java/com/kunminx/dispatch/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 13) | @Test
Condensed preview — 103 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (219K chars).
[
  {
    "path": ".gitignore",
    "chars": 1436,
    "preview": "# 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# Ge"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 3243,
    "preview": "![](https://s2.loli.net/2023/09/14/POldYf3s7EQ9B6c.jpg)\n\n&nbsp;\n\n&nbsp;\n\n### [🌏 English README](https://github.com/KunMi"
  },
  {
    "path": "README_EN.md",
    "chars": 4923,
    "preview": "**Development story**:\n\n[《Android: Solving the Pain Points of MVI Architecture in Practice》](https://blog.devgenius.io/a"
  },
  {
    "path": "app/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "app/build.gradle",
    "chars": 2555,
    "preview": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion appTargetSdk\n    defaultConfig {\n        minSdk"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/androidTest/java/com/kunminx/purenote/ExampleInstrumentedTest.java",
    "chars": 747,
    "preview": "package com.kunminx.purenote;\n\nimport static org.junit.Assert.assertEquals;\n\nimport android.content.Context;\n\nimport and"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 818,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package="
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/App.java",
    "chars": 279,
    "preview": "package com.kunminx.purenote;\n\nimport android.app.Application;\n\nimport com.kunminx.architecture.utils.Utils;\n\n/**\n * Cre"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/bean/Note.java",
    "chars": 2894,
    "preview": "package com.kunminx.purenote.data.bean;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport androidx.annota"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/bean/Weather.java",
    "chars": 829,
    "preview": "package com.kunminx.purenote.data.bean;\n\nimport java.util.List;\n/**\n * Create by KunMinX at 2022/8/24\n */\npublic class W"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/config/Key.java",
    "chars": 223,
    "preview": "package com.kunminx.purenote.data.config;\n\n/**\n * Create by KunMinX at 2022/8/15\n */\npublic class Key {\n  public final s"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/DataRepository.java",
    "chars": 3285,
    "preview": "package com.kunminx.purenote.data.repo;\n\nimport android.annotation.SuppressLint;\n\nimport androidx.annotation.NonNull;\nim"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/NoteDao.java",
    "chars": 648,
    "preview": "package com.kunminx.purenote.data.repo;\n\nimport androidx.room.Dao;\nimport androidx.room.Delete;\nimport androidx.room.Ins"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/NoteDataBase.java",
    "chars": 362,
    "preview": "package com.kunminx.purenote.data.repo;\n\nimport androidx.room.Database;\nimport androidx.room.RoomDatabase;\n\nimport com.k"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/data/repo/WeatherService.java",
    "chars": 423,
    "preview": "package com.kunminx.purenote.data.repo;\n\nimport com.kunminx.purenote.data.bean.Weather;\n\nimport io.reactivex.Observable;"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_Api.java",
    "chars": 747,
    "preview": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.purenote.data.bean.Weather;\nimport com.kunminx.sealed.an"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_ComplexIntent.java",
    "chars": 749,
    "preview": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.sealed.annotation.Param;\nimport com.kunminx.sealed.annot"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_Messages.java",
    "chars": 575,
    "preview": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.sealed.annotation.SealedClass;\n/**\n * TODO:可用于 Java 1.8 "
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/intent/_NoteIntent.java",
    "chars": 987,
    "preview": "package com.kunminx.purenote.domain.intent;\n\nimport com.kunminx.purenote.data.bean.Note;\nimport com.kunminx.sealed.annot"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/message/PageMessenger.java",
    "chars": 793,
    "preview": "package com.kunminx.purenote.domain.message;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com."
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/request/ComplexRequester.java",
    "chars": 2308,
    "preview": "package com.kunminx.purenote.domain.request;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com."
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/request/NoteRequester.java",
    "chars": 2478,
    "preview": "package com.kunminx.purenote.domain.request;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com."
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/domain/request/WeatherRequester.java",
    "chars": 1956,
    "preview": "package com.kunminx.purenote.domain.request;\n\nimport com.kunminx.architecture.domain.dispatch.MviDispatcher;\nimport com."
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/adapter/NoteAdapter.java",
    "chars": 1811,
    "preview": "package com.kunminx.purenote.ui.adapter;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunminx.architec"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/EditorFragment.java",
    "chars": 4806,
    "preview": "package com.kunminx.purenote.ui.page;\n\nimport android.os.Bundle;\nimport android.text.TextUtils;\n\nimport androidx.navigat"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/ListFragment.java",
    "chars": 5413,
    "preview": "package com.kunminx.purenote.ui.page;\n\nimport com.kunminx.architecture.domain.dispatch.GlobalConfigs;\nimport com.kunminx"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/MainActivity.java",
    "chars": 2760,
    "preview": "package com.kunminx.purenote.ui.page;\n\nimport android.util.Log;\n\nimport com.kunminx.architecture.ui.page.BaseActivity;\ni"
  },
  {
    "path": "app/src/main/java/com/kunminx/purenote/ui/page/SettingFragment.java",
    "chars": 2095,
    "preview": "package com.kunminx.purenote.ui.page;\n\nimport com.kunminx.architecture.domain.dispatch.GlobalConfigs;\nimport com.kunminx"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_enter.xml",
    "chars": 362,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_exit.xml",
    "chars": 288,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_pop_enter.xml",
    "chars": 220,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/anim/x_fragment_pop_exit.xml",
    "chars": 293,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:durat"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_add.xml",
    "chars": 344,
    "preview": "<vector android:height=\"48dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_arrow_back.xml",
    "chars": 371,
    "preview": "<vector android:height=\"48dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 649,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/adapter_note_list.xml",
    "chars": 4825,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/fragment_editor.xml",
    "chars": 3884,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <data>\n\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_list.xml",
    "chars": 5636,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app="
  },
  {
    "path": "app/src/main/res/layout/fragment_settings.xml",
    "chars": 6044,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tool"
  },
  {
    "path": "app/src/main/res/navigation/nav_graph.xml",
    "chars": 1563,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 645,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purpl"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 66,
    "preview": "<resources>\n    <dimen name=\"fab_margin\">16dp</dimen>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 768,
    "preview": "<resources>\n    <string name=\"app_name\">PureNote</string>\n    <string name=\"title\">标题</string>\n    <string name=\"content"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "chars": 499,
    "preview": "<resources>\n\n    <style name=\"Theme.PureNote\" parent=\"Theme.MaterialComponents.DayNight.NoActionBar\">\n        <item name"
  },
  {
    "path": "app/src/test/java/com/kunminx/purenote/ExampleUnitTest.java",
    "chars": 382,
    "preview": "package com.kunminx.purenote;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.junit.Test;\n\n/**\n * Example loca"
  },
  {
    "path": "architecture/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "architecture/build.gradle",
    "chars": 1584,
    "preview": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion appTargetSdk\n    defaultConfig {\n        minSdkVers"
  },
  {
    "path": "architecture/consumer-rules.pro",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "architecture/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "architecture/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java",
    "chars": 749,
    "preview": "package com.kunminx.architecture;\n\nimport android.content.Context;\n\nimport androidx.test.platform.app.InstrumentationReg"
  },
  {
    "path": "architecture/src/main/AndroidManifest.xml",
    "chars": 97,
    "preview": "<?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",
    "chars": 1289,
    "preview": "package com.kunminx.architecture.data.response;\n\nimport android.annotation.SuppressLint;\n\nimport io.reactivex.Observable"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/DataResult.java",
    "chars": 1244,
    "preview": "/*\n *\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResponseStatus.java",
    "chars": 1694,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/data/response/ResultSource.java",
    "chars": 153,
    "preview": "package com.kunminx.architecture.data.response;\n\n/**\n * Create by KunMinX at 2020/11/30\n */\npublic enum ResultSource {\n "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/adapter/BaseBindingAdapter.java",
    "chars": 3121,
    "preview": "package com.kunminx.architecture.ui.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport andro"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/bind/ClickProxy.java",
    "chars": 391,
    "preview": "package com.kunminx.architecture.ui.bind;\n\nimport android.view.View;\n/**\n * Create by KunMinX at 2022/8/18\n */\npublic cl"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/bind/CommonBindingAdapter.java",
    "chars": 2878,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseActivity.java",
    "chars": 3376,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/BaseFragment.java",
    "chars": 3358,
    "preview": "/*\n * Copyright 2018-present KunMinX\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/page/StateHolder.java",
    "chars": 170,
    "preview": "package com.kunminx.architecture.ui.page;\n\nimport androidx.lifecycle.ViewModel;\n\n/**\n * Create by KunMinX at 2022/8/11\n "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/ui/view/SwipeMenuLayout.java",
    "chars": 12461,
    "preview": "package com.kunminx.architecture.ui.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerA"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/AdaptScreenUtils.java",
    "chars": 2248,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.content.res.Resources;\nimport android.util.DisplayMetrics;\nimpor"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/TimeUtils.java",
    "chars": 1332,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\n\nimport java.sql.Date;\nimport java.text"
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/ToastUtils.java",
    "chars": 437,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.widget.Toast;\n\n/**\n * Create by KunMinX at 2021/8/19\n */\npublic "
  },
  {
    "path": "architecture/src/main/java/com/kunminx/architecture/utils/Utils.java",
    "chars": 2169,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Application;\nimport "
  },
  {
    "path": "architecture/src/main/res/values/attrs.xml",
    "chars": 281,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"SwipeMenuLayout\">\n        <attr name=\"sw"
  },
  {
    "path": "architecture/src/test/java/com/kunminx/architecture/ExampleUnitTest.java",
    "chars": 375,
    "preview": "package com.kunminx.architecture;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * Example local unit "
  },
  {
    "path": "build.gradle",
    "chars": 1088,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\napply plugin: 'io.c"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 230,
    "preview": "#Thu Jun 30 19:13:32 CST 2022\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributio"
  },
  {
    "path": "gradle.properties",
    "chars": 234,
    "preview": "android.enableJetifier=true\nandroid.injected.testOnly=false\nandroid.useAndroidX=true\norg.gradle.caching=true\norg.gradle."
  },
  {
    "path": "gradlew",
    "chars": 5766,
    "preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
  },
  {
    "path": "gradlew.bat",
    "chars": 2674,
    "preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "keyvalue-dispatch/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "keyvalue-dispatch/build.gradle",
    "chars": 2059,
    "preview": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\ngroup = 'com.github.KunMinX'\n\next"
  },
  {
    "path": "keyvalue-dispatch/consumer-rules.pro",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "keyvalue-dispatch/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "keyvalue-dispatch/src/androidTest/java/com/kunminx/architecture/ExampleInstrumentedTest.java",
    "chars": 756,
    "preview": "package com.kunminx.architecture;\n\nimport android.content.Context;\n\nimport androidx.test.platform.app.InstrumentationReg"
  },
  {
    "path": "keyvalue-dispatch/src/main/AndroidManifest.xml",
    "chars": 102,
    "preview": "<?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",
    "chars": 1735,
    "preview": "package com.kunminx.architecture.domain.dispatch;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.App"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/dispatch/KeyValueDispatcher.java",
    "chars": 2352,
    "preview": "package com.kunminx.architecture.domain.dispatch;\n\nimport com.kunminx.architecture.domain.event.KeyValueMsg;\nimport com."
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/domain/event/KeyValueMsg.java",
    "chars": 234,
    "preview": "package com.kunminx.architecture.domain.event;\n\n/**\n * Create by KunMinX at 2022/8/15\n */\npublic class KeyValueMsg {\n  p"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/AppUtils.java",
    "chars": 2320,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Application;\n\nimport"
  },
  {
    "path": "keyvalue-dispatch/src/main/java/com/kunminx/architecture/utils/SPUtils.java",
    "chars": 12057,
    "preview": "package com.kunminx.architecture.utils;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport "
  },
  {
    "path": "keyvalue-dispatch/src/test/java/com/kunminx/architecture/ExampleUnitTest.java",
    "chars": 375,
    "preview": "package com.kunminx.architecture;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * Example local unit "
  },
  {
    "path": "mvi-dispatch/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "mvi-dispatch/build.gradle",
    "chars": 2010,
    "preview": "apply plugin: 'com.android.library'\napply plugin: 'com.github.dcendents.android-maven'\ngroup = 'com.github.KunMinX'\n\next"
  },
  {
    "path": "mvi-dispatch/consumer-rules.pro",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "mvi-dispatch/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "mvi-dispatch/src/androidTest/java/com/kunminx/dispatch/ExampleInstrumentedTest.java",
    "chars": 741,
    "preview": "package com.kunminx.dispatch;\n\nimport android.content.Context;\n\nimport androidx.test.platform.app.InstrumentationRegistr"
  },
  {
    "path": "mvi-dispatch/src/main/AndroidManifest.xml",
    "chars": 93,
    "preview": "<?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",
    "chars": 4119,
    "preview": "package com.kunminx.architecture.domain.dispatch;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.App"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/domain/queue/FixedLengthList.java",
    "chars": 768,
    "preview": "package com.kunminx.architecture.domain.queue;\n\nimport java.util.LinkedList;\n\n/**\n * Create by KunMinX at 2022/7/5\n */\np"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/OneTimeMessage.java",
    "chars": 5419,
    "preview": "package com.kunminx.architecture.domain.result;\n\nimport static androidx.lifecycle.Lifecycle.State.DESTROYED;\nimport stat"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/domain/result/SafeIterableMap.java",
    "chars": 10214,
    "preview": "/*\n * Copyright 2018 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/ui/scope/ApplicationInstance.java",
    "chars": 708,
    "preview": "package com.kunminx.architecture.ui.scope;\n\nimport androidx.annotation.NonNull;\nimport androidx.lifecycle.ViewModelStore"
  },
  {
    "path": "mvi-dispatch/src/main/java/com/kunminx/architecture/ui/scope/ViewModelScope.java",
    "chars": 1266,
    "preview": "package com.kunminx.architecture.ui.scope;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatA"
  },
  {
    "path": "mvi-dispatch/src/test/java/com/kunminx/dispatch/ExampleUnitTest.java",
    "chars": 371,
    "preview": "package com.kunminx.dispatch;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * Example local unit test"
  },
  {
    "path": "publish-mavencentral.gradle",
    "chars": 6913,
    "preview": "apply plugin: 'maven-publish'\napply plugin: 'signing'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('so"
  },
  {
    "path": "settings.gradle",
    "chars": 122,
    "preview": "rootProject.name = \"PureNote\"\ninclude ':app'\ninclude ':architecture'\ninclude ':mvi-dispatch'\ninclude ':keyvalue-dispatch"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the KunMinX/MVI-Dispatcher GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 103 files (190.9 KB), approximately 52.4k tokens, and a symbol index with 414 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!