Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1d0489b
refactor: Introduce MiniRegistry interface and refactor Mini class to…
finxo May 22, 2026
8215507
feat: Add KSP test module with reducer-only test support
finxo May 27, 2026
587cac4
feat: Implement multi-registry support for Mini processor with new te…
finxo Jun 1, 2026
55e973b
feat: Add multi-registry sample with separate feature modules
finxo Jun 1, 2026
89dcedb
refactor: Require explicit MiniRegistry in link functions and remove …
finxo Jun 8, 2026
f0ca7ef
refactor: Migrate to module-local bootstrap and require explicit regi…
finxo Jun 8, 2026
e1f9c96
refactor: Remove Timber dependency and replace with standard Android Log
finxo Jun 8, 2026
f069ef3
chore: Update copyright headers to 2026
finxo Jun 8, 2026
13683b0
refactor: Change generated registry naming to use package segments in…
finxo Jun 8, 2026
ddbf316
refactor: Migrate sample-counter-feature from KAPT to KSP
finxo Jun 8, 2026
8429e96
docs: Update README to clarify multi-registry examples and add test c…
finxo Jun 8, 2026
0340926
refactor: Simplify multi-registry sample and isolate message feature …
finxo Jun 8, 2026
1134731
feat: Integrate isolated consumer message feature into the sample app…
finxo Jun 8, 2026
1335d9e
refactor: Simplify generated registry class naming and remove package…
finxo Jun 15, 2026
6ffc356
Fix: Mini.kt
finxo Jun 15, 2026
d2d5808
Fix: Processor.kt
finxo Jun 15, 2026
88b8ffd
Fix: ContainerBuilders.kt
finxo Jun 15, 2026
c558103
Fix: README.md
finxo Jun 16, 2026
cc962cc
Fix: README.md
finxo Jun 16, 2026
99a904f
Fix: ContainerBuilders.kt
finxo Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
.idea/deploymentTargetSelector.xml
.idea/migrations.xml
.idea/other.xml
.idea/AndroidProjectSystem.xml
.idea/deviceManager.xml
.idea/markdown.xml

# mpeltonen/sbt-idea plugin
.idea_modules/
Expand Down
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,33 +257,63 @@ Given the example `Store`s and `Action`s explained before, the workflow would be
- The Store changes its state to the given values from `LoginCompletedAction`.
- The view will react (for example, redirecting to another home view) if the task was success or shows an error if not.

You can execute another sample in the `app` package. It contains two different samples executing two types of `StateContainer`s:
You can execute the sample in the `app` package. It contains two different samples executing two types of `StateContainer`s:
- `StoreSampleActivity` class uses a `Store` as a `StateContainer`.
- `ViewModelSampleActivity` class uses a `ViewModel` as a `StateContainer`.
- `CounterFeatureSampleActivity` class shows a feature module running with its own local Mini runtime inside the main sample app.

The repository also includes `mini-processor-multiregistry-test`, a small JVM example that validates isolated coexistence for generated registries coming from different modules.
For a separate consumer project example outside the main app build, see `samples/isolated-consumer/`, which contains its own `app` and `message-feature` modules.

## How to use
### Setting up Mini
You'll need to add the following snippet to the class that initializes your application (for example, in Android you would set this in your `Application`'s `onCreate` method).

```kotlin
val stores = listOf<Store<*>>() // Here you'll set-up you store list, you can retrieve it using your preferred DI framework
val dispatcher = MiniGen.newDispatcher() // Create a new dispatcher
val dispatcher = Dispatcher() // Create a new dispatcher
val registry = mini.codegen.Mini_Generated() // Generated MiniRegistry for this module

// Initialize Mini
storeSubscriptions = MiniGen.subscribe(dispatcher, stores)
storeSubscriptions = Mini.link(registry, dispatcher, stores)
stores.forEach { store ->
store.initialize()
}

// Optional: add logging middleware to log action events
dispatcher.addMiddleware(LoggerMiddleware(stores)) { tag, msg ->
Log.d(tag, msg)
}
dispatcher.addMiddleware(
LoggerMiddleware(stores, logger = { _, tag, msg ->
Log.d(tag, msg)
})
)
```

