Skip to content

Commit 984abfb

Browse files
authored
feat: Implement multi-registry support with separate feature modules (#131)
* refactor: Introduce MiniRegistry interface and refactor Mini class to support multiple registries * feat: Add KSP test module with reducer-only test support * feat: Implement multi-registry support for Mini processor with new test module and documentation updates * feat: Add multi-registry sample with separate feature modules * refactor: Require explicit MiniRegistry in link functions and remove dynamic loading * refactor: Migrate to module-local bootstrap and require explicit registry linking * refactor: Remove Timber dependency and replace with standard Android Log * chore: Update copyright headers to 2026 * refactor: Change generated registry naming to use package segments instead of suffixes * refactor: Migrate sample-counter-feature from KAPT to KSP * docs: Update README to clarify multi-registry examples and add test commands * refactor: Simplify multi-registry sample and isolate message feature into a separate consumer project * feat: Integrate isolated consumer message feature into the sample application * refactor: Simplify generated registry class naming and remove package hashing fallback * Fix: Mini.kt By: nicolasmertanen * Fix: Processor.kt By: nicolasmertanen * Fix: ContainerBuilders.kt By: DaniAguion * Fix: README.md By: adriangl * Fix: README.md By: adriangl * Fix: ContainerBuilders.kt By: adriangl
1 parent ea00956 commit 984abfb

50 files changed

Lines changed: 1340 additions & 83 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
.idea/deploymentTargetSelector.xml
7878
.idea/migrations.xml
7979
.idea/other.xml
80+
.idea/AndroidProjectSystem.xml
81+
.idea/deviceManager.xml
82+
.idea/markdown.xml
8083

8184
# mpeltonen/sbt-idea plugin
8285
.idea_modules/

README.md

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,33 +257,63 @@ Given the example `Store`s and `Action`s explained before, the workflow would be
257257
- The Store changes its state to the given values from `LoginCompletedAction`.
258258
- The view will react (for example, redirecting to another home view) if the task was success or shows an error if not.
259259

260-
You can execute another sample in the `app` package. It contains two different samples executing two types of `StateContainer`s:
260+
You can execute the sample in the `app` package. It contains two different samples executing two types of `StateContainer`s:
261261
- `StoreSampleActivity` class uses a `Store` as a `StateContainer`.
262262
- `ViewModelSampleActivity` class uses a `ViewModel` as a `StateContainer`.
263+
- `CounterFeatureSampleActivity` class shows a feature module running with its own local Mini runtime inside the main sample app.
264+
265+
The repository also includes `mini-processor-multiregistry-test`, a small JVM example that validates isolated coexistence for generated registries coming from different modules.
266+
For a separate consumer project example outside the main app build, see `samples/isolated-consumer/`, which contains its own `app` and `message-feature` modules.
263267

264268
## How to use
265269
### Setting up Mini
266270
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).
267271

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

272277
// Initialize Mini
273-
storeSubscriptions = MiniGen.subscribe(dispatcher, stores)
278+
storeSubscriptions = Mini.link(registry, dispatcher, stores)
274279
stores.forEach { store ->
275280
store.initialize()
276281
}
277282

278283
// Optional: add logging middleware to log action events
279-
dispatcher.addMiddleware(LoggerMiddleware(stores)) { tag, msg ->
280-
Log.d(tag, msg)
281-
}
284+
dispatcher.addMiddleware(
285+
LoggerMiddleware(stores, logger = { _, tag, msg ->
286+
Log.d(tag, msg)
287+
})
288+
)
282289
```
283290

284291
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.
285292

286293
## Advanced usages
294+
295+
### Multi-module support
296+
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.
297+
298+
If more than one module uses Mini, assign a distinct `mini.registryName` to each to avoid duplicate class errors:
299+
300+
KAPT:
301+
```kotlin
302+
kapt {
303+
arguments {
304+
arg("mini.registryName", "feature")
305+
}
306+
}
307+
```
308+
309+
KSP:
310+
```kotlin
311+
ksp {
312+
arg("mini.registryName", "feature")
313+
}
314+
```
315+
316+
The generated registry will be placed under `mini.codegen.<name>.Mini_Generated` (e.g. `mini.codegen.feature.Mini_Generated`).
287317
### Kotlin Flow Utils
288318
Mini includes some utility extensions over Kotlin `Flow` to make easier listen state changes over the `StateContainer`s.
289319

@@ -447,6 +477,22 @@ kapt.use.worker.api=true
447477
org.gradle.caching=true
448478
```
449479

480+
## Verification
481+
You can verify the repository with these commands:
482+
483+
```bash
484+
./gradlew :mini-common:test
485+
./gradlew :mini-processor-test:test
486+
./gradlew :mini-processor-ksp-test:test
487+
./gradlew :mini-processor-reducer-only-test:test
488+
./gradlew :mini-processor-multiregistry-test:test
489+
./gradlew test
490+
```
491+
492+
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.
493+
494+
The Android sample app also includes `CounterFeatureSampleActivity`, which uses a feature module that owns its own local Mini runtime.
495+
450496
## Known issues
451497
### KSP gotchas
452498
#### KSP code is not recognized by the IntelliJ IDEs

