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 Testing documentation */ @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 ================================================ ================================================ 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 CREATOR = new Creator() { @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 lives; public String getStatus() { return status; } public String getCount() { return count; } public String getInfo() { return info; } public String getInfocode() { return infocode; } public List 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> getNotes() { return AsyncTask.doIO(emitter -> emitter.onNext(mDataBase.noteDao().getNotes())); } public Observable insertNote(Note note) { return AsyncTask.doIO(emitter -> { mDataBase.noteDao().insertNote(note); emitter.onNext(true); }); } public Observable updateNote(Note note) { return AsyncTask.doIO(emitter -> { mDataBase.noteDao().updateNote(note); emitter.onNext(true); }); } public Observable deleteNote(Note note) { return AsyncTask.doIO(emitter -> { mDataBase.noteDao().deleteNote(note); emitter.onNext(true); }); } @SuppressLint("CheckResult") public Observable 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 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 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 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 { /** * 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 { 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 更是如此, *

* Create by KunMinX at 2022/6/14 */ public class NoteRequester extends MviDispatcher { /** * 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 更是如此, *

* Create by KunMinX at 2022/8/24 */ public class WeatherRequester extends MviDispatcher { 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() { @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 { public NoteAdapter(List 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 tempNote = new State<>(new Note()); public final State title = new State<>(""); public final State content = new State<>(""); public final State tip = new State<>(Utils.getApp().getString(R.string.edit)); public final State time = new State<>(Utils.getApp().getString(R.string.new_note)); public final State 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 list = new ArrayList<>(); public final State emptyViewShow = new State<>(false); public final State loadingWeather = new State<>(false); public final State 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 testString = new State<>(""); public final State testBoolean = new State<>(false); } } ================================================ FILE: app/src/main/res/anim/x_fragment_enter.xml ================================================ ================================================ FILE: app/src/main/res/anim/x_fragment_exit.xml ================================================ ================================================ FILE: app/src/main/res/anim/x_fragment_pop_enter.xml ================================================ ================================================ FILE: app/src/main/res/anim/x_fragment_pop_exit.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_add.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_arrow_back.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/adapter_note_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_editor.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_settings.xml ================================================