Repository: izumin5210/Droidux Branch: master Commit: 8510117dae0a Files: 183 Total size: 265.8 KB Directory structure: gitextract_k8nenrgu/ ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── build.gradle ├── droidux/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── info/ │ │ └── izumin/ │ │ └── android/ │ │ └── droidux/ │ │ ├── Action.java │ │ ├── BaseStore.java │ │ ├── Dispatcher.java │ │ ├── History.java │ │ ├── Middleware.java │ │ ├── OnStateChangedListener.java │ │ ├── StoreImpl.java │ │ ├── UndoableState.java │ │ ├── UndoableStoreImpl.java │ │ ├── action/ │ │ │ ├── HistoryAction.java │ │ │ ├── RedoAction.java │ │ │ └── UndoAction.java │ │ ├── annotation/ │ │ │ ├── Dispatchable.java │ │ │ ├── Reducer.java │ │ │ ├── Store.java │ │ │ └── Undoable.java │ │ └── exception/ │ │ └── NotInitializedException.java │ └── test/ │ └── groovy/ │ └── info/ │ └── izumin/ │ └── android/ │ └── droidux/ │ ├── DispatcherTest.groovy │ ├── HistoryTest.groovy │ └── action/ │ └── HistoryActionTest.groovy ├── droidux-processor/ │ ├── .gitignore │ ├── build.gradle │ ├── libs/ │ │ └── databinding-library.jar │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── info/ │ │ └── izumin/ │ │ └── android/ │ │ └── droidux/ │ │ └── processor/ │ │ ├── AbstractProcessingStep.java │ │ ├── DroiduxProcessor.java │ │ ├── ReducerProcessingStep.java │ │ ├── StoreProcessingStep.java │ │ ├── exception/ │ │ │ ├── InvalidDispatchableDeclarationException.java │ │ │ ├── InvalidReducerDeclarationException.java │ │ │ └── InvalidStoreDelcarationException.java │ │ ├── generator/ │ │ │ ├── StoreBuilderClassGenerator.java │ │ │ ├── StoreClassGenerator.java │ │ │ └── StoreImplClassGenerator.java │ │ ├── model/ │ │ │ ├── BuilderModel.java │ │ │ ├── DispatchableModel.java │ │ │ ├── DispatcherModel.java │ │ │ ├── ReducerModel.java │ │ │ ├── StoreImplModel.java │ │ │ ├── StoreMethodModel.java │ │ │ └── StoreModel.java │ │ ├── util/ │ │ │ ├── AnnotationUtils.java │ │ │ ├── PoetUtils.java │ │ │ └── StringUtils.java │ │ └── validator/ │ │ ├── DispatchableValidator.java │ │ ├── ReducerValidator.java │ │ └── StoreValidator.java │ └── test/ │ └── java/ │ └── info/ │ └── izumin/ │ └── android/ │ └── droidux/ │ └── processor/ │ ├── DroiduxProcessorTest.java │ └── fixture/ │ ├── Counter.java │ ├── CounterReducer.java │ ├── Source.java │ ├── TodoList.java │ ├── TodoListReducer.java │ └── action/ │ ├── AddTodoItemAction.java │ ├── ClearCountAction.java │ ├── CompleteTodoItemAction.java │ ├── IncrementCountAction.java │ ├── InitializeCountAction.java │ └── SquareCountAction.java ├── examples/ │ ├── counter/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── info/ │ │ │ └── izumin/ │ │ │ └── android/ │ │ │ └── droidux/ │ │ │ └── example/ │ │ │ └── counter/ │ │ │ ├── Counter.java │ │ │ ├── CounterReducer.java │ │ │ ├── MainActivity.java │ │ │ ├── MainEventHandlers.java │ │ │ ├── RootStore.java │ │ │ └── action/ │ │ │ ├── DecrementCountAction.java │ │ │ └── IncrementCountAction.java │ │ └── res/ │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ ├── todomvc/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── info/ │ │ │ └── izumin/ │ │ │ └── android/ │ │ │ └── droidux/ │ │ │ └── ApplicationTest.java │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── info/ │ │ │ └── izumin/ │ │ │ └── android/ │ │ │ └── droidux/ │ │ │ └── example/ │ │ │ └── todomvc/ │ │ │ ├── App.java │ │ │ ├── MainActivity.java │ │ │ ├── MainActivityHelper.java │ │ │ ├── RootStore.java │ │ │ ├── TodoListAdapter.java │ │ │ ├── action/ │ │ │ │ ├── AddTodoAction.java │ │ │ │ ├── ClearCompletedTodoAction.java │ │ │ │ ├── DeleteTodoAction.java │ │ │ │ └── ToggleCompletedTodoAction.java │ │ │ ├── entity/ │ │ │ │ └── TodoList.java │ │ │ ├── middleware/ │ │ │ │ └── Logger.java │ │ │ └── reducer/ │ │ │ └── TodoListReducer.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ └── list_item_todo.xml │ │ ├── menu/ │ │ │ └── main_manu.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ ├── todos-with-dagger/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── info/ │ │ │ └── izumin/ │ │ │ └── android/ │ │ │ └── droidux/ │ │ │ └── example/ │ │ │ └── todoswithdagger/ │ │ │ └── ApplicationTest.java │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── info/ │ │ │ │ └── izumin/ │ │ │ │ └── android/ │ │ │ │ └── droidux/ │ │ │ │ └── example/ │ │ │ │ └── todoswithdagger/ │ │ │ │ ├── App.java │ │ │ │ ├── AppComponent.java │ │ │ │ ├── AppModule.java │ │ │ │ ├── RootStore.java │ │ │ │ ├── action/ │ │ │ │ │ ├── AddTodoAction.java │ │ │ │ │ ├── ClearCompletedTodoAction.java │ │ │ │ │ ├── ClearNewTodoTextAction.java │ │ │ │ │ ├── DeleteTodoAction.java │ │ │ │ │ ├── ToggleCompletedTodoAction.java │ │ │ │ │ └── UpdateNewTodoTextAction.java │ │ │ │ ├── adapter/ │ │ │ │ │ └── TodoListAdapter.java │ │ │ │ ├── entity/ │ │ │ │ │ └── TodoList.java │ │ │ │ ├── module/ │ │ │ │ │ └── main/ │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ ├── MainComponent.java │ │ │ │ │ ├── MainEventHandlers.java │ │ │ │ │ ├── MainModule.java │ │ │ │ │ ├── MainPresenter.java │ │ │ │ │ └── MainView.java │ │ │ │ ├── reducer/ │ │ │ │ │ └── TodoListReducer.java │ │ │ │ └── util/ │ │ │ │ └── ViewBindingUtils.java │ │ │ └── res/ │ │ │ ├── layout/ │ │ │ │ ├── activity_main.xml │ │ │ │ └── list_item_todo.xml │ │ │ ├── menu/ │ │ │ │ └── main_manu.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── values-w820dp/ │ │ │ └── dimens.xml │ │ └── test/ │ │ └── java/ │ │ └── info/ │ │ └── izumin/ │ │ └── android/ │ │ └── droidux/ │ │ └── example/ │ │ └── todoswithdagger/ │ │ └── ExampleUnitTest.java │ └── todos-with-undo/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── info/ │ │ └── izumin/ │ │ └── android/ │ │ └── droidux/ │ │ └── example/ │ │ └── todoswithundo/ │ │ └── ApplicationTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── info/ │ │ │ └── izumin/ │ │ │ └── android/ │ │ │ └── droidux/ │ │ │ └── example/ │ │ │ └── todoswithundo/ │ │ │ ├── App.java │ │ │ ├── MainActivity.java │ │ │ ├── MainActivityHelper.java │ │ │ ├── RootStore.java │ │ │ ├── TodoListAdapter.java │ │ │ ├── action/ │ │ │ │ ├── AddTodoAction.java │ │ │ │ ├── ClearCompletedTodoAction.java │ │ │ │ ├── DeleteTodoAction.java │ │ │ │ └── ToggleCompletedTodoAction.java │ │ │ ├── entity/ │ │ │ │ └── TodoList.java │ │ │ ├── middleware/ │ │ │ │ └── Logger.java │ │ │ └── reducer/ │ │ │ └── TodoListReducer.java │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ └── list_item_todo.xml │ │ ├── menu/ │ │ │ └── main_manu.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── values-w820dp/ │ │ └── dimens.xml │ └── test/ │ └── java/ │ └── info/ │ └── izumin/ │ └── android/ │ └── droidux/ │ └── example/ │ └── todoswithundo/ │ └── ExampleUnitTest.java ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── middlewares/ │ └── droidux-thunk/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── info/ │ │ │ └── izumin/ │ │ │ └── android/ │ │ │ └── droidux/ │ │ │ └── thunk/ │ │ │ ├── AsyncAction.java │ │ │ └── ThunkMiddleware.java │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ └── groovy/ │ └── info/ │ └── izumin/ │ └── android/ │ └── droidux/ │ └── thunk/ │ └── ThunkMiddlewareTest.groovy └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/android,intellij,gradle ### Android ### # Built application files *.apk *.ap_ # Files for the Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ # 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 Patch ### gen-external-apklibs ### Intellij ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio *.iml ## Directory-based project format: .idea/ # if you remove the above rule, at least ignore the following: # User-specific stuff: # .idea/workspace.xml # .idea/tasks.xml # .idea/dictionaries # Sensitive or high-churn files: # .idea/dataSources.ids # .idea/dataSources.xml # .idea/sqlDataSources.xml # .idea/dynamic.xml # .idea/uiDesigner.xml # Gradle: # .idea/gradle.xml # .idea/libraries # Mongo Explorer plugin: # .idea/mongoSettings.xml ## File-based project format: *.ipr *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties ### Gradle ### .gradle build/ # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar ================================================ FILE: .travis.yml ================================================ language: android jdk: oraclejdk8 android: components: - tools - build-tools-27.0.3 - android-27 - extra-android-support - extra-android-m2repository licenses: - 'android-sdk-preview-license-52d11cd2' - 'android-sdk-license-.+' - 'google-gdk-license-.+' before_install: - yes | sdkmanager "platforms;android-27" cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock script: - ./gradlew --full-stacktrace -q test ================================================ FILE: LICENSE.md ================================================ 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: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and 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 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 2015 izumin5210 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 ================================================ # Droidux [![Build Status](https://travis-ci.org/izumin5210/Droidux.svg)](https://travis-ci.org/izumin5210/Droidux) [![Download](https://api.bintray.com/packages/izumin5210/maven/droidux/images/download.svg) ](https://bintray.com/izumin5210/maven/droidux/_latestVersion) [![Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/izumin5210/Droidux/blob/master/LICENSE.md) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Droidux-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/2892) Droidux is "predictable state container" implementation, inspired by **[Redux][redux]**. ## Features Droidux is influenced by [Three principles][three-principles] of Redux. > * Single source of truth > - The state of your whole application is stored in an object tree inside a single store. > * State is read-only > - The only way to mutate the state is to emit an action, an object describing what happened. > * Mutations are written as pure functions > - To specify how the state tree is transformed by actions, you write pure reducers. > > [Three Principles | Redux][three-principles] Features of Droidux are following: * All mutations can be observed via Flowable from [RxJava][rxjava] * All mutations are automatically notified to views via [Data Binding][databinding] ### Data flow ![Droidux data flow](droidux.png) see also: [Introduction to Redux // Speaker Deck](https://speakerdeck.com/axross/introduction-to-redux) (in Japanese) ## Installation Droidux depends on [RxJava][rxjava] and [Data Binding][databinding]. Add to your project build.gradle file: ```groovy apply plugin: 'com.android.application' dependencies { compile 'info.izumin.android:droidux:0.6.0' compile 'io.reactivex.rxjava2:rxjava:2.1.8' compile 'io.reactivex.rxjava2:rxandroid:2.0.1' annotationProcessor 'info.izumin.android:droidux-processor:0.6.0' } ``` And also you need to setup [Data Binding][databinding]. When you use `AsyncAction`, you need to add [droidux-thunk](https://github.com/izumin5210/Droidux/tree/master/middlewares/droidux-thunk). ```groovy compile 'info.izumin.android:droidux-thunk:0.3.0' ``` ## Usage ### Quick example ```java /** * This is a state class. * It can be as simple as possible implementation, like POJO, or immutable object. */ public class Counter { private final int count; public Counter(int count) { this.count = count; } public int getCount() { return count; } } /** * This is a reducer class. * It should be applied @Reducer annotation is given a state class as an argument. * It describe whether the reducer should handle which actions. */ @Reducer(Counter.class) public class CounterReducer { /** * This is a method to handle actions. * It should be applied @Dispatchable annotation is given an action class as an parameter. * It describe how to transform the state into the next state when dispatched actions. * It should return the next state instance, and it is preferred instantiate the new state. * * This example handle IncrementCountAction, + and it returns new counter instance that state is incremented. */ @Dispatchable(IncrementCountAction.class) public Counter increment(Counter state) { return new Counter(state.getCount() + 1); } @Dispatchable(DecrementCountAction.class) public Counter decrement(Counter state) { return new Counter(state.getCount() - 1); } @Dispatchable(ClearCountAction.class) public Counter clear() { return new Counter(0); } } /** * This is a store interface. * It should be applied @Store annotation and passing reducer classes as parameters. * Droidux generates an implementation of getter method, observe method and dispatch method from user-defined interface. */ @Store(CounterReducer.class) public interface CounterStore extends BaseStore { Counter getCounter(); Flowable observeCounter(); } /** * They are action classes. They should extend Action class. */ public class IncrementCountAction implements Action {} public class DecrementCountAction implements Action {} public class ClearCountAction implements Action {} // Instantiate a Droidux store holding the state of your app. // Its class is generated automatically from Reducer class. // // The instantiating should use Builder class, // and it should register a reducer instance and an initial state. // // Its APIs in this example are following: // - Flowable dispatch(Action action) // - Flowable observeCounter() // - Counter getCounter() CounterStore store = DroiduxCounterStore.builder() .setReducer(new CounterReducer(), new Counter(0)) .build(); // Counter: 0 // You can observe to the updates using RxJava interface. store.observe((counter) -> Log.d(TAG, counter.toString())); // The only way to mutate the internal state is to dispatch an action. store.dispatch(new IncrementCountAction()).subscribe(); // Counter: 1 store.dispatch(new IncrementCountAction()).subscribe(); // Counter: 2 store.dispatch(new IncrementCountAction()).subscribe(); // Counter: 3 store.dispatch(new DecrementCountAction()).subscribe(); // Counter: 2 store.dispatch(new ClearCountAction()).subscribe(); // Counter: 0 ``` ### Data Binding ```java // If you use databinding, yor store interface must extend `android.databinding.Observable`. @Store(CounterReducer.class) public interface CounterStore extends BaseStore, android.databinding.Observable { // You should annotate the getter method with @Bindable @Bindable Counter getCounter(); } CounterStore store = DroiduxCounterStore.builder() // Pass the field id generated by DataBinding annotation processor. .setReducer(new CounterReducer(), new Counter(0), BR.counter) .build(); ``` Layout file is following: ```xml ``` ### Combined store ```java @Store({CounterReducer.class, TodoListReducer.class}) class RootStore extends BaseStore { Counter getCounter(); Flowable observeCounter(); TodoList getTodoList(); Flowable observeTodoList(); } RootStore store = DroiduxRootStore.builder() .setReducer(new CounterReducer(), new Counter(0)) .setReducer(new TodoListReducer(), new TodoList()) .addMiddleware(new Logger()) .build(); store.dispatch(new IncrementCountAction()).subscribe(); // Counter: 1, Todo: 0 store.dispatch(new AddTodoAction("new task")).subscribe(); // Counter: 1, Todo: 1 ``` ### Middleware ```java class Logger extends Middleware { @Override public Flowable beforeDispatch(Action action) { Log.d("[prev counter]", String.valueOf(getStore().count())); Log.d("[action]", action.getClass().getSimpleName()); return Flowable.just(action); } @Override public Flowable afterDispatch(Action action) { Log.d("[next counter]", String.valueOf(getStore().count())); return Flowable.just(action); } } // Instantiate store class CounterStore store = DroiduxCounterStore.builder() .setReducer(new CounterReducer(), new Counter(0)) .addMiddleware(new Logger()) // apply logger middleware .build(); // Counter: 0 store.dispatch(new IncrementCountAction()).subscribe(); // logcat: // [prev counter]: 0 // [action]: IncrementCountAction // [next counter]: 1 store.dispatch(new IncrementCountAction()).subscribe(); // logcat: // [prev counter]: 1 // [action]: IncrementCountAction // [next counter]: 2 store.dispatch(new ClearCountAction()).subscribe(); // logcat: // [prev counter]: 2 // [action]: ClearCountAction // [next counter]: 0 ``` ### Undo / Redo ```java class TodoList extends ArrayList implements UndoableState { @Override public TodoList clone() { // ... } public static Todo { // ... } } @Undoable @Reducer(TodoList.class) class TodoListReducer { @Dispatchable(AddTodoAction.class) public TodoList add(TodoList state, AddTodoAction action) { // ... } @Dispatchable(CompleteTodoAction.class) public TodoList complete(TodoList state, CompleteTodoAction action) { // ... } } @Store(TodoListReducer.class) public interface TodoListStore { TodoList todoList(); Flowable observeTodoList(); } class AddTodoAction implements Action { // ... } class CompleteTodoAction implements Action { // ... } TodoListStore store = DroiduxTodoListStore.builder() .setReducer(new TodoListReducer(), new TodoList()) .build(); store.dispatch(new AddTodoAction("item 1")).subscribe(); // ["item 1"] store.dispatch(new AddTodoAction("item 2")).subscribe(); // ["item 1", "item 2"] store.dispatch(new AddTodoAction("item 3")).subscribe(); // ["item 1", "item 2", "item 3"] store.dispatch(new CompleteTodoAction("item 2")).subscribe(); // ["item 1", "item 3"] store.dispatch(new AddTodoAction("item 4")).subscribe(); // ["item 1", "item 3", "item 4"] store.dispatch(new UndoAction(TodoList.class)).subscribe(); // => ["item 1", "item 3"] store.dispatch(new UndoAction(TodoList.class)).subscribe(); // => ["item 1", "item 2", "item 3"] store.dispatch(new RedoAction(TodoList.class)).subscribe(); // => ["item 1", "item 3"] ``` ### Async action Use [droidux-thunk](https://github.com/izumin5210/Droidux/tree/master/middlewares/droidux-thunk). ```java class FetchTodoListAction implements AsyncAction { private final TodoListApi client; public FetchTodoListAction(TodoListApi client) { this.client = client; } public Flowable call(Dispatcher dispatcher) { return dispatcher.dispatch(new DoingFetchAction()) .flatMap(_action -> client.fetch()) .map(todoList -> { this.todoList = todoList; return new ReceiveTodoListAction(todoList); }); } } class ReceiveTodoListAction implements Action { private final TodoList todoList; public ReceiveTodoListAction(TodoList todoList) { this.todoList = todoList; } public TodoList getTodoList() { return todoList; } } TodoListStore store = DroiduxTodoListStore.builder() .setReducer(new TodoListReducer(), new TodoList()) .addMiddleware(new ThunkMiddleware()) .build(); store.dispatch(new FetchTodoListAction(client)).subscribe(); ``` ## Examples * [Counter](https://github.com/izumin5210/Droidux/tree/master/examples/counter) * [TodoMVC](https://github.com/izumin5210/Droidux/tree/master/examples/todomvc) * [Todos with Undo](https://github.com/izumin5210/Droidux/tree/master/examples/todos-with-undo) * [Todos with Dagger 2](https://github.com/izumin5210/Droidux/tree/master/examples/todos-with-dagger) ## License ``` Copyright 2015 izumin5210 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. ``` [redux]: https://github.com/reactjs/redux [rxjava]: https://github.com/ReactiveX/RxJava [three-principles]: http://redux.js.org/docs/introduction/ThreePrinciples.html [databinding]: http://developer.android.com/tools/data-binding/guide.html ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() maven { url "https://jitpack.io" } maven { url 'https://maven.google.com/' name 'Google' } } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.novoda:bintray-release:0.3.4' classpath 'com.github.groovy:groovy-android-gradle-plugin:1b77dd6763' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() google() } } task clean(type: Delete) { delete rootProject.buildDir } ext { def versionMajor = 0 def versionMinor = 6 def versionPatch = 0 versionName = "${versionMajor}.${versionMinor}.${versionPatch}" compileSdkVersion = 27 buildToolsVersion = '27.0.3' minSdkVersion = 15 targetSdkVersion = compileSdkVersion databindingBaseLibraryVersion = '1.0' databindingLibraryVersion = '1.0-rc3' supportLibrariesVersion = '23.1.1' rxJava2Version = '2.1.8' rxAndroid2Version = '2.0.1' spockCoreVersion = '1.0-groovy-2.4' cglibVersion = '2.2' } ================================================ FILE: droidux/.gitignore ================================================ /build ================================================ FILE: droidux/build.gradle ================================================ apply plugin: 'java' apply plugin: 'groovy' apply plugin: 'com.novoda.bintray-release' targetCompatibility = JavaVersion.VERSION_1_7 sourceCompatibility = JavaVersion.VERSION_1_7 dependencies { compile "io.reactivex.rxjava2:rxjava:${project.rxJava2Version}" testCompile "org.spockframework:spock-core:${project.spockCoreVersion}" testCompile "cglib:cglib-nodep:${project.cglibVersion}" } publish { userOrg = project_bintray_org groupId = project_group artifactId = 'droidux' version = project.versionName description = '"Predictable state container" implementation, inspired by Redux for JS.' website = project_url } ================================================ FILE: droidux/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /usr/local/opt/android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # 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 *; #} -dontwarn android.databinding.** -dontwarn java.lang.invoke.* ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/Action.java ================================================ package info.izumin.android.droidux; /** * Created by izumin on 11/2/15. */ public interface Action { } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/BaseStore.java ================================================ package info.izumin.android.droidux; import io.reactivex.Flowable; import io.reactivex.Single; /** * Created by izumin on 12/6/15. */ public interface BaseStore { Flowable dispatch(Action action); } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/Dispatcher.java ================================================ package info.izumin.android.droidux; import java.util.Arrays; import java.util.List; import java.util.ListIterator; import io.reactivex.Flowable; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; /** * Created by izumin on 11/28/15. */ public class Dispatcher { public static final String TAG = Dispatcher.class.getSimpleName(); private final List middlewares; private final List storeImpls; public Dispatcher(List middlewares, StoreImpl... storeImpls) { this.middlewares = middlewares; this.storeImpls = Arrays.asList(storeImpls); } public Flowable dispatch(Action action) { return Flowable.just(action) .flatMap(new Function>() { @Override public Flowable apply(Action action) throws Exception { return applyMiddlewaresBeforeDispatch(action); } }).doOnNext(new Consumer() { @Override public void accept(Action action) throws Exception { for (StoreImpl store : storeImpls) { store.dispatch(action); } } }) .flatMap(new Function>() { @Override public Flowable apply(Action action) throws Exception { return applyMiddlewaresAfterDispatch(action); } }); } private Flowable applyMiddlewaresBeforeDispatch(Action action) { Flowable o = Flowable.just(action); for (final Middleware mw : middlewares) { o = o.flatMap(new Function>() { @Override public Flowable apply(Action a) throws Exception { return mw.beforeDispatch(a); } }); } return o; } private Flowable applyMiddlewaresAfterDispatch(Action action) { Flowable o = Flowable.just(action); ListIterator iterator = middlewares.listIterator(middlewares.size()); while(iterator.hasPrevious()) { final Middleware mw = iterator.previous(); o = o.flatMap(new Function>() { @Override public Flowable apply(Action a) throws Exception { return mw.afterDispatch(a); } }); } return o; } } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/History.java ================================================ package info.izumin.android.droidux; import java.util.ArrayDeque; import java.util.Deque; /** * Created by izumin on 11/23/15. */ public class History> { public static final String TAG = History.class.getSimpleName(); private static final int DEFAULT_LIMIT = 100; private final Deque past; private final Deque future; private T present; private int limit = DEFAULT_LIMIT; public History(T initialState) { past = new ArrayDeque<>(); future = new ArrayDeque<>(); present = initialState; } public T getPresent() { return present; } public void insert(T state) { past.addFirst(present); if (past.size() > limit) { past.removeLast(); } future.clear(); present = state; } public T undo() { if (isUndoable()) { future.addFirst(present); present = past.removeFirst(); } return present; } public T redo() { if (isRedoable()) { past.addFirst(present); present = future.removeFirst(); } return present; } public boolean isUndoable() { return past.size() > 0; } public boolean isRedoable() { return future.size() > 0; } public void setLimit(int limit) { this.limit = limit; while (past.size() > limit) { past.removeLast(); } while (future.size() > limit) { future.removeLast(); } } } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/Middleware.java ================================================ package info.izumin.android.droidux; import io.reactivex.Flowable; /** * Created by izumin on 11/2/15. */ public abstract class Middleware { public static final String TAG = Middleware.class.getSimpleName(); private S store; private Dispatcher dispatcher; public void onAttach(S store, Dispatcher dispatcher) { this.store = store; this.dispatcher = dispatcher; } protected S getStore() { return store; } protected Dispatcher getDispatcher() { return dispatcher; } public abstract Flowable beforeDispatch(Action action); public abstract Flowable afterDispatch(Action action); } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/OnStateChangedListener.java ================================================ package info.izumin.android.droidux; /** * Created by izumin on 12/6/15. */ public interface OnStateChangedListener { void onStateChanged(T state); } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/StoreImpl.java ================================================ package info.izumin.android.droidux; import java.util.HashSet; import java.util.Set; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; import io.reactivex.subjects.BehaviorSubject; /** * Created by izumin on 11/2/15. */ public abstract class StoreImpl { public static final String TAG = StoreImpl.class.getSimpleName(); private final BehaviorSubject subject; private T state; private final R reducer; private final Set> listeners; protected StoreImpl(T state, R reducer) { this.state = state; this.reducer = reducer; subject = BehaviorSubject.create(); listeners = new HashSet<>(); } public Flowable observe() { return observe(BackpressureStrategy.DROP); } public Flowable observe(BackpressureStrategy strategy) { return subject.toFlowable(strategy); } public T getState() { return state; } protected void setState(T state) { this.state = state; subject.onNext(state); for (OnStateChangedListener listener : listeners) { listener.onStateChanged(state); } } protected R getReducer() { return reducer; } public void addListener(OnStateChangedListener listener) { listeners.add(listener); } protected abstract void dispatch(Action action); } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/UndoableState.java ================================================ package info.izumin.android.droidux; /** * Created by izumin on 11/24/15. */ public interface UndoableState extends Cloneable { T clone(); } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/UndoableStoreImpl.java ================================================ package info.izumin.android.droidux; import info.izumin.android.droidux.action.HistoryAction; /** * Created by izumin on 11/25/15. */ public abstract class UndoableStoreImpl, R> extends StoreImpl { public static final String TAG = UndoableStoreImpl.class.getSimpleName(); private final History history; protected UndoableStoreImpl(T state, R reducer) { super(state, reducer); history = new History<>(state); } @Override protected void dispatch(Action action) { if (HistoryAction.class.isAssignableFrom(action.getClass())) { HistoryAction historyAction = (HistoryAction) action; if (((HistoryAction) action).isAssignableTo(getReducer())) { setStateWithoutKeepingHistory(historyAction.handle(history)); } } } protected void setStateWithoutKeepingHistory(T state) { super.setState(state); } @Override protected void setState(T state) { history.insert(state); super.setState(state); } @Override public T getState() { return history.getPresent(); } public History getHistory() { return history; } public boolean isUndoable() { return history.isUndoable(); } public boolean isRedoable() { return history.isRedoable(); } public void setLimit(int limit) { history.setLimit(limit); } } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/action/HistoryAction.java ================================================ package info.izumin.android.droidux.action; import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; import info.izumin.android.droidux.Action; import info.izumin.android.droidux.History; import info.izumin.android.droidux.UndoableState; import info.izumin.android.droidux.annotation.Reducer; import info.izumin.android.droidux.annotation.Undoable; /** * Created by izumin on 11/24/15. */ public class HistoryAction implements Action { public static final String TAG = HistoryAction.class.getSimpleName(); enum Kind { UNDO { @Override > T handle(History history) { return history.undo(); } }, REDO { @Override > T handle(History history) { return history.redo(); } }; abstract > T handle(History history); } private final Kind kind; private final Class targetReducerType; public HistoryAction(Kind kind, Class targetReducerType) { for (Class annotationType : getNecessaryAnnotationTypes()) { if (targetReducerType.getAnnotation(annotationType) == null) { throw new IllegalArgumentException(); } } this.kind = kind; this.targetReducerType = targetReducerType; } public boolean isAssignableTo(R reducer) { return targetReducerType.equals(reducer.getClass()); } public > T handle(History history) { return kind.handle(history); } protected Set> getNecessaryAnnotationTypes() { return new HashSet>() {{ add(Reducer.class); add(Undoable.class); }}; } } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/action/RedoAction.java ================================================ package info.izumin.android.droidux.action; /** * Created by izumin on 11/24/15. */ public class RedoAction extends HistoryAction { public static final String TAG = RedoAction.class.getSimpleName(); public RedoAction(Class targetReducerType) { super(Kind.REDO, targetReducerType); } } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/action/UndoAction.java ================================================ package info.izumin.android.droidux.action; /** * Created by izumin on 11/24/15. */ public class UndoAction extends HistoryAction { public static final String TAG = UndoAction.class.getSimpleName(); public UndoAction(Class targetReducerType) { super(Kind.UNDO, targetReducerType); } } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/annotation/Dispatchable.java ================================================ package info.izumin.android.droidux.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by izumin on 11/2/15. */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface Dispatchable { Class value(); } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/annotation/Reducer.java ================================================ package info.izumin.android.droidux.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by izumin on 11/2/15. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Reducer { Class value(); } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/annotation/Store.java ================================================ package info.izumin.android.droidux.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by izumin on 11/2/15. */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface Store { Class[] value() default {}; } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/annotation/Undoable.java ================================================ package info.izumin.android.droidux.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created by izumin on 11/23/15. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Undoable { } ================================================ FILE: droidux/src/main/java/info/izumin/android/droidux/exception/NotInitializedException.java ================================================ package info.izumin.android.droidux.exception; /** * Created by izumin on 11/8/15. */ public class NotInitializedException extends RuntimeException { public NotInitializedException(String message) { super(message); } } ================================================ FILE: droidux/src/test/groovy/info/izumin/android/droidux/DispatcherTest.groovy ================================================ package info.izumin.android.droidux import io.reactivex.Flowable import spock.lang.Specification /** * Created by izumin on 11/24/15. */ class DispatcherTest extends Specification { def store1 def store2 def middleware1 def middleware2 def action def dispatcher def setup() { store1 = Mock(StoreImpl.class, constructorArgs: [null, null]) store2 = Mock(StoreImpl.class, constructorArgs: [null, null]) middleware1 = Mock(Middleware.class) middleware2 = Mock(Middleware.class) action = Mock(Action.class) def middlewares = new ArrayList() middlewares.add(middleware1) middlewares.add(middleware2) dispatcher = new Dispatcher(middlewares, store1, store2) } def "#dispatch()"() { when: dispatcher.dispatch(action).subscribe() then: 1 * middleware1.beforeDispatch(action) >> Flowable.just(action) then: 1 * middleware2.beforeDispatch(action) >> Flowable.just(action) then: 1 * store1.dispatch(action) then: 1 * store2.dispatch(action) then: 1 * middleware2.afterDispatch(action) >> Flowable.just(action) then: 1 * middleware1.afterDispatch(action) >> Flowable.just(action) } } ================================================ FILE: droidux/src/test/groovy/info/izumin/android/droidux/HistoryTest.groovy ================================================ package info.izumin.android.droidux import spock.lang.Specification; /** * Created by izumin on new Counter(1)new Counter(1)/new Counter(2)new Counter(4)/new Counter(1)new Counter(5). */ class HistoryTest extends Specification { def history def past def future static class Counter implements UndoableState { def count Counter(count) { this.count = count } @Override boolean equals(Object o) { return (o instanceof Counter) && (o.hashCode() == hashCode()) } @Override int hashCode() { return count } @Override Counter clone() { return new Counter(count) } } def setup() { history = new History(new Counter(0)) past = History.metaClass.getAttribute(history, "past") future = History.metaClass.getAttribute(history, "future") } def getPresent() { History.metaClass.getAttribute(history, "present") } def "#insert()"() { when: history.insert(new Counter(1)) then: getPresent() == new Counter(1) when: history.insert(new Counter(3)) then: getPresent() == new Counter(3) } def "#undo()"() { when: history.insert(new Counter(1)) then: past.size() == 1 history.undo() == new Counter(0) past.size() == 0 when: history.insert(new Counter(3)) history.insert(new Counter(5)) then: past.size() == 2 history.undo() == new Counter(3) past.size() == 1 history.undo() == new Counter(0) past.size() == 0 history.undo() == new Counter(0) } def "#redo()"() { when: history.insert(new Counter(1)) history.undo() then: future.size() == 1 history.redo() == new Counter(1) future.size() == 0 when: history.insert(new Counter(3)) history.insert(new Counter(5)) history.undo() history.undo() then: future.size() == 2 history.redo() == new Counter(3) future.size() == 1 history.redo() == new Counter(5) future.size() == 0 history.redo() == new Counter(5) when: history.insert(new Counter(8)) history.insert(new Counter(7)) history.undo() history.undo() then: future.size() == 2 history.insert(new Counter(4)) future.size() == 0 } def "#setLimit()"() { when: history.setLimit(3) history.insert(new Counter(1)) then: past.size() == 1 when: history.insert(new Counter(2)) then: past.size() == 2 when: history.insert(new Counter(3)) then: past.size() == 3 when: history.insert(new Counter(4)) then: past.size() == 3 history.undo() == new Counter(3) history.undo() == new Counter(2) history.undo() == new Counter(1) history.undo() == new Counter(1) when: history.insert(new Counter(7)) history.insert(new Counter(8)) history.insert(new Counter(6)) history.insert(new Counter(5)) then: past.size() == 3 history.setLimit(2) past.size() == 2 history.undo() == new Counter(6) history.undo() == new Counter(8) history.undo() == new Counter(8) } } ================================================ FILE: droidux/src/test/groovy/info/izumin/android/droidux/action/HistoryActionTest.groovy ================================================ package info.izumin.android.droidux.action import info.izumin.android.droidux.UndoableState import info.izumin.android.droidux.annotation.Reducer import info.izumin.android.droidux.annotation.Undoable import spock.lang.Specification /** * Created by izumin on 12/5/15. */ class HistoryActionTest extends Specification { class Counter implements UndoableState { @Override Counter clone() { return new Counter() } } class SubUndoableDummyReducer extends UndoableDummyReducer {} @Undoable @Reducer(Counter.class) class UndoableDummyReducer {} @Reducer(String.class) class DummyReducer {} class DummyClass {} def "when pass the class annotated with @Reducer and @Undoable to the constructor"() { when: new HistoryAction(HistoryAction.Kind.UNDO, UndoableDummyReducer.class) then: noExceptionThrown() } def "when pass the class is not annotated with @Reducer to the constructor"() { when: new HistoryAction(HistoryAction.Kind.UNDO, DummyReducer.class) then: thrown(IllegalArgumentException.class) } def "when pass the class that has no annotations to the constructor"() { when: new HistoryAction(HistoryAction.Kind.UNDO, DummyClass.class) then: thrown(IllegalArgumentException.class) } def "isAssignableTo()"() { setup: def action = new HistoryAction(HistoryAction.Kind.UNDO, UndoableDummyReducer.class) expect: action.isAssignableTo(reducer) == result where: reducer | result new UndoableDummyReducer() | true new Object() | false new DummyReducer() | false new SubUndoableDummyReducer() | false } } ================================================ FILE: droidux-processor/.gitignore ================================================ /build ================================================ FILE: droidux-processor/build.gradle ================================================ import org.gradle.internal.jvm.Jvm apply plugin: 'java' apply plugin: 'com.novoda.bintray-release' targetCompatibility = JavaVersion.VERSION_1_7 sourceCompatibility = JavaVersion.VERSION_1_7 sourceSets { main { java { srcDirs = ['src/main/java', '../droidux/src/main/java'] } } } dependencies { compile 'com.squareup:javapoet:1.7.0' compile 'com.google.auto:auto-common:0.6' compile 'com.google.auto.service:auto-service:1.0-rc2' compile "io.reactivex.rxjava2:rxjava:${project.rxJava2Version}" compile "com.android.databinding:baseLibrary:${project.databindingBaseLibraryVersion}" compile fileTree(dir: './libs', includes: ['*.jar']) testCompile 'junit:junit:4.12' testCompile 'org.assertj:assertj-core:2.2.0' testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'com.google.testing.compile:compile-testing:0.6' testCompile files(Jvm.current().getToolsJar()) testCompile 'com.google.android:android:4.1.1.4' } publish { userOrg = project_bintray_org groupId = project_group artifactId = 'droidux-processor' version = project.versionName description = 'Code generator for info.izumin.android.droidux' website = project_url } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/AbstractProcessingStep.java ================================================ package info.izumin.android.droidux.processor; import com.google.auto.common.BasicAnnotationProcessor; import com.squareup.javapoet.JavaFile; import java.io.IOException; import javax.annotation.processing.Filer; /** * Created by izumin on 11/26/15. */ public abstract class AbstractProcessingStep implements BasicAnnotationProcessor.ProcessingStep { public static final String TAG = AbstractProcessingStep.class.getSimpleName(); private final Filer filer; public AbstractProcessingStep(Filer filer) { this.filer = filer; } protected void write(JavaFile file) { try { file.writeTo(filer); } catch (IOException e) { e.printStackTrace(); } } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/DroiduxProcessor.java ================================================ package info.izumin.android.droidux.processor; import com.google.auto.common.BasicAnnotationProcessor; import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableList; import javax.annotation.processing.Filer; import javax.annotation.processing.Processor; import javax.lang.model.SourceVersion; @AutoService(Processor.class) public class DroiduxProcessor extends BasicAnnotationProcessor { @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override protected Iterable initSteps() { return ImmutableList.of( new ReducerProcessingStep(getFiler()), new StoreProcessingStep(getFiler()) ); } private Filer getFiler() { return processingEnv.getFiler(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/ReducerProcessingStep.java ================================================ package info.izumin.android.droidux.processor; import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; import javax.annotation.processing.Filer; import javax.lang.model.element.Element; import info.izumin.android.droidux.annotation.Reducer; /** * Created by izumin on 11/26/15. */ public class ReducerProcessingStep extends AbstractProcessingStep { public static final String TAG = ReducerProcessingStep.class.getSimpleName(); public ReducerProcessingStep(Filer filer) { super(filer); } @Override public Set> annotations() { return ImmutableSet.>of(Reducer.class); } @Override public Set process(SetMultimap, Element> elementsByAnnotation) { // for (Element element : elementsByAnnotation.get(Reducer.class)) { // write(new StoreImplClassGenerator(new ReducerModel((TypeElement) element)).createJavaFile()); // } return new HashSet<>(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/StoreProcessingStep.java ================================================ package info.izumin.android.droidux.processor; import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; import javax.annotation.processing.Filer; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import info.izumin.android.droidux.annotation.Store; import info.izumin.android.droidux.processor.generator.StoreClassGenerator; import info.izumin.android.droidux.processor.generator.StoreImplClassGenerator; import info.izumin.android.droidux.processor.model.StoreImplModel; import info.izumin.android.droidux.processor.model.StoreModel; /** * Created by izumin on 11/26/15. */ public class StoreProcessingStep extends AbstractProcessingStep { public static final String TAG = StoreProcessingStep.class.getSimpleName(); public StoreProcessingStep(Filer filer) { super(filer); } @Override public Set> annotations() { return ImmutableSet.>of(Store.class); } @Override public Set process(SetMultimap, Element> elementsByAnnotation) { for (Element element : elementsByAnnotation.get(Store.class)) { StoreModel model = new StoreModel((TypeElement) element); for (StoreImplModel storeImplModel : model.getStoreImplModels()) { write(new StoreImplClassGenerator(storeImplModel).createJavaFile()); } write(new StoreClassGenerator(model).createJavaFile()); } return new HashSet<>(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/exception/InvalidDispatchableDeclarationException.java ================================================ package info.izumin.android.droidux.processor.exception; /** * Created by izumin on 11/7/15. */ public class InvalidDispatchableDeclarationException extends RuntimeException { public InvalidDispatchableDeclarationException(String message) { super(message); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/exception/InvalidReducerDeclarationException.java ================================================ package info.izumin.android.droidux.processor.exception; /** * Created by izumin on 11/8/15. */ public class InvalidReducerDeclarationException extends RuntimeException { public InvalidReducerDeclarationException(String message) { super(message); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/exception/InvalidStoreDelcarationException.java ================================================ package info.izumin.android.droidux.processor.exception; /** * Created by izumin on 11/24/15. */ public class InvalidStoreDelcarationException extends RuntimeException { public InvalidStoreDelcarationException(String message) { super(message); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/generator/StoreBuilderClassGenerator.java ================================================ package info.izumin.android.droidux.processor.generator; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.util.ArrayList; import java.util.List; import javax.lang.model.element.Modifier; import info.izumin.android.droidux.Middleware; import info.izumin.android.droidux.exception.NotInitializedException; import info.izumin.android.droidux.processor.model.BuilderModel; import info.izumin.android.droidux.processor.model.ReducerModel; import info.izumin.android.droidux.processor.model.StoreImplModel; import info.izumin.android.droidux.processor.model.StoreModel; import static info.izumin.android.droidux.processor.util.PoetUtils.getParameterSpec; import static info.izumin.android.droidux.processor.util.StringUtils.getLowerCamelFromUpperCamel; /** * Created by izumin on 11/2/15. */ public class StoreBuilderClassGenerator { public static final String TAG = StoreBuilderClassGenerator.class.getSimpleName(); static final String ERROR_MESSAGE_NOT_INITIALIZED_EXCEPTION = "$T has not been initialized."; private final BuilderModel builderModel; public StoreBuilderClassGenerator(StoreModel storeModel) { builderModel = storeModel.getBuilderModel(); } public TypeSpec createBuilderTypeSpec() { return TypeSpec.classBuilder(builderModel.getClassName().simpleName()) .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .addFields(createFieldSpecs()) .addMethod(createAddMiddlewareMethodSpec()) .addMethod(createBuilderConstructor()) .addMethods(createReducerSetterMethodSpecs()) .addMethods(createReducerAndStateSetterMethodSpecs()) .addMethod(createBuildMethodSpec()) .build(); } private List createFieldSpecs() { List specs = new ArrayList<>(); specs.add(FieldSpec.builder(BuilderModel.MIDDLEWARES_TYPE, BuilderModel.MIDDLEWARES_VARIABLE_NAME, Modifier.FINAL, Modifier.PRIVATE).build()); specs.addAll(FluentIterable.from(builderModel.getReducerModels()) .transform(new Function() { @Override public FieldSpec apply(ReducerModel input) { return FieldSpec.builder(input.getClassName(), input.getVariableName(), Modifier.PRIVATE).build(); } }) .toList()); specs.addAll(FluentIterable.from(builderModel.getReducerModels()) .transform(new Function() { @Override public FieldSpec apply(ReducerModel input) { return FieldSpec.builder(input.getState(), input.getStateVariableName(), Modifier.PRIVATE).build(); } }) .toList()); specs.addAll(FluentIterable.from(builderModel.getStoreModel().getStoreImplModels()) .filter(new Predicate() { @Override public boolean apply(StoreImplModel input) { return input.isBindable(); } }) .transform(new Function() { @Override public FieldSpec apply(StoreImplModel input) { return FieldSpec.builder(TypeName.INT, input.getFieldIdName(), Modifier.PRIVATE) .build(); } }) .toList()); return specs; } private MethodSpec createBuilderConstructor() { return MethodSpec.constructorBuilder() .addModifiers(Modifier.PRIVATE) .addStatement("$N = new $T<>()", BuilderModel.MIDDLEWARES_VARIABLE_NAME, ArrayList.class) .build(); } private MethodSpec createAddMiddlewareMethodSpec() { return MethodSpec.methodBuilder(BuilderModel.ADD_MIDDLEWARE_METHOD_NAME) .addModifiers(Modifier.PUBLIC) .returns(builderModel.getClassName()) .addParameter(getParameterSpec(Middleware.class)) .addStatement("$N.add($N)", BuilderModel.MIDDLEWARES_VARIABLE_NAME, getLowerCamelFromUpperCamel(Middleware.class.getName())) .addStatement("return this") .build(); } private List createReducerSetterMethodSpecs() { return FluentIterable.from(builderModel.getStoreModel().getStoreImplModels()) .transform(new Function() { @Override public MethodSpec apply(StoreImplModel input) { MethodSpec.Builder builder = MethodSpec.methodBuilder(BuilderModel.REDUCER_SETTER_METHOD_NAME) .addModifiers(Modifier.PUBLIC) .returns(builderModel.getClassName()) .addParameter(getParameterSpec(input.getReducerModel().getClassName())); if (input.isBindable()) { builder = builder.addParameter(TypeName.INT, input.getFieldIdName()) .addStatement("this.$N = $N", input.getFieldIdName(), input.getFieldIdName()); } return builder.addStatement("this.$N = $N", input.getReducerModel().getVariableName(), input.getReducerModel().getVariableName()) .addStatement("return this") .build(); } }) .toList(); } private List createReducerAndStateSetterMethodSpecs() { return FluentIterable.from(builderModel.getStoreModel().getStoreImplModels()) .transform(new Function() { @Override public MethodSpec apply(StoreImplModel input) { MethodSpec.Builder builder = MethodSpec.methodBuilder(BuilderModel.REDUCER_SETTER_METHOD_NAME) .addModifiers(Modifier.PUBLIC) .returns(builderModel.getClassName()) .addParameter(getParameterSpec(input.getReducerModel().getClassName())) .addParameter(getParameterSpec(input.getState())); if (input.isBindable()) { builder = builder.addParameter(TypeName.INT, input.getFieldIdName()); } builder = builder.addStatement("this.$N = $N", input.getStateVariableName(), input.getStateVariableName()); if (input.isBindable()) { builder = builder.addStatement("return $N($N, $N)", BuilderModel.REDUCER_SETTER_METHOD_NAME, input.getReducerModel().getVariableName(), input.getFieldIdName()); } else { builder = builder.addStatement("return $N($N)", BuilderModel.REDUCER_SETTER_METHOD_NAME, input.getReducerModel().getVariableName()); } return builder.build(); } }) .toList(); } private MethodSpec createBuildMethodSpec() { MethodSpec.Builder builder = MethodSpec.methodBuilder(BuilderModel.BUILD_METHOD_NAME) .addModifiers(Modifier.PUBLIC) .returns(builderModel.getStoreModel().getClassName()); for (ReducerModel reducerModel : builderModel.getReducerModels()) { builder = builder .beginControlFlow("if ($N == null)", reducerModel.getVariableName()) .addStatement("throw new $T(\"" + ERROR_MESSAGE_NOT_INITIALIZED_EXCEPTION + "\")", NotInitializedException.class, reducerModel.getClassName()) .endControlFlow(); } return builder .addStatement("return new $T(this)", builderModel.getStoreModel().getClassName()) .build(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/generator/StoreClassGenerator.java ================================================ package info.izumin.android.droidux.processor.generator; import android.databinding.BaseObservable; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.util.ArrayList; import java.util.List; import javax.lang.model.element.Modifier; import info.izumin.android.droidux.Action; import info.izumin.android.droidux.Middleware; import info.izumin.android.droidux.OnStateChangedListener; import info.izumin.android.droidux.processor.model.BuilderModel; import info.izumin.android.droidux.processor.model.DispatcherModel; import info.izumin.android.droidux.processor.model.StoreImplModel; import info.izumin.android.droidux.processor.model.StoreMethodModel; import info.izumin.android.droidux.processor.model.StoreModel; import io.reactivex.Flowable; import io.reactivex.Single; import static info.izumin.android.droidux.processor.util.PoetUtils.getOverrideAnnotation; import static info.izumin.android.droidux.processor.util.PoetUtils.getParameterSpec; import static info.izumin.android.droidux.processor.util.StringUtils.getLowerCamelFromUpperCamel; /** * Created by izumin on 11/3/15. */ public class StoreClassGenerator { public static final String TAG = StoreClassGenerator.class.getSimpleName(); private final StoreModel storeModel; public StoreClassGenerator(StoreModel storeModel) { this.storeModel = storeModel; } public JavaFile createJavaFile() { return JavaFile.builder(storeModel.getClassName().packageName(), createTypeSpec()) .skipJavaLangImports(true).build(); } private TypeSpec createTypeSpec() { return TypeSpec.classBuilder(storeModel.getClassName().simpleName()) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addSuperinterface(storeModel.getInterfaceName()) .superclass(TypeName.get(BaseObservable.class)) .addFields(createFieldSpecs()) .addMethod(createConstructor()) .addMethod(createBuilderMethodSpec()) .addMethods(createGetterMethodSpecs()) .addMethod(createDispatchMethodSpec()) .addType(new StoreBuilderClassGenerator(storeModel).createBuilderTypeSpec()) .build(); } private List createFieldSpecs() { List specs = new ArrayList<>(); specs.addAll(FluentIterable.from(storeModel.getStoreImplModels()) .transform(new Function() { @Override public FieldSpec apply(StoreImplModel input) { return FieldSpec.builder(input.getClassName(), input.getVariableName(), Modifier.PRIVATE, Modifier.FINAL).build(); } }).toList()); specs.add(DispatcherModel.fieldSpec()); return specs; } private MethodSpec createConstructor() { MethodSpec.Builder builder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PROTECTED) .addParameter(getParameterSpec(storeModel.getBuilderModel().getClassName(), Modifier.FINAL)); for (StoreImplModel storeImpl : storeModel.getStoreImplModels()) { builder = builder.addStatement("$N = new $T($N.$N, $N.$N)", storeImpl.getVariableName(), storeImpl.getClassName(), BuilderModel.VARIABLE_NAME, storeImpl.getStateVariableName(), BuilderModel.VARIABLE_NAME, storeImpl.getReducerModel().getVariableName()); if (storeImpl.isBindable()) { TypeSpec listener = TypeSpec.anonymousClassBuilder("") .addSuperinterface(ParameterizedTypeName.get(ClassName.get(OnStateChangedListener.class), storeImpl.getState())) .addMethod( MethodSpec.methodBuilder(StoreImplModel.ON_STATE_CHANGED_METHOD_NAME) .addAnnotation(getOverrideAnnotation()) .addModifiers(Modifier.PUBLIC) .addParameter(getParameterSpec(storeImpl.getState())) .addStatement("$N($N.$N)", StoreImplModel.NOTIFY_PROPERTY_CHANGED_METHOD_NAME, BuilderModel.VARIABLE_NAME, storeImpl.getFieldIdName()) .build() ) .build(); builder = builder.addStatement("$N.$N($L)", storeImpl.getVariableName(), StoreImplModel.ADD_LISTENER_METHOD_NAME, listener); } } final String middlewareFiledName = "middleware"; return builder .addStatement("$N = new $N($N.$N, $N)", DispatcherModel.VARIABLE_NAME, DispatcherModel.CLASS_NAME, BuilderModel.VARIABLE_NAME, BuilderModel.MIDDLEWARES_VARIABLE_NAME, FluentIterable.from(storeModel.getStoreImplModels()) .transform(new Function() { @Override public String apply(StoreImplModel input) { return input.getVariableName(); } }).join(Joiner.on(", "))) .beginControlFlow("for ($T $N : $N.$N)", Middleware.class, middlewareFiledName, BuilderModel.VARIABLE_NAME, StoreModel.MIDDLEWARES_FIELD_NAME) .addStatement("$N.$N(this, $N)", middlewareFiledName, StoreModel.ATTACH_MIDDLEWARE_METHOD_NAME, DispatcherModel.VARIABLE_NAME) .endControlFlow() .build(); } private MethodSpec createBuilderMethodSpec() { return MethodSpec.methodBuilder(StoreModel.BUILDER_METHOD_NAME) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(storeModel.getBuilderModel().getClassName()) .addStatement("return new $T()", storeModel.getBuilderModel().getClassName()) .build(); } private List createGetterMethodSpecs() { return FluentIterable.from(storeModel.getMethodModels()) .transform(new Function() { @Override public MethodSpec apply(StoreMethodModel input) { return MethodSpec.methodBuilder(input.getName()) .addAnnotation(getOverrideAnnotation()) .addModifiers(Modifier.PUBLIC) .returns(TypeName.get(input.getReturnType())) .addParameters(input.getParameters()) .addCode(input.getCodeBlock()) .build(); } }) .toList(); } private MethodSpec createDispatchMethodSpec() { return MethodSpec.methodBuilder(StoreModel.DISPATCH_METHOD_NAME) .addAnnotation(getOverrideAnnotation()) .addModifiers(Modifier.PUBLIC) .returns(ParameterizedTypeName.get(ClassName.get(Flowable.class), ClassName.get(Action.class))) .addParameter(getParameterSpec(Action.class)) .addStatement("return $N.$N($N)", DispatcherModel.VARIABLE_NAME, DispatcherModel.DISPATCH_METHOD_NAME, getLowerCamelFromUpperCamel(ClassName.get(Action.class).simpleName())) .build(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/generator/StoreImplClassGenerator.java ================================================ package info.izumin.android.droidux.processor.generator; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.util.ArrayList; import java.util.List; import javax.lang.model.element.Modifier; import info.izumin.android.droidux.Action; import info.izumin.android.droidux.StoreImpl; import info.izumin.android.droidux.UndoableStoreImpl; import info.izumin.android.droidux.processor.model.DispatchableModel; import info.izumin.android.droidux.processor.model.StoreImplModel; import static info.izumin.android.droidux.processor.util.PoetUtils.getOverrideAnnotation; import static info.izumin.android.droidux.processor.util.PoetUtils.getParameterSpec; /** * Created by izumin on 11/2/15. */ public class StoreImplClassGenerator { public static final String TAG = StoreImplClassGenerator.class.getSimpleName(); private final StoreImplModel storeImplModel; public StoreImplClassGenerator(StoreImplModel storeImplModel) { this.storeImplModel = storeImplModel; } public JavaFile createJavaFile() { return JavaFile.builder(storeImplModel.getPackageName(), createTypeSpec()) .skipJavaLangImports(true).build(); } private TypeSpec createTypeSpec() { return TypeSpec.classBuilder(storeImplModel.getClassName().simpleName()) .addModifiers(Modifier.FINAL) .superclass(ParameterizedTypeName.get(ClassName.get(storeImplModel.isUndoable() ? UndoableStoreImpl.class : StoreImpl.class), storeImplModel.getState(), storeImplModel.getReducerModel().getClassName())) .addMethod(createConstructor()) .addMethod(createMethodSpec()) .build(); } private MethodSpec createConstructor() { return MethodSpec.constructorBuilder() .addModifiers(Modifier.PROTECTED) .addParameter(storeImplModel.getState(), StoreImplModel.STATE_VARIABLE_NAME) .addParameter(storeImplModel.getReducerModel().getClassName(), StoreImplModel.REDUCER_VARIABLE_NAME) .addStatement("super($N, $N)", StoreImplModel.STATE_VARIABLE_NAME, StoreImplModel.REDUCER_VARIABLE_NAME) .build(); } private MethodSpec createMethodSpec() { return MethodSpec.methodBuilder(StoreImplModel.DISPATCH_METHOD_NAME) .addAnnotation(getOverrideAnnotation()) .addModifiers(Modifier.PROTECTED) .returns(TypeName.VOID) .addParameter(getParameterSpec(Action.class)) .addCode(createCodeBlock()) .build(); } private CodeBlock createCodeBlock() { CodeBlock.Builder builder = CodeBlock.builder(); final String ACTION_FIELD = "action"; final String ACTION_CLASS_FIELD = "actionClass"; final String RESULT_FIELD = "result"; final String STATE_GETTER = storeImplModel.isUndoable() ? "getState().clone()" : "getState()"; if (storeImplModel.isUndoable()) { builder = builder.addStatement("super.$N($N)", StoreImplModel.DISPATCH_METHOD_NAME, ACTION_FIELD); } builder = builder.addStatement("Class $N = $N.getClass()", Action.class, ACTION_CLASS_FIELD, ACTION_FIELD) .addStatement("$T $N = null", storeImplModel.getState(), RESULT_FIELD); for (final DispatchableModel dispatchableModel : storeImplModel.getReducerModel().getDispatchableModels()) { final List args = new ArrayList<>(); args.add(RESULT_FIELD); args.add(StoreImplModel.REDUCER_GETTER_METHOD_NAME); args.add(dispatchableModel.getMethodName()); final String format = "$N = $N().$N(" + FluentIterable.from(dispatchableModel.getArguments()) .transform(new Function() { @Override public String apply(ClassName input) { if (input.simpleName().equals(dispatchableModel.getAction().simpleName())) { args.add(dispatchableModel.getAction()); args.add(ACTION_FIELD); return "($T) $N"; } else if (input.simpleName().equals(storeImplModel.getState().simpleName())) { return STATE_GETTER; } else { return ""; } } }).join(Joiner.on(", ")) + ")"; builder = builder.beginControlFlow("if ($T.class.isAssignableFrom($N))", dispatchableModel.getAction(), ACTION_CLASS_FIELD) .addStatement(format, args.toArray()) .endControlFlow(); } return builder .beginControlFlow("if ($N != null)", RESULT_FIELD) .addStatement("setState($N)", RESULT_FIELD) .endControlFlow() .build(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/model/BuilderModel.java ================================================ package info.izumin.android.droidux.processor.model; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.ParameterizedTypeName; import java.util.List; import info.izumin.android.droidux.Middleware; import static info.izumin.android.droidux.processor.util.StringUtils.getLowerCamelFromUpperCamel; /** * Created by izumin on 11/28/15. */ public class BuilderModel { public static final String TAG = BuilderModel.class.getSimpleName(); public static final String CLASS_NAME = "Builder"; public static final String VARIABLE_NAME = getLowerCamelFromUpperCamel(CLASS_NAME); public static final String ADD_MIDDLEWARE_METHOD_NAME = "addMiddleware"; public static final String STATE_SETTER_METHOD_NAME = "setInitialState"; public static final String REDUCER_SETTER_METHOD_NAME = "setReducer"; public static final String BUILD_METHOD_NAME = "build"; public static final ParameterizedTypeName MIDDLEWARES_TYPE = ParameterizedTypeName.get(List.class, Middleware.class); public static final String MIDDLEWARES_VARIABLE_NAME = "middlewares"; private final ClassName className; private final StoreModel storeModel; private final List reducerModels; public BuilderModel(StoreModel storeModel) { this.storeModel = storeModel; this.className = storeModel.getClassName().nestedClass(CLASS_NAME); this.reducerModels = storeModel.getReducerModels(); } public StoreModel getStoreModel() { return storeModel; } public ClassName getClassName() { return className; } public List getReducerModels() { return reducerModels; } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/model/DispatchableModel.java ================================================ package info.izumin.android.droidux.processor.model; import com.google.auto.common.MoreTypes; import com.google.common.base.Function; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import info.izumin.android.droidux.annotation.Dispatchable; import info.izumin.android.droidux.processor.validator.DispatchableValidator; import static com.google.auto.common.MoreTypes.asTypeElement; import static info.izumin.android.droidux.processor.util.AnnotationUtils.getTypeFromAnnotation; /** * Created by izumin on 11/3/15. */ public class DispatchableModel { public static final String TAG = DispatchableModel.class.getSimpleName(); private final ExecutableElement element; private final ReducerModel reducerModel; private final ClassName action; private final String methodName; private final List arguments; public DispatchableModel(ExecutableElement element, ReducerModel reducerModel) { this.element = element; this.reducerModel = reducerModel; this.methodName = element.getSimpleName().toString(); this.action = ClassName.get(asTypeElement(getTypeFromAnnotation(element, Dispatchable.class, "value"))); this.arguments = FluentIterable.from(element.getParameters()) .transform(new Function() { @Override public ClassName apply(VariableElement input) { return ClassName.get(MoreTypes.asTypeElement(input.asType())); } }).toList(); DispatchableValidator.validate(this); } public ClassName getAction() { return action; } public String getMethodName() { return methodName; } public int argumentCount() { return element.getParameters().size(); } public ExecutableElement getElement() { return element; } public List getArguments() { return arguments; } public ClassName getReducerClassName() { return reducerModel.getClassName(); } public ClassName getState() { return reducerModel.getState(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/model/DispatcherModel.java ================================================ package info.izumin.android.droidux.processor.model; import com.squareup.javapoet.FieldSpec; import javax.lang.model.element.Modifier; import info.izumin.android.droidux.Dispatcher; import static info.izumin.android.droidux.processor.util.StringUtils.getLowerCamelFromUpperCamel; /** * Created by izumin on 11/28/15. */ public class DispatcherModel { public static final String TAG = DispatcherModel.class.getSimpleName(); public static final Class CLASS = Dispatcher.class; public static final String CLASS_NAME = "Dispatcher"; public static final String VARIABLE_NAME = getLowerCamelFromUpperCamel(CLASS_NAME); public static final String DISPATCH_METHOD_NAME = "dispatch"; public static FieldSpec fieldSpec() { return FieldSpec.builder(CLASS, VARIABLE_NAME, Modifier.PRIVATE, Modifier.FINAL).build(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/model/ReducerModel.java ================================================ package info.izumin.android.droidux.processor.model; import com.squareup.javapoet.ClassName; import java.util.ArrayList; import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import info.izumin.android.droidux.annotation.Dispatchable; import info.izumin.android.droidux.annotation.Reducer; import info.izumin.android.droidux.annotation.Undoable; import info.izumin.android.droidux.processor.util.StringUtils; import info.izumin.android.droidux.processor.validator.ReducerValidator; import static com.google.auto.common.MoreTypes.asTypeElement; import static info.izumin.android.droidux.processor.util.AnnotationUtils.findMethodsByAnnotation; import static info.izumin.android.droidux.processor.util.AnnotationUtils.getTypeFromAnnotation; /** * Created by izumin on 11/3/15. */ public class ReducerModel { public static final String TAG = ReducerModel.class.getSimpleName(); public static final String CLASS_NAME_SUFFIX = "Reducer"; private final TypeElement element; private final TypeElement stateElement; private final ClassName state; private final ClassName className; private final String qualifiedName; private final String packageName; private final String variableName; private String stateName; private String stateVariableName; private final boolean isUndoable; private List dispatchableModels; public ReducerModel(TypeElement element) { this.element = element; this.stateElement = asTypeElement(getTypeFromAnnotation(element, Reducer.class, "value")); this.state = ClassName.get(stateElement); this.stateName = state.simpleName(); this.stateVariableName = StringUtils.getLowerCamelFromUpperCamel(stateName); this.isUndoable = element.getAnnotation(Undoable.class) != null; this.className = ClassName.get(element); this.qualifiedName = element.getQualifiedName().toString(); this.packageName = StringUtils.getPackageName(qualifiedName); this.variableName = StringUtils.getLowerCamelFromUpperCamel(className.simpleName()); this.dispatchableModels = new ArrayList<>(); for (ExecutableElement el : findMethodsByAnnotation(element, Dispatchable.class)) { dispatchableModels.add(new DispatchableModel(el, this)); } ReducerValidator.validate(this); } public TypeElement getElement() { return element; } public TypeElement getStateElement() { return stateElement; } public ClassName getState() { return state; } public ClassName getClassName() { return className; } public String getQualifiedName() { return qualifiedName; } public String getPackageName() { return packageName; } public String getVariableName() { return variableName; } public String getStateName() { return stateName; } public String getStateVariableName() { return stateVariableName; } public List getDispatchableModels() { return dispatchableModels; } public boolean isUndoable() { return isUndoable; } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/model/StoreImplModel.java ================================================ package info.izumin.android.droidux.processor.model; import com.squareup.javapoet.ClassName; import static info.izumin.android.droidux.processor.util.StringUtils.getLowerCamelFromUpperCamel; import static info.izumin.android.droidux.processor.util.StringUtils.replaceSuffix; /** * Created by izumin on 11/3/15. */ public class StoreImplModel { public static final String TAG = StoreImplModel.class.getSimpleName(); public static final String REDUCER_VARIABLE_NAME = "reducer"; public static final String STATE_VARIABLE_NAME = "state"; public static final String DISPATCH_METHOD_NAME = "dispatch"; public static final String STATE_GETTER_METHOD_NAME = "getState"; public static final String REDUCER_GETTER_METHOD_NAME = "getReducer"; public static final String STATE_OBSERVE_METHOD_NAME = "observe"; public static final String ADD_LISTENER_METHOD_NAME = "addListener"; public static final String ON_STATE_CHANGED_METHOD_NAME = "onStateChanged"; public static final String NOTIFY_PROPERTY_CHANGED_METHOD_NAME = "notifyPropertyChanged"; private static final String CLASS_NAME_SUFFIX = "StoreImpl"; private static final String REDUCER_CLASS_NAME_SUFFIX = "Reducer"; private static final String FIELD_ID_SUFFIX = "FieldId"; private final ClassName className; private final String variableName; private final String storeImplName; private final StoreModel storeModel; private final ReducerModel reducerModel; private boolean isBindable = false; public StoreImplModel(StoreModel storeModel, ReducerModel reducerModel) { this.storeModel = storeModel; this.reducerModel = reducerModel; this.storeImplName = replaceSuffix(reducerModel.getClassName().simpleName(), REDUCER_CLASS_NAME_SUFFIX, CLASS_NAME_SUFFIX); this.variableName = getLowerCamelFromUpperCamel(storeImplName); this.className = ClassName.bestGuess(storeModel.getClassName() + "_" + storeImplName); } public ClassName getState() { return reducerModel.getState(); } public String getPackageName() { return storeModel.getClassName().packageName(); } public String getStateName() { return reducerModel.getStateName(); } public String getStateVariableName() { return reducerModel.getStateVariableName(); } public boolean isUndoable() { return reducerModel.isUndoable(); } public String getStoreImplName() { return storeImplName; } public ClassName getClassName() { return className; } public String getVariableName() { return variableName; } public ReducerModel getReducerModel() { return reducerModel; } public String getFieldIdName() { return getStateVariableName() + FIELD_ID_SUFFIX; } public boolean isBindable() { return isBindable; } public void setIsBindable(boolean isBindable) { this.isBindable = isBindable; } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/model/StoreMethodModel.java ================================================ package info.izumin.android.droidux.processor.model; import android.databinding.Bindable; import com.google.auto.common.MoreTypes; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeName; import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import io.reactivex.BackpressureStrategy; import io.reactivex.Flowable; /** * Created by izumin on 11/28/15. */ public class StoreMethodModel { public static final String TAG = StoreMethodModel.class.getSimpleName(); enum Kind { DISPATCH, GETTER, OBSERVE, OBSERVE_WITH_BPS, UNKNOWN } private static final Class OBSERVE_METHOD_CLASS = Flowable.class; private static final Class BACKPRESSURE_STRATEGY_PARAM_CLASS = BackpressureStrategy.class; private final ExecutableElement element; private final StoreModel storeModel; private final Kind kind; private final DeclaredType returnType; private boolean isBindable = false; private String bpsParamName = ""; public StoreMethodModel(ExecutableElement element, StoreModel storeModel) { this.element = element; this.storeModel = storeModel; List states = FluentIterable.from(storeModel.getStoreImplModels()) .transform(new Function() { @Override public TypeName apply(StoreImplModel input) { return input.getState(); } }).toList(); returnType = MoreTypes.asDeclared(element.getReturnType()); if (states.contains(ClassName.get(returnType))) { kind = Kind.GETTER; isBindable = element.getAnnotation(Bindable.class) != null; } else if (getName().equals(DispatcherModel.DISPATCH_METHOD_NAME)) { kind = Kind.DISPATCH; } else if (MoreTypes.isTypeOf(OBSERVE_METHOD_CLASS, returnType.asElement().asType()) && states.contains(ClassName.get(returnType.getTypeArguments().get(0)))) { if (element.getParameters() != null && !element.getParameters().isEmpty() && MoreTypes.isTypeOf(BACKPRESSURE_STRATEGY_PARAM_CLASS, element.getParameters().get(0).asType())) { bpsParamName = element.getParameters().get(0).getSimpleName().toString(); kind = Kind.OBSERVE_WITH_BPS; } else { kind = Kind.OBSERVE; } } else { kind = Kind.UNKNOWN; } } public String getName() { return element.getSimpleName().toString(); } public DeclaredType getReturnType() { return returnType; } public CodeBlock getCodeBlock() { switch (kind) { case GETTER: return CodeBlock.builder().addStatement("return $N.$N()", FluentIterable.from(storeModel.getStoreImplModels()) .filter(new Predicate() { @Override public boolean apply(StoreImplModel input) { return ClassName.get(returnType).equals(input.getState()); } }) .get(0).getVariableName(), StoreImplModel.STATE_GETTER_METHOD_NAME).build(); case OBSERVE: return CodeBlock.builder().addStatement("return $N.$N()", FluentIterable.from(storeModel.getStoreImplModels()) .filter(new Predicate() { @Override public boolean apply(StoreImplModel input) { return ClassName.get(returnType.getTypeArguments().get(0)).equals(input.getState()); } }) .get(0).getVariableName(), StoreImplModel.STATE_OBSERVE_METHOD_NAME).build(); case OBSERVE_WITH_BPS: return CodeBlock.builder().addStatement("return $N.$N(" + bpsParamName + ")", FluentIterable.from(storeModel.getStoreImplModels()) .filter(new Predicate() { @Override public boolean apply(StoreImplModel input) { return ClassName.get(returnType.getTypeArguments().get(0)).equals(input.getState()); } }) .get(0).getVariableName(), StoreImplModel.STATE_OBSERVE_METHOD_NAME).build(); case DISPATCH: return CodeBlock.builder() .addStatement("return $N.$N($N)", DispatcherModel.VARIABLE_NAME, DispatcherModel.DISPATCH_METHOD_NAME, getParameters().get(0).name) .build(); } return CodeBlock.builder().build(); } public List getParameters() { return FluentIterable.from(element.getParameters()) .transform(new Function() { @Override public ParameterSpec apply(VariableElement input) { return ParameterSpec.builder(TypeName.get(input.asType()), input.getSimpleName().toString()).build(); } }).toList(); } public boolean isBindable() { return isBindable; } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/model/StoreModel.java ================================================ package info.izumin.android.droidux.processor.model; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import info.izumin.android.droidux.annotation.Store; import info.izumin.android.droidux.processor.validator.StoreValidator; import static com.google.auto.common.MoreTypes.asTypeElement; import static info.izumin.android.droidux.processor.util.AnnotationUtils.getTypesFromAnnotation; /** * Created by izumin on 11/3/15. */ public class StoreModel { public static final String TAG = StoreModel.class.getSimpleName(); public static final String MIDDLEWARES_FIELD_NAME = "middlewares"; public static final String ATTACH_MIDDLEWARE_METHOD_NAME = "onAttach"; public static final String BUILDER_METHOD_NAME = "builder"; public static final String DISPATCH_METHOD_NAME = "dispatch"; private static final String CLASS_NAME_PREFIX = "Droidux"; private final TypeElement element; private final ClassName interfaceName; private final ClassName className; private final List reducerModels; private final List storeImplModels; private final List methodModels; private final BuilderModel builderModel; public StoreModel(TypeElement element) { this.element = element; this.interfaceName = ClassName.get(element); this.className = ClassName.get(interfaceName.packageName(), CLASS_NAME_PREFIX + interfaceName.simpleName()); StoreValidator.validate(this); reducerModels = FluentIterable.from(getTypesFromAnnotation(element, Store.class, "value")) .transform(new Function() { @Override public ReducerModel apply(TypeMirror input) { return new ReducerModel(asTypeElement(input)); } }).toList(); storeImplModels = FluentIterable.from(reducerModels) .transform(new Function() { @Override public StoreImplModel apply(ReducerModel input) { return new StoreImplModel(StoreModel.this, input); } }).toList(); methodModels = FluentIterable.from(element.getEnclosedElements()) .filter(ExecutableElement.class) .transform(new Function() { @Override public StoreMethodModel apply(ExecutableElement input) { return new StoreMethodModel(input, StoreModel.this); } }) .toList(); for (final StoreImplModel storeImplModel : storeImplModels) { storeImplModel.setIsBindable(FluentIterable.from(methodModels) .filter(new Predicate() { @Override public boolean apply(StoreMethodModel input) { return ClassName.get(input.getReturnType()).equals(storeImplModel.getState()); } }) .anyMatch(new Predicate() { @Override public boolean apply(StoreMethodModel input) { return input.isBindable(); } })); } this.builderModel = new BuilderModel(this); } public TypeElement getElement() { return element; } public ClassName getInterfaceName() { return interfaceName; } public ClassName getClassName() { return className; } public List getReducerModels() { return reducerModels; } public List getStoreImplModels() { return storeImplModels; } public List getMethodModels() { return methodModels; } public BuilderModel getBuilderModel() { return builderModel; } public boolean isBindable() { return FluentIterable.from(methodModels) .anyMatch(new Predicate() { @Override public boolean apply(StoreMethodModel input) { return input.isBindable(); } }); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/util/AnnotationUtils.java ================================================ package info.izumin.android.droidux.processor.util; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.util.List; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.AnnotationValueVisitor; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.SimpleAnnotationValueVisitor6; import static com.google.auto.common.AnnotationMirrors.getAnnotationValue; import static com.google.auto.common.MoreElements.getAnnotationMirror; /** * Created by izumin on 11/2/15. */ public final class AnnotationUtils { public static final String TAG = AnnotationUtils.class.getSimpleName(); private AnnotationUtils() { throw new AssertionError("constructor of the utility class should not be called"); } public static List findMethodsByAnnotation(Element element, final Class clazz) { return FluentIterable.from(element.getEnclosedElements()) .filter(new Predicate() { @Override public boolean apply(Element input) { return input.getAnnotation(clazz) != null; } }) .transform(new Function() { @Override public ExecutableElement apply(Element input) { return (ExecutableElement) input; } }) .toList(); } public static TypeMirror getTypeFromAnnotation(Element element, Class annotationType, String argName) { AnnotationMirror am = getAnnotationMirror(element, annotationType).get(); AnnotationValue av = getAnnotationValue(am, argName); return TO_TYPE.visit(av); } public static List getTypesFromAnnotation(Element element, Class annotationType, String argName) { AnnotationMirror am = getAnnotationMirror(element, annotationType).get(); AnnotationValue av = getAnnotationValue(am, argName); return TO_LIST_OF_TYPE.visit(av); } private static final AnnotationValueVisitor, Void> TO_LIST_OF_TYPE = new SimpleAnnotationValueVisitor6, Void>() { @Override public ImmutableList visitArray(List vals, Void aVoid) { return FluentIterable.from(vals).transform(new Function() { @Override public TypeMirror apply(AnnotationValue input) { return TO_TYPE.visit(input); } }).toList(); } }; private static final AnnotationValueVisitor TO_TYPE = new SimpleAnnotationValueVisitor6() { @Override public TypeMirror visitType(TypeMirror t, Void aVoid) { return t; } }; } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/util/PoetUtils.java ================================================ package info.izumin.android.droidux.processor.util; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.TypeName; import java.lang.annotation.Annotation; import javax.lang.model.element.Modifier; import static info.izumin.android.droidux.processor.util.StringUtils.getLowerCamelFromUpperCamel; /** * Created by izumin on 11/2/15. */ public final class PoetUtils { public static final String TAG = PoetUtils.class.getSimpleName(); private PoetUtils() { throw new AssertionError("constructor of the utility class should not be called"); } public static ParameterSpec getParameterSpec(ClassName className, Modifier... modifiers) { return ParameterSpec.builder( className.box(), getLowerCamelFromUpperCamel(className.simpleName()), modifiers ).build(); } public static ParameterSpec getParameterSpec(Class clazz, Modifier... modifiers) { return ParameterSpec.builder( TypeName.get(clazz), getLowerCamelFromUpperCamel(clazz.getSimpleName()), modifiers ).build(); } public static AnnotationSpec getOverrideAnnotation() { return getAnnotationSpec(Override.class); } public static AnnotationSpec getAnnotationSpec(Class clazz) { return AnnotationSpec.builder(clazz).build(); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/util/StringUtils.java ================================================ package info.izumin.android.droidux.processor.util; /** * Created by izumin on 11/2/15. */ public final class StringUtils { public static final String TAG = StringUtils.class.getSimpleName(); private StringUtils() { throw new AssertionError("constructor of the utility class should not be called"); } public static String getPackageName(String qualifiedName) { return qualifiedName.substring(0, qualifiedName.lastIndexOf(".")); } public static String getClassName(String qualifiedName) { return qualifiedName.substring(qualifiedName.lastIndexOf(".") + 1); } public static String replaceSuffix(String base, String target, String replacement) { return base.substring(0, base.lastIndexOf(target)) + replacement; } public static String getLowerCamelFromUpperCamel(String upperCamel) { upperCamel = getClassName(getClassName(upperCamel)); return upperCamel.substring(0, 1).toLowerCase() + upperCamel.substring(1); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/validator/DispatchableValidator.java ================================================ package info.izumin.android.droidux.processor.validator; import com.squareup.javapoet.ClassName; import javax.lang.model.element.VariableElement; import info.izumin.android.droidux.processor.exception.InvalidDispatchableDeclarationException; import info.izumin.android.droidux.processor.model.DispatchableModel; import static com.google.auto.common.MoreTypes.asTypeElement; /** * Created by izumin on 11/29/15. */ public final class DispatchableValidator { private DispatchableValidator() { throw new AssertionError("constructor of the utility class should not be called"); } public static void validate(DispatchableModel model) { for (VariableElement element : model.getElement().getParameters()) { if (!isAction(model, element) && !isState(model, element)) { throw new InvalidDispatchableDeclarationException( "@Dispatchable method can have arguments only state or action. " + methodName(model) + " has more than one invalid argument." ); } } if (!isValidReturnType(model)) { throw new InvalidDispatchableDeclarationException( "@Dispatchable method must return new state. " + "But " + methodName(model) + " returns invalid type." ); } } private static boolean isAction(DispatchableModel model, VariableElement element) { return model.getAction().equals(ClassName.get(asTypeElement(element.asType()))); } private static boolean isState(DispatchableModel model, VariableElement element) { return model.getState().equals(ClassName.get(asTypeElement(element.asType()))); } private static boolean isValidReturnType(DispatchableModel model) { return ClassName.get(asTypeElement(model.getElement().getReturnType())) .equals(model.getState()); } private static String argName(VariableElement element) { return "[" + element.asType().toString() + " " + element.getSimpleName() + "]"; } private static String methodName(DispatchableModel model) { return model.getReducerClassName().simpleName() + "#" + model.getMethodName() + "()"; } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/validator/ReducerValidator.java ================================================ package info.izumin.android.droidux.processor.validator; import com.google.auto.common.MoreTypes; import com.google.common.base.Function; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import info.izumin.android.droidux.UndoableState; import info.izumin.android.droidux.processor.exception.InvalidReducerDeclarationException; import info.izumin.android.droidux.processor.model.ReducerModel; /** * Created by izumin on 11/29/15. */ public final class ReducerValidator { private ReducerValidator() { throw new AssertionError("constructor of the utility class should not be called"); } public static void validate(ReducerModel model) { if (!isValidClassName(model)) { throw new InvalidReducerDeclarationException( "@Reducer class name must end with \"" + ReducerModel.CLASS_NAME_SUFFIX + "\". " + "\"" + model.getClassName().simpleName() + "\" has invalid class name." ); } if (model.isUndoable() && !hasUndoableState(model.getStateElement())) { throw new InvalidReducerDeclarationException( "@Reducer class annotated with @Undoable must have the state implements \"UndoableState\". " + model.getState().simpleName() + " state of " + model.getClassName().simpleName() + " does not implement it." ); } } private static boolean isValidClassName(ReducerModel model) { return model.getClassName().simpleName().endsWith(ReducerModel.CLASS_NAME_SUFFIX); } private static boolean hasUndoableState(TypeElement stateElement) { return FluentIterable.from(stateElement.getInterfaces()) .transform(new Function() { @Override public TypeName apply(TypeMirror input) { return ClassName.get(MoreTypes.asTypeElement(input)); } }) .contains(TypeName.get(UndoableState.class)); } } ================================================ FILE: droidux-processor/src/main/java/info/izumin/android/droidux/processor/validator/StoreValidator.java ================================================ package info.izumin.android.droidux.processor.validator; import com.google.auto.common.MoreTypes; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; import javax.lang.model.type.TypeMirror; import info.izumin.android.droidux.BaseStore; import info.izumin.android.droidux.annotation.Reducer; import info.izumin.android.droidux.annotation.Store; import info.izumin.android.droidux.processor.exception.InvalidStoreDelcarationException; import info.izumin.android.droidux.processor.model.StoreModel; import static info.izumin.android.droidux.processor.util.AnnotationUtils.getTypesFromAnnotation; /** * Created by izumin on 11/29/15. */ public final class StoreValidator { private StoreValidator() { throw new AssertionError("constructor of the utility class should not be called"); } public static void validate(StoreModel model) { if (!hasAnnotatedReducers(model)) { throw new InvalidStoreDelcarationException( "Values of @Store annotation must have only classes annotated with \"@Reducer\"." + "But " + model.getInterfaceName().simpleName() + " has invalid value." ); } if (!doesExtendBaseStore(model)) { throw new InvalidStoreDelcarationException( "The interface that is annotated @Store must extend \"BaseStore\"." ); } } private static boolean hasAnnotatedReducers(StoreModel model) { return FluentIterable.from(getTypesFromAnnotation(model.getElement(), Store.class, "value")) .filter(new Predicate() { @Override public boolean apply(TypeMirror input) { return MoreTypes.asTypeElement(input).getAnnotation(Reducer.class) == null; } }).size() == 0; } private static boolean doesExtendBaseStore(StoreModel model) { return FluentIterable.from(model.getElement().getInterfaces()) .anyMatch(new Predicate() { @Override public boolean apply(TypeMirror input) { return TypeName.get(BaseStore.class).equals(ClassName.get(input)); } }); } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/DroiduxProcessorTest.java ================================================ package info.izumin.android.droidux.processor; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import javax.tools.JavaFileObject; import info.izumin.android.droidux.processor.fixture.Source; import io.reactivex.BackpressureStrategy; import static com.google.common.truth.Truth.assert_; import static com.google.testing.compile.JavaFileObjects.forSourceLines; import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; /** * Created by izumin on 11/2/15. */ public class DroiduxProcessorTest { public static final String TAG = DroiduxProcessorTest.class.getSimpleName(); @Rule public ExpectedException expectedException = ExpectedException.none(); private static void assertJavaSource(JavaFileObject target, JavaFileObject generated, JavaFileObject... rest) { assert_().about(javaSource()) .that(target) .processedWith(new DroiduxProcessor()) .compilesWithoutError() .and() .generatesSources(generated, rest); } @Test public void singleReducer() { assertJavaSource( forSourceLines("RootStore", Source.Counter.TARGET), forSourceLines("DroiduxRootStore_CounterStoreImpl", Source.StoreImpl.COUNTER), forSourceLines("DroiduxRootStore", Source.Counter.GENERATED_STORE) ); } @Test public void singleReducer_with_BackpressureStrategySpecification() { assertJavaSource( forSourceLines("RootStore", Source.CounterWithBackpressureStrategy.TARGET), forSourceLines("DroiduxRootStore_CounterStoreImpl", Source.StoreImpl.COUNTER), forSourceLines("DroiduxRootStore", Source.CounterWithBackpressureStrategy.GENERATED_STORE) ); } @Test public void singleReducerBindable() { assertJavaSource( forSourceLines("RootStore", Source.BindableCounter.TARGET), forSourceLines("DroiduxRootStore_CounterStoreImpl", Source.StoreImpl.COUNTER), forSourceLines("DroiduxRootStore", Source.BindableCounter.GENERATED_STORE) ); } @Test public void combinedTwoReducers() { assertJavaSource( forSourceLines("RootStore", Source.CombinedTwoReducers.TARGET), forSourceLines("DroiduxRootStore_CounterStoreImpl", Source.StoreImpl.COUNTER), forSourceLines("DroiduxRootStore_TodoListStoreImpl", Source.StoreImpl.TODO_LIST), forSourceLines("DroiduxRootStore", Source.CombinedTwoReducers.GENERATED) ); } @Test public void combinedReducerAndBindableReducer() { assertJavaSource( forSourceLines("RootStore", Source.CombinedReducerAndBindableReducer.TARGET), forSourceLines("DroiduxRootStore_CounterStoreImpl", Source.StoreImpl.COUNTER), forSourceLines("DroiduxRootStore_TodoListStoreImpl", Source.StoreImpl.TODO_LIST), forSourceLines("DroiduxRootStore", Source.CombinedReducerAndBindableReducer.GENERATED) ); } @Test public void dispatchableMethodTakesWrongStateType() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "@Dispatchable method can have arguments only state or action. " + "CounterReducer#increment() has more than one invalid argument." ); assertJavaSource( forSourceLines("CounterStore", Source.DispatchableTakesWrongStateType.TARGET), forSourceLines("DroiduxCounterStore_CounterStoreImpl", Source.EMPTY), forSourceLines("DroiduxCounterStore", Source.EMPTY) ); } @Test public void dispatchableMethodTakesWrongActionType() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "@Dispatchable method can have arguments only state or action. " + "TodoListReducer#addItem() has more than one invalid argument." ); assertJavaSource( forSourceLines("TodoListStore", Source.DispatchableTakesWrongActionType.TARGET), forSourceLines("DroiduxTodoListStore_TodoListStoreImpl", Source.EMPTY), forSourceLines("DroiduxTodoListStore", Source.EMPTY) ); } @Test public void dispatchableMethodReturnsWrongType() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "@Dispatchable method can have arguments only state or action. " + "CounterReducer#increment() has more than one invalid argument." ); assertJavaSource( forSourceLines("CounterStore", Source.DispatchableTakesExtraArguments.TARGET), forSourceLines("DroiduxCounterStore_CounterStoreImpl", Source.EMPTY), forSourceLines("DroiduxCounterStore", Source.EMPTY) ); } @Test public void dispatchableMethodShouldReturnState() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "@Dispatchable method must return new state. " + "But CounterReducer#increment() returns invalid type." ); assertJavaSource( forSourceLines("CounterStore", Source.DispatchableMethosReturnsWrongType.TARGET), forSourceLines("DroiduxCounterStore_CounterStoreImpl", Source.EMPTY), forSourceLines("DroiduxCounterStore", Source.EMPTY) ); } @Test public void reducerWithoutSuffix() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "@Reducer class name must end with \"Reducer\". \"CounterReduce\" has invalid class name." ); assertJavaSource( forSourceLines("CounterStore", Source.ReducerWithoutSuffix.TARGET), forSourceLines("DroiduxCounterStore_CounterStoreImpl", Source.EMPTY), forSourceLines("DroiduxCounterStore", Source.EMPTY) ); } @Test public void undoableReducerWithoutUndoableState() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "@Reducer class annotated with @Undoable must have the state implements \"UndoableState\". " + "Counter state of CounterReducer does not implement it." ); assertJavaSource( forSourceLines("CounterStore", Source.UndoableReducerWithoutUndoableState.TARGET), forSourceLines("DroiduxCounterStore_CounterStoreImpl", Source.EMPTY), forSourceLines("DroiduxCounterStore", Source.EMPTY) ); } @Test public void storeHasClassThatIsNotAnnotatedWithReducer() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "Values of @Store annotation must have only classes annotated with \"@Reducer\"." + "But CounterStore has invalid value." ); assertJavaSource( forSourceLines("CounterStore", Source.StoreHasInvalidValue.TARGET), forSourceLines("DroiduxCounterStore_CounterStoreImpl", Source.EMPTY), forSourceLines("DroiduxCounterStore", Source.EMPTY) ); } @Test public void storeNotExtendBaseStore() { expectedException.expect(RuntimeException.class); expectedException.expectMessage( "The interface that is annotated @Store must extend \"BaseStore\"." ); assertJavaSource( forSourceLines("CounterStore", Source.StoreNotExtendsBaseStore.TARGET), forSourceLines("DroiduxCounterStore_CounterStoreImpl", Source.EMPTY), forSourceLines("DroiduxCounterStore", Source.EMPTY) ); } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/Counter.java ================================================ package info.izumin.android.droidux.processor.fixture; /** * Created by izumin on 11/2/15. */ public class Counter { public static final String TAG = Counter.class.getSimpleName(); private int count; public Counter(int count) { this.count = count; } public int getCount() { return count; } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/CounterReducer.java ================================================ package info.izumin.android.droidux.processor.fixture; import info.izumin.android.droidux.annotation.Dispatchable; import info.izumin.android.droidux.annotation.Reducer; import info.izumin.android.droidux.processor.fixture.action.ClearCountAction; import info.izumin.android.droidux.processor.fixture.action.IncrementCountAction; import info.izumin.android.droidux.processor.fixture.action.InitializeCountAction; import info.izumin.android.droidux.processor.fixture.action.SquareCountAction; /** * Created by izumin on 11/3/15. */ @Reducer(Counter.class) public class CounterReducer { @Dispatchable(IncrementCountAction.class) public Counter increment(Counter state, IncrementCountAction action) { return new Counter(state.getCount() + action.getValue()); } @Dispatchable(SquareCountAction.class) public Counter square(Counter state) { return new Counter(state.getCount() * state.getCount()); } @Dispatchable(InitializeCountAction.class) public Counter initialize(InitializeCountAction action) { return new Counter(action.getValue()); } @Dispatchable(ClearCountAction.class) public Counter clear() { return new Counter(0); } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/Source.java ================================================ package info.izumin.android.droidux.processor.fixture; /** * Created by izumin on 11/2/15. */ public final class Source { public static final String TAG = Source.class.getSimpleName(); public static final String[] EMPTY = {}; public static final class StoreImpl { public static final String[] COUNTER = { "package info.izumin.android.droidux.sample;", "", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.StoreImpl;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import info.izumin.android.droidux.processor.fixture.action.ClearCountAction;", "import info.izumin.android.droidux.processor.fixture.action.IncrementCountAction;", "import info.izumin.android.droidux.processor.fixture.action.InitializeCountAction;", "import info.izumin.android.droidux.processor.fixture.action.SquareCountAction;", "", "final class DroiduxRootStore_CounterStoreImpl extends StoreImpl {", " protected DroiduxRootStore_CounterStoreImpl(Counter state, CounterReducer reducer) {", " super(state, reducer);", " }", "", " @Override", " protected void dispatch(Action action) {", " Class actionClass = action.getClass();", " Counter result = null;", " if (IncrementCountAction.class.isAssignableFrom(actionClass)) {", " result = getReducer().increment(getState(), (IncrementCountAction) action);", " }", " if (SquareCountAction.class.isAssignableFrom(actionClass)) {", " result = getReducer().square(getState());", " }", " if (InitializeCountAction.class.isAssignableFrom(actionClass)) {", " result = getReducer().initialize((InitializeCountAction) action);", " }", " if (ClearCountAction.class.isAssignableFrom(actionClass)) {", " result = getReducer().clear();", " }", " if (result != null) {", " setState(result);", " }", " }", "}" }; public static final String[] TODO_LIST = { "package info.izumin.android.droidux.sample;", "", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.UndoableStoreImpl;", "import info.izumin.android.droidux.processor.fixture.TodoList;", "import info.izumin.android.droidux.processor.fixture.TodoListReducer;", "import info.izumin.android.droidux.processor.fixture.action.AddTodoItemAction;", "", "final class DroiduxRootStore_TodoListStoreImpl extends UndoableStoreImpl {", " protected DroiduxCounterStore_CounterStoreImpl(TodoList state, TodoListReducer reducer) {", " super(state, reducer);", " }", "", " @Override", " protected void dispatch(Action action) {", " super.dispatch(action);", " Class actionClass = action.getClass();", " TodoList result = null;", " if (AddTodoItemAction.class.isAssignableFrom(actionClass)) {", " result = getReducer().add(getState().clone(), (AddTodoItemAction) action);", " }", " if (result != null) {", " setState(result);", " }", " }", "}" }; } public static class Counter { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import io.reactivex.Flowable;", "@Store({CounterReducer.class})", "public interface RootStore extends BaseStore {", " Counter counter();", " Flowable observeCounter();", "}" }; public static final String[] GENERATED_STORE = { "package info.izumin.android.droidux.sample;", "", "import android.databinding.BaseObservable;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.Dispatcher;", "import info.izumin.android.droidux.Middleware;", "import info.izumin.android.droidux.exception.NotInitializedException;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import io.reactivex.Flowable;", "import java.util.ArrayList;", "import java.util.List;", "", "public final class DroiduxRootStore extends BaseObservable implements RootStore {", " private final DroiduxRootStore_CounterStoreImpl counterStoreImpl;", " private final Dispatcher dispatcher;", "", " protected DroiduxRootStore(final Builder builder) {", " counterStoreImpl= new DroiduxRootStore_CounterStoreImpl(builder.counter, builder.counterReducer);", " dispatcher = new Dispatcher(builder.middlewares, counterStoreImpl);", " for (Middleware middleware : builder.middlewares) {", " middleware.onAttach(this, dispatcher);", " }", " }", "", " public static Builder builder() {", " return new Builder();", " }", "", " @Override", " public Counter counter() {", " return counterStoreImpl.getState();", " }", "", " @Override", " public Flowable observeCounter() {", " return counterStoreImpl.observe();", " }", "", " @Override", " public Flowable dispatch(Action action) {", " return dispatcher.dispatch(action)", " }", "", " public static final class Builder {", " private final List middlewares;", " private CounterReducer counterReducer;", " private Counter counter;", "", " private Builder() {", " middlewares = new ArrayList<>();", " }", "", " public Builder addMiddleware(Middleware middleware) {", " middlewares.add(middleware);", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer) {", " this.counterReducer = counterReducer;", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer, Counter counter) {", " this.counter = counter;", " return setReducer(counterReducer);", " }", "", " public DroiduxRootStore build() {", " if (counterReducer == null) {", " throw new NotInitializedException(\"CounterReducer has not been initialized.\");", " }", " return new DroiduxRootStore(this);", " }", " }", "}" }; } public static class CounterWithBackpressureStrategy { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import io.reactivex.BackpressureStrategy;", "import io.reactivex.Flowable;", "@Store({CounterReducer.class})", "public interface RootStore extends BaseStore {", " Counter counter();", " Flowable observeCounter();", " Flowable observeCounter(BackpressureStrategy strategy);", "}" }; public static final String[] GENERATED_STORE = { "package info.izumin.android.droidux.sample;", "", "import android.databinding.BaseObservable;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.Dispatcher;", "import info.izumin.android.droidux.Middleware;", "import info.izumin.android.droidux.exception.NotInitializedException;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import io.reactivex.BackpressureStrategy;", "import io.reactivex.Flowable;", "import java.util.ArrayList;", "import java.util.List;", "", "public final class DroiduxRootStore extends BaseObservable implements RootStore {", " private final DroiduxRootStore_CounterStoreImpl counterStoreImpl;", " private final Dispatcher dispatcher;", "", " protected DroiduxRootStore(final Builder builder) {", " counterStoreImpl= new DroiduxRootStore_CounterStoreImpl(builder.counter, builder.counterReducer);", " dispatcher = new Dispatcher(builder.middlewares, counterStoreImpl);", " for (Middleware middleware : builder.middlewares) {", " middleware.onAttach(this, dispatcher);", " }", " }", "", " public static Builder builder() {", " return new Builder();", " }", "", " @Override", " public Counter counter() {", " return counterStoreImpl.getState();", " }", "", " @Override", " public Flowable observeCounter() {", " return counterStoreImpl.observe();", " }", "", "", " @Override", " public Flowable observeCounter(BackpressureStrategy strategy) {", " return counterStoreImpl.observe(strategy);", " }", "", " @Override", " public Flowable dispatch(Action action) {", " return dispatcher.dispatch(action)", " }", "", " public static final class Builder {", " private final List middlewares;", " private CounterReducer counterReducer;", " private Counter counter;", "", " private Builder() {", " middlewares = new ArrayList<>();", " }", "", " public Builder addMiddleware(Middleware middleware) {", " middlewares.add(middleware);", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer) {", " this.counterReducer = counterReducer;", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer, Counter counter) {", " this.counter = counter;", " return setReducer(counterReducer);", " }", "", " public DroiduxRootStore build() {", " if (counterReducer == null) {", " throw new NotInitializedException(\"CounterReducer has not been initialized.\");", " }", " return new DroiduxRootStore(this);", " }", " }", "}" }; } public static class BindableCounter { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import android.databinding.Bindable;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import io.reactivex.Flowable;", "@Store({CounterReducer.class})", "public interface RootStore extends BaseStore, android.databinding.Observable {", " @Bindable Counter counter();", " Flowable observeCounter();", "}" }; public static final String[] GENERATED_STORE = { "package info.izumin.android.droidux.sample;", "", "import android.databinding.BaseObservable;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.Dispatcher;", "import info.izumin.android.droidux.Middleware;", "import info.izumin.android.droidux.OnStateChangedListener;", "import info.izumin.android.droidux.exception.NotInitializedException;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import io.reactivex.Flowable;", "import java.util.ArrayList;", "import java.util.List;", "", "public final class DroiduxRootStore extends BaseObservable implements RootStore {", " private final DroiduxRootStore_CounterStoreImpl counterStoreImpl;", " private final Dispatcher dispatcher;", "", " protected DroiduxRootStore(final Builder builder) {", " counterStoreImpl = new DroiduxRootStore_CounterStoreImpl(builder.counter, builder.counterReducer);", " counterStoreImpl.addListener(new OnStateChangedListener() {", " @Override", " public void onStateChanged(Counter counter) {", " notifyPropertyChanged(builder.counterFieldId);", " }", " });", " dispatcher = new Dispatcher(builder.middlewares, counterStoreImpl);", " for (Middleware middleware : builder.middlewares) {", " middleware.onAttach(this, dispatcher);", " }", " }", "", " public static Builder builder() {", " return new Builder();", " }", "", " @Override", " public Counter counter() {", " return counterStoreImpl.getState();", " }", "", " @Override", " public Flowable observeCounter() {", " return counterStoreImpl.observe();", " }", "", " @Override", " public Flowable dispatch(Action action) {", " return dispatcher.dispatch(action)", " }", "", " public static final class Builder {", " private final List middlewares;", " private CounterReducer counterReducer;", " private Counter counter;", " private int counterFieldId;", "", " private Builder() {", " middlewares = new ArrayList<>();", " }", "", " public Builder addMiddleware(Middleware middleware) {", " middlewares.add(middleware);", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer, int counterFieldId) {", " this.counterFieldId = counterFieldId;", " this.counterReducer = counterReducer;", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer, Counter counter, int counterFieldId) {", " this.counter = counter;", " return setReducer(counterReducer, counterFieldId);", " }", "", " public DroiduxRootStore build() {", " if (counterReducer == null) {", " throw new NotInitializedException(\"CounterReducer has not been initialized.\");", " }", " return new DroiduxRootStore(this);", " }", " }", "}" }; } public static class CombinedTwoReducers { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import info.izumin.android.droidux.processor.fixture.TodoListReducer;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.TodoList;", "import io.reactivex.Flowable;", "@Store({CounterReducer.class, TodoListReducer.class})", "public interface RootStore extends BaseStore {", " Counter counter();", " Flowable observeCounter();", " TodoList todoList();", " Flowable observeTodoList();", "}" }; public static final String[] GENERATED = { "package info.izumin.android.droidux.sample;", "", "import android.databinding.BaseObservable;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.Dispatcher;", "import info.izumin.android.droidux.Middleware;", "import info.izumin.android.droidux.exception.NotInitializedException;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import info.izumin.android.droidux.processor.fixture.TodoList;", "import info.izumin.android.droidux.processor.fixture.TodoListReducer;", "import io.reactivex.Flowable;", "import java.util.ArrayList;", "import java.util.List;", "", "public final class DroiduxRootStore extends BaseObservable implements RootStore {", " private final DroiduxRootStore_CounterStoreImpl counterStoreImpl;", " private final DroiduxRootStore_TodoListStoreImpl todoListStoreImpl;", " private final Dispatcher dispatcher;", "", " protected DroiduxRootStore(final Builder builder) {", " counterStoreImpl= new DroiduxRootStore_CounterStoreImpl(builder.counter, builder.counterReducer);", " todoListStoreImpl= new DroiduxRootStore_TodoListStoreImpl(builder.todoList, builder.todoListReducer);", " dispatcher = new Dispatcher(builder.middlewares, counterStoreImpl, todoListStoreImpl);", " for (Middleware middleware : builder.middlewares) {", " middleware.onAttach(this, dispatcher);", " }", " }", "", " public static Builder builder() {", " return new Builder();", " }", "", " @Override", " public Counter counter() {", " return counterStoreImpl.getState();", " }", "", " @Override", " public Flowable observeCounter() {", " return counterStoreImpl.observe();", " }", "", " @Override", " public TodoList todoList() {", " return todoListStoreImpl.getState();", " }", "", " @Override", " public Flowable observeTodoList() {", " return todoListStoreImpl.observe();", " }", "", " @Override", " public Flowable dispatch(Action action) {", " return dispatcher.dispatch(action)", " }", "", " public static final class Builder {", " private final List middlewares;", " private CounterReducer counterReducer;", " private TodoListReducer todoListReducer;", " private Counter counter;", " private TodoList todoList;", " ", " private Builder() {", " middlewares = new ArrayList<>();", " }", "", " public Builder addMiddleware(Middleware middleware) {", " middlewares.add(middleware);", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer) {", " this.counterReducer = counterReducer;", " return this;", " }", "", " public Builder setReducer(TodoListReducer todoListReducer) {", " this.todoListReducer = todoListReducer;", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer, Counter counter) {", " this.counter = counter;", " return setReducer(counterReducer);", " }", "", " public Builder setReducer(TodoListReducer todoListReducer, TodoList todoList) {", " this.todoList = todoList;", " return setReducer(todoListReducer);", " }", "", " public DroiduxRootStore build() {", " if (counterReducer == null) {", " throw new NotInitializedException(\"CounterReducer has not been initialized.\");", " }", " if (todoListReducer == null) {", " throw new NotInitializedException(\"TodoListReducer has not been initialized.\");", " }", " return new DroiduxRootStore(this);", " }", " }", "}" }; } public static class CombinedReducerAndBindableReducer { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import android.databinding.Bindable;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import info.izumin.android.droidux.processor.fixture.TodoListReducer;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.TodoList;", "import io.reactivex.Flowable;", "@Store({CounterReducer.class, TodoListReducer.class})", "public interface RootStore extends BaseStore, android.databinding.Observable {", " Counter counter();", " Flowable observeCounter();", " @Bindable TodoList todoList();", " Flowable observeTodoList();", "}" }; public static final String[] GENERATED = { "package info.izumin.android.droidux.sample;", "", "import android.databinding.BaseObservable;", "import info.izumin.android.droidux.Action;", "import info.izumin.android.droidux.Dispatcher;", "import info.izumin.android.droidux.Middleware;", "import info.izumin.android.droidux.OnStateChangedListener;", "import info.izumin.android.droidux.exception.NotInitializedException;", "import info.izumin.android.droidux.processor.fixture.Counter;", "import info.izumin.android.droidux.processor.fixture.CounterReducer;", "import info.izumin.android.droidux.processor.fixture.TodoList;", "import info.izumin.android.droidux.processor.fixture.TodoListReducer;", "import io.reactivex.Flowable;", "import java.util.ArrayList;", "import java.util.List;", "", "public final class DroiduxRootStore extends BaseObservable implements RootStore {", " private final DroiduxRootStore_CounterStoreImpl counterStoreImpl;", " private final DroiduxRootStore_TodoListStoreImpl todoListStoreImpl;", " private final Dispatcher dispatcher;", "", " protected DroiduxRootStore(final Builder builder) {", " counterStoreImpl= new DroiduxRootStore_CounterStoreImpl(builder.counter, builder.counterReducer);", " todoListStoreImpl= new DroiduxRootStore_TodoListStoreImpl(builder.todoList, builder.todoListReducer);", " todoListStoreImpl.addListener(new OnStateChangedListener() {", " @Override", " public void onStateChanged(TodoList todoList) {", " notifyPropertyChanged(builder.todoListFieldId);", " }", " });", " dispatcher = new Dispatcher(builder.middlewares, counterStoreImpl, todoListStoreImpl);", " for (Middleware middleware : builder.middlewares) {", " middleware.onAttach(this, dispatcher);", " }", " }", "", " public static Builder builder() {", " return new Builder();", " }", "", " @Override", " public Counter counter() {", " return counterStoreImpl.getState();", " }", "", " @Override", " public Flowable observeCounter() {", " return counterStoreImpl.observe();", " }", "", " @Override", " public TodoList todoList() {", " return todoListStoreImpl.getState();", " }", "", " @Override", " public Flowable observeTodoList() {", " return todoListStoreImpl.observe();", " }", "", " @Override", " public Flowable dispatch(Action action) {", " return dispatcher.dispatch(action)", " }", "", " public static final class Builder {", " private final List middlewares;", " private CounterReducer counterReducer;", " private TodoListReducer todoListReducer;", " private Counter counter;", " private TodoList todoList;", " private int todoListFieldId;", " ", " private Builder() {", " middlewares = new ArrayList<>();", " }", "", " public Builder addMiddleware(Middleware middleware) {", " middlewares.add(middleware);", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer) {", " this.counterReducer = counterReducer;", " return this;", " }", "", " public Builder setReducer(TodoListReducer todoListReducer, int todoListFieldId) {", " this.todoListFieldId = todoListFieldId;", " this.todoListReducer = todoListReducer;", " return this;", " }", "", " public Builder setReducer(CounterReducer counterReducer, Counter counter) {", " this.counter = counter;", " return setReducer(counterReducer);", " }", "", " public Builder setReducer(TodoListReducer todoListReducer, TodoList todoList, int todoListFieldId) {", " this.todoList = todoList;", " return setReducer(todoListReducer, todoListFieldId);", " }", "", " public DroiduxRootStore build() {", " if (counterReducer == null) {", " throw new NotInitializedException(\"CounterReducer has not been initialized.\");", " }", " if (todoListReducer == null) {", " throw new NotInitializedException(\"TodoListReducer has not been initialized.\");", " }", " return new DroiduxRootStore(this);", " }", " }", "}" }; } public static class DispatchableTakesWrongStateType { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Dispatchable;", "import info.izumin.android.droidux.annotation.Reducer;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.processor.fixture.action.IncrementCountAction;", "import info.izumin.android.droidux.processor.fixture.Counter;", "@Store(CounterStore.CounterReducer.class)", "public interface CounterStore extends BaseStore {", " @Reducer(Counter.class)", " public static class CounterReducer {", " @Dispatchable(IncrementCountAction.class)", " public Counter increment(Object state, IncrementCountAction action) {", " return null;", " }", " }", "}" }; } public static class DispatchableTakesWrongActionType { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Dispatchable;", "import info.izumin.android.droidux.annotation.Reducer;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.processor.fixture.action.AddTodoItemAction;", "import info.izumin.android.droidux.processor.fixture.action.CompleteTodoItemAction;", "import info.izumin.android.droidux.processor.fixture.TodoList;", "@Store(TodoListStore.TodoListReducer.class)", "public interface TodoListStore extends BaseStore {", " @Reducer(TodoList.class)", " public static class TodoListReducer {", " @Dispatchable(CompleteTodoItemAction.class)", " public TodoList addItem(TodoList state, AddTodoItemAction action) {", " return null;", " }", " }", "}" }; } public static class DispatchableTakesExtraArguments { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Dispatchable;", "import info.izumin.android.droidux.annotation.Reducer;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.processor.fixture.action.IncrementCountAction;", "import info.izumin.android.droidux.processor.fixture.Counter;", "@Store(CounterStore.CounterReducer.class)", "public interface CounterStore extends BaseStore {", " @Reducer(Counter.class)", " public static class CounterReducer {", " @Dispatchable(IncrementCountAction.class)", " public Counter increment(Counter state, IncrementCountAction action, String extra) {", " return null;", " }", " }", "}" }; } public static class DispatchableMethosReturnsWrongType{ public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Dispatchable;", "import info.izumin.android.droidux.annotation.Reducer;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.processor.fixture.action.IncrementCountAction;", "import info.izumin.android.droidux.processor.fixture.Counter;", "@Store(CounterStore.CounterReducer.class)", "public interface CounterStore extends BaseStore {", " @Reducer(Counter.class)", " public static class CounterReducer {", " @Dispatchable(IncrementCountAction.class)", " public Object increment(Counter state, IncrementCountAction action) {", " return null;", " }", " }", "}" }; } public static class ReducerWithoutSuffix { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Dispatchable;", "import info.izumin.android.droidux.annotation.Reducer;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.processor.fixture.action.IncrementCountAction;", "import info.izumin.android.droidux.processor.fixture.Counter;", "@Store(CounterStore.CounterReduce.class)", "public interface CounterStore extends BaseStore {", " @Reducer(Counter.class)", " public static class CounterReduce {", " @Dispatchable(IncrementCountAction.class)", " public Counter increment(Counter state, IncrementCountAction action) {", " return null;", " }", " }", "}" }; } public static class UndoableReducerWithoutUndoableState { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Reducer;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.annotation.Undoable;", "import info.izumin.android.droidux.processor.fixture.Counter;", "@Store(CounterStore.CounterReducer.class)", "public interface CounterStore extends BaseStore {", " @Undoable", " @Reducer(Counter.class)", " public static class CounterReducer {", " }", "}" }; } public static class StoreHasInvalidValue { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Store;", "@Store(CounterStore.CounterReducer.class)", "public interface CounterStore extends BaseStore {", " public static class CounterReducer {", " }", "}" }; } public static class StoreNotExtendsBaseStore { public static final String[] TARGET = { "package info.izumin.android.droidux.sample;", "import info.izumin.android.droidux.BaseStore;", "import info.izumin.android.droidux.annotation.Reducer;", "import info.izumin.android.droidux.annotation.Store;", "import info.izumin.android.droidux.processor.fixture.Counter;", "@Store(CounterStore.CounterReducer.class)", "public interface CounterStore {", " @Reducer(Counter.class)", " public static class CounterReducer {", " }", "}" }; } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/TodoList.java ================================================ package info.izumin.android.droidux.processor.fixture; import java.util.ArrayList; import info.izumin.android.droidux.UndoableState; /** * Created by izumin on 11/2/15. */ public class TodoList extends ArrayList implements UndoableState { @Override public TodoList clone() { return (TodoList) super.clone(); } public static class Item { private String body; private boolean isCompleted; public Item(String body) { this.body = body; this.isCompleted = false; } public String getBody() { return body; } public boolean isCompleted() { return isCompleted; } public void setCompleted(boolean isCompleted) { this.isCompleted = isCompleted; } } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/TodoListReducer.java ================================================ package info.izumin.android.droidux.processor.fixture; import info.izumin.android.droidux.annotation.Dispatchable; import info.izumin.android.droidux.annotation.Reducer; import info.izumin.android.droidux.annotation.Undoable; import info.izumin.android.droidux.processor.fixture.action.AddTodoItemAction; /** * Created by izumin on 11/3/15. */ @Undoable @Reducer(TodoList.class) public class TodoListReducer { @Dispatchable(AddTodoItemAction.class) public TodoList add(TodoList state, AddTodoItemAction action) { TodoList newState = new TodoList(); newState.addAll(state); newState.add(new TodoList.Item(action.getValue())); return newState; } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/action/AddTodoItemAction.java ================================================ package info.izumin.android.droidux.processor.fixture.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 11/2/15. */ public class AddTodoItemAction implements Action { public static final String TAG = AddTodoItemAction.class.getSimpleName(); private final String value; public AddTodoItemAction(String value) { this.value = value; } public String getValue() { return value; } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/action/ClearCountAction.java ================================================ package info.izumin.android.droidux.processor.fixture.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 11/2/15. */ public class ClearCountAction implements Action { public static final String TAG = ClearCountAction.class.getSimpleName(); public ClearCountAction() { } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/action/CompleteTodoItemAction.java ================================================ package info.izumin.android.droidux.processor.fixture.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 11/2/15. */ public class CompleteTodoItemAction implements Action { public static final String TAG = CompleteTodoItemAction.class.getSimpleName(); private final int id; public CompleteTodoItemAction(int id) { this.id = id; } public int getId() { return id; } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/action/IncrementCountAction.java ================================================ package info.izumin.android.droidux.processor.fixture.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 11/2/15. */ public class IncrementCountAction implements Action { public static final String TAG = IncrementCountAction.class.getSimpleName(); private final int value; public IncrementCountAction(int value) { this.value = value; } public int getValue() { return value; } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/action/InitializeCountAction.java ================================================ package info.izumin.android.droidux.processor.fixture.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 11/2/15. */ public class InitializeCountAction implements Action { public static final String TAG = InitializeCountAction.class.getSimpleName(); private final int value; public InitializeCountAction(int value) { this.value = value; } public int getValue() { return value; } } ================================================ FILE: droidux-processor/src/test/java/info/izumin/android/droidux/processor/fixture/action/SquareCountAction.java ================================================ package info.izumin.android.droidux.processor.fixture.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 11/2/15. */ public class SquareCountAction implements Action { public static final String TAG = SquareCountAction.class.getSimpleName(); public SquareCountAction() { } } ================================================ FILE: examples/counter/.gitignore ================================================ /build ================================================ FILE: examples/counter/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion project.compileSdkVersion buildToolsVersion project.buildToolsVersion defaultConfig { applicationId "info.izumin.android.droidux.example.counter" minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion versionCode 1 versionName "1.0" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } packagingOptions { exclude 'META-INF/services/javax.annotation.processing.Processor' } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } dataBinding { enabled = true } } dependencies { compile "io.reactivex.rxjava2:rxjava:${project.rxJava2Version}" compile "io.reactivex.rxjava2:rxandroid:${project.rxAndroid2Version}" compile project(':droidux') annotationProcessor project(':droidux-processor') compile project(':middlewares:droidux-thunk') compile "com.android.support:appcompat-v7:${project.supportLibrariesVersion}" compile "com.android.support:design:${project.supportLibrariesVersion}" } ================================================ FILE: examples/counter/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /usr/local/opt/android-sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # 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 *; #} ================================================ FILE: examples/counter/src/main/AndroidManifest.xml ================================================ ================================================ FILE: examples/counter/src/main/java/info/izumin/android/droidux/example/counter/Counter.java ================================================ package info.izumin.android.droidux.example.counter; /** * Created by izumin on 12/6/15. */ public class Counter { public static final String TAG = Counter.class.getSimpleName(); private final int count; public Counter(int count) { this.count = count; } public int getCount() { return count; } } ================================================ FILE: examples/counter/src/main/java/info/izumin/android/droidux/example/counter/CounterReducer.java ================================================ package info.izumin.android.droidux.example.counter; import info.izumin.android.droidux.annotation.Dispatchable; import info.izumin.android.droidux.annotation.Reducer; import info.izumin.android.droidux.example.counter.action.DecrementCountAction; import info.izumin.android.droidux.example.counter.action.IncrementCountAction; /** * Created by izumin on 12/6/15. */ @Reducer(Counter.class) public class CounterReducer { public static final String TAG = CounterReducer.class.getSimpleName(); @Dispatchable(IncrementCountAction.class) public Counter increment(Counter state) { return new Counter(state.getCount() + 1); } @Dispatchable(DecrementCountAction.class) public Counter decrement(Counter state) { return new Counter(state.getCount() - 1); } } ================================================ FILE: examples/counter/src/main/java/info/izumin/android/droidux/example/counter/MainActivity.java ================================================ package info.izumin.android.droidux.example.counter; import android.databinding.DataBindingUtil; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; import info.izumin.android.droidux.example.counter.action.DecrementCountAction; import info.izumin.android.droidux.example.counter.action.IncrementCountAction; import info.izumin.android.droidux.example.counter.databinding.ActivityMainBinding; public class MainActivity extends AppCompatActivity implements MainEventHandlers { private RootStore store; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); store = DroiduxRootStore.builder() .setReducer(new CounterReducer(), new Counter(0), BR.counter) .build(); binding.setHandlers(this); binding.setStore(store); } @Override public void onClickBtnIncrement(View v) { store.dispatch(new IncrementCountAction()).subscribe(); } @Override public void onClickBtnDecrement(View v) { store.dispatch(new DecrementCountAction()).subscribe(); } } ================================================ FILE: examples/counter/src/main/java/info/izumin/android/droidux/example/counter/MainEventHandlers.java ================================================ package info.izumin.android.droidux.example.counter; import android.view.View; /** * Created by izumin on 12/6/15. */ public interface MainEventHandlers { void onClickBtnIncrement(View v); void onClickBtnDecrement(View v); } ================================================ FILE: examples/counter/src/main/java/info/izumin/android/droidux/example/counter/RootStore.java ================================================ package info.izumin.android.droidux.example.counter; import android.databinding.Bindable; import info.izumin.android.droidux.BaseStore; import info.izumin.android.droidux.annotation.Store; /** * Created by izumin on 12/6/15. */ @Store(CounterReducer.class) public interface RootStore extends BaseStore, android.databinding.Observable { @Bindable Counter getCounter(); } ================================================ FILE: examples/counter/src/main/java/info/izumin/android/droidux/example/counter/action/DecrementCountAction.java ================================================ package info.izumin.android.droidux.example.counter.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 12/6/15. */ public class DecrementCountAction implements Action { public static final String TAG = DecrementCountAction.class.getSimpleName(); } ================================================ FILE: examples/counter/src/main/java/info/izumin/android/droidux/example/counter/action/IncrementCountAction.java ================================================ package info.izumin.android.droidux.example.counter.action; import info.izumin.android.droidux.Action; /** * Created by izumin on 12/6/15. */ public class IncrementCountAction implements Action { public static final String TAG = IncrementCountAction.class.getSimpleName(); } ================================================ FILE: examples/counter/src/main/res/layout/activity_main.xml ================================================