app/build.gradle.kts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 HyperDevs
2+
* Copyright 2026 HyperDevs
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,10 @@ plugins {
2222
alias(libs.plugins.convention.androidApp)
2323
}
2424

25+
ksp {
26+
arg("mini.registryName", "app_sample")
27+
}
28+
2529
android {
2630
namespace = "mini.android.sample"
2731

@@ -69,6 +73,8 @@ android {
6973
dependencies {
7074
implementation(project(":mini-android"))
7175
implementation(project(":mini-kodein-android"))
76+
implementation(project(":sample-counter-feature"))
77+
implementation(project(":isolated-consumer-message-feature"))
7278

7379
// kapt(project(":mini-processor"))
7480
ksp(project(":mini-processor"))
@@ -91,4 +97,4 @@ dependencies {
9197
androidTestImplementation(libs.androidx.test.runner)
9298
androidTestImplementation(libs.espresso)
9399
androidTestImplementation(libs.androidx.test.junit)
94-
}
100+
}

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
android:exported="false"
4747
android:label="@string/view_model_sample_label"
4848
android:theme="@style/AppTheme" />
49+
50+
<activity
51+
android:name=".CounterFeatureSampleActivity"
52+
android:exported="false"
53+
android:label="@string/counter_feature_sample_label"
54+
android:theme="@style/AppTheme" />
4955
</application>
5056

51-
</manifest>
57+
</manifest>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2026 HyperDevs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package mini.android.sample
18+
19+
import android.util.Log
20+
import android.os.Bundle
21+
import androidx.activity.compose.setContent
22+
import androidx.appcompat.app.AppCompatActivity
23+
import androidx.compose.foundation.layout.Arrangement
24+
import androidx.compose.foundation.layout.Column
25+
import androidx.compose.foundation.layout.fillMaxSize
26+
import androidx.compose.foundation.layout.padding
27+
import androidx.compose.material3.Button
28+
import androidx.compose.material3.Scaffold
29+
import androidx.compose.material3.Text
30+
import androidx.compose.runtime.Composable
31+
import androidx.compose.runtime.collectAsState
32+
import androidx.compose.runtime.getValue
33+
import androidx.compose.runtime.rememberCoroutineScope
34+
import androidx.compose.ui.Alignment
35+
import androidx.compose.ui.Modifier
36+
import androidx.compose.ui.unit.dp
37+
import kotlinx.coroutines.launch
38+
import mini.android.sample.counter.CounterState
39+
import mini.android.sample.counter.CounterFeatureRuntime
40+
import mini.android.sample.ui.theme.AppTheme
41+
import sample.consumer.message.MessageFeatureRuntime
42+
43+
class CounterFeatureSampleActivity : AppCompatActivity() {
44+
45+
private val counterFeature = CounterFeatureRuntime()
46+
private val messageFeature = MessageFeatureRuntime()
47+
48+
override fun onCreate(savedInstanceState: Bundle?) {
49+
super.onCreate(savedInstanceState)
50+
51+
Log.d("MiniSample", "Counter feature and external message feature run with separate local Mini runtimes")
52+
53+
setContent {
54+
AppTheme {
55+
CounterFeatureSampleScreen()
56+
}
57+
}
58+
}
59+
60+
override fun onDestroy() {
61+
counterFeature.close()
62+
messageFeature.close()
63+
super.onDestroy()
64+
}
65+
66+
@Composable
67+
private fun CounterFeatureSampleScreen() {
68+
val coroutineScope = rememberCoroutineScope()
69+
val counterState by counterFeature.flow().collectAsState(initial = CounterState())
70+
val messageState by messageFeature.flow().collectAsState(initial = messageFeature.state)
71+
72+
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
73+
Column(
74+
modifier = Modifier
75+
.fillMaxSize()
76+
.padding(innerPadding)
77+
.padding(16.dp),
78+
horizontalAlignment = Alignment.CenterHorizontally,
79+
verticalArrangement = Arrangement.Center
80+
) {
81+
Text("Counter feature state: ${counterState.count}")
82+
Button(onClick = {
83+
counterFeature.increment(coroutineScope)
84+
}) {
85+
Text("Run counter feature")
86+
}
87+
88+
Text("External message feature state: ${messageState.text}")
89+
Button(onClick = {
90+
coroutineScope.launch {
91+
messageFeature.advance()
92+
}
93+
}) {
94+
Text("Advance external message feature")
95+
}
96+
97+
Button(onClick = {
98+
coroutineScope.launch {
99+
messageFeature.setMessage("from-main-app")
100+
}
101+
}) {
102+
Text("Set external message")
103+
}
104+
}
105+
}
106+
}
107+
}

app/src/main/kotlin/mini/android/sample/MainActivity.kt

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 HyperDevs
2+
* Copyright 2026 HyperDevs
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,29 +17,20 @@
1717
package mini.android.sample
1818

1919
import android.content.Intent
20-
import android.net.Uri
2120
import android.os.Bundle
22-
import android.widget.Button
2321
import androidx.activity.compose.setContent
2422
import androidx.activity.enableEdgeToEdge
2523
import androidx.appcompat.app.AppCompatActivity
26-
import androidx.compose.foundation.background
2724
import androidx.compose.foundation.layout.Arrangement
28-
import androidx.compose.foundation.layout.Box
2925
import androidx.compose.foundation.layout.Column
3026
import androidx.compose.foundation.layout.fillMaxSize
3127
import androidx.compose.foundation.layout.padding
3228
import androidx.compose.material3.Button
33-
import androidx.compose.material3.MaterialTheme
3429
import androidx.compose.material3.Scaffold
3530
import androidx.compose.material3.Text
3631
import androidx.compose.runtime.Composable
37-
import androidx.compose.runtime.remember
3832
import androidx.compose.ui.Alignment
3933
import androidx.compose.ui.Modifier
40-
import androidx.compose.ui.graphics.Color
41-
import androidx.compose.ui.platform.LocalContext
42-
import androidx.compose.ui.res.stringResource
4334
import androidx.compose.ui.tooling.preview.Preview
4435
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
4536
import mini.android.sample.ui.theme.AppTheme
@@ -64,6 +55,11 @@ class MainActivity : AppCompatActivity() {
6455
Intent(this, ViewModelSampleActivity::class.java).apply {
6556
startActivity(this)
6657
}
58+
},
59+
onGoToFeatureRuntimeSampleClicked = {
60+
Intent(this, CounterFeatureSampleActivity::class.java).apply {
61+
startActivity(this)
62+
}
6763
}
6864
)
6965
}
@@ -74,14 +70,16 @@ class MainActivity : AppCompatActivity() {
7470
@Composable
7571
private fun MainScreen(modifier: Modifier = Modifier,
7672
onGoToStoreSampleClicked: () -> Unit = {},
77-
onGoToViewModelSampleClicked: () -> Unit = {}) {
73+
onGoToViewModelSampleClicked: () -> Unit = {},
74+
onGoToFeatureRuntimeSampleClicked: () -> Unit = {}) {
7875
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
7976
MainContent(
8077
modifier = modifier
8178
.fillMaxSize()
8279
.padding(innerPadding),
8380
onGoToStoreSampleClicked = onGoToStoreSampleClicked,
84-
onGoToViewModelSampleClicked = onGoToViewModelSampleClicked
81+
onGoToViewModelSampleClicked = onGoToViewModelSampleClicked,
82+
onGoToFeatureRuntimeSampleClicked = onGoToFeatureRuntimeSampleClicked
8583
)
8684
}
8785
}
@@ -90,7 +88,8 @@ private fun MainScreen(modifier: Modifier = Modifier,
9088
private fun MainContent(
9189
modifier: Modifier = Modifier,
9290
onGoToStoreSampleClicked: () -> Unit = {},
93-
onGoToViewModelSampleClicked: () -> Unit = {}
91+
onGoToViewModelSampleClicked: () -> Unit = {},
92+
onGoToFeatureRuntimeSampleClicked: () -> Unit = {}
9493
) {
9594
Column(
9695
modifier = modifier.fillMaxSize(),
@@ -103,6 +102,9 @@ private fun MainContent(
103102
Button(onClick = onGoToViewModelSampleClicked) {
104103
Text("Go to ViewModel sample")
105104
}
105+
Button(onClick = onGoToFeatureRuntimeSampleClicked) {
106+
Text("Go to Counter feature sample")
107+
}
106108
}
107109
}
108110

app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 HyperDevs
2+
* Copyright 2026 HyperDevs
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,13 +39,15 @@ import kotlinx.coroutines.launch
3939
import mini.*
4040
import mini.android.FluxActivity
4141
import mini.android.sample.ui.theme.AppTheme
42+
import mini.codegen.app_sample.Mini_Generated
4243

4344
private val dispatcher = Dispatcher()
45+
private val appRegistry = Mini_Generated()
4446

4547
class MainStore : Store<MainState>() {
4648

4749
init {
48-
Mini.link(dispatcher, this).track()
50+
Mini.link(appRegistry, dispatcher, this).track()
4951
}
5052

5153
@Reducer

app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 HyperDevs
2+
* Copyright 2026 HyperDevs
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,8 +39,10 @@ import mini.*
3939
import mini.android.FluxActivity
4040
import mini.android.FluxStoreViewModel
4141
import mini.android.sample.ui.theme.AppTheme
42+
import mini.codegen.app_sample.Mini_Generated
4243

4344
private val dispatcher = Dispatcher()
45+
private val appRegistry = Mini_Generated()
4446

4547
class MainViewModelReducer : NestedStateContainer<MainState>() {
4648

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

6264
init {
63-
Mini.link(dispatcher, listOf(this, reducerSlice)).track()
65+
Mini.link(appRegistry, dispatcher, listOf(this, reducerSlice)).track()
6466
}
6567

6668
override fun saveState(state: MainState, handle: SavedStateHandle) {

0 commit comments

Comments
 (0)