As soon as you do this, you'll have Mini up and running. You'll then need to declare your `Action`s, `Store`s and `State` as mentioned previously. The sample [app](app) contains examples regarding app configuration.

## Advanced usages

### Multi-module support
Mini supports multi-module and multi-library setups. Each Mini-enabled module generates its own registry, so modules coexist on the classpath without class collisions. Each module bootstraps its own `Dispatcher`, stores, and registry independently.

If more than one module uses Mini, assign a distinct `mini.registryName` to each to avoid duplicate class errors:

KAPT:
```kotlin
kapt {
arguments {
arg("mini.registryName", "feature")
}
}
```

KSP:
```kotlin
ksp {
arg("mini.registryName", "feature")
}
```

The generated registry will be placed under `mini.codegen.<name>.Mini_Generated` (e.g. `mini.codegen.feature.Mini_Generated`).
### Kotlin Flow Utils
Mini includes some utility extensions over Kotlin `Flow` to make easier listen state changes over the `StateContainer`s.

Expand Down Expand Up @@ -447,6 +477,22 @@ kapt.use.worker.api=true
org.gradle.caching=true
```

## Verification
You can verify the repository with these commands:

```bash
./gradlew :mini-common:test
./gradlew :mini-processor-test:test
./gradlew :mini-processor-ksp-test:test
./gradlew :mini-processor-reducer-only-test:test
./gradlew :mini-processor-multiregistry-test:test
./gradlew test
```

These checks cover explicit local registry bootstrap, reducer-only modules, KAPT and KSP generation paths, isolated coexistence across generated registries, and the repository test suite.

The Android sample app also includes `CounterFeatureSampleActivity`, which uses a feature module that owns its own local Mini runtime.

## Known issues
### KSP gotchas
#### KSP code is not recognized by the IntelliJ IDEs
Expand Down
10 changes: 8 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 HyperDevs
* Copyright 2026 HyperDevs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,10 @@ plugins {
alias(libs.plugins.convention.androidApp)
}

ksp {
arg("mini.registryName", "app_sample")
}

android {
namespace = "mini.android.sample"

Expand Down Expand Up @@ -69,6 +73,8 @@ android {
dependencies {
implementation(project(":mini-android"))
implementation(project(":mini-kodein-android"))
implementation(project(":sample-counter-feature"))
implementation(project(":isolated-consumer-message-feature"))

// kapt(project(":mini-processor"))
ksp(project(":mini-processor"))
Expand All @@ -91,4 +97,4 @@ dependencies {
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.espresso)
androidTestImplementation(libs.androidx.test.junit)
}
}
8 changes: 7 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
android:exported="false"
android:label="@string/view_model_sample_label"
android:theme="@style/AppTheme" />

<activity
android:name=".CounterFeatureSampleActivity"
android:exported="false"
android:label="@string/counter_feature_sample_label"
android:theme="@style/AppTheme" />
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2026 HyperDevs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package mini.android.sample

import android.util.Log
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import mini.android.sample.counter.CounterState
import mini.android.sample.counter.CounterFeatureRuntime
import mini.android.sample.ui.theme.AppTheme
import sample.consumer.message.MessageFeatureRuntime

class CounterFeatureSampleActivity : AppCompatActivity() {

private val counterFeature = CounterFeatureRuntime()
private val messageFeature = MessageFeatureRuntime()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Log.d("MiniSample", "Counter feature and external message feature run with separate local Mini runtimes")

setContent {
AppTheme {
CounterFeatureSampleScreen()
}
}
}

override fun onDestroy() {
counterFeature.close()
messageFeature.close()
super.onDestroy()
}

@Composable
private fun CounterFeatureSampleScreen() {
val coroutineScope = rememberCoroutineScope()
val counterState by counterFeature.flow().collectAsState(initial = CounterState())
val messageState by messageFeature.flow().collectAsState(initial = messageFeature.state)

Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Counter feature state: ${counterState.count}")
Button(onClick = {
counterFeature.increment(coroutineScope)
}) {
Text("Run counter feature")
}

Text("External message feature state: ${messageState.text}")
Button(onClick = {
coroutineScope.launch {
messageFeature.advance()
}
}) {
Text("Advance external message feature")
}

Button(onClick = {
coroutineScope.launch {
messageFeature.setMessage("from-main-app")
}
}) {
Text("Set external message")
}
}
}
}
}
28 changes: 15 additions & 13 deletions app/src/main/kotlin/mini/android/sample/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 HyperDevs
* Copyright 2026 HyperDevs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,29 +17,20 @@
package mini.android.sample

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Button
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import mini.android.sample.ui.theme.AppTheme
Expand All @@ -64,6 +55,11 @@ class MainActivity : AppCompatActivity() {
Intent(this, ViewModelSampleActivity::class.java).apply {
startActivity(this)
}
},
onGoToFeatureRuntimeSampleClicked = {
Intent(this, CounterFeatureSampleActivity::class.java).apply {
startActivity(this)
}
}
)
}
Expand All @@ -74,14 +70,16 @@ class MainActivity : AppCompatActivity() {
@Composable
private fun MainScreen(modifier: Modifier = Modifier,
onGoToStoreSampleClicked: () -> Unit = {},
onGoToViewModelSampleClicked: () -> Unit = {}) {
onGoToViewModelSampleClicked: () -> Unit = {},
onGoToFeatureRuntimeSampleClicked: () -> Unit = {}) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
MainContent(
modifier = modifier
.fillMaxSize()
.padding(innerPadding),
onGoToStoreSampleClicked = onGoToStoreSampleClicked,
onGoToViewModelSampleClicked = onGoToViewModelSampleClicked
onGoToViewModelSampleClicked = onGoToViewModelSampleClicked,
onGoToFeatureRuntimeSampleClicked = onGoToFeatureRuntimeSampleClicked
)
}
}
Expand All @@ -90,7 +88,8 @@ private fun MainScreen(modifier: Modifier = Modifier,
private fun MainContent(
modifier: Modifier = Modifier,
onGoToStoreSampleClicked: () -> Unit = {},
onGoToViewModelSampleClicked: () -> Unit = {}
onGoToViewModelSampleClicked: () -> Unit = {},
onGoToFeatureRuntimeSampleClicked: () -> Unit = {}
) {
Column(
modifier = modifier.fillMaxSize(),
Expand All @@ -103,6 +102,9 @@ private fun MainContent(
Button(onClick = onGoToViewModelSampleClicked) {
Text("Go to ViewModel sample")
}
Button(onClick = onGoToFeatureRuntimeSampleClicked) {
Text("Go to Counter feature sample")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 HyperDevs
* Copyright 2026 HyperDevs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -39,13 +39,15 @@ import kotlinx.coroutines.launch
import mini.*
import mini.android.FluxActivity
import mini.android.sample.ui.theme.AppTheme
import mini.codegen.app_sample.Mini_Generated

private val dispatcher = Dispatcher()
private val appRegistry = Mini_Generated()

class MainStore : Store<MainState>() {

init {
Mini.link(dispatcher, this).track()
Mini.link(appRegistry, dispatcher, this).track()
}

@Reducer
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 HyperDevs
* Copyright 2026 HyperDevs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -39,8 +39,10 @@ import mini.*
import mini.android.FluxActivity
import mini.android.FluxStoreViewModel
import mini.android.sample.ui.theme.AppTheme
import mini.codegen.app_sample.Mini_Generated

private val dispatcher = Dispatcher()
private val appRegistry = Mini_Generated()

class MainViewModelReducer : NestedStateContainer<MainState>() {

Expand All @@ -60,7 +62,7 @@ class MainStoreViewModel(savedStateHandle: SavedStateHandle) :
private val reducerSlice = MainViewModelReducer().apply { parent = this@MainStoreViewModel }

init {
Mini.link(dispatcher, listOf(this, reducerSlice)).track()
Mini.link(appRegistry, dispatcher, listOf(this, reducerSlice)).track()
}

override fun saveState(state: MainState, handle: SavedStateHandle) {
Expand Down
Loading