├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .idea
└── vcs.xml
├── LICENSE
├── README.md
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── mini-android
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── minikorp
│ └── mini
│ └── android
│ ├── FluxActivity.kt
│ ├── FluxFragment.kt
│ ├── FluxViewModel.kt
│ └── ViewUtil.kt
├── mini-common
├── .gitignore
├── build.gradle.kts
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── minikorp
│ │ └── mini
│ │ ├── AndroidInterop.kt
│ │ ├── Annotations.kt
│ │ ├── CloseableTracker.kt
│ │ ├── CompositeCloseable.kt
│ │ ├── Dispatcher.kt
│ │ ├── LoggerMiddleware.kt
│ │ ├── Middleware.kt
│ │ ├── Mini.kt
│ │ ├── NestedStateContainer.kt
│ │ ├── Resource.kt
│ │ ├── StateContainer.kt
│ │ ├── Store.kt
│ │ ├── StoreFlow.kt
│ │ └── Threading.kt
│ └── test
│ └── kotlin
│ └── com
│ └── minikorp
│ └── mini
│ ├── CompositeCloseableTest.kt
│ ├── DispatcherTest.kt
│ ├── LoggerMiddlewareTest.kt
│ ├── ResourceTest.kt
│ ├── SampleStore.kt
│ ├── StoreFlowTest.kt
│ ├── StoreTest.kt
│ ├── TestAction.kt
│ └── TestDispatcher.kt
├── mini-processor-test
├── .gitignore
├── build.gradle.kts
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── minikorp
│ │ └── mini
│ │ └── test
│ │ ├── AnyAction.kt
│ │ ├── BasicState.kt
│ │ └── ReducersStore.kt
│ └── test
│ └── java
│ └── com
│ └── minikorp
│ └── mini
│ └── test
│ └── ReducersStoreTest.kt
├── mini-processor
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── minikorp
│ │ └── mini
│ │ ├── ActionTypesGenerator.kt
│ │ ├── MiniProcessor.java
│ │ ├── Processor.kt
│ │ ├── ProcessorUtils.kt
│ │ └── ReducersGenerator.kt
│ └── resources
│ └── META-INF
│ ├── gradle
│ └── incremental.annotation.processors
│ └── services
│ └── javax.annotation.processing.Processor
├── mini.codestyle.json
├── sample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── androidsample
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── androidsample
│ │ │ └── MainActivity.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── example
│ └── androidsample
│ └── ExampleUnitTest.kt
├── scripts
├── bump-tag.sh
├── latest-version.sh
├── release.sh
└── semver.sh
└── settings.gradle
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: release
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the master branch
7 | on:
8 | push:
9 | branches: [ master ]
10 |
11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
12 | jobs:
13 | # This workflow contains a single job called "build"
14 | build:
15 | # The type of runner that the job will run on
16 | runs-on: ubuntu-latest
17 |
18 | # Steps represent a sequence of tasks that will be executed as part of the job
19 | steps:
20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
21 | - uses: actions/checkout@v2
22 |
23 | # Runs a set of commands using the runners shell
24 | - name: build project
25 | run: |
26 | cd $GITHUB_WORKSPACE
27 | sh scripts/release.sh
28 | - name: maven publish
29 | run: |
30 | cd $GITHUB_WORKSPACE
31 | chmod +x gradlew
32 | ./gradlew publish
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea/
4 | /local.properties
5 | /.idea/workspace.xml
6 | /.idea/libraries
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 |
12 |
13 | # Created by https://www.gitignore.io/api/java,gradle,intellij,osx,windows,linux
14 |
15 | ### Intellij ###
16 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
17 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
18 |
19 | # User-specific stuff:
20 | .idea/**/workspace.xml
21 | .idea/**/tasks.xml
22 |
23 | # Sensitive or high-churn files:
24 | .idea/**/dataSources/
25 | .idea/**/dataSources.ids
26 | .idea/**/dataSources.xml
27 | .idea/**/dataSources.local.xml
28 | .idea/**/sqlDataSources.xml
29 | .idea/**/dynamic.xml
30 | .idea/**/uiDesigner.xml
31 |
32 | # Gradle:
33 | .idea/**/gradle.xml
34 | .idea/**/libraries
35 |
36 | # Mongo Explorer plugin:
37 | .idea/**/mongoSettings.xml
38 |
39 | ## File-based project format:
40 | *.iws
41 |
42 | ## Plugin-specific files:
43 |
44 | # IntelliJ
45 | /out/
46 |
47 | # mpeltonen/sbt-idea plugin
48 | .idea_modules/
49 |
50 | # JIRA plugin
51 | atlassian-ide-plugin.xml
52 |
53 | # Crashlytics plugin (for Android Studio and IntelliJ)
54 | com_crashlytics_export_strings.xml
55 | crashlytics.properties
56 | crashlytics-build.properties
57 | fabric.properties
58 |
59 | ### Intellij Patch ###
60 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
61 |
62 | # *.iml
63 | # modules.xml
64 | # .idea/misc.xml
65 | # *.ipr
66 |
67 | ### Java ###
68 | # Compiled class file
69 | *.class
70 |
71 | # Log file
72 | *.message
73 |
74 | # BlueJ files
75 | *.ctxt
76 |
77 | # Mobile Tools for Java (J2ME)
78 | .mtj.tmp/
79 |
80 | # Package Files #
81 | *.jar
82 | *.war
83 | *.ear
84 | *.zip
85 | *.tar.gz
86 | *.rar
87 |
88 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
89 | hs_err_pid*
90 |
91 | ### Linux ###
92 | *~
93 |
94 | # temporary files which can be created if a process still has a handle open of a deleted file
95 | .fuse_hidden*
96 |
97 | # KDE directory preferences
98 | .directory
99 |
100 | # Linux trash folder which might appear on any partition or disk
101 | .Trash-*
102 |
103 | # .nfs files are created when an open file is removed but is still being accessed
104 | .nfs*
105 |
106 | ### OSX ###
107 | *.DS_Store
108 | .AppleDouble
109 | .LSOverride
110 |
111 | # Icon must end with two \r
112 | Icon
113 |
114 |
115 | # Thumbnails
116 | ._*
117 |
118 | # Files that might appear in the root of a volume
119 | .DocumentRevisions-V100
120 | .fseventsd
121 | .Spotlight-V100
122 | .TemporaryItems
123 | .Trashes
124 | .VolumeIcon.icns
125 | .com.apple.timemachine.donotpresent
126 |
127 | # Directories potentially created on remote AFP share
128 | .AppleDB
129 | .AppleDesktop
130 | Network Trash Folder
131 | Temporary Items
132 | .apdisk
133 |
134 | ### Windows ###
135 | # Windows thumbnail cache files
136 | Thumbs.db
137 | ehthumbs.db
138 | ehthumbs_vista.db
139 |
140 | # Folder config file
141 | Desktop.ini
142 |
143 | # Recycle Bin used on file shares
144 | $RECYCLE.BIN/
145 |
146 | # Windows Installer files
147 | *.cab
148 | *.msi
149 | *.msm
150 | *.msp
151 |
152 | # Windows shortcuts
153 | *.lnk
154 |
155 | ### Gradle ###
156 | .gradle
157 | /build/
158 |
159 | # Ignore Gradle GUI config
160 | gradle-app.setting
161 |
162 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
163 | !gradle-wrapper.jar
164 |
165 | # Cache of project
166 | .gradletasknamecache
167 |
168 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
169 | # gradle/wrapper/gradle-wrapper.properties
170 |
171 | # End of https://www.gitignore.io/api/java,gradle,intellij,osx,windows,linux
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Pablo Orgaz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Latest version: [](https://jitpack.io/#minikorp/mini)
2 |
3 |
4 | # Mini
5 | Mini is a minimal Flux architecture written in Kotlin that also adds a mix of useful features to build UIs fast.
6 |
7 | ### Purpose
8 | You should use this library if you aim to develop a reactive application with good performance (no reflection using code-gen).
9 | Feature development using Mini is fast compared to traditional architectures (like CLEAN or MVP), low boilerplate and state based models make feature integration and bugfixing easy as well as removing several families of problems like concurrency or view consistency across screens.
10 |
11 | ## How to Use
12 | ### Actions
13 |
14 | Annotate action classes with `@Action` or extend `BaseAction`.
15 |
16 | Marking a class as action will make all the action and all supertypes available for listening from `@Reducer` functions.
17 |
18 | ### Dispatcher
19 |
20 | ```kotlin
21 | //Dispatch an action on the main thread synchronously
22 | dispatcher.dispatch(LoginAction(username = "user", password = "123"))
23 |
24 | //Post an event that will dispatch the action on the UI thread and return immediately.
25 | dispatcher.dispatchAsync(LoginAction(username = "user", password = "123"))
26 | ```
27 |
28 | ### Store
29 | The Stores are holders for application state and state mutation logic. In order to do so they expose pure reducer functions that are later invoked by the dispatcher.
30 |
31 | The state is plain object (usually a data class) that holds all information needed to display the view. State should always be inmutable. State classes should avoid using framework elements (View, Camera, Cursor...) in order to facilitate testing.
32 |
33 | Stores subscribe to actions to change the application state after a dispatch. Mini generates the code that links dispatcher actions and stores using the `@Reducer` annotation over a **non-private function that receives an `@Action` as parameter**.
34 |
35 | ### Generated code
36 |
37 | Mini generates `mini.MiniGen` class at compilation time to use as factory for `Dispatcher` and automatic `@Reducer` subscription calls. MiniGen is not required to use Mini, but encouraged to reduce boilerplate.
38 |
39 | ```kotlin
40 | val dispatcher = MiniGen.newDispatcher()
41 | val stores = listOf(your stores...)
42 | //Bind @Reducer functions with dispatcher.
43 | MiniGen.subscribe(dispatcher, stores)
44 | ```
45 |
46 | ### View changes
47 | Each ``Store`` exposes a custom `StoreCallback` though the method `subscribe` or a `Flowable` if you wanna make use of RxJava. Both of them emits changes produced on their states, allowing the view to listen reactive the state changes. Being able to update the UI according to the new `Store` state.
48 |
49 | ```kotlin
50 | //Using Flow
51 | userStore
52 | .flow()
53 | .onEach { updateUserName(it.name) }
54 | .launchIn(lifecycleScope)
55 |
56 | // Default callback
57 | userStore
58 | .subscribe { state -> updateUserName(state.name) }
59 | ```
60 |
61 | ### Logging
62 | Mini includes a custom `LoggerInterceptor` to log any change in your `Store` states produced from an `Action`. This will allow you to keep track of your actions, changes and side-effects more easily.
63 |
64 | ## Gradle
65 |
66 | [](https://jitpack.io/#minikorp/mini)
67 |
68 | ```groovy
69 | apply plugin: kotlin
70 | apply plugin: kotlin-kapt
71 |
72 | dependencies {
73 | def mini_version = "4.2.0" //See latest version tag at top
74 | implementation "com.github.minikorp.mini:mini-common:$mini_version"
75 | kapt "com.github.minikorp.mini:mini-processor:$mini_version"
76 | //Optional dependencies
77 | implementation "com.github.minikorp.mini:mini-rx:$mini_version" //Rx bindings
78 | implementation "com.github.minikorp.mini:mini-flow:$mini_version" //Flow bindings
79 | implementation "com.github.minikorp.mini:mini-android:$mini_version" //Android utilities
80 | }
81 | ```
82 |
83 | ## Issues
84 |
85 | Jetifier might crash your build without reason,
86 | add this line to gradle.properties to exclude the compiler or fully disable it.
87 |
88 | ```properties
89 | android.jetifier.blacklist=mini-processor.*\\.jar #Blacklist
90 | android.enableJetifier=false #Or disable
91 | ```
92 |
93 | Compiling JDK >8 might fail, make sure you set compatibility to java 8
94 | both for Android and kapt.
95 |
96 | ```groovy
97 | android {
98 | compileOptions {
99 | sourceCompatibility "1.8"
100 | targetCompatibility "1.8"
101 | }
102 | }
103 |
104 | kapt {
105 | javacOptions {
106 | option("-source", "8")
107 | option("-target", "8")
108 | }
109 | }
110 | ```
111 |
112 | ## Build performance
113 |
114 | Make sure to enable incremental apt and worker api for faster builds with kapt.
115 |
116 | ```properties
117 | # Some performance improvements
118 | org.gradle.parallel=true
119 | org.gradle.configureondemand=true
120 | org.gradle.caching=true
121 | org.gradle.daemon=true
122 | kapt.incremental.apt=true
123 | kapt.use.worker.api=true
124 | ```
125 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | google()
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath(kotlin("gradle-plugin", version = "1.4.21"))
9 | classpath("com.android.tools.build:gradle:3.6.4")
10 | }
11 | }
12 |
13 | plugins {
14 | kotlin("jvm") version "1.4.21"
15 | `maven-publish`
16 | }
17 |
18 | fun runCommand(command: String): String {
19 | val runtime = Runtime.getRuntime()
20 | val process = if (System.getProperty("os.name").toLowerCase().contains("windows")) {
21 | runtime.exec("bash $command")
22 | } else {
23 | runtime.exec(command)
24 | }
25 | val stream = process.apply { waitFor() }.inputStream
26 | return stream.reader().readText().trim()
27 | }
28 |
29 |
30 | subprojects {
31 |
32 | apply(plugin = "maven-publish")
33 | version = runCommand("$rootDir/scripts/latest-version.sh")
34 | group = "com.minikorp"
35 |
36 | afterEvaluate {
37 | val modules = arrayOf("mini-common", "mini-processor", "mini-android")
38 | if (this.name !in modules) return@afterEvaluate
39 |
40 | if (tasks.findByName("sourcesJar") == null) {
41 | tasks.register("sourcesJar", Jar::class) {
42 | archiveClassifier.set("sources")
43 | from(sourceSets["main"].allSource)
44 | }
45 | }
46 |
47 | this.publishing {
48 | repositories {
49 | maven {
50 | name = "GitHubPackages"
51 | url = uri("https://maven.pkg.github.com/minikorp/mini")
52 | credentials {
53 | username = "minikorp"
54 | password = System.getenv("GITHUB_TOKEN")
55 | }
56 | }
57 | }
58 |
59 | publications {
60 | if (publications.findByName("gpr") == null) {
61 | register("gpr", MavenPublication::class) {
62 | from(components["java"])
63 | artifact(tasks.findByName("sourcesJar"))
64 | }
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 |
72 | allprojects {
73 | repositories {
74 | jcenter()
75 | mavenCentral()
76 | maven { url = uri("https://jitpack.io") }
77 | google()
78 | }
79 | }
80 |
81 | repositories {
82 | jcenter()
83 | mavenCentral()
84 | //maven { url = URI.parse("https://jitpack.io") }
85 | google()
86 | }
87 |
88 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will take*
4 | # any settings specified in this file.
5 | # For more details on how to fn your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | android.useAndroidX=true
11 | android.enableJetifier=false
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 |
17 | # Some performance improvements
18 | org.gradle.parallel=true
19 | org.gradle.configureondemand=true
20 | org.gradle.caching=true
21 | org.gradle.daemon=true
22 | kapt.incremental.apt=true
23 | kapt.use.worker.api=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jan 20 09:55:33 CET 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/mini-android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/mini-android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | compileSdkVersion(30)
8 | buildToolsVersion("29.0.3")
9 |
10 | defaultConfig {
11 | minSdkVersion(14)
12 | targetSdkVersion(30)
13 | versionCode = 1
14 | versionName = "1.0"
15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | getByName("release") {
19 | isMinifyEnabled = false
20 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | api(project(":mini-common"))
27 |
28 | api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1")
29 | api("androidx.appcompat:appcompat:1.2.0")
30 |
31 | val lifecycleVersion = "2.3.0-rc01"
32 | api("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
33 | api("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
34 | api("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycleVersion")
35 |
36 |
37 | testImplementation("junit:junit:4.13.1")
38 | androidTestImplementation("androidx.test:runner:1.3.0")
39 | androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
40 | }
41 |
42 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
43 | kotlinOptions.freeCompilerArgs += listOf(
44 | "-Xuse-experimental=kotlin.Experimental",
45 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
46 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview"
47 | )
48 | }
49 |
50 | val sourcesJar by tasks.registering(Jar::class) {
51 | @Suppress("UnstableApiUsage")
52 | archiveClassifier.set("sources")
53 | from(android.sourceSets["main"].java.sourceFiles)
54 | }
55 |
56 | publishing {
57 | publications {
58 | create("gpr") {
59 | artifact(tasks.findByName("sourcesJar"))
60 | afterEvaluate {
61 | from(components["release"])
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/mini-android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 |
24 | #Generated code will go on this package and is accessed using reflection
25 | #so keep it as is
26 | #noinspection ShrinkerUnresolvedReference
27 | -keep class com.minikorp.mini.codegen.** { *; }
28 |
--------------------------------------------------------------------------------
/mini-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/mini-android/src/main/java/com/minikorp/mini/android/FluxActivity.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.android
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.lifecycle.lifecycleScope
6 | import com.minikorp.mini.CloseableTracker
7 | import com.minikorp.mini.DefaultCloseableTracker
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.launchIn
11 | import kotlinx.coroutines.launch
12 | import kotlin.coroutines.CoroutineContext
13 |
14 | abstract class FluxActivity : AppCompatActivity(),
15 | CloseableTracker by DefaultCloseableTracker(),
16 | CoroutineScope {
17 |
18 | override val coroutineContext: CoroutineContext
19 | get() = lifecycleScope.coroutineContext
20 |
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | lifecycleScope.launch { whenCreated(savedInstanceState) }
25 | }
26 |
27 | override fun onResume() {
28 | super.onResume()
29 | lifecycleScope.launch { whenResumed() }
30 | }
31 |
32 | override fun onPause() {
33 | super.onPause()
34 | lifecycleScope.launch { whenPaused() }
35 | }
36 |
37 | override fun onDestroy() {
38 | lifecycleScope.launch { whenDestroyed() }
39 | close()
40 | super.onDestroy()
41 | }
42 |
43 | fun Flow.launchInLifecycleScope() {
44 | launchIn(lifecycleScope)
45 | }
46 |
47 | protected open suspend fun whenCreated(savedInstanceState: Bundle?) = Unit
48 | protected open suspend fun whenResumed() = Unit
49 | protected open suspend fun whenPaused() = Unit
50 | protected open suspend fun whenDestroyed() = Unit
51 |
52 | }
--------------------------------------------------------------------------------
/mini-android/src/main/java/com/minikorp/mini/android/FluxFragment.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.android
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import androidx.lifecycle.lifecycleScope
6 | import com.minikorp.mini.CloseableTracker
7 | import com.minikorp.mini.DefaultCloseableTracker
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.launchIn
11 | import kotlinx.coroutines.launch
12 | import kotlin.coroutines.CoroutineContext
13 |
14 | abstract class FluxFragment : Fragment(),
15 | CloseableTracker by DefaultCloseableTracker(),
16 | CoroutineScope {
17 |
18 | override val coroutineContext: CoroutineContext
19 | get() = lifecycleScope.coroutineContext
20 |
21 | override fun onActivityCreated(savedInstanceState: Bundle?) {
22 | super.onActivityCreated(savedInstanceState)
23 | lifecycleScope.launch { whenCreated(savedInstanceState) }
24 | }
25 |
26 | override fun onResume() {
27 | super.onResume()
28 | lifecycleScope.launch { whenResumed() }
29 | }
30 |
31 | override fun onPause() {
32 | super.onPause()
33 | lifecycleScope.launch { whenPaused() }
34 | }
35 |
36 | override fun onDestroy() {
37 | lifecycleScope.launch { whenDestroyed() }
38 | close()
39 | super.onDestroy()
40 | }
41 |
42 | fun Flow.launchOnUi() {
43 | launchIn(lifecycleScope)
44 | }
45 |
46 | protected open suspend fun whenCreated(savedInstanceState: Bundle?) = Unit
47 | protected open suspend fun whenResumed() = Unit
48 | protected open suspend fun whenPaused() = Unit
49 | protected open suspend fun whenDestroyed() = Unit
50 | }
--------------------------------------------------------------------------------
/mini-android/src/main/java/com/minikorp/mini/android/FluxViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.android
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import com.minikorp.mini.CloseableTracker
6 | import com.minikorp.mini.DefaultCloseableTracker
7 | import com.minikorp.mini.StateContainer
8 | import com.minikorp.mini.assertOnUiThread
9 | import java.io.Closeable
10 | import java.util.concurrent.CopyOnWriteArrayList
11 |
12 | abstract class FluxViewModel(
13 | val savedStateHandle: SavedStateHandle) :
14 | ViewModel(),
15 | StateContainer,
16 | CloseableTracker by DefaultCloseableTracker() {
17 |
18 |
19 | class ViewModelSubscription internal constructor(private val vm: FluxViewModel<*>,
20 | private val fn: Any) : Closeable {
21 | override fun close() {
22 | vm.listeners.remove(fn)
23 | }
24 | }
25 |
26 | private var _state: Any? = StateContainer.Companion.NoState
27 | private val listeners = CopyOnWriteArrayList<(S) -> Unit>()
28 |
29 | override val state: S
30 | get() {
31 | if (_state === StateContainer.Companion.NoState) {
32 | synchronized(this) {
33 | if (_state === StateContainer.Companion.NoState) {
34 | _state = restoreState(savedStateHandle) ?: initialState()
35 | }
36 | }
37 | }
38 | @Suppress("UNCHECKED_CAST")
39 | return _state as S
40 | }
41 |
42 |
43 | override fun setState(newState: S) {
44 | assertOnUiThread()
45 | performStateChange(newState)
46 | }
47 |
48 | private fun performStateChange(newState: S) {
49 | if (_state != newState) {
50 | _state = newState
51 | saveState(newState, savedStateHandle)
52 | listeners.forEach {
53 | it(newState)
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * Persist the state, no-op by default.
60 | *
61 | * ```handle.set("state", state)```
62 | */
63 | open fun saveState(state: S, handle: SavedStateHandle) {
64 | //No-op
65 | }
66 |
67 | /**
68 | * Restore the state from the [SavedStateHandle] or null if nothing was saved.
69 | *
70 | * ```handle.get("state")```
71 | */
72 | open fun restoreState(handle: SavedStateHandle): S? {
73 | return null
74 | }
75 |
76 | override fun subscribe(hotStart: Boolean, fn: (S) -> Unit): Closeable {
77 | listeners.add(fn)
78 | if (hotStart) fn(state)
79 | return ViewModelSubscription(this, fn)
80 | }
81 |
82 | override fun onCleared() {
83 | super.onCleared()
84 | close()
85 | }
86 | }
--------------------------------------------------------------------------------
/mini-android/src/main/java/com/minikorp/mini/android/ViewUtil.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.android
2 |
3 | import android.content.res.Resources
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.LayoutRes
8 | import com.minikorp.mini.Resource
9 | import kotlin.math.roundToInt
10 |
11 | /**
12 | * Toggle two views between content / loading / error based on [Resource] state.
13 | *
14 | * Has no effect when resource is idle.
15 | */
16 | fun toggleViewsVisibility(
17 | resource: Resource<*>,
18 | contentView: View? = null,
19 | loadingView: View? = null,
20 | errorView: View? = null,
21 | idleView: View? = null,
22 | invisibilityType: Int = View.INVISIBLE
23 | ) {
24 | val newVisibilities = arrayOf(invisibilityType, invisibilityType, invisibilityType, invisibilityType)
25 | val indexToMakeVisible =
26 | when {
27 | resource.isSuccess -> 0
28 | resource.isLoading -> 1
29 | resource.isFailure -> 2
30 | resource.isEmpty -> 3
31 | else -> throw UnsupportedOperationException()
32 | }
33 | newVisibilities[indexToMakeVisible] = View.VISIBLE
34 | contentView?.visibility = newVisibilities[0]
35 | loadingView?.visibility = newVisibilities[1]
36 | errorView?.visibility = newVisibilities[2]
37 | idleView?.visibility = newVisibilities[3]
38 | }
39 |
40 | fun ViewGroup.inflateNoAttach(@LayoutRes layout: Int): View {
41 | return LayoutInflater.from(this.context).inflate(layout, this, false)
42 | }
43 |
44 | fun View.makeVisible() = run { visibility = View.VISIBLE }
45 | fun View.makeInvisible() = run { visibility = View.INVISIBLE }
46 | fun View.makeGone() = run { visibility = View.GONE }
47 |
48 | /**
49 | * 8.dp -> 8dp in value in pixels
50 | */
51 | val Number.dp: Int get() = (this.toFloat() * Resources.getSystem().displayMetrics.density).roundToInt()
52 |
--------------------------------------------------------------------------------
/mini-common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/mini-common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | }
4 |
5 | dependencies {
6 | implementation(kotlin("stdlib-jdk8"))
7 | api(kotlin("reflect"))
8 | compileOnly("com.google.android:android:4.1.1.4")
9 |
10 | val coroutines = "1.3.9"
11 | api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines")
12 |
13 | testImplementation("junit:junit:4.13.1")
14 | testImplementation("org.amshove.kluent:kluent:1.44")
15 | }
16 |
17 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
18 | kotlinOptions.freeCompilerArgs += listOf(
19 | "-Xuse-experimental=kotlin.Experimental",
20 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
21 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview"
22 | )
23 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/AndroidInterop.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | /**
4 | * Check if running on android device / emulator or jvm
5 | */
6 | internal val isAndroid by lazy {
7 | try {
8 | android.os.Build.VERSION.SDK_INT != 0
9 | } catch (e: Throwable) {
10 | false
11 | }
12 | }
13 |
14 | fun requireAndroid() {
15 | if (!isAndroid) {
16 | throw UnsupportedOperationException("This method can only be called from android environment")
17 | }
18 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/Annotations.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import java.lang.annotation.Inherited
4 |
5 | const val DEFAULT_PRIORITY = 100
6 |
7 | /**
8 | * Mark a type as action for code generation. All actions must include this annotation
9 | * or dispatcher won't work properly.
10 | */
11 | @Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
12 | @Retention(AnnotationRetention.RUNTIME)
13 | @Inherited
14 | annotation class Action
15 |
16 |
17 | /**
18 | * Mark a function declared in a [StateContainer] as a reducer function.
19 | *
20 | * Reducers function must have two parameters, the state that must have same time
21 | * as the [StateContainer] state, and the action being handled.
22 | *
23 | * If the reducer function is not pure, only the action parameter is allowed
24 | * and function should have no return.
25 | */
26 | @Target(AnnotationTarget.FUNCTION)
27 | @Retention(AnnotationRetention.RUNTIME)
28 | annotation class Reducer(val priority: Int = DEFAULT_PRIORITY)
29 |
30 |
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/CloseableTracker.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import java.io.Closeable
4 |
5 | interface CloseableTracker : Closeable {
6 | /**
7 | * Start tracking a disposable.
8 | */
9 | fun T.track(): T
10 | }
11 |
12 | class DefaultCloseableTracker : CloseableTracker {
13 | private val closeables = CompositeCloseable()
14 | override fun close() = closeables.close()
15 | override fun T.track(): T {
16 | closeables.add(this)
17 | return this
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/CompositeCloseable.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import java.io.Closeable
4 |
5 | /**
6 | * A collection of closeables.
7 | */
8 | class CompositeCloseable : Closeable {
9 | private val items = ArrayList()
10 |
11 | override fun close() {
12 | synchronized(this) {
13 | items.forEach { it.close() }
14 | items.clear()
15 | }
16 | }
17 |
18 | fun add(closeable: T): T {
19 | synchronized(this) {
20 | items.add(closeable)
21 | }
22 | return closeable
23 | }
24 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/Dispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import kotlinx.coroutines.*
4 | import java.io.Closeable
5 | import java.util.*
6 | import java.util.concurrent.atomic.AtomicInteger
7 | import kotlin.coroutines.CoroutineContext
8 | import kotlin.coroutines.EmptyCoroutineContext
9 | import kotlin.reflect.KClass
10 |
11 | private typealias DispatchCallback = suspend (Any) -> Unit
12 |
13 |
14 | /**
15 | * Hub for actions. Use code generation with [Mini]
16 | * or provide action type map information and manually handle subscriptions.
17 | *
18 | * @param strictMode Verify calling thread, only disable in production!
19 | *
20 | */
21 | class Dispatcher(private val strictMode: Boolean = false) {
22 |
23 | /**
24 | * All types an action can be observed as.
25 | * If map is empty, the runtime type itself will be used. If using code generation,
26 | * [Mini.actionTypes] will contain a map with all super types of @[Action] annotated classes.
27 | */
28 | var actionTypeMap: Map, List>> = emptyMap()
29 |
30 | /**
31 | * Action at the top of the dispatch stack.
32 | */
33 | val lastAction: Any? get() = actionStack.firstOrNull()
34 |
35 | private val subscriptionCaller: Chain = object : Chain {
36 | override suspend fun proceed(action: Any): Any {
37 | val types = actionTypeMap[action::class]
38 | ?: error("${action::class.simpleName} is not action")
39 | //Ensure reducer is called on Main dispatcher
40 | types.forEach { type ->
41 | subscriptions[type]?.forEach { it.fn(action) }
42 | }
43 | return action
44 | }
45 | }
46 |
47 | private val middlewares: MutableList = ArrayList()
48 | private var middlewareChain: Chain = buildChain()
49 | private val actionStack: Stack = Stack()
50 |
51 | internal val subscriptions: MutableMap, MutableSet> = HashMap()
52 |
53 | private fun buildChain(): Chain {
54 | return middlewares.fold(subscriptionCaller) { chain, middleware ->
55 | object : Chain {
56 | override suspend fun proceed(action: Any): Any {
57 | return middleware.intercept(action, chain)
58 | }
59 | }
60 | }
61 | }
62 |
63 | fun addMiddleware(middleware: Middleware) {
64 | synchronized(this) {
65 | middlewares += middleware
66 | middlewareChain = buildChain()
67 | }
68 | }
69 |
70 | fun removeInterceptor(middleware: Middleware) {
71 | synchronized(this) {
72 | middlewares -= middleware
73 | middlewareChain = buildChain()
74 | }
75 | }
76 |
77 | inline fun subscribe(priority: Int = 100,
78 | noinline callback: suspend (A) -> Unit): Closeable {
79 | return subscribe(A::class, priority, callback)
80 | }
81 |
82 | @Suppress("UNCHECKED_CAST")
83 | fun subscribe(clazz: KClass,
84 | priority: Int = 100,
85 | callback: suspend (T) -> Unit): Closeable {
86 | synchronized(subscriptions) {
87 | val reg = DispatcherSubscription(this, clazz, priority, callback as DispatchCallback)
88 | val set = subscriptions.getOrPut(clazz) {
89 | TreeSet { a, b ->
90 | //Sort by priority, then by id for equal priority
91 | val p = a.priority.compareTo(b.priority)
92 | if (p == 0) a.id.compareTo(b.id)
93 | else p
94 | }
95 | }
96 | set.add(reg)
97 | return reg
98 | }
99 | }
100 |
101 | fun unregister(registration: DispatcherSubscription) {
102 | synchronized(subscriptions) {
103 | subscriptions[registration.type]?.remove(registration)
104 | }
105 | }
106 |
107 |
108 | /**
109 | * Dispatch an action on the main thread using an unconfined dispatcher
110 | * so it's safe for even loops.
111 | */
112 | suspend fun dispatch(action: Any) {
113 | if (isAndroid) {
114 | withContext(Dispatchers.Main.immediate) { doDispatch(action) }
115 | } else {
116 | doDispatch(action)
117 | }
118 | }
119 |
120 | /**
121 | * Dispatch an action, blocking the thread until it's complete.
122 | *
123 | * Calling from UI thread will throw an exception since it can potentially result
124 | * in ANR error.
125 | */
126 | fun dispatchBlocking(action: Any) {
127 | if (strictMode) assertOnBgThread()
128 | runBlocking {
129 | dispatch(action)
130 | }
131 | }
132 |
133 | private suspend fun doDispatch(action: Any) {
134 | actionStack.push(action)
135 | middlewareChain.proceed(action)
136 | actionStack.pop()
137 | }
138 |
139 | /**
140 | * Handle for a dispatcher subscription.
141 | */
142 | data class DispatcherSubscription internal constructor(val dispatcher: Dispatcher,
143 | val type: KClass<*>,
144 | val priority: Int,
145 | val fn: DispatchCallback) : Closeable {
146 | companion object {
147 | private val idCounter = AtomicInteger()
148 | }
149 |
150 | override fun close() {
151 | if (disposed) return
152 | dispatcher.unregister(this)
153 | disposed = true
154 | }
155 |
156 | //Alias for close
157 | fun dispose() = close()
158 |
159 | val id = idCounter.getAndIncrement()
160 | var disposed = false
161 | }
162 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/LoggerMiddleware.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import android.util.Log
4 | import java.util.concurrent.atomic.AtomicInteger
5 |
6 | /** Actions implementing this interface won't log anything, including nested calls */
7 | interface SilentAction
8 |
9 | /**
10 | * Actions implementing this interface will log nested actions visually since they will
11 | * most likely dispatch other actions.
12 | */
13 | interface SuspendingAction
14 |
15 | internal fun extractClassName(clazz: Class<*>): String {
16 | return clazz.name.substringAfterLast(".")
17 | }
18 |
19 | /**
20 | * Action logging for stores.
21 | */
22 | class LoggerMiddleware(stores: Collection>,
23 | private val tag: String = "MiniLog",
24 | private val diffFunction: ((a: Any?, b: Any?) -> String)? = null,
25 | private val logger: (priority: Int, tag: String, msg: String) -> Unit) : Middleware {
26 |
27 | private var actionCounter = AtomicInteger(0)
28 |
29 | private val stores = stores.toList()
30 |
31 | override suspend fun intercept(action: Any, chain: Chain): Any {
32 | if (action is SilentAction) chain.proceed(action) //Do nothing
33 |
34 | val isSuspending = action is SuspendingAction
35 | val beforeStates: Array = Array(stores.size) { }
36 | val afterStates: Array = Array(stores.size) { }
37 | val actionName = extractClassName(action.javaClass)
38 |
39 | stores.forEachIndexed { idx, store -> beforeStates[idx] = store.state }
40 |
41 | val (upCorner, downCorner) = if (isSuspending) {
42 | "╔═════ " to "╚════> "
43 | } else {
44 | "┌── " to "└─> "
45 | }
46 |
47 | val prelude = "[${"${actionCounter.getAndIncrement() % 100}".padStart(2, '0')}] "
48 |
49 | logger(Log.DEBUG, tag, "$prelude$upCorner$actionName")
50 | logger(Log.DEBUG, tag, "$prelude$action")
51 |
52 | //Pass it down
53 | val start = System.nanoTime()
54 | val outAction = chain.proceed(action)
55 | val processTime = (System.nanoTime() - start) / 1000000
56 |
57 | stores.forEachIndexed { idx, store -> afterStates[idx] = store.state }
58 |
59 | if (!isSuspending) {
60 | for (i in beforeStates.indices) {
61 | val oldState = beforeStates[i]
62 | val newState = afterStates[i]
63 | if (oldState !== newState) {
64 | val line = "$prelude│ ${stores[i].javaClass.name}"
65 | logger(Log.VERBOSE, tag, "$line: $newState")
66 | diffFunction?.invoke(oldState, newState)?.let { diff ->
67 | logger(Log.DEBUG, tag, "$line: $diff")
68 | }
69 | }
70 | }
71 | }
72 |
73 | logger(Log.DEBUG, tag, "$prelude$downCorner$actionName ${processTime}ms")
74 |
75 | return outAction
76 | }
77 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/Middleware.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | /**
4 | * Middleware that will be called for every dispatch to modify the
5 | * action or perform side effects like logging.
6 | *
7 | * Call chain.proceed(action) with the new action or dispatcher chain will be broken.
8 | */
9 | interface Middleware {
10 | suspend fun intercept(action: Any, chain: Chain): Any
11 | }
12 |
13 | /**
14 | * A chain of interceptors. Call [proceed] with
15 | * the intercepted action or directly handle it.
16 | */
17 | interface Chain {
18 | suspend fun proceed(action: Any): Any
19 | }
20 |
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/Mini.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import java.io.Closeable
4 | import java.lang.IllegalStateException
5 | import kotlin.reflect.KClass
6 |
7 | const val DISPATCHER_FACTORY_CLASS_NAME = "com.minikorp.mini.codegen.Mini_Generated"
8 |
9 | abstract class Mini {
10 |
11 | companion object {
12 |
13 | private val miniInstance: Mini by lazy {
14 | try {
15 | Class.forName(DISPATCHER_FACTORY_CLASS_NAME).getField("INSTANCE").get(null) as Mini
16 | } catch (ex: Throwable) {
17 | throw ClassNotFoundException("Failed to load generated class $DISPATCHER_FACTORY_CLASS_NAME, " +
18 | "most likely kapt did not run, add it as dependency to the project", ex)
19 | }
20 | }
21 |
22 | /**
23 | * Generate all subscriptions from @[Reducer] annotated methods and bundle
24 | * into a single Closeable.
25 | */
26 | fun link(dispatcher: Dispatcher, container: StateContainer<*>): Closeable {
27 | ensureDispatcherInitialized(dispatcher)
28 | return miniInstance.subscribe(dispatcher, container)
29 | }
30 |
31 | /**
32 | * Generate all subscriptions from @[Reducer] annotated methods and bundle
33 | * into a single Closeable.
34 | */
35 | fun link(dispatcher: Dispatcher, containers: Iterable>): Closeable {
36 | ensureDispatcherInitialized(dispatcher)
37 | return miniInstance.subscribe(dispatcher, containers)
38 | }
39 |
40 | private fun ensureDispatcherInitialized(dispatcher: Dispatcher) {
41 | if (dispatcher.actionTypeMap.isEmpty()) {
42 | dispatcher.actionTypeMap = miniInstance.actionTypes
43 | }
44 | }
45 |
46 | }
47 |
48 | /**
49 | * All the types an action can be subscribed as.
50 | */
51 | abstract val actionTypes: Map, List>>
52 |
53 | /**
54 | * Link all [Reducer] functions present in the store to the dispatcher.
55 | */
56 | protected abstract fun subscribe(dispatcher: Dispatcher, container: StateContainer): Closeable
57 |
58 | /**
59 | * Link all [Reducer] functions present in the store to the dispatcher.
60 | */
61 | protected fun subscribe(dispatcher: Dispatcher, containers: Iterable>): Closeable {
62 | val c = CompositeCloseable()
63 | containers.forEach { container ->
64 | c.add(subscribe(dispatcher, container))
65 | }
66 | return c
67 | }
68 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/NestedStateContainer.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import java.io.Closeable
4 |
5 | /**
6 | * Utility class to allow splitting [StateContainer] into chunks so not all reducers live in the same
7 | * file.
8 | *
9 | * From a state container
10 | *
11 | * ```
12 | * class Reducer : NestedStateContainer() {
13 | * @Reducer
14 | * fun reduceOneAction(...)
15 | * }
16 | *
17 | * class MyStore {
18 | * val reducer = Reducer(this)
19 | *
20 | * init {
21 | * Mini.link(dispatcher, listOf(this, reducer))
22 | * }
23 | *
24 | * @Reducer
25 | * fun globalReduceFn(...)
26 | * }
27 | * ```
28 | */
29 | abstract class NestedStateContainer(var parent: StateContainer? = null) : StateContainer {
30 | override val state: S
31 | get() = parent!!.state
32 |
33 | override fun setState(newState: S) {
34 | parent!!.setState(newState)
35 | }
36 |
37 | override fun subscribe(hotStart: Boolean, fn: (S) -> Unit): Closeable {
38 | return parent!!.subscribe(hotStart, fn)
39 | }
40 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/Resource.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UNCHECKED_CAST")
2 |
3 | package com.minikorp.mini
4 |
5 | /**
6 | * Simple wrapper to map ongoing tasks (network / database) for view implementation.
7 | *
8 | * Similar to kotlin [Result] but with loading and empty state.
9 | */
10 | open class Resource @PublishedApi internal constructor(val value: Any?) {
11 |
12 | val isSuccess: Boolean get() = !isLoading && !isFailure && !isEmpty
13 | val isEmpty: Boolean get() = value is Empty
14 | val isFailure: Boolean get() = value is Failure
15 | val isLoading: Boolean get() = value is Loading<*>
16 |
17 | internal class Empty {
18 | override fun toString(): String = "Empty()"
19 | }
20 |
21 | @PublishedApi
22 | internal data class Failure(val exception: Throwable?) {
23 | override fun toString(): String = "Failure($exception)"
24 | }
25 |
26 | @PublishedApi
27 | internal data class Loading(val value: U? = null) {
28 | override fun toString(): String = "Loading($value)"
29 | }
30 |
31 | /**
32 | * Get the current value if successful, or null for other cases.
33 | */
34 | fun getOrNull(): T? =
35 | when {
36 | isSuccess -> value as T?
37 | else -> null
38 | }
39 |
40 | fun exceptionOrNull(): Throwable? =
41 | when (value) {
42 | is Failure -> value.exception
43 | else -> null
44 | }
45 |
46 | companion object {
47 | fun success(value: T): Resource = Resource(value)
48 | fun failure(exception: Throwable? = null): Resource = Resource(Failure(exception))
49 | fun loading(value: T? = null): Resource = Resource(Loading(value))
50 | fun empty(): Resource = Resource(Empty())
51 |
52 | /** Alias for loading */
53 | fun idle(value: T? = null): Resource = Resource(Loading(value))
54 | }
55 |
56 | override fun toString(): String {
57 | return value.toString()
58 | }
59 | }
60 |
61 | /**
62 | * An alias for a empty resource.
63 | */
64 | typealias Task = Resource
65 |
66 |
67 | inline fun Resource.onSuccess(crossinline action: (data: T) -> Unit): Resource {
68 | if (isSuccess) action(value as T)
69 | return this
70 | }
71 |
72 | inline fun Resource.onFailure(crossinline action: (error: Throwable?) -> Unit): Resource {
73 | if (isFailure) action((value as Resource.Failure).exception)
74 | return this
75 | }
76 |
77 | inline fun Resource.onLoading(crossinline action: (data: T?) -> Unit): Resource {
78 | if (isLoading) action((value as Resource.Loading).value)
79 | return this
80 | }
81 |
82 | inline fun Resource.onEmpty(crossinline action: () -> Unit): Resource {
83 | if (isEmpty) action()
84 | return this
85 | }
86 |
87 | /** Alias of [onEmpty] for Task */
88 | inline fun Task.onIdle(crossinline action: () -> Unit) = onEmpty(action)
89 |
90 | inline fun Resource.map(crossinline transform: (data: T) -> R): Resource {
91 | if (isSuccess) return Resource.success(transform(value as T))
92 | return Resource(value)
93 | }
94 |
95 | /** All tasks succeeded. */
96 | fun Iterable>.allSuccessful(): Boolean {
97 | return this.all { it.isSuccess }
98 | }
99 |
100 | /** Any tasks failed. */
101 | fun Iterable>.anyFailure(): Boolean {
102 | return this.any { it.isFailure }
103 | }
104 |
105 | /** Any task is running. */
106 | fun Iterable>.anyLoading(): Boolean {
107 | return this.any { it.isLoading }
108 | }
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/StateContainer.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import org.jetbrains.annotations.TestOnly
4 | import java.io.Closeable
5 | import java.lang.reflect.ParameterizedType
6 |
7 | /**
8 | * Common interface for state containers.
9 | */
10 | interface StateContainer {
11 |
12 | companion object {
13 | /**
14 | * Token to mark a state as not initialized.
15 | */
16 | object NoState
17 | }
18 |
19 | val state: S
20 |
21 | fun setState(newState: S)
22 |
23 | /**
24 | * Register a observer to state changes.
25 | *
26 | * @return [Closeable] to cancel the subscription.
27 | */
28 | fun subscribe(hotStart: Boolean = true, fn: (S) -> Unit): Closeable
29 |
30 | /**
31 | * The initial state of the container. By default it will invoke the primary constructor
32 | * of the State type parameter. If this constructor is not accessible provide your own
33 | * implementation of this method.
34 | */
35 | @Suppress("UNCHECKED_CAST")
36 | fun initialState(): S {
37 | val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
38 | as Class
39 | try {
40 | val constructor = type.getDeclaredConstructor()
41 | constructor.isAccessible = true
42 | return constructor.newInstance()
43 | } catch (e: Exception) {
44 | throw RuntimeException("Missing default no-args constructor for the state $type", e)
45 | }
46 | }
47 |
48 | /**
49 | * Test only method, don't use in app code.
50 | * Will force state change on UI so it can be called from
51 | * espresso thread.
52 | */
53 | @TestOnly
54 | fun setTestState(s: S) {
55 | if (isAndroid) {
56 | onUiSync {
57 | setState(s)
58 | }
59 | } else {
60 | setState(s)
61 | }
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/Store.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import java.io.Closeable
4 | import java.util.concurrent.CopyOnWriteArrayList
5 |
6 | /**
7 | * Basic state holder.
8 | */
9 | abstract class Store : Closeable,
10 | StateContainer,
11 | CloseableTracker by DefaultCloseableTracker() {
12 |
13 | class StoreSubscription internal constructor(private val store: Store<*>,
14 | private val fn: Any) : Closeable {
15 | override fun close() {
16 | store.listeners.remove(fn)
17 | }
18 | }
19 |
20 | private var _state: Any? = StateContainer.Companion.NoState
21 | private val listeners = CopyOnWriteArrayList<(S) -> Unit>()
22 |
23 | /**
24 | * Initialize the store after dependency injection is complete.
25 | */
26 | open fun initialize() {
27 | //No-op
28 | }
29 |
30 | /**
31 | * Set new state and notify listeners, only callable from the main thread.
32 | */
33 | override fun setState(newState: S) {
34 | assertOnUiThread()
35 | performStateChange(newState)
36 | }
37 |
38 | override fun subscribe(hotStart: Boolean, fn: (S) -> Unit): Closeable {
39 | listeners.add(fn)
40 | if (hotStart) fn(state)
41 | return StoreSubscription(this, fn)
42 | }
43 |
44 | override val state: S
45 | get() {
46 | if (_state === StateContainer.Companion.NoState) {
47 | synchronized(this) {
48 | if (_state === StateContainer.Companion.NoState) {
49 | _state = initialState()
50 | }
51 | }
52 | }
53 | @Suppress("UNCHECKED_CAST")
54 | return _state as S
55 | }
56 |
57 | private fun performStateChange(newState: S) {
58 | //State mutation should to happen on UI thread
59 | if (_state != newState) {
60 | _state = newState
61 | listeners.forEach {
62 | it(newState)
63 | }
64 | }
65 | }
66 |
67 | override fun close() {
68 | listeners.clear() //Remove all listeners
69 | close()
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/StoreFlow.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import kotlinx.coroutines.channels.Channel
4 | import kotlinx.coroutines.flow.*
5 |
6 |
7 | /**
8 | * Map multiple objects into a list to run an effect on any change.
9 | */
10 | fun Flow.selectMany(vararg mappers: suspend (T) -> R): Flow> {
11 | return this.map { state ->
12 | mappers.map { fn -> fn(state) }
13 | }.distinctUntilChanged()
14 | }
15 |
16 | /**
17 | * Combination of [Flow.map] and [Flow.distinctUntilChanged].
18 | */
19 | fun Flow.select(mapper: suspend (T) -> R): Flow {
20 | return this.map { mapper(it) }
21 | .distinctUntilChanged()
22 | }
23 |
24 | /**
25 | * Combination of [Flow.map] and [Flow.distinctUntilChanged] ignoring null values.
26 | */
27 | fun Flow.selectNotNull(mapper: suspend (T) -> R?): Flow {
28 | return this.map { mapper(it) }
29 | .filterNotNull()
30 | .distinctUntilChanged()
31 | }
32 |
33 | /**
34 | * Emit a value when the filter passes comparing the last emited value and current value.
35 | */
36 | fun Flow.onEachChange(filter: (prev: T, next: T) -> Boolean, fn: (T) -> Unit): Flow {
37 | return distinctUntilChanged().runningReduce { prev, next ->
38 | if (filter(prev, next)) {
39 | fn(next)
40 | }
41 | next
42 | }
43 | }
44 |
45 | /**
46 | * Emit a value when the value goes `from` from to `to`.
47 | */
48 | fun Flow.onEachChange(from: T, to: T, fn: (T) -> Unit): Flow {
49 | return onEachChange({ prev, next -> prev == from && next == to }, fn)
50 | }
51 |
52 | /**
53 | * Emit when the value goes `true` from to `false` (it disables).
54 | */
55 | fun Flow.onEachDisable(fn: (Boolean) -> Unit): Flow {
56 | return onEachChange(from = true, to = false, fn)
57 | }
58 |
59 | /**
60 | * Emit when the value goes `false` from to `true` (it enables).
61 | */
62 | fun Flow.onEachEnable(fn: (Boolean) -> Unit): Flow {
63 | return onEachChange(from = false, to = true, fn)
64 | }
65 |
66 |
67 | /**
68 | * Return the channel that will emit state changes.
69 | *
70 | * @param hotStart emit current state when starting.
71 | */
72 | fun StateContainer.channel(hotStart: Boolean = true,
73 | capacity: Int = Channel.BUFFERED): Channel {
74 | val channel = Channel(capacity)
75 | val subscription = subscribe(hotStart) {
76 | channel.offer(it)
77 | }
78 | @Suppress("EXPERIMENTAL_API_USAGE")
79 | channel.invokeOnClose {
80 | subscription.close()
81 | }
82 | return channel
83 | }
84 |
85 | /**
86 | * Return the flow that will emit state changes.
87 | *
88 | * @param hotStart emit current state when starting.
89 | */
90 | fun StateContainer.flow(hotStart: Boolean = true, capacity: Int = Channel.BUFFERED): Flow {
91 | return channel(hotStart = hotStart, capacity = capacity).receiveAsFlow()
92 | }
93 |
--------------------------------------------------------------------------------
/mini-common/src/main/java/com/minikorp/mini/Threading.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import java.util.concurrent.Semaphore
6 |
7 | val uiHandler by lazy {
8 | requireAndroid()
9 | Handler(Looper.getMainLooper())
10 | }
11 |
12 | fun assertOnUiThread() {
13 | if (!isAndroid) return
14 | if (Looper.myLooper() != Looper.getMainLooper()) {
15 | error("This method can only be called from the main application thread")
16 | }
17 | }
18 |
19 | fun assertOnBgThread() {
20 | if (!isAndroid) return
21 | if (Looper.myLooper() == Looper.getMainLooper()) {
22 | error("This method can only be called from non UI threads")
23 | }
24 | }
25 |
26 | @JvmOverloads
27 | inline fun onUi(delayMs: Long = 0, crossinline block: () -> Unit) {
28 | requireAndroid()
29 | if (delayMs > 0) uiHandler.postDelayed({ block() }, delayMs)
30 | else uiHandler.post { block() }
31 | }
32 |
33 | inline fun onUiSync(crossinline block: () -> T) {
34 | uiHandler.postSync(block)
35 | }
36 |
37 | inline fun Handler.postSync(crossinline block: () -> T) {
38 | requireAndroid()
39 | if (Looper.myLooper() == this.looper) {
40 | block()
41 | } else {
42 | val sem = Semaphore(0)
43 | post {
44 | block()
45 | sem.release()
46 | }
47 | sem.acquireUninterruptibly()
48 | }
49 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/CompositeCloseableTest.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import org.amshove.kluent.`should be equal to`
4 | import org.junit.Test
5 | import java.io.Closeable
6 |
7 | class CompositeCloseableTest {
8 |
9 | @Test
10 | fun itemsAreClosed() {
11 | val c = CompositeCloseable()
12 | val dummyCloseable = DummyCloseable()
13 | c.add(dummyCloseable)
14 | c.close()
15 | c.close()
16 |
17 | dummyCloseable.closed.`should be equal to`(1)
18 | }
19 |
20 | class DummyCloseable : Closeable {
21 | var closed = 0
22 | override fun close() {
23 | closed++
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/DispatcherTest.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import org.amshove.kluent.`should be equal to`
4 | import org.junit.Test
5 |
6 | class DispatcherTest {
7 |
8 | @Test
9 | fun `subscriptions are added`() {
10 | val dispatcher = newTestDispatcher()
11 | var called = 0
12 | dispatcher.subscribe {
13 | called++
14 | }
15 | dispatcher.dispatchBlocking(TestAction())
16 | called `should be equal to` 1
17 | }
18 |
19 | @Test
20 | fun `order is respected for same priority`() {
21 | val dispatcher = newTestDispatcher()
22 | val calls = ArrayList()
23 | dispatcher.subscribe {
24 | calls.add(0)
25 | }
26 | dispatcher.subscribe {
27 | calls.add(1)
28 | }
29 | dispatcher.dispatchBlocking(TestAction())
30 | calls[0] `should be equal to` 0
31 | calls[1] `should be equal to` 1
32 | }
33 |
34 | @Test
35 | fun `order is respected for different priority`() {
36 | val dispatcher = newTestDispatcher()
37 | val calls = ArrayList()
38 | dispatcher.subscribe(priority = 10) {
39 | calls.add(0)
40 | }
41 | dispatcher.subscribe(priority = 0) {
42 | calls.add(1)
43 | }
44 | dispatcher.dispatchBlocking(TestAction())
45 | calls[0] `should be equal to` 1
46 | calls[1] `should be equal to` 0
47 | }
48 |
49 | @Test
50 | fun `disposing registration removes subscription`() {
51 | val dispatcher = newTestDispatcher()
52 | var called = 0
53 | dispatcher.subscribe {
54 | called++
55 | }.close()
56 | dispatcher.dispatchBlocking(TestAction())
57 | called `should be equal to` 0
58 | }
59 |
60 | @Test
61 | fun `interceptors are called`() {
62 | val dispatcher = newTestDispatcher()
63 | var called = 0
64 | val interceptor = object : Middleware {
65 | override suspend fun intercept(action: Any, chain: Chain): Any {
66 | called++
67 | return chain.proceed(action)
68 | }
69 | }
70 | dispatcher.addMiddleware(interceptor)
71 | dispatcher.dispatchBlocking(TestAction())
72 | called `should be equal to` 1
73 | }
74 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/LoggerMiddlewareTest.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import org.amshove.kluent.`should not be empty`
4 | import org.junit.Test
5 |
6 | class LoggerMiddlewareTest {
7 |
8 | @Test
9 | fun `logs are printed`() {
10 | val store = SampleStore()
11 | val dispatcher = newTestDispatcher()
12 | dispatcher.subscribe {
13 | store.setState("Action sent")
14 | }
15 |
16 | val out = StringBuilder()
17 | dispatcher.addMiddleware(LoggerMiddleware(listOf(store),
18 | logger = { priority, tag, msg ->
19 | println("[$priority][$tag] $msg")
20 | out.append(priority).append(tag).append(msg)
21 | }))
22 | dispatcher.dispatchBlocking(TestAction())
23 | out.toString().`should not be empty`()
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/ResourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import org.amshove.kluent.`should be equal to`
4 | import org.amshove.kluent.`should be null`
5 | import org.amshove.kluent.`should equal`
6 | import org.amshove.kluent.`should not be null`
7 | import org.junit.Before
8 | import org.junit.Test
9 |
10 | class ResourceTest {
11 |
12 | private var successValue: Any? = null
13 | private var errorValue: Any? = null
14 | private var loadingValue: Any? = null
15 | private var emptyValue: Any? = null
16 |
17 | @Before
18 | fun before() {
19 | successValue = null
20 | errorValue = null
21 | loadingValue = null
22 | emptyValue = null
23 | }
24 |
25 | private fun check(resource: Resource) {
26 | var called = 0
27 | resource
28 | .onSuccess {
29 | called++
30 | successValue = it
31 | }.onFailure {
32 | called++
33 | errorValue = it
34 | }.onLoading {
35 | called++
36 | loadingValue = it
37 | }.onEmpty {
38 | called++
39 | emptyValue = true
40 | }
41 | called `should be equal to` 1
42 | }
43 |
44 | @Test
45 | fun `success calls`() {
46 | check(Resource.success("abc"))
47 | successValue `should equal` "abc"
48 | }
49 |
50 | @Test
51 | fun isEmpty() {
52 | check(Resource.empty())
53 | emptyValue `should equal` true
54 | }
55 |
56 | @Test
57 | fun isFailure() {
58 | val ex = RuntimeException("ABC")
59 | check(Resource.failure(ex))
60 | errorValue `should equal` ex
61 | }
62 |
63 | @Test
64 | fun isLoading() {
65 | check(Resource.loading("abc"))
66 | loadingValue `should equal` "abc"
67 | }
68 |
69 | @Test
70 | fun getOrNull() {
71 | Resource.empty().getOrNull().`should be null`()
72 | Resource.success("abc").getOrNull().`should not be null`()
73 | }
74 |
75 | @Test
76 | fun exceptionOrNull() {
77 | Resource.failure(RuntimeException()).exceptionOrNull().`should not be null`()
78 | Resource.success("abc").exceptionOrNull().`should be null`()
79 | }
80 |
81 | @Test
82 | fun map() {
83 | Resource.success("abc")
84 | .map { 0 }
85 | .getOrNull()?.`should be equal to`(0)
86 |
87 | Resource.failure()
88 | .map { 0 }
89 | .getOrNull()?.`should be null`()
90 | }
91 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/SampleStore.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 |
4 | class SampleStore : Store() {
5 |
6 | companion object {
7 | const val INITIAL_STATE = "initial"
8 | }
9 |
10 | override fun initialState(): String = INITIAL_STATE
11 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/StoreFlowTest.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import kotlinx.coroutines.*
4 | import kotlinx.coroutines.flow.*
5 | import org.amshove.kluent.`should be equal to`
6 | import org.amshove.kluent.`should equal`
7 | import org.junit.Test
8 | import java.util.concurrent.Executors
9 |
10 | class StoreFlowTest {
11 |
12 | private val testScope =
13 | CoroutineScope(Executors.newScheduledThreadPool(1).asCoroutineDispatcher())
14 |
15 | @Test(timeout = 1000)
16 | fun `flow sends initial state on collection`(): Unit = runBlocking {
17 | val store = SampleStore()
18 | var observedState = SampleStore.INITIAL_STATE
19 |
20 | val job = store.flow(hotStart = false)
21 | .onEach { observedState = it }
22 | .take(1)
23 | .launchIn(testScope)
24 |
25 | store.setState("abc") //Set before collect
26 |
27 | job.join()
28 | observedState `should be equal to` "abc"
29 | Unit
30 | }
31 |
32 | @Test(timeout = 1000)
33 | fun `flow sends updates to all`(): Unit = runBlocking {
34 | val store = SampleStore()
35 | val called = intArrayOf(0, 0)
36 |
37 | val job1 = store.flow()
38 | .onEach { called[0]++ }
39 | .take(2)
40 | .launchIn(testScope)
41 |
42 | val job2 = store.flow()
43 | .onEach { called[1]++ }
44 | .take(2)
45 | .launchIn(testScope)
46 |
47 | store.setState("abc")
48 |
49 | job1.join()
50 | job2.join()
51 |
52 | //Called two times, on for initial state, one for updated stated
53 | called.`should equal`(intArrayOf(2, 2))
54 | Unit
55 | }
56 |
57 | @Test(timeout = 1000)
58 | fun `channel sends updates`(): Unit = runBlocking {
59 | val store = SampleStore()
60 | var observedState = ""
61 | val scope = CoroutineScope(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
62 | val job = scope.launch {
63 | observedState = store.channel().receive()
64 | }
65 | store.setState("abc")
66 | job.join()
67 | observedState `should be equal to` "abc"
68 | Unit
69 | }
70 |
71 | @Test(timeout = 1000)
72 | fun `flow closes`(): Unit = runBlocking {
73 | val store = SampleStore()
74 | var observedState = store.state
75 |
76 | val scope = CoroutineScope(Job())
77 | store.flow()
78 | .onEach {
79 | observedState = it
80 | }
81 | .launchIn(scope)
82 |
83 | scope.cancel() //Cancel the scope
84 | store.setState("abc")
85 |
86 | observedState `should be equal to` SampleStore.INITIAL_STATE
87 | Unit
88 | }
89 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/StoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import org.amshove.kluent.`should be equal to`
4 | import org.junit.Test
5 |
6 | class StoreTest {
7 |
8 | @Test
9 | fun `state is updated`() {
10 | val store = SampleStore()
11 | store.setState("abc")
12 | store.state `should be equal to` "abc"
13 | }
14 |
15 | @Test
16 | fun `observers are called`() {
17 | val store = SampleStore()
18 | var state = ""
19 | store.subscribe {
20 | state = it
21 | }
22 | store.setState("abc")
23 | state `should be equal to` "abc"
24 | }
25 |
26 | @Test
27 | fun `initial state is sent on subscribe`() {
28 | val store = SampleStore()
29 | var state = ""
30 | store.subscribe {
31 | state = it
32 | }
33 | state `should be equal to` "initial"
34 | }
35 |
36 | @Test
37 | fun `observers are removed on close`() {
38 | val store = SampleStore()
39 | var state = ""
40 | val closeable = store.subscribe(hotStart = false) {
41 | state = it
42 | }
43 | closeable.close()
44 | store.setState("abc")
45 | state `should be equal to` ""
46 | }
47 | }
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/TestAction.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 |
4 | @Action
5 | data class TestAction(val value: String = "dummy")
--------------------------------------------------------------------------------
/mini-common/src/test/kotlin/com/minikorp/mini/TestDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import com.minikorp.mini.Dispatcher
4 | import kotlin.coroutines.EmptyCoroutineContext
5 | import kotlin.reflect.KClass
6 | import kotlin.reflect.jvm.jvmErasure
7 |
8 | fun newTestDispatcher(): Dispatcher {
9 | return Dispatcher().apply {
10 | actionTypeMap = newReflectiveMap()
11 | }
12 | }
13 |
14 | private fun reflectActionTypes(type: KClass<*>, depth: Int = 0): List {
15 | return type.supertypes
16 | .asSequence()
17 | .map { (it.jvmErasure.java as Class<*>).kotlin }
18 | .map { reflectActionTypes(it, depth + 1) }
19 | .flatten()
20 | .plus(ReflectedType(type, depth))
21 | .toList()
22 | }
23 |
24 | private class ReflectedType(val clazz: KClass<*>, val depth: Int)
25 |
26 | private fun newReflectiveMap(): Map, List>> {
27 | return object : Map, List>> {
28 | private val genericTypes = listOf(Object::class)
29 | private val map = HashMap, List>>()
30 | override val entries: Set, List>>> = map.entries
31 | override val keys: Set> = map.keys
32 | override val size: Int = map.size
33 | override val values: Collection>> = map.values
34 | override fun containsKey(key: KClass<*>): Boolean = map.containsKey(key)
35 | override fun containsValue(value: List>): Boolean = map.containsValue(value)
36 | override fun isEmpty(): Boolean = map.isEmpty()
37 | override fun get(key: KClass<*>): List> {
38 | return map.getOrPut(key) {
39 | reflectActionTypes(key)
40 | .asSequence()
41 | .sortedBy { it.depth }
42 | .map { it.clazz }
43 | .filter { it !in genericTypes }
44 | .toList()
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/mini-processor-test/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/mini-processor-test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | kotlin("kapt")
4 | }
5 |
6 |
7 | repositories {
8 | mavenCentral()
9 | }
10 |
11 | dependencies {
12 | implementation(project(":mini-common"))
13 | kapt(project(":mini-processor"))
14 |
15 | testImplementation("junit:junit:4.12")
16 | testImplementation("org.amshove.kluent:kluent:1.44")
17 | }
18 |
19 | tasks {
20 | compileKotlin {
21 | kotlinOptions.jvmTarget = "1.8"
22 | }
23 | compileTestKotlin {
24 | kotlinOptions.jvmTarget = "1.8"
25 | }
26 | }
--------------------------------------------------------------------------------
/mini-processor-test/src/main/java/com/minikorp/mini/test/AnyAction.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.test
2 |
3 | import com.minikorp.mini.Action
4 |
5 | @Action
6 | data class AnyAction(val value: String)
--------------------------------------------------------------------------------
/mini-processor-test/src/main/java/com/minikorp/mini/test/BasicState.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.test
2 |
3 | data class BasicState(val value: String = "initial")
--------------------------------------------------------------------------------
/mini-processor-test/src/main/java/com/minikorp/mini/test/ReducersStore.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.test
2 |
3 | import com.minikorp.mini.Reducer
4 | import com.minikorp.mini.Store
5 | import kotlinx.coroutines.yield
6 |
7 |
8 | class ReducersStore : Store() {
9 |
10 | companion object {
11 | @Reducer
12 | fun staticImpureReducer(action: AnyAction) {
13 |
14 | }
15 |
16 | @Reducer
17 | suspend fun staticSuspendingImpureReducer(action: AnyAction) {
18 | yield()
19 | }
20 |
21 | @Reducer
22 | fun staticPureReducer(state: BasicState, action: AnyAction): BasicState {
23 | return state.copy(value = action.value)
24 | }
25 |
26 | @Reducer
27 | suspend fun staticSuspendingPureReducer(state: BasicState, action: AnyAction): BasicState {
28 | yield()
29 | return state.copy(value = action.value)
30 | }
31 | }
32 |
33 | @Reducer
34 | fun impureReducer(action: AnyAction) {
35 |
36 | }
37 |
38 | @Reducer
39 | suspend fun impureSuspendingReducer(action: AnyAction) {
40 | yield()
41 | }
42 |
43 | @Reducer
44 | fun pureReducer(state: BasicState, action: AnyAction): BasicState {
45 | return state.copy(value = action.value)
46 | }
47 |
48 | @Reducer
49 | suspend fun pureSuspendingReducer(state: BasicState, action: AnyAction): BasicState {
50 | yield()
51 | return state.copy(value = action.value)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/mini-processor-test/src/test/java/com/minikorp/mini/test/ReducersStoreTest.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini.test
2 |
3 | import com.minikorp.mini.Dispatcher
4 | import com.minikorp.mini.Mini
5 | import kotlinx.coroutines.runBlocking
6 | import org.amshove.kluent.`should equal`
7 | import org.junit.Test
8 |
9 |
10 | internal class ReducersStoreTest {
11 |
12 | private val store = ReducersStore()
13 | private val dispatcher = Dispatcher().apply {
14 | Mini.link(this, listOf(store))
15 | }
16 |
17 | @Test
18 | fun `pure reducers are called`() {
19 | runBlocking {
20 | dispatcher.dispatch(AnyAction("changed"))
21 | store.state.value.`should equal`("changed")
22 | }
23 | }
24 |
25 | @Test
26 | fun `pure static reducers are called`() {
27 | runBlocking {
28 | dispatcher.dispatch(AnyAction("changed"))
29 | store.state.value.`should equal`("changed")
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/mini-processor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/mini-processor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | }
4 |
5 | dependencies {
6 | implementation(project(":mini-common"))
7 | implementation("com.google.auto:auto-common:0.10")
8 | implementation("com.squareup:kotlinpoet:1.7.2")
9 | implementation("net.ltgt.gradle.incap:incap-processor:0.2")
10 | implementation("net.ltgt.gradle.incap:incap:0.2")
11 | testImplementation("junit:junit:4.13.1")
12 | testImplementation("com.google.testing.compile:compile-testing:0.15")
13 | }
--------------------------------------------------------------------------------
/mini-processor/src/main/java/com/minikorp/mini/ActionTypesGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import com.squareup.kotlinpoet.*
4 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
5 | import javax.lang.model.element.Element
6 | import javax.lang.model.element.Modifier
7 | import javax.lang.model.type.TypeMirror
8 | import kotlin.reflect.KClass
9 |
10 | object ActionTypesGenerator {
11 | fun generate(container: TypeSpec.Builder, elements: Set) {
12 |
13 | val actionModels = elements.filter { Modifier.ABSTRACT !in it.modifiers }
14 | .map { ActionModel(it) }
15 |
16 | container.apply {
17 | val anyClassTypeName = KClass::class.asTypeName().parameterizedBy(STAR)
18 | val listTypeName = List::class.asTypeName().parameterizedBy(anyClassTypeName)
19 | val mapType = Map::class
20 | .asClassName()
21 | .parameterizedBy(anyClassTypeName, listTypeName)
22 |
23 | val prop = PropertySpec.builder(Mini::actionTypes.name, mapType)
24 | .addModifiers(KModifier.OVERRIDE)
25 | .initializer(CodeBlock.builder()
26 | .add("mapOf(\n⇥")
27 | .apply {
28 | actionModels.forEach { actionModel ->
29 | val comma = if (actionModel != actionModels.last()) "," else ""
30 | add("«")
31 | add("%T::class to ", actionModel.typeName)
32 | add(actionModel.listOfSupertypesCodeBlock())
33 | add(comma)
34 | add("\n»")
35 | }
36 | }
37 | .add("⇤)")
38 | .build())
39 | addProperty(prop.build())
40 | }.build()
41 | }
42 | }
43 |
44 | class ActionModel(element: Element) {
45 | private val type = element.asType()
46 | private val javaObject = ClassName.bestGuess("java.lang.Object")
47 |
48 | val typeName = type.asTypeName()
49 | private val superTypes = collectTypes(type)
50 | .sortedBy { it.depth }
51 | .filter { it.typeName != javaObject }
52 | .map { it.typeName }
53 | .plus(ANY)
54 |
55 | fun listOfSupertypesCodeBlock(): CodeBlock {
56 | val format = superTypes.joinToString(",\n") { "%T::class" }
57 | val args = superTypes.toTypedArray()
58 | return CodeBlock.of("listOf($format)", *args)
59 | }
60 |
61 | private fun collectTypes(mirror: TypeMirror, depth: Int = 0): Set {
62 | //We want to add by depth
63 | val superTypes = typeUtils.directSupertypes(mirror).toSet()
64 | .map { collectTypes(it, depth + 1) }
65 | .flatten()
66 | return setOf(ActionSuperType(mirror.asTypeName(), depth)) + superTypes
67 | }
68 |
69 | class ActionSuperType(val typeName: TypeName, val depth: Int) {
70 | override fun equals(other: Any?): Boolean {
71 | if (this === other) return true
72 | if (javaClass != other?.javaClass) return false
73 | other as ActionSuperType
74 | if (typeName != other.typeName) return false
75 | return true
76 | }
77 |
78 | override fun hashCode(): Int {
79 | return typeName.hashCode()
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/mini-processor/src/main/java/com/minikorp/mini/MiniProcessor.java:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini;
2 |
3 | import net.ltgt.gradle.incap.IncrementalAnnotationProcessor;
4 | import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType;
5 |
6 | import java.util.Set;
7 |
8 | import javax.annotation.processing.AbstractProcessor;
9 | import javax.annotation.processing.ProcessingEnvironment;
10 | import javax.annotation.processing.RoundEnvironment;
11 | import javax.annotation.processing.SupportedOptions;
12 | import javax.lang.model.SourceVersion;
13 | import javax.lang.model.element.TypeElement;
14 |
15 | /**
16 | * Dummy Java wrapper that delegates to Kotlin one
17 | */
18 | @IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.AGGREGATING)
19 | @SupportedOptions("kapt.kotlin.generated")
20 | public class MiniProcessor extends AbstractProcessor {
21 |
22 | private final Processor processor = new Processor();
23 |
24 | @Override
25 | public synchronized void init(ProcessingEnvironment processingEnvironment) {
26 | super.init(processingEnvironment);
27 | processor.init(processingEnvironment);
28 | }
29 |
30 | @Override
31 | public Set getSupportedAnnotationTypes() {
32 | return processor.getSupportedAnnotationTypes();
33 | }
34 |
35 | @Override
36 | public SourceVersion getSupportedSourceVersion() {
37 | return processor.getSupportedSourceVersion();
38 | }
39 |
40 | @Override
41 | public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
42 | return processor.process(roundEnvironment);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/mini-processor/src/main/java/com/minikorp/mini/Processor.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import com.squareup.kotlinpoet.ClassName
4 | import com.squareup.kotlinpoet.FileSpec
5 | import com.squareup.kotlinpoet.TypeSpec
6 | import javax.annotation.processing.ProcessingEnvironment
7 | import javax.annotation.processing.RoundEnvironment
8 | import javax.lang.model.SourceVersion
9 |
10 | class Processor {
11 |
12 | val supportedAnnotationTypes: MutableSet = mutableSetOf(
13 | Reducer::class.java, Action::class.java)
14 | .map { it.canonicalName }.toMutableSet()
15 | val supportedSourceVersion: SourceVersion = SourceVersion.RELEASE_8
16 |
17 | fun init(environment: ProcessingEnvironment) {
18 | env = environment
19 | typeUtils = env.typeUtils
20 | elementUtils = env.elementUtils
21 | }
22 |
23 | fun process(roundEnv: RoundEnvironment): Boolean {
24 |
25 | val roundActions = roundEnv.getElementsAnnotatedWith(Action::class.java)
26 | val roundReducers = roundEnv.getElementsAnnotatedWith(Reducer::class.java)
27 |
28 | if (roundActions.isEmpty()) return false
29 |
30 | val className = ClassName.bestGuess(DISPATCHER_FACTORY_CLASS_NAME)
31 | val file = FileSpec.builder(className.packageName, className.simpleName)
32 | val container = TypeSpec.objectBuilder(className)
33 | .addKdoc("Automatically generated, do not edit.\n")
34 | .superclass(Mini::class)
35 |
36 | //Get non-abstract actions
37 | try {
38 | ActionTypesGenerator.generate(container, roundActions)
39 | ReducersGenerator.generate(container, roundReducers)
40 | } catch (e: Throwable) {
41 | if (e !is ProcessorException) {
42 | logError(
43 | "Compiler crashed, open an issue please!\n" +
44 | " ${e.stackTraceString()}"
45 | )
46 | }
47 | }
48 |
49 | file.addType(container.build())
50 | file.build().writeToFile(sourceElements = ((roundActions + roundReducers).toTypedArray()))
51 |
52 | return true
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/mini-processor/src/main/java/com/minikorp/mini/ProcessorUtils.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import com.squareup.kotlinpoet.ClassName
4 | import com.squareup.kotlinpoet.FileSpec
5 | import com.squareup.kotlinpoet.TypeName
6 | import com.squareup.kotlinpoet.asTypeName
7 | import java.io.ByteArrayOutputStream
8 | import java.io.PrintStream
9 | import javax.annotation.processing.ProcessingEnvironment
10 | import javax.lang.model.element.Element
11 | import javax.lang.model.element.ElementKind
12 | import javax.lang.model.element.ExecutableElement
13 | import javax.lang.model.element.TypeElement
14 | import javax.lang.model.type.DeclaredType
15 | import javax.lang.model.type.TypeMirror
16 | import javax.lang.model.util.Elements
17 | import javax.lang.model.util.Types
18 | import javax.tools.Diagnostic
19 | import javax.tools.StandardLocation
20 |
21 | lateinit var env: ProcessingEnvironment
22 | lateinit var elementUtils: Elements
23 | lateinit var typeUtils: Types
24 |
25 | fun Throwable.stackTraceString(): String {
26 | val out = ByteArrayOutputStream()
27 | printStackTrace(PrintStream(out))
28 | return out.toString()
29 | }
30 |
31 | val Element.isMethod: Boolean get() = this.kind == ElementKind.METHOD
32 | val Element.isClass: Boolean get() = this.kind == ElementKind.CLASS
33 |
34 | fun ExecutableElement.isSuspending(): Boolean {
35 | return parameters.last().asType().toString().startsWith("kotlin.coroutines.Continuation")
36 | }
37 |
38 | fun TypeMirror.asElement(): Element = asElementOrNull()!!
39 | fun TypeMirror.asElementOrNull(): Element? = typeUtils.asElement(this)
40 |
41 | fun TypeMirror.getSupertypes(): MutableList = typeUtils.directSupertypes(this)
42 | fun TypeMirror.getAllSuperTypes(depth: Int = 0): Set {
43 | //We want to add by depth
44 | val superTypes = typeUtils.directSupertypes(this).toSet()
45 | .map { it.getAllSuperTypes(depth + 1) }
46 | .flatten()
47 | return setOf(this) + superTypes
48 | }
49 |
50 | fun TypeMirror.qualifiedName(): String = toString()
51 | fun Element.qualifiedName(): String = toString()
52 | fun Element.getPackageName(): String {
53 | //xxx.xxx.simpleName
54 | //xxx.xxx
55 | val fullName = toString()
56 | val simpleNameLength = simpleName.toString().count() + 1 //Remove the last dot
57 | return fullName.dropLast(simpleNameLength)
58 | }
59 |
60 | infix fun TypeMirror.assignableTo(base: TypeMirror): Boolean = typeUtils.isAssignable(base, this)
61 | infix fun TypeMirror.isSubtypeOf(base: TypeMirror): Boolean = typeUtils.isSubtype(this, base)
62 |
63 | fun Element.asTypeElement(): TypeElement = asTypeElementOrNull()!!
64 | fun Element.asTypeElementOrNull(): TypeElement? = this as? TypeElement
65 |
66 | fun TypeMirror.asTypeElement(): TypeElement = asTypeElementOrNull()!!
67 | fun TypeMirror.asTypeElementOrNull(): TypeElement? = asElementOrNull()?.asTypeElementOrNull()
68 |
69 | fun TypeMirror.asDeclaredType(): DeclaredType = asDeclaredTypeOrNull()!!
70 | fun TypeMirror.asDeclaredTypeOrNull(): DeclaredType? = this as? DeclaredType
71 |
72 | infix fun TypeMirror.isSameType(other: TypeMirror?): Boolean {
73 | if (other == null) return false
74 | return typeUtils.isSameType(this, other)
75 | }
76 |
77 | fun Element.getSuperClass() = asTypeElement().superclass.asElement()
78 | fun Element.getSuperClassTypeParameter(position: Int) = asTypeElement()
79 | .superclass.asDeclaredType().typeArguments[position].asElement()
80 |
81 |
82 | class ProcessorException : IllegalStateException()
83 |
84 | fun compilePrecondition(check: Boolean,
85 | message: String,
86 | element: Element? = null) {
87 | if (!check) {
88 | logError(message, element)
89 | throw ProcessorException()
90 | }
91 | }
92 |
93 | fun logError(message: String, element: Element? = null) {
94 | logMessage(Diagnostic.Kind.ERROR, message, element)
95 | }
96 |
97 | fun logWarning(message: String, element: Element? = null) {
98 | logMessage(Diagnostic.Kind.MANDATORY_WARNING, message, element)
99 | }
100 |
101 | fun logMessage(kind: Diagnostic.Kind, message: String, element: Element? = null) {
102 | env.messager.printMessage(kind, "\n" + message, element)
103 | }
104 |
105 | //KotlinPoet utils
106 |
107 | fun FileSpec.writeToFile(vararg sourceElements: Element) {
108 | val kotlinFileObject = env.filer
109 | .createResource(StandardLocation.SOURCE_OUTPUT, packageName, "$name.kt", *sourceElements)
110 | val openWriter = kotlinFileObject.openWriter()
111 | writeTo(openWriter)
112 | openWriter.close()
113 | }
114 |
115 | /**
116 | * Map [java.lang.Object] to [Any]
117 | */
118 | fun TypeName.safeAnyTypeName(): TypeName {
119 | return if (this is ClassName && this == ClassName("java.lang", "Object")) {
120 | Any::class.asTypeName()
121 | } else {
122 | this
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/mini-processor/src/main/java/com/minikorp/mini/ReducersGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.minikorp.mini
2 |
3 | import com.squareup.kotlinpoet.*
4 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
5 | import java.io.Closeable
6 | import javax.lang.model.element.Element
7 | import javax.lang.model.element.ElementKind
8 | import javax.lang.model.element.ExecutableElement
9 | import javax.lang.model.element.Modifier
10 | import javax.lang.model.type.DeclaredType
11 |
12 | object ReducersGenerator {
13 |
14 | fun generate(container: TypeSpec.Builder, elements: Set) {
15 | val reducers = elements.map { ReducerModel(it) }
16 | .groupBy { it.container.typeName }
17 |
18 | val whenBlock = CodeBlock.builder()
19 | .addStatement("val c = %T()", CompositeCloseable::class)
20 | .addStatement("when (container) {").indent()
21 | .apply {
22 | reducers.forEach { (containerName, reducerFunctions) ->
23 | addStatement("is %T -> {", containerName).indent()
24 | reducerFunctions.forEach { function ->
25 | add("c.add(dispatcher.subscribe<%T>(priority=%L) { action -> ",
26 | function.actionTypeName,
27 | function.priority
28 | )
29 | add(function.generateCallBlock("container", "action"))
30 | addStatement("})")
31 | }
32 | unindent().addStatement("}")
33 | }
34 | }
35 | .unindent()
36 | .addStatement("}") //Close when
37 | .addStatement("return c")
38 | .build()
39 |
40 | val typeParam = TypeVariableName("T")
41 | val oneParam = StateContainer::class.asTypeName().parameterizedBy(typeParam)
42 |
43 | val registerOneFn = FunSpec.builder("subscribe")
44 | .addModifiers(KModifier.OVERRIDE)
45 | .addTypeVariable(typeParam)
46 | .addParameter("dispatcher", Dispatcher::class)
47 | .addParameter("container", oneParam)
48 | .returns(Closeable::class)
49 | .addCode(whenBlock)
50 | .build()
51 |
52 | container.addFunction(registerOneFn)
53 | }
54 | }
55 |
56 | class ReducerModel(element: Element) {
57 | private val function = element as ExecutableElement
58 |
59 | private val isPure: Boolean
60 | private val isSuspending: Boolean
61 |
62 | val container: ContainerModel
63 | val priority = element.getAnnotation(Reducer::class.java).priority
64 |
65 | val actionTypeName: TypeName
66 | val returnTypeName: TypeName
67 |
68 | init {
69 | compilePrecondition(
70 | check = function.modifiers.contains(Modifier.PUBLIC),
71 | message = "Reducer functions must be public.",
72 | element = element
73 | )
74 |
75 | isSuspending = function.isSuspending()
76 | container = ContainerModel(element.enclosingElement)
77 | val parameters: List
78 |
79 | if (isSuspending) {
80 | //Hacky check to get return type of a kotlin continuation
81 | val continuationTypeParameter = (function.parameters.last().asType() as DeclaredType)
82 | .typeArguments[0].asTypeName() as WildcardTypeName
83 | returnTypeName = continuationTypeParameter.inTypes.first()
84 | parameters = function.parameters.dropLast(1).map { it.asType().asTypeName() }
85 | } else {
86 | returnTypeName = function.returnType.asTypeName()
87 | parameters = function.parameters.map { it.asType().asTypeName() }
88 | }
89 |
90 | if (returnTypeName == UNIT) {
91 | isPure = false
92 | actionTypeName = function.parameters[0].asType().asTypeName().safeAnyTypeName()
93 | compilePrecondition(
94 | check = parameters.size == 1,
95 | message = "Expected exactly one action parameter",
96 | element = element
97 | )
98 | } else {
99 | isPure = true
100 | compilePrecondition(
101 | check = parameters.size == 2,
102 | message = "Expected exactly two parameters, ${container.stateTypeName} and action",
103 | element = element
104 | )
105 | val stateTypeName = parameters[0]
106 | actionTypeName = parameters[1].safeAnyTypeName()
107 |
108 | compilePrecondition(
109 | check = stateTypeName == container.stateTypeName,
110 | message = "Expected ${container.stateTypeName} as first state parameter",
111 | element = element
112 | )
113 |
114 | compilePrecondition(
115 | check = returnTypeName == container.stateTypeName,
116 | message = "Expected ${container.stateTypeName} as return value",
117 | element = element
118 | )
119 | }
120 |
121 | }
122 |
123 | fun generateCallBlock(containerParam: String, actionParam: String): CodeBlock {
124 |
125 | val receiver = if (container.isStatic) {
126 | CodeBlock.of("%T.${function.simpleName}", container.typeName)
127 | } else {
128 | CodeBlock.of("${containerParam}.${function.simpleName}")
129 | }
130 |
131 | val call = if (isPure) {
132 | CodeBlock.of("(${containerParam}.state, $actionParam)")
133 | } else {
134 | CodeBlock.of("($actionParam)")
135 | }
136 |
137 | return if (isPure) {
138 | CodeBlock.builder()
139 | .add("${containerParam}.setState(")
140 | .add(receiver)
141 | .add(call)
142 | .add(")")
143 | .build()
144 | } else {
145 | CodeBlock.builder()
146 | .add(receiver)
147 | .add(call)
148 | .build()
149 | }
150 | }
151 | }
152 |
153 | class ContainerModel(element: Element) {
154 | val typeName: TypeName
155 | val stateTypeName: TypeName
156 | val isStatic: Boolean
157 |
158 | init {
159 | compilePrecondition(
160 | check = element.kind == ElementKind.CLASS,
161 | message = "Reducers must be declared inside StoreContainer classes",
162 | element = element
163 | )
164 |
165 | val mainTypeName = element.asType().asTypeName()
166 | val parent = element.enclosingElement
167 |
168 | isStatic = if (parent != null && parent.kind == ElementKind.CLASS) {
169 | val parentTypeName = parent.asType().asTypeName()
170 | "$parentTypeName.Companion" == mainTypeName.toString()
171 | } else {
172 | false
173 | }
174 |
175 | val realContainer = if (isStatic) element.enclosingElement else element
176 | typeName = realContainer.asType().asTypeName()
177 |
178 | val superTypes = realContainer.asType().getAllSuperTypes().map { it.asTypeName() }
179 | val stateContainerType = superTypes
180 | .find { it is ParameterizedTypeName && it.rawType == StateContainer::class.asTypeName() }
181 | compilePrecondition(
182 | check = stateContainerType != null,
183 | message = "Reducers must be declared in a StateContainer",
184 | element = element
185 | )
186 |
187 | stateTypeName = (stateContainerType!! as ParameterizedTypeName).typeArguments[0]
188 | }
189 |
190 | override fun toString(): String {
191 | return stateTypeName.toString()
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/mini-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors:
--------------------------------------------------------------------------------
1 | com.minikorp.mini.MiniProcessor,aggregating
--------------------------------------------------------------------------------
/mini-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor:
--------------------------------------------------------------------------------
1 | com.minikorp.mini.MiniProcessor
--------------------------------------------------------------------------------
/mini.codestyle.json:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | xmlns:android
21 |
22 | ^$
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | xmlns:.*
32 |
33 | ^$
34 |
35 |
36 | BY_NAME
37 |
38 |
39 |
40 |
41 |
42 |
43 | .*:id
44 |
45 | http://schemas.android.com/apk/res/android
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | .*:name
55 |
56 | http://schemas.android.com/apk/res/android
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | name
66 |
67 | ^$
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | style
77 |
78 | ^$
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | .*
88 |
89 | ^$
90 |
91 |
92 | BY_NAME
93 |
94 |
95 |
96 |
97 |
98 |
99 | .*
100 |
101 | http://schemas.android.com/apk/res/android
102 |
103 |
104 | ANDROID_ATTRIBUTE_ORDER
105 |
106 |
107 |
108 |
109 |
110 |
111 | .*
112 |
113 | .*
114 |
115 |
116 | BY_NAME
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | compileSdkVersion 30
9 | buildToolsVersion "30.0.3"
10 |
11 | defaultConfig {
12 | applicationId "com.example.androidsample"
13 | minSdkVersion 16
14 | targetSdkVersion 30
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | }
35 |
36 | dependencies {
37 |
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.21"
39 | implementation 'androidx.core:core-ktx:1.3.2'
40 | implementation 'androidx.appcompat:appcompat:1.2.0'
41 | implementation 'com.google.android.material:material:1.2.1'
42 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
43 | implementation "androidx.activity:activity-ktx:1.1.0"
44 | implementation "androidx.fragment:fragment-ktx:1.2.5"
45 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
46 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
47 | kapt project(":mini-processor")
48 | implementation project(":mini-android")
49 |
50 | testImplementation 'junit:junit:4.13.1'
51 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01'
52 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
53 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
54 | }
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/example/androidsample/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.androidsample
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.androidsample", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/example/androidsample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.androidsample
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.widget.ProgressBar
6 | import android.widget.TextView
7 | import android.widget.Toast
8 | import androidx.activity.viewModels
9 | import androidx.lifecycle.SavedStateHandle
10 | import com.minikorp.mini.*
11 | import com.minikorp.mini.android.FluxActivity
12 | import com.minikorp.mini.android.FluxViewModel
13 | import kotlinx.coroutines.delay
14 | import kotlinx.coroutines.flow.onEach
15 | import kotlinx.coroutines.launch
16 | import java.io.Closeable
17 | import java.io.Serializable
18 |
19 | val dispatcher = Dispatcher()
20 |
21 | data class State(
22 | val text: String = "0",
23 | val loading: Boolean = false
24 | ) : Serializable
25 |
26 | @Action
27 | data class SetLoadingAction(val loading: Boolean)
28 |
29 | @Action
30 | data class SetTextAction(val text: String)
31 |
32 | @Action
33 | interface AnalyticsAction
34 |
35 | @Action
36 | class LongUseCaseAction(val userName: String) : AnalyticsAction, SuspendingAction
37 |
38 |
39 | /**
40 | * Use any name you like for suspending actions, or use reducer
41 | */
42 | typealias UseCase = Reducer
43 |
44 |
45 | class MainStore : Store() {
46 |
47 | init {
48 | Mini.link(dispatcher, this).track()
49 | }
50 |
51 | @Reducer
52 | fun handleLoading(state: State, action: SetLoadingAction): State {
53 | return state.copy(loading = action.loading)
54 | }
55 |
56 |
57 | @Reducer
58 | fun handleSetTextAction(state: State, action: SetTextAction): State {
59 | return state.copy(text = action.text)
60 | }
61 |
62 | @Reducer
63 | fun handleAnalyticsAction(action: AnalyticsAction) {
64 | //Log to analytics
65 | }
66 |
67 | @Reducer
68 | fun handleAnyAction(action: Any) {
69 | //Log to analytics
70 | }
71 |
72 | @UseCase
73 | suspend fun useCase(s: LongUseCaseAction) {
74 | if (state.loading) return
75 | dispatcher.dispatch(SetLoadingAction(true))
76 | dispatcher.dispatch(SetTextAction("Loading from network..."))
77 | delay(5000)
78 | dispatcher.dispatch(SetTextAction("Hello From UseCase"))
79 | dispatcher.dispatch(SetLoadingAction(false))
80 | }
81 | }
82 |
83 | class MainViewModelReducer : NestedStateContainer() {
84 |
85 | @Reducer
86 | fun handleLoading(state: State, action: SetLoadingAction): State {
87 | return state.copy(loading = action.loading)
88 | }
89 |
90 | @Reducer
91 | fun handleSetTextAction(state: State, action: SetTextAction): State {
92 | return state.copy(text = action.text)
93 | }
94 | }
95 |
96 | class MainViewModel(savedStateHandle: SavedStateHandle) : FluxViewModel(savedStateHandle) {
97 | private val reducerSlice = MainViewModelReducer().apply { parent = this }
98 |
99 | init {
100 | Mini.link(dispatcher, listOf(this, reducerSlice)).track()
101 | }
102 |
103 | override fun saveState(state: State, handle: SavedStateHandle) {
104 | println("State saved")
105 | handle.set("state", state)
106 | }
107 |
108 | override fun restoreState(handle: SavedStateHandle): State? {
109 | val restored = handle.get("state")
110 | println("State restored $restored")
111 | return restored
112 | }
113 |
114 | @UseCase
115 | suspend fun useCase(action: LongUseCaseAction) {
116 | if (state.loading) return
117 | dispatcher.dispatch(SetLoadingAction(true))
118 | delay(2000)
119 | dispatcher.dispatch(SetTextAction("${state.text.toInt() + 1}"))
120 | dispatcher.dispatch(SetLoadingAction(false))
121 | }
122 | }
123 |
124 | class MainActivity : FluxActivity() {
125 |
126 | lateinit var textView: TextView
127 | lateinit var progressBar: ProgressBar
128 | private val vm: MainViewModel by viewModels()
129 |
130 | override suspend fun whenCreated(savedInstanceState: Bundle?) {
131 | setContentView(R.layout.activity_main)
132 | textView = findViewById(R.id.textView)
133 | progressBar = findViewById(R.id.progressBar)
134 |
135 | textView.setOnClickListener {
136 | launch {
137 | dispatcher.dispatch(LongUseCaseAction("Pablo"))
138 | //Decide on the state after usecase is done
139 | //I won't run until use case is done
140 | }
141 | }
142 |
143 | vm.flow().onEach {
144 | textView.text = it.toString()
145 | progressBar.visibility = if (it.loading) View.VISIBLE else View.INVISIBLE
146 | }.launchInLifecycleScope()
147 |
148 | vm.flow()
149 | .select { it.loading }
150 | .onEachDisable {
151 | Toast.makeText(this@MainActivity, "Finished loading", Toast.LENGTH_SHORT).show()
152 | }.launchInLifecycleScope()
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
27 |
28 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minikorp/mini/0ad3eb7c261050562ed049d92c9e42477b28ed7d/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AndroidSample
3 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/sample/src/test/java/com/example/androidsample/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.androidsample
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/scripts/bump-tag.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd "$(dirname "$0")" || exit
4 | ls
5 | pwd
6 | latest_tag=$(./latest-version.sh)
7 | new_tag=$(./semver.sh bump "$1" "$latest_tag")
8 | echo "$latest_tag -> $new_tag"
9 | git tag "$new_tag"
10 |
--------------------------------------------------------------------------------
/scripts/latest-version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | tags=$(git tag --sort=-version:refname)
4 | latest_tag=$(echo "$tags" | head -1)
5 | echo "$latest_tag"
6 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | require_clean_work_tree() {
4 | # Update the index
5 | git update-index -q --ignore-submodules --refresh
6 | err=0
7 |
8 | # Disallow unstaged changes in the working tree
9 | if ! git diff-files --quiet --ignore-submodules --; then
10 | echo >&2 "cannot $1: you have unstaged changes."
11 | git diff-files --name-status -r --ignore-submodules -- >&2
12 | err=1
13 | fi
14 |
15 | # Disallow uncommitted changes in the index
16 | if ! git diff-index --cached --quiet HEAD --ignore-submodules --; then
17 | echo >&2 "cannot $1: your index contains uncommitted changes."
18 | git diff-index --cached --name-status -r --ignore-submodules HEAD -- >&2
19 | err=1
20 | fi
21 |
22 | if [ "$err" -eq "1" ]; then
23 | echo >&2 "Please commit or stash them."
24 | exit 1
25 | fi
26 | }
27 |
28 | cd "$(dirname "$0")" || exit
29 |
30 | # Ensure tags are up to date
31 | require_clean_work_tree "Generate release"
32 | git pull
33 | git pull --tags
34 | require_clean_work_tree "Generate release"
35 |
36 | cd .. # move to project root
37 | ./gradlew clean build test || exit 1
38 |
39 | # bump version and push
40 | cd "$(dirname "$0")" || exit
41 | ./bump-tag.sh patch
42 | git push --tags
43 | git push
44 |
--------------------------------------------------------------------------------
/scripts/semver.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Orignal author fsaintjacques: https://github.com/fsaintjacques/semver-tool
4 |
5 | set -o errexit -o nounset -o pipefail
6 |
7 | NAT='0|[1-9][0-9]*'
8 | ALPHANUM='[0-9]*[A-Za-z-][0-9A-Za-z-]*'
9 | IDENT="$NAT|$ALPHANUM"
10 | FIELD='[0-9A-Za-z-]+'
11 |
12 | SEMVER_REGEX="\
13 | ^[vV]?\
14 | ($NAT)\\.($NAT)\\.($NAT)\
15 | (\\-(${IDENT})(\\.(${IDENT}))*)?\
16 | (\\+${FIELD}(\\.${FIELD})*)?$"
17 |
18 | PROG=semver
19 | PROG_VERSION="3.0.0"
20 |
21 | USAGE="\
22 | Usage:
23 | $PROG bump (major|minor|patch|release|prerel |build )
24 | $PROG compare
25 | $PROG get (major|minor|patch|release|prerel|build)
26 | $PROG --help
27 | $PROG --version
28 |
29 | Arguments:
30 | A version must match the following regular expression:
31 | \"${SEMVER_REGEX}\"
32 | In English:
33 | -- The version must match X.Y.Z[-PRERELEASE][+BUILD]
34 | where X, Y and Z are non-negative integers.
35 | -- PRERELEASE is a dot separated sequence of non-negative integers and/or
36 | identifiers composed of alphanumeric characters and hyphens (with
37 | at least one non-digit). Numeric identifiers must not have leading
38 | zeros. A hyphen (\"-\") introduces this optional part.
39 | -- BUILD is a dot separated sequence of identifiers composed of alphanumeric
40 | characters and hyphens. A plus (\"+\") introduces this optional part.
41 |
42 | See definition.
43 |
44 | A string as defined by PRERELEASE above.
45 |
46 | A string as defined by BUILD above.
47 |
48 | Options:
49 | -v, --version Print the version of this tool.
50 | -h, --help Print this help message.
51 |
52 | Commands:
53 | bump Bump by one of major, minor, patch; zeroing or removing
54 | subsequent parts. \"bump prerel\" sets the PRERELEASE part and
55 | removes any BUILD part. \"bump build\" sets the BUILD part.
56 | \"bump release\" removes any PRERELEASE or BUILD parts.
57 | The bumped version is written to stdout.
58 |
59 | compare Compare with , output to stdout the
60 | following values: -1 if is newer, 0 if equal, 1 if
61 | older. The BUILD part is not used in comparisons.
62 |
63 | get Extract given part of , where part is one of major, minor,
64 | patch, prerel, build, or release.
65 |
66 | See also:
67 | https://semver.org -- Semantic Versioning 2.0.0"
68 |
69 | function error() {
70 | echo -e "$1" >&2
71 | exit 1
72 | }
73 |
74 | function usage-help() {
75 | error "$USAGE"
76 | }
77 |
78 | function usage-version() {
79 | echo -e "${PROG}: $PROG_VERSION"
80 | exit 0
81 | }
82 |
83 | function validate-version() {
84 | local version=$1
85 | if [[ "$version" =~ $SEMVER_REGEX ]]; then
86 | # if a second argument is passed, store the result in var named by $2
87 | if [ "$#" -eq "2" ]; then
88 | local major=${BASH_REMATCH[1]}
89 | local minor=${BASH_REMATCH[2]}
90 | local patch=${BASH_REMATCH[3]}
91 | local prere=${BASH_REMATCH[4]}
92 | local build=${BASH_REMATCH[8]}
93 | eval "$2=(\"$major\" \"$minor\" \"$patch\" \"$prere\" \"$build\")"
94 | else
95 | echo "$version"
96 | fi
97 | else
98 | error "version $version does not match the semver scheme 'X.Y.Z(-PRERELEASE)(+BUILD)'. See help for more information."
99 | fi
100 | }
101 |
102 | function is-nat() {
103 | [[ "$1" =~ ^($NAT)$ ]]
104 | }
105 |
106 | function is-null() {
107 | [ -z "$1" ]
108 | }
109 |
110 | function order-nat() {
111 | [ "$1" -lt "$2" ] && {
112 | echo -1
113 | return
114 | }
115 | [ "$1" -gt "$2" ] && {
116 | echo 1
117 | return
118 | }
119 | echo 0
120 | }
121 |
122 | function order-string() {
123 | [[ $1 < $2 ]] && {
124 | echo -1
125 | return
126 | }
127 | [[ $1 > $2 ]] && {
128 | echo 1
129 | return
130 | }
131 | echo 0
132 | }
133 |
134 | # given two (named) arrays containing NAT and/or ALPHANUM fields, compare them
135 | # one by one according to semver 2.0.0 spec. Return -1, 0, 1 if left array ($1)
136 | # is less-than, equal, or greater-than the right array ($2). The longer array
137 | # is considered greater-than the shorter if the shorter is a prefix of the longer.
138 | #
139 | function compare-fields() {
140 | local l="$1[@]"
141 | local r="$2[@]"
142 | local leftfield=("${!l}")
143 | local rightfield=("${!r}")
144 | local left
145 | local right
146 |
147 | local i=$((-1))
148 | local order=$((0))
149 |
150 | while true; do
151 | [ $order -ne 0 ] && {
152 | echo $order
153 | return
154 | }
155 |
156 | : $((i++))
157 | left="${leftfield[$i]}"
158 | right="${rightfield[$i]}"
159 |
160 | is-null "$left" && is-null "$right" && {
161 | echo 0
162 | return
163 | }
164 | is-null "$left" && {
165 | echo -1
166 | return
167 | }
168 | is-null "$right" && {
169 | echo 1
170 | return
171 | }
172 |
173 | is-nat "$left" && is-nat "$right" && {
174 | order=$(order-nat "$left" "$right")
175 | continue
176 | }
177 | is-nat "$left" && {
178 | echo -1
179 | return
180 | }
181 | is-nat "$right" && {
182 | echo 1
183 | return
184 | }
185 | {
186 | order=$(order-string "$left" "$right")
187 | continue
188 | }
189 | done
190 | }
191 |
192 | # shellcheck disable=SC2206 # checked by "validate"; ok to expand prerel id's into array
193 | function compare-version() {
194 | local order
195 | validate-version "$1" V
196 | validate-version "$2" V_
197 |
198 | # compare major, minor, patch
199 |
200 | local left=("${V[0]}" "${V[1]}" "${V[2]}")
201 | local right=("${V_[0]}" "${V_[1]}" "${V_[2]}")
202 |
203 | order=$(compare-fields left right)
204 | [ "$order" -ne 0 ] && {
205 | echo "$order"
206 | return
207 | }
208 |
209 | # compare pre-release ids when M.m.p are equal
210 |
211 | local prerel="${V[3]:1}"
212 | local prerel_="${V_[3]:1}"
213 | local left=(${prerel//./ })
214 | local right=(${prerel_//./ })
215 |
216 | # if left and right have no pre-release part, then left equals right
217 | # if only one of left/right has pre-release part, that one is less than simple M.m.p
218 |
219 | [ -z "$prerel" ] && [ -z "$prerel_" ] && {
220 | echo 0
221 | return
222 | }
223 | [ -z "$prerel" ] && {
224 | echo 1
225 | return
226 | }
227 | [ -z "$prerel_" ] && {
228 | echo -1
229 | return
230 | }
231 |
232 | # otherwise, compare the pre-release id's
233 |
234 | compare-fields left right
235 | }
236 |
237 | function command-bump() {
238 | local new
239 | local version
240 | local sub_version
241 | local command
242 |
243 | case $# in
244 | 2) case $1 in
245 | major | minor | patch | release)
246 | command=$1
247 | version=$2
248 | ;;
249 | *) usage-help ;;
250 | esac ;;
251 | 3) case $1 in
252 | prerel | build)
253 | command=$1
254 | sub_version=$2 version=$3
255 | ;;
256 | *) usage-help ;;
257 | esac ;;
258 | *) usage-help ;;
259 | esac
260 |
261 | validate-version "$version" parts
262 | # shellcheck disable=SC2154
263 | local major="${parts[0]}"
264 | local minor="${parts[1]}"
265 | local patch="${parts[2]}"
266 | local prere="${parts[3]}"
267 | local build="${parts[4]}"
268 |
269 | case "$command" in
270 | major) new="$((major + 1)).0.0" ;;
271 | minor) new="${major}.$((minor + 1)).0" ;;
272 | patch) new="${major}.${minor}.$((patch + 1))" ;;
273 | release) new="${major}.${minor}.${patch}" ;;
274 | prerel) new=$(validate-version "${major}.${minor}.${patch}-${sub_version}") ;;
275 | build) new=$(validate-version "${major}.${minor}.${patch}${prere}+${sub_version}") ;;
276 | *) usage-help ;;
277 | esac
278 |
279 | echo "$new"
280 | exit 0
281 | }
282 |
283 | function command-compare() {
284 | local v
285 | local v_
286 |
287 | case $# in
288 | 2)
289 | v=$(validate-version "$1")
290 | v_=$(validate-version "$2")
291 | ;;
292 | *) usage-help ;;
293 | esac
294 |
295 | set +u # need unset array element to evaluate to null
296 | compare-version "$v" "$v_"
297 | exit 0
298 | }
299 |
300 | # shellcheck disable=SC2034
301 | function command-get() {
302 | local part version
303 |
304 | if [[ "$#" -ne "2" ]] || [[ -z "$1" ]] || [[ -z "$2" ]]; then
305 | usage-help
306 | exit 0
307 | fi
308 |
309 | part="$1"
310 | version="$2"
311 |
312 | validate-version "$version" parts
313 | local major="${parts[0]}"
314 | local minor="${parts[1]}"
315 | local patch="${parts[2]}"
316 | local prerel="${parts[3]:1}"
317 | local build="${parts[4]:1}"
318 | local release="${major}.${minor}.${patch}"
319 |
320 | case "$part" in
321 | major | minor | patch | release | prerel | build) echo "${!part}" ;;
322 | *) usage-help ;;
323 | esac
324 |
325 | exit 0
326 | }
327 |
328 | case $# in
329 | 0)
330 | echo "Unknown command: $*"
331 | usage-help
332 | ;;
333 | esac
334 |
335 | case $1 in
336 | --help | -h)
337 | echo -e "$USAGE"
338 | exit 0
339 | ;;
340 | --version | -v) usage-version ;;
341 | bump)
342 | shift
343 | command-bump "$@"
344 | ;;
345 | get)
346 | shift
347 | command-get "$@"
348 | ;;
349 | compare)
350 | shift
351 | command-compare "$@"
352 | ;;
353 | *)
354 | echo "Unknown arguments: $*"
355 | usage-help
356 | ;;
357 | esac
358 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':sample'
2 | include ':mini-processor', ':mini-common', ':mini-android', ':mini-processor-test'
3 |
--------------------------------------------------------------------------------