From 1d0489b88a501d0f12cd37e642303aa32ccd13fb Mon Sep 17 00:00:00 2001 From: finxo Date: Fri, 22 May 2026 08:36:40 +0200 Subject: [PATCH 01/20] refactor: Introduce MiniRegistry interface and refactor Mini class to support multiple registries --- mini-common/src/main/java/mini/Mini.kt | 76 +++++++++++---- .../src/main/java/mini/MiniRegistry.kt | 26 +++++ mini-common/src/test/kotlin/mini/MiniTest.kt | 96 +++++++++++++++++++ 3 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 mini-common/src/main/java/mini/MiniRegistry.kt create mode 100644 mini-common/src/test/kotlin/mini/MiniTest.kt diff --git a/mini-common/src/main/java/mini/Mini.kt b/mini-common/src/main/java/mini/Mini.kt index c7e6bc5..fe49e63 100644 --- a/mini-common/src/main/java/mini/Mini.kt +++ b/mini-common/src/main/java/mini/Mini.kt @@ -19,22 +19,48 @@ package mini import java.io.Closeable +import java.util.ServiceLoader import kotlin.reflect.KClass const val DISPATCHER_FACTORY_CLASS_NAME = "mini.codegen.Mini_Generated" -abstract class Mini { +internal object MiniRuntime { + var loadRegistries: () -> List = { + ServiceLoader.load(MiniRegistry::class.java, Mini::class.java.classLoader).iterator().asSequence().toList() + } - companion object { + var loadLegacyRegistry: () -> MiniRegistry = { + try { + Class.forName(DISPATCHER_FACTORY_CLASS_NAME).getField("INSTANCE").get(null) as MiniRegistry + } catch (ex: Throwable) { + throw ClassNotFoundException("Failed to load generated class $DISPATCHER_FACTORY_CLASS_NAME, " + + "most likely the annotation processor did not run, add it as dependency to the project", ex) + } + } - private val miniInstance: Mini by lazy { + fun reset() { + loadRegistries = { + ServiceLoader.load(MiniRegistry::class.java, Mini::class.java.classLoader).iterator().asSequence().toList() + } + loadLegacyRegistry = { try { - Class.forName(DISPATCHER_FACTORY_CLASS_NAME).getField("INSTANCE").get(null) as Mini + Class.forName(DISPATCHER_FACTORY_CLASS_NAME).getField("INSTANCE").get(null) as MiniRegistry } catch (ex: Throwable) { throw ClassNotFoundException("Failed to load generated class $DISPATCHER_FACTORY_CLASS_NAME, " + "most likely the annotation processor did not run, add it as dependency to the project", ex) } } + } +} + +abstract class Mini : MiniRegistry { + + companion object { + + private fun registries(): List { + val registries = MiniRuntime.loadRegistries() + return if (registries.isNotEmpty()) registries else listOf(MiniRuntime.loadLegacyRegistry()) + } /** * Generate all subscriptions from @[Reducer] annotated methods and bundle @@ -42,7 +68,12 @@ abstract class Mini { */ fun link(dispatcher: Dispatcher, container: StateContainer<*>): Closeable { ensureDispatcherInitialized(dispatcher) - return miniInstance.subscribe(dispatcher, container) + val c = CompositeCloseable() + val registries = registries() + registries.forEach { registry -> + c.add(registry.subscribe(dispatcher, container)) + } + return c } /** @@ -51,36 +82,49 @@ abstract class Mini { */ fun link(dispatcher: Dispatcher, containers: Iterable>): Closeable { ensureDispatcherInitialized(dispatcher) - return miniInstance.subscribe(dispatcher, containers) + val c = CompositeCloseable() + val registries = registries() + containers.forEach { container -> + registries.forEach { registry -> + c.add(registry.subscribe(dispatcher, container)) + } + } + return c } private fun ensureDispatcherInitialized(dispatcher: Dispatcher) { if (dispatcher.actionTypeMap.isEmpty()) { - dispatcher.actionTypeMap = miniInstance.actionTypes + dispatcher.actionTypeMap = mergeActionTypes(registries()) } } - } + private fun mergeActionTypes(registries: List): Map, List>> { + return registries + .asSequence() + .flatMap { it.actionTypes.asSequence() } + .groupBy({ it.key }, { it.value }) + .mapValues { (_, values) -> values.flatten().distinct() } + } - /** - * All the types an action can be subscribed as. - */ - abstract val actionTypes: Map, List>> + } /** * Link all [Reducer] functions present in the store to the dispatcher. */ - protected abstract fun subscribe(dispatcher: Dispatcher, - container: StateContainer): Closeable + abstract override fun subscribe(dispatcher: Dispatcher, + container: StateContainer): Closeable /** * Link all [Reducer] functions present in the store to the dispatcher. */ protected fun subscribe(dispatcher: Dispatcher, containers: Iterable>): Closeable { val c = CompositeCloseable() + val registries = registries() containers.forEach { container -> - c.add(subscribe(dispatcher, container)) + registries.forEach { registry -> + c.add(registry.subscribe(dispatcher, container)) + } } return c } -} \ No newline at end of file +} diff --git a/mini-common/src/main/java/mini/MiniRegistry.kt b/mini-common/src/main/java/mini/MiniRegistry.kt new file mode 100644 index 0000000..ca13236 --- /dev/null +++ b/mini-common/src/main/java/mini/MiniRegistry.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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 + +import java.io.Closeable +import kotlin.reflect.KClass + +interface MiniRegistry { + val actionTypes: Map, List>> + + fun subscribe(dispatcher: Dispatcher, container: StateContainer): Closeable +} diff --git a/mini-common/src/test/kotlin/mini/MiniTest.kt b/mini-common/src/test/kotlin/mini/MiniTest.kt new file mode 100644 index 0000000..efb8983 --- /dev/null +++ b/mini-common/src/test/kotlin/mini/MiniTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2024 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 + +import org.amshove.kluent.`should be equal to` +import org.junit.After +import org.junit.Test +import java.io.Closeable +import kotlin.reflect.KClass + +class MiniTest { + + @After + fun tearDown() { + MiniRuntime.reset() + } + + @Test + fun `link initializes dispatcher action types from all registries`() { + val dispatcher = Dispatcher() + val store = SampleStore() + MiniRuntime.loadRegistries = { + listOf( + TestRegistry( + actionTypes = mapOf(TestAction::class to listOf(TestAction::class, Any::class)), + accepts = setOf(SampleStore::class) + ), + TestRegistry( + actionTypes = mapOf(TestAction::class to listOf(TestAction::class, State::class)), + accepts = emptySet() + ) + ) + } + + Mini.link(dispatcher, store).close() + + dispatcher.actionTypeMap[TestAction::class] `should be equal to` listOf(TestAction::class, Any::class, State::class) + } + + @Test + fun `link delegates subscriptions to all registries`() { + val dispatcher = Dispatcher() + val store = SampleStore() + val appRegistry = TestRegistry(emptyMap(), accepts = setOf(SampleStore::class)) + val unrelatedRegistry = TestRegistry(emptyMap(), accepts = emptySet()) + MiniRuntime.loadRegistries = { listOf(appRegistry, unrelatedRegistry) } + + Mini.link(dispatcher, store).close() + + appRegistry.subscriptionCount `should be equal to` 1 + unrelatedRegistry.subscriptionCount `should be equal to` 1 + } + + @Test + fun `link falls back to legacy registry when no new registries are present`() { + val dispatcher = Dispatcher() + val store = SampleStore() + val legacyRegistry = TestRegistry( + actionTypes = mapOf(TestAction::class to listOf(TestAction::class)), + accepts = setOf(SampleStore::class) + ) + MiniRuntime.loadRegistries = { emptyList() } + MiniRuntime.loadLegacyRegistry = { legacyRegistry } + + Mini.link(dispatcher, store).close() + + dispatcher.actionTypeMap[TestAction::class] `should be equal to` listOf(TestAction::class) + legacyRegistry.subscriptionCount `should be equal to` 1 + } + + private class TestRegistry( + override val actionTypes: Map, List>>, + private val accepts: Set> + ) : MiniRegistry { + var subscriptionCount = 0 + + override fun subscribe(dispatcher: Dispatcher, container: StateContainer): Closeable { + subscriptionCount++ + return Closeable { } + } + } +} From 8215507c4e0c1e70cff6c2ff6dc9bdd08df72c67 Mon Sep 17 00:00:00 2001 From: finxo Date: Wed, 27 May 2026 12:31:14 +0200 Subject: [PATCH 02/20] feat: Add KSP test module with reducer-only test support --- mini-processor-ksp-test/.gitignore | 1 + mini-processor-ksp-test/build.gradle.kts | 45 +++++++++++++++++ .../main/java/mini/ksptest/KspAnyAction.kt | 22 +++++++++ .../main/java/mini/ksptest/KspBasicState.kt | 21 ++++++++ .../java/mini/ksptest/KspReducersStore.kt | 27 ++++++++++ .../java/mini/ksptest/KspReducersStoreTest.kt | 39 +++++++++++++++ mini-processor-reducer-only-test/.gitignore | 1 + .../build.gradle.kts | 45 +++++++++++++++++ .../java/mini/reduceronly/ReducerOnlyState.kt | 21 ++++++++ .../java/mini/reduceronly/ReducerOnlyStore.kt | 28 +++++++++++ .../mini/reduceronly/ReducerOnlyStoreTest.kt | 40 +++++++++++++++ .../processor/common/ContainerBuilders.kt | 49 ++++++++++++++++--- .../kapt/MiniAnnotationProcessor.java | 2 +- .../java/mini/processor/kapt/Processor.kt | 11 ++++- .../mini/processor/kapt/ProcessorUtils.kt | 12 +++++ .../mini/processor/ksp/MiniSymbolProcessor.kt | 35 +++++++++---- .../ksp/MiniSymbolProcessorProvider.kt | 6 ++- settings.gradle.kts | 4 +- 18 files changed, 386 insertions(+), 23 deletions(-) create mode 100644 mini-processor-ksp-test/.gitignore create mode 100644 mini-processor-ksp-test/build.gradle.kts create mode 100644 mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt create mode 100644 mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt create mode 100644 mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt create mode 100644 mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt create mode 100644 mini-processor-reducer-only-test/.gitignore create mode 100644 mini-processor-reducer-only-test/build.gradle.kts create mode 100644 mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt create mode 100644 mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt create mode 100644 mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt diff --git a/mini-processor-ksp-test/.gitignore b/mini-processor-ksp-test/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/mini-processor-ksp-test/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mini-processor-ksp-test/build.gradle.kts b/mini-processor-ksp-test/build.gradle.kts new file mode 100644 index 0000000..4b020a5 --- /dev/null +++ b/mini-processor-ksp-test/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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. + */ + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.ksp) +} + +dependencies { + implementation(project(":mini-common")) + ksp(project(":mini-processor")) + + implementation(libs.kotlinx.coroutines.core) + + testImplementation(libs.junit) + testImplementation(libs.kluent) +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = libs.versions.java.sdk.get() + } +} + +java { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) +} + +kotlin { + jvmToolchain(libs.versions.java.sdk.get().toInt()) +} diff --git a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt new file mode 100644 index 0000000..e1d5f90 --- /dev/null +++ b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 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.ksptest + +import mini.Action + +@Action +data class KspAnyAction(val value: String) diff --git a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt new file mode 100644 index 0000000..09415cd --- /dev/null +++ b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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.ksptest + +import mini.State + +data class KspBasicState(val value: String = "initial") : State diff --git a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt new file mode 100644 index 0000000..8dc94fe --- /dev/null +++ b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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.ksptest + +import mini.Reducer +import mini.Store + +class KspReducersStore : Store() { + @Reducer + fun pureReducer(state: KspBasicState, action: KspAnyAction): KspBasicState { + return state.copy(value = action.value) + } +} diff --git a/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt new file mode 100644 index 0000000..c96697c --- /dev/null +++ b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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.ksptest + +import kotlinx.coroutines.runBlocking +import mini.Dispatcher +import mini.Mini +import org.amshove.kluent.`should be equal to` +import org.junit.Test + +internal class KspReducersStoreTest { + + private val store = KspReducersStore() + private val dispatcher = Dispatcher().apply { + Mini.link(this, listOf(store)) + } + + @Test + fun `pure reducers are called through ksp generated registry`() { + runBlocking { + dispatcher.dispatch(KspAnyAction("changed")) + store.state.value `should be equal to` "changed" + } + } +} diff --git a/mini-processor-reducer-only-test/.gitignore b/mini-processor-reducer-only-test/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/mini-processor-reducer-only-test/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mini-processor-reducer-only-test/build.gradle.kts b/mini-processor-reducer-only-test/build.gradle.kts new file mode 100644 index 0000000..63df44c --- /dev/null +++ b/mini-processor-reducer-only-test/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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. + */ + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.kapt) +} + +dependencies { + implementation(project(":mini-common")) + implementation(project(":mini-processor-test")) + kapt(project(":mini-processor")) + + testImplementation(libs.junit) + testImplementation(libs.kluent) + testImplementation(libs.kotlinx.coroutines.core) +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = libs.versions.java.sdk.get() + } +} + +java { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) +} + +kotlin { + jvmToolchain(libs.versions.java.sdk.get().toInt()) +} diff --git a/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt new file mode 100644 index 0000000..27d2b88 --- /dev/null +++ b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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.reduceronly + +import mini.State + +data class ReducerOnlyState(val value: String = "initial") : State diff --git a/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt new file mode 100644 index 0000000..cfb9e90 --- /dev/null +++ b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.reduceronly + +import mini.Reducer +import mini.Store +import mini.test.AnyAction + +class ReducerOnlyStore : Store() { + @Reducer + fun pureReducer(state: ReducerOnlyState, action: AnyAction): ReducerOnlyState { + return state.copy(value = action.value) + } +} diff --git a/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt new file mode 100644 index 0000000..3ab8784 --- /dev/null +++ b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.reduceronly + +import kotlinx.coroutines.runBlocking +import mini.Dispatcher +import mini.Mini +import mini.test.AnyAction +import org.amshove.kluent.`should be equal to` +import org.junit.Test + +internal class ReducerOnlyStoreTest { + + private val store = ReducerOnlyStore() + private val dispatcher = Dispatcher().apply { + Mini.link(this, listOf(store)) + } + + @Test + fun `reducers are generated without local actions`() { + runBlocking { + dispatcher.dispatch(AnyAction("changed")) + store.state.value `should be equal to` "changed" + } + } +} diff --git a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt index 6ada906..7cb32e2 100644 --- a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt +++ b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt @@ -19,20 +19,55 @@ package mini.processor.common import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec -import mini.DISPATCHER_FACTORY_CLASS_NAME import mini.Mini +const val MINI_REGISTRY_NAME_OPTION = "mini.registryName" +const val MINI_REGISTRY_PACKAGE_NAME = "mini.codegen" + +private const val GENERATED_REGISTRY_PREFIX = "Mini_Generated_" + data class ContainerBuilders( val fileSpecBuilder: FileSpec.Builder, - val typeSpecBuilder: TypeSpec.Builder + val typeSpecBuilder: TypeSpec.Builder, + val className: ClassName ) -fun getContainerBuilders(): ContainerBuilders { - val containerClassName = ClassName.bestGuess(DISPATCHER_FACTORY_CLASS_NAME) +fun getContainerBuilders(registryName: String?, packageNames: Iterable): ContainerBuilders { + val containerClassName = generatedRegistryClassName(registryName, packageNames) val containerFile = FileSpec.builder(containerClassName.packageName, containerClassName.simpleName) - val container = TypeSpec.objectBuilder(containerClassName) + val container = TypeSpec.classBuilder(containerClassName) .addKdoc("Automatically generated, do not edit.\n") .superclass(Mini::class) - return ContainerBuilders(containerFile, container) -} \ No newline at end of file + return ContainerBuilders(containerFile, container, containerClassName) +} + +fun generatedRegistryClassName(registryName: String?, packageNames: Iterable): ClassName { + val suffix = registryName + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let(::sanitizeRegistryName) + ?: packageNames + .map(::sanitizePackageName) + .filter { it.isNotEmpty() } + .sorted() + .joinToString("_") + .takeIf { it.isNotEmpty() } + ?.let(::shortHash) + ?: "default" + return ClassName(MINI_REGISTRY_PACKAGE_NAME, "$GENERATED_REGISTRY_PREFIX$suffix") +} + +private fun sanitizeRegistryName(name: String): String { + return name.replace(Regex("[^A-Za-z0-9_]"), "_") +} + +private fun sanitizePackageName(packageName: String): String { + return packageName.replace('.', '_') +} + +private fun shortHash(value: String): String { + return value.encodeToByteArray().fold(0x811c9dc5.toInt()) { acc, byte -> + (acc xor byte.toInt()) * 16777619 + }.toUInt().toString(16) +} diff --git a/mini-processor/src/main/java/mini/processor/kapt/MiniAnnotationProcessor.java b/mini-processor/src/main/java/mini/processor/kapt/MiniAnnotationProcessor.java index 7655be7..d5f4dea 100644 --- a/mini-processor/src/main/java/mini/processor/kapt/MiniAnnotationProcessor.java +++ b/mini-processor/src/main/java/mini/processor/kapt/MiniAnnotationProcessor.java @@ -32,7 +32,7 @@ * Dummy Java wrapper that delegates to Kotlin one */ @IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.AGGREGATING) -@SupportedOptions("kapt.kotlin.generated") +@SupportedOptions({"kapt.kotlin.generated", "mini.registryName"}) public class MiniAnnotationProcessor extends AbstractProcessor { private final Processor processor = new Processor(); diff --git a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt index f550820..c8b6318 100644 --- a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt +++ b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt @@ -21,6 +21,7 @@ package mini.processor.kapt import mini.Action import mini.Reducer import mini.processor.common.ProcessorException +import mini.processor.common.MINI_REGISTRY_NAME_OPTION import mini.processor.common.actions.ActionTypesGenerator import mini.processor.common.getContainerBuilders import mini.processor.common.reducers.ReducersGenerator @@ -49,9 +50,14 @@ class Processor { val roundActions = roundEnv.getElementsAnnotatedWith(Action::class.java) val roundReducers = roundEnv.getElementsAnnotatedWith(Reducer::class.java) - if (roundActions.isEmpty()) return false + if (roundActions.isEmpty() && roundReducers.isEmpty()) return false - val (containerFile, container) = getContainerBuilders() + val registryName = env.options[MINI_REGISTRY_NAME_OPTION] + val packageNames = (roundActions + roundReducers) + .map { elementUtils.getPackageOf(it).qualifiedName.toString() } + .distinct() + + val (containerFile, container, className) = getContainerBuilders(registryName, packageNames) try { ActionTypesGenerator(KaptActionTypesGeneratorDelegate(roundActions)).generate(container) @@ -69,6 +75,7 @@ class Processor { .addType(container.build()) .build() .writeToFile(sourceElements = ((roundActions + roundReducers).toTypedArray())) + writeRegistryServiceFile(className.canonicalName, *((roundActions + roundReducers).toTypedArray())) return true } diff --git a/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt b/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt index 784b2cc..433d416 100644 --- a/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt +++ b/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt @@ -89,6 +89,18 @@ fun FileSpec.writeToFile(vararg sourceElements: Element) { openWriter.close() } +fun writeRegistryServiceFile(registryClassName: String, vararg sourceElements: Element) { + val serviceFileObject = env.filer.createResource( + StandardLocation.CLASS_OUTPUT, + "", + "META-INF/services/mini.MiniRegistry", + *sourceElements + ) + serviceFileObject.openWriter().use { writer -> + writer.write("$registryClassName\n") + } +} + /** * Map [java.lang.Object] to [Any] */ diff --git a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt index 6875d9b..f0db1eb 100644 --- a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt +++ b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt @@ -1,10 +1,12 @@ package mini.processor.ksp import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration import com.squareup.kotlinpoet.ksp.writeTo import mini.Action import mini.Reducer @@ -17,28 +19,34 @@ import mini.processor.ksp.actions.KspActionTypesGeneratorDelegate import mini.processor.ksp.reducers.KspReducersGeneratorDelegate class MiniSymbolProcessor( - private val codeGenerator: CodeGenerator + private val codeGenerator: CodeGenerator, + private val registryName: String? ) : SymbolProcessor { override fun process(resolver: Resolver): List { // Get elements with the @Reducer or @Action annotations - val actionSymbols = resolver.getSymbolsWithAnnotation(Action::class.java.canonicalName) - val reducerSymbols = resolver.getSymbolsWithAnnotation(Reducer::class.java.canonicalName) + val actionSymbols = resolver.getSymbolsWithAnnotation(Action::class.java.canonicalName).toList() + val reducerSymbols = resolver.getSymbolsWithAnnotation(Reducer::class.java.canonicalName).toList() // Collect the files that contain the symbols, we will use this to set the originating files // for the generated code and incremental processing. - val originatingKsFiles = (actionSymbols + reducerSymbols). - filterIsInstance() + val originatingKsFiles = (actionSymbols + reducerSymbols) + .filterIsInstance() .mapNotNull { it.containingFile } .distinct() .toList() - if (!actionSymbols.iterator().hasNext()) return emptyList() + if (actionSymbols.isEmpty() && reducerSymbols.isEmpty()) return emptyList() - val (containerFile, container) = getContainerBuilders() + val packageNames = (actionSymbols + reducerSymbols) + .filterIsInstance() + .map { it.packageName.asString() } + .distinct() + + val (containerFile, container, className) = getContainerBuilders(registryName, packageNames) try { - ActionTypesGenerator(KspActionTypesGeneratorDelegate(actionSymbols)).generate(container) - ReducersGenerator(KspReducersGeneratorDelegate(reducerSymbols)).generate(container) + ActionTypesGenerator(KspActionTypesGeneratorDelegate(actionSymbols.asSequence())).generate(container) + ReducersGenerator(KspReducersGeneratorDelegate(reducerSymbols.asSequence())).generate(container) } catch (e: Throwable) { if (e !is ProcessorException) { kspLogError( @@ -56,7 +64,14 @@ class MiniSymbolProcessor( aggregating = true, originatingKSFiles = originatingKsFiles ) + codeGenerator.createNewFileByPath( + Dependencies(aggregating = true, *originatingKsFiles.toTypedArray()), + "META-INF/services/mini.MiniRegistry", + "" + ).writer().use { writer -> + writer.write("${className.canonicalName}\n") + } return emptyList() } -} \ No newline at end of file +} diff --git a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessorProvider.kt b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessorProvider.kt index 3b095ad..1169fe1 100644 --- a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessorProvider.kt +++ b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessorProvider.kt @@ -3,13 +3,15 @@ package mini.processor.ksp import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider +import mini.processor.common.MINI_REGISTRY_NAME_OPTION class MiniSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { logger = environment.logger return MiniSymbolProcessor( - codeGenerator = environment.codeGenerator + codeGenerator = environment.codeGenerator, + registryName = environment.options[MINI_REGISTRY_NAME_OPTION] ) } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a876413..1ab7e10 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,8 @@ include(":mini-processor") include(":mini-common") include(":mini-android") include(":mini-processor-test") +include(":mini-processor-ksp-test") +include(":mini-processor-reducer-only-test") include(":mini-kodein") include(":mini-kodein-android") include(":mini-kodein-android-compose") @@ -47,4 +49,4 @@ dependencyResolutionManagement { gradlePluginPortal() maven { url = java.net.URI("https://jitpack.io") } } -} \ No newline at end of file +} From 587cac4c285e26ebadcc78cd40d33dc12506cb55 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 1 Jun 2026 08:50:55 +0200 Subject: [PATCH 03/20] feat: Implement multi-registry support for Mini processor with new test module and documentation updates --- README.md | 91 +++++++++++++++++-- mini-processor-multiregistry-test/.gitignore | 1 + .../build.gradle.kts | 44 +++++++++ .../MultiRegistryIntegrationTest.kt | 54 +++++++++++ settings.gradle.kts | 1 + 5 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 mini-processor-multiregistry-test/.gitignore create mode 100644 mini-processor-multiregistry-test/build.gradle.kts create mode 100644 mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt diff --git a/README.md b/README.md index 7fce70f..0793d40 100644 --- a/README.md +++ b/README.md @@ -257,32 +257,98 @@ 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`. +For a reviewer-facing multi-registry example, see the JVM integration test module `mini-processor-multiregistry-test`, which loads generated registries from both KAPT and KSP modules on the same classpath. + ## 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>() // 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 // Initialize Mini -storeSubscriptions = MiniGen.subscribe(dispatcher, stores) +storeSubscriptions = Mini.link(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. +### Multi-module registry model +Mini now generates one registry per consumer module instead of relying on a single global generated class. + +- Modules with Mini reducers generate their own `MiniRegistry` implementation. +- Registries are discovered automatically at runtime when you call `Mini.link(...)`. +- Reducer-only modules still generate registries even when they do not declare local `@Action` classes. +- If no new registries are found, Mini falls back to the legacy single generated registry for backward compatibility. + +This allows multiple consumer modules to coexist on the same classpath without generated class collisions. + +### Bootstrap patterns +There are two valid ways to bootstrap Mini in an app that uses multiple modules: + +#### Host-global bootstrap +Use this when a host app owns the shared `Dispatcher` and the set of stores. + +```kotlin +val dispatcher = Dispatcher() +val stores = listOf(featureStoreA, featureStoreB) + +val storeSubscriptions = Mini.link(dispatcher, stores) +stores.forEach { it.initialize() } +``` + +This is the most common choice when all modules participate in one shared runtime. + +#### Module-local bootstrap +Use this when a feature module is intentionally isolated and owns its own `Dispatcher`, stores, and DI graph. + +```kotlin +val dispatcher = Dispatcher() +val featureStore = FeatureStore(featureController) + +val storeSubscriptions = Mini.link(dispatcher, listOf(featureStore)) +featureStore.initialize() +``` + +This is useful for reusable modules or self-contained appcomponents that are embedded in a host app without sharing the host runtime. + +### Registry naming +By default, Mini derives a stable generated registry name from the packages that contain the annotated elements in the module. If you want an explicit name, provide the `mini.registryName` processor option. + +KAPT example: + +```kotlin +kapt { + arguments { + arg("mini.registryName", "feature") + } +} +``` + +KSP example: + +```kotlin +ksp { + arg("mini.registryName", "feature") +} +``` + +Use `mini.registryName` when you want a readable, predictable generated class name. Leave it unset when the stable fallback naming is sufficient. + ## Advanced usages ### Kotlin Flow Utils Mini includes some utility extensions over Kotlin `Flow` to make easier listen state changes over the `StateContainer`s. @@ -447,6 +513,19 @@ kapt.use.worker.api=true org.gradle.caching=true ``` +## How to verify this change +The multi-registry implementation can be verified inside this repository without relying on an external host app: + +```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 +``` + +The `mini-processor-multiregistry-test` module is the smallest reviewer-facing example that demonstrates generated registries from different modules coexisting on the same classpath. + ## Known issues ### KSP gotchas #### KSP code is not recognized by the IntelliJ IDEs diff --git a/mini-processor-multiregistry-test/.gitignore b/mini-processor-multiregistry-test/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/mini-processor-multiregistry-test/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mini-processor-multiregistry-test/build.gradle.kts b/mini-processor-multiregistry-test/build.gradle.kts new file mode 100644 index 0000000..be42abf --- /dev/null +++ b/mini-processor-multiregistry-test/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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. + */ + +plugins { + alias(libs.plugins.kotlin.jvm) +} + +dependencies { + implementation(project(":mini-common")) + implementation(project(":mini-processor-test")) + implementation(project(":mini-processor-ksp-test")) + + testImplementation(libs.junit) + testImplementation(libs.kluent) + testImplementation(libs.kotlinx.coroutines.core) +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = libs.versions.java.sdk.get() + } +} + +java { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) +} + +kotlin { + jvmToolchain(libs.versions.java.sdk.get().toInt()) +} diff --git a/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt new file mode 100644 index 0000000..f1b1313 --- /dev/null +++ b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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.multiregistry + +import kotlinx.coroutines.runBlocking +import mini.Dispatcher +import mini.Mini +import mini.ksptest.KspAnyAction +import mini.ksptest.KspReducersStore +import mini.test.AnyAction +import mini.test.ReducersStore +import org.amshove.kluent.`should be equal to` +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class MultiRegistryIntegrationTest { + + private val kaptStore = ReducersStore() + private val kspStore = KspReducersStore() + private val dispatcher = Dispatcher().apply { + Mini.link(this, listOf(kaptStore, kspStore)) + } + + @Test + fun `action type map merges registries from different modules`() { + assertTrue(dispatcher.actionTypeMap.containsKey(AnyAction::class)) + assertTrue(dispatcher.actionTypeMap.containsKey(KspAnyAction::class)) + } + + @Test + fun `reducers from multiple generated registries are invoked on one dispatcher`() { + runBlocking { + dispatcher.dispatch(AnyAction("kapt-changed")) + dispatcher.dispatch(KspAnyAction("ksp-changed")) + + kaptStore.state.value `should be equal to` "kapt-changed" + kspStore.state.value `should be equal to` "ksp-changed" + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1ab7e10..c7228b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ include(":mini-common") include(":mini-android") include(":mini-processor-test") include(":mini-processor-ksp-test") +include(":mini-processor-multiregistry-test") include(":mini-processor-reducer-only-test") include(":mini-kodein") include(":mini-kodein-android") From 55e973b72cf416d92049db4a0092f7d8e89f5dc6 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 1 Jun 2026 12:05:43 +0200 Subject: [PATCH 04/20] feat: Add multi-registry sample with separate feature modules --- .gitignore | 3 + README.md | 4 + app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 8 +- .../mini/android/sample/MainActivity.kt | 26 ++-- .../sample/MultiRegistrySampleActivity.kt | 130 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 4 +- sample-counter-feature/.gitignore | 1 + sample-counter-feature/build.gradle.kts | 49 +++++++ .../src/main/AndroidManifest.xml | 2 + .../android/sample/counter/CounterFeature.kt | 34 +++++ sample-message-feature/.gitignore | 1 + sample-message-feature/build.gradle.kts | 49 +++++++ .../src/main/AndroidManifest.xml | 2 + .../android/sample/message/MessageFeature.kt | 49 +++++++ settings.gradle.kts | 2 + 17 files changed, 355 insertions(+), 15 deletions(-) create mode 100644 app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt create mode 100644 sample-counter-feature/.gitignore create mode 100644 sample-counter-feature/build.gradle.kts create mode 100644 sample-counter-feature/src/main/AndroidManifest.xml create mode 100644 sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt create mode 100644 sample-message-feature/.gitignore create mode 100644 sample-message-feature/build.gradle.kts create mode 100644 sample-message-feature/src/main/AndroidManifest.xml create mode 100644 sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt diff --git a/.gitignore b/.gitignore index 127240d..99628ac 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 0793d40..d10bc46 100644 --- a/README.md +++ b/README.md @@ -260,8 +260,10 @@ Given the example `Store`s and `Action`s explained before, the workflow would be 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`. +- `MultiRegistrySampleActivity` class uses two feature modules that generate separate registries and are linked by one host `Dispatcher`. For a reviewer-facing multi-registry example, see the JVM integration test module `mini-processor-multiregistry-test`, which loads generated registries from both KAPT and KSP modules on the same classpath. +For a visual Android demonstration, run the sample app and open `MultiRegistrySampleActivity`. ## How to use ### Setting up Mini @@ -526,6 +528,8 @@ The multi-registry implementation can be verified inside this repository without The `mini-processor-multiregistry-test` module is the smallest reviewer-facing example that demonstrates generated registries from different modules coexisting on the same classpath. +If you want to inspect the same idea in a running Android sample, launch the `:app` module and open `MultiRegistrySampleActivity`, which links two feature modules backed by separate generated registries. + ## Known issues ### KSP gotchas #### KSP code is not recognized by the IntelliJ IDEs diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e2bd9a6..4128cbb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,6 +69,8 @@ android { dependencies { implementation(project(":mini-android")) implementation(project(":mini-kodein-android")) + implementation(project(":sample-counter-feature")) + implementation(project(":sample-message-feature")) // kapt(project(":mini-processor")) ksp(project(":mini-processor")) @@ -85,10 +87,11 @@ dependencies { implementation(libs.bundles.compose) implementation(libs.androidx.activity) implementation(libs.bundles.androidx.lifecycle) + implementation(libs.timber) // Test testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.espresso) androidTestImplementation(libs.androidx.test.junit) -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ff0a311..23fcbf4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,12 @@ android:exported="false" android:label="@string/view_model_sample_label" android:theme="@style/AppTheme" /> + + - \ No newline at end of file + diff --git a/app/src/main/kotlin/mini/android/sample/MainActivity.kt b/app/src/main/kotlin/mini/android/sample/MainActivity.kt index 7bacd7c..183053a 100644 --- a/app/src/main/kotlin/mini/android/sample/MainActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/MainActivity.kt @@ -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 @@ -64,6 +55,11 @@ class MainActivity : AppCompatActivity() { Intent(this, ViewModelSampleActivity::class.java).apply { startActivity(this) } + }, + onGoToMultiRegistrySampleClicked = { + Intent(this, MultiRegistrySampleActivity::class.java).apply { + startActivity(this) + } } ) } @@ -74,14 +70,16 @@ class MainActivity : AppCompatActivity() { @Composable private fun MainScreen(modifier: Modifier = Modifier, onGoToStoreSampleClicked: () -> Unit = {}, - onGoToViewModelSampleClicked: () -> Unit = {}) { + onGoToViewModelSampleClicked: () -> Unit = {}, + onGoToMultiRegistrySampleClicked: () -> Unit = {}) { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MainContent( modifier = modifier .fillMaxSize() .padding(innerPadding), onGoToStoreSampleClicked = onGoToStoreSampleClicked, - onGoToViewModelSampleClicked = onGoToViewModelSampleClicked + onGoToViewModelSampleClicked = onGoToViewModelSampleClicked, + onGoToMultiRegistrySampleClicked = onGoToMultiRegistrySampleClicked ) } } @@ -90,7 +88,8 @@ private fun MainScreen(modifier: Modifier = Modifier, private fun MainContent( modifier: Modifier = Modifier, onGoToStoreSampleClicked: () -> Unit = {}, - onGoToViewModelSampleClicked: () -> Unit = {} + onGoToViewModelSampleClicked: () -> Unit = {}, + onGoToMultiRegistrySampleClicked: () -> Unit = {} ) { Column( modifier = modifier.fillMaxSize(), @@ -103,6 +102,9 @@ private fun MainContent( Button(onClick = onGoToViewModelSampleClicked) { Text("Go to ViewModel sample") } + Button(onClick = onGoToMultiRegistrySampleClicked) { + Text("Go to Multi-registry sample") + } } } diff --git a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt new file mode 100644 index 0000000..1ad8643 --- /dev/null +++ b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2024 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.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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +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 mini.Dispatcher +import mini.LoggerMiddleware +import mini.Mini +import mini.flow +import mini.android.sample.counter.CounterState +import mini.android.sample.counter.CounterStore +import mini.android.sample.counter.IncrementCounterAction +import mini.android.sample.message.AdvanceMessageAction +import mini.android.sample.message.MessageState +import mini.android.sample.message.MessageStore +import mini.android.sample.message.SetMessageAction +import mini.android.sample.ui.theme.AppTheme +import timber.log.Timber +import java.io.Closeable + +class MultiRegistrySampleActivity : AppCompatActivity() { + + private val dispatcher = Dispatcher() + private val counterStore = CounterStore() + private val messageStore = MessageStore() + private lateinit var storeSubscriptions: Closeable + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + storeSubscriptions = Mini.link(dispatcher, listOf(counterStore, messageStore)) + dispatcher.addMiddleware( + LoggerMiddleware(listOf(counterStore, messageStore), logger = { _, tag, msg -> + Timber.tag(tag).d(msg) + }) + ) + counterStore.initialize() + messageStore.initialize() + + setContent { + AppTheme { + MultiRegistrySampleScreen() + } + } + } + + override fun onDestroy() { + if (this::storeSubscriptions.isInitialized) { + storeSubscriptions.close() + } + counterStore.close() + messageStore.close() + super.onDestroy() + } + + @Composable + private fun MultiRegistrySampleScreen() { + val coroutineScope = rememberCoroutineScope() + val counterState by counterStore.flow().collectAsState(initial = CounterState()) + val messageState by messageStore.flow().collectAsState(initial = MessageState()) + + 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}") + Spacer(modifier = Modifier.height(12.dp)) + Button(onClick = { + dispatcher.dispatchOn(IncrementCounterAction(), coroutineScope) + }) { + Text("Dispatch action to counter module") + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text("Message feature state: ${messageState.text}") + Spacer(modifier = Modifier.height(12.dp)) + Button(onClick = { + dispatcher.dispatchOn(AdvanceMessageAction(), coroutineScope) + }) { + Text("Dispatch action to message module") + } + + Spacer(modifier = Modifier.height(12.dp)) + Button(onClick = { + dispatcher.dispatchOn(SetMessageAction("custom-message"), coroutineScope) + }) { + Text("Dispatch custom message action") + } + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e27044d..c4e6253 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,4 +20,5 @@ Mini Sample Store Sample View Model Sample + Multi-Registry Sample diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb00a08..25585d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ incap = "1.0.0" espresso = "3.6.1" kluent = "1.72" +timber = "5.0.1" androidGradlePlugin = "8.5.0" @@ -89,6 +90,7 @@ espresso = { group = "androidx.test.espresso", name = "espresso-core", version.r junit = { group = "junit", name = "junit", version = "4.13.2" } kluent = { group = "org.amshove.kluent", name = "kluent", version.ref = "kluent" } +timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -110,4 +112,4 @@ androidx-lifecycle = ["androidx-lifecycle-runtime", "androidx-lifecycle-viewmode kotlinpoet = ["kotlinpoet", "kotlinpoet-ksp"] androidx = ["androidx-appcompat", "androidx-appcompat-resources", "androidx-core", "androidx-core-splashscreen", "androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", "androidx-lifecycle-livedata", "androidx-lifecycle-runtime-compose", "androidx-annotation", "androidx-webkit", "androidx-activity", "androidx-activity-compose"] -compose = ["compose-runtime", "compose-runtime-livedata", "compose-ui", "compose-ui-tooling", "compose-ui-util", "compose-foundation", "compose-material", "compose-materialicons", "compose-navigation", "compose-material3"] \ No newline at end of file +compose = ["compose-runtime", "compose-runtime-livedata", "compose-ui", "compose-ui-tooling", "compose-ui-util", "compose-foundation", "compose-material", "compose-materialicons", "compose-navigation", "compose-material3"] diff --git a/sample-counter-feature/.gitignore b/sample-counter-feature/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample-counter-feature/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample-counter-feature/build.gradle.kts b/sample-counter-feature/build.gradle.kts new file mode 100644 index 0000000..ecdf63e --- /dev/null +++ b/sample-counter-feature/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.convention.androidLib) +} + +android { + namespace = "mini.android.sample.counter" + + compileSdk = libs.versions.android.compileSdk.get().toInt() + buildToolsVersion = libs.versions.android.buildTools.get() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + + compileOptions { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + } + + kotlinOptions { + jvmTarget = libs.versions.java.sdk.get() + } +} + +dependencies { + implementation(project(":mini-common")) + kapt(project(":mini-processor")) + + implementation(libs.kotlin.stdlib) +} diff --git a/sample-counter-feature/src/main/AndroidManifest.xml b/sample-counter-feature/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/sample-counter-feature/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt b/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt new file mode 100644 index 0000000..3b3071b --- /dev/null +++ b/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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.counter + +import mini.Action +import mini.Reducer +import mini.State +import mini.Store + +data class CounterState(val count: Int = 0) : State + +@Action +data class IncrementCounterAction(val amount: Int = 1) + +class CounterStore : Store() { + @Reducer + fun increment(state: CounterState, action: IncrementCounterAction): CounterState { + return state.copy(count = state.count + action.amount) + } +} diff --git a/sample-message-feature/.gitignore b/sample-message-feature/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample-message-feature/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample-message-feature/build.gradle.kts b/sample-message-feature/build.gradle.kts new file mode 100644 index 0000000..f04f817 --- /dev/null +++ b/sample-message-feature/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.ksp) + alias(libs.plugins.convention.androidLib) +} + +android { + namespace = "mini.android.sample.message" + + compileSdk = libs.versions.android.compileSdk.get().toInt() + buildToolsVersion = libs.versions.android.buildTools.get() + + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + + compileOptions { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + } + + kotlinOptions { + jvmTarget = libs.versions.java.sdk.get() + } +} + +dependencies { + implementation(project(":mini-common")) + ksp(project(":mini-processor")) + + implementation(libs.kotlin.stdlib) +} diff --git a/sample-message-feature/src/main/AndroidManifest.xml b/sample-message-feature/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8072ee0 --- /dev/null +++ b/sample-message-feature/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt new file mode 100644 index 0000000..68a972b --- /dev/null +++ b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.message + +import mini.Action +import mini.Reducer +import mini.State +import mini.Store + +data class MessageState( + val text: String = "idle", + val version: Int = 0 +) : State + +@Action +data class SetMessageAction(val value: String) + +@Action +data class AdvanceMessageAction(val prefix: String = "message") + +class MessageStore : Store() { + @Reducer + fun setMessage(state: MessageState, action: SetMessageAction): MessageState { + return state.copy(text = action.value) + } + + @Reducer + fun advanceMessage(state: MessageState, action: AdvanceMessageAction): MessageState { + val nextVersion = state.version + 1 + return state.copy( + text = "${action.prefix}-$nextVersion", + version = nextVersion + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c7228b7..a048d88 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,8 @@ @file:Suppress("UnstableApiUsage") include(":app") +include(":sample-counter-feature") +include(":sample-message-feature") include(":mini-processor") include(":mini-common") include(":mini-android") From 89dcedb692edd357a4cbf8f5941e685589876566 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 09:59:13 +0200 Subject: [PATCH 05/20] refactor: Require explicit MiniRegistry in link functions and remove dynamic loading --- mini-common/src/main/java/mini/Mini.kt | 77 ++++--------------- mini-common/src/test/kotlin/mini/MiniTest.kt | 72 ++++++++--------- mini-processor-ksp-test/build.gradle.kts | 4 + .../java/mini/ksptest/KspReducersStoreTest.kt | 3 +- .../MultiRegistryIntegrationTest.kt | 24 ++++-- .../build.gradle.kts | 6 ++ .../mini/reduceronly/ReducerOnlyStoreTest.kt | 3 +- mini-processor-test/build.gradle.kts | 6 ++ .../test/java/mini/test/ReducersStoreTest.kt | 5 +- .../java/mini/processor/kapt/Processor.kt | 24 +++++- .../mini/processor/kapt/ProcessorUtils.kt | 12 --- .../mini/processor/ksp/MiniSymbolProcessor.kt | 29 ++++--- 12 files changed, 125 insertions(+), 140 deletions(-) diff --git a/mini-common/src/main/java/mini/Mini.kt b/mini-common/src/main/java/mini/Mini.kt index fe49e63..eb41ad3 100644 --- a/mini-common/src/main/java/mini/Mini.kt +++ b/mini-common/src/main/java/mini/Mini.kt @@ -19,60 +19,21 @@ package mini import java.io.Closeable -import java.util.ServiceLoader -import kotlin.reflect.KClass - -const val DISPATCHER_FACTORY_CLASS_NAME = "mini.codegen.Mini_Generated" - -internal object MiniRuntime { - var loadRegistries: () -> List = { - ServiceLoader.load(MiniRegistry::class.java, Mini::class.java.classLoader).iterator().asSequence().toList() - } - - var loadLegacyRegistry: () -> MiniRegistry = { - try { - Class.forName(DISPATCHER_FACTORY_CLASS_NAME).getField("INSTANCE").get(null) as MiniRegistry - } catch (ex: Throwable) { - throw ClassNotFoundException("Failed to load generated class $DISPATCHER_FACTORY_CLASS_NAME, " + - "most likely the annotation processor did not run, add it as dependency to the project", ex) - } - } - - fun reset() { - loadRegistries = { - ServiceLoader.load(MiniRegistry::class.java, Mini::class.java.classLoader).iterator().asSequence().toList() - } - loadLegacyRegistry = { - try { - Class.forName(DISPATCHER_FACTORY_CLASS_NAME).getField("INSTANCE").get(null) as MiniRegistry - } catch (ex: Throwable) { - throw ClassNotFoundException("Failed to load generated class $DISPATCHER_FACTORY_CLASS_NAME, " + - "most likely the annotation processor did not run, add it as dependency to the project", ex) - } - } - } -} abstract class Mini : MiniRegistry { companion object { - private fun registries(): List { - val registries = MiniRuntime.loadRegistries() - return if (registries.isNotEmpty()) registries else listOf(MiniRuntime.loadLegacyRegistry()) - } - /** * Generate all subscriptions from @[Reducer] annotated methods and bundle * into a single Closeable. */ - fun link(dispatcher: Dispatcher, container: StateContainer<*>): Closeable { - ensureDispatcherInitialized(dispatcher) + fun link(registry: MiniRegistry, + dispatcher: Dispatcher, + container: StateContainer<*>): Closeable { + ensureDispatcherInitialized(registry, dispatcher) val c = CompositeCloseable() - val registries = registries() - registries.forEach { registry -> - c.add(registry.subscribe(dispatcher, container)) - } + c.add(registry.subscribe(dispatcher, container)) return c } @@ -80,32 +41,23 @@ abstract class Mini : MiniRegistry { * Generate all subscriptions from @[Reducer] annotated methods and bundle * into a single Closeable. */ - fun link(dispatcher: Dispatcher, containers: Iterable>): Closeable { - ensureDispatcherInitialized(dispatcher) + fun link(registry: MiniRegistry, + dispatcher: Dispatcher, + containers: Iterable>): Closeable { + ensureDispatcherInitialized(registry, dispatcher) val c = CompositeCloseable() - val registries = registries() containers.forEach { container -> - registries.forEach { registry -> - c.add(registry.subscribe(dispatcher, container)) - } + c.add(registry.subscribe(dispatcher, container)) } return c } - private fun ensureDispatcherInitialized(dispatcher: Dispatcher) { + private fun ensureDispatcherInitialized(registry: MiniRegistry, dispatcher: Dispatcher) { if (dispatcher.actionTypeMap.isEmpty()) { - dispatcher.actionTypeMap = mergeActionTypes(registries()) + dispatcher.actionTypeMap = registry.actionTypes } } - private fun mergeActionTypes(registries: List): Map, List>> { - return registries - .asSequence() - .flatMap { it.actionTypes.asSequence() } - .groupBy({ it.key }, { it.value }) - .mapValues { (_, values) -> values.flatten().distinct() } - } - } /** @@ -119,11 +71,8 @@ abstract class Mini : MiniRegistry { */ protected fun subscribe(dispatcher: Dispatcher, containers: Iterable>): Closeable { val c = CompositeCloseable() - val registries = registries() containers.forEach { container -> - registries.forEach { registry -> - c.add(registry.subscribe(dispatcher, container)) - } + c.add(subscribe(dispatcher, container)) } return c } diff --git a/mini-common/src/test/kotlin/mini/MiniTest.kt b/mini-common/src/test/kotlin/mini/MiniTest.kt index efb8983..e542b4a 100644 --- a/mini-common/src/test/kotlin/mini/MiniTest.kt +++ b/mini-common/src/test/kotlin/mini/MiniTest.kt @@ -17,74 +17,68 @@ package mini import org.amshove.kluent.`should be equal to` -import org.junit.After import org.junit.Test import java.io.Closeable import kotlin.reflect.KClass class MiniTest { - @After - fun tearDown() { - MiniRuntime.reset() - } - @Test - fun `link initializes dispatcher action types from all registries`() { + fun `link initializes dispatcher action types from explicit registry`() { val dispatcher = Dispatcher() val store = SampleStore() - MiniRuntime.loadRegistries = { - listOf( - TestRegistry( - actionTypes = mapOf(TestAction::class to listOf(TestAction::class, Any::class)), - accepts = setOf(SampleStore::class) - ), - TestRegistry( - actionTypes = mapOf(TestAction::class to listOf(TestAction::class, State::class)), - accepts = emptySet() - ) - ) - } + val registry = TestRegistry( + actionTypes = mapOf(TestAction::class to listOf(TestAction::class, Any::class)) + ) - Mini.link(dispatcher, store).close() + Mini.link(registry, dispatcher, store).close() - dispatcher.actionTypeMap[TestAction::class] `should be equal to` listOf(TestAction::class, Any::class, State::class) + dispatcher.actionTypeMap[TestAction::class] `should be equal to` listOf(TestAction::class, Any::class) } @Test - fun `link delegates subscriptions to all registries`() { + fun `link delegates subscriptions only to explicit registry`() { val dispatcher = Dispatcher() val store = SampleStore() - val appRegistry = TestRegistry(emptyMap(), accepts = setOf(SampleStore::class)) - val unrelatedRegistry = TestRegistry(emptyMap(), accepts = emptySet()) - MiniRuntime.loadRegistries = { listOf(appRegistry, unrelatedRegistry) } + val registry = TestRegistry(emptyMap()) - Mini.link(dispatcher, store).close() + Mini.link(registry, dispatcher, store).close() - appRegistry.subscriptionCount `should be equal to` 1 - unrelatedRegistry.subscriptionCount `should be equal to` 1 + registry.subscriptionCount `should be equal to` 1 } @Test - fun `link falls back to legacy registry when no new registries are present`() { + fun `link supports multiple containers with one local registry`() { val dispatcher = Dispatcher() + val firstStore = SampleStore() + val secondStore = SampleStore() + val registry = TestRegistry(emptyMap()) + + Mini.link(registry, dispatcher, listOf(firstStore, secondStore)).close() + + registry.subscriptionCount `should be equal to` 2 + } + + @Test + fun `link does not override a dispatcher action map that is already initialized`() { + val existingActionMap: Map, List>> = mapOf( + TestAction::class to listOf(State::class) + ) + val dispatcher = Dispatcher().apply { + actionTypeMap = existingActionMap + } val store = SampleStore() - val legacyRegistry = TestRegistry( - actionTypes = mapOf(TestAction::class to listOf(TestAction::class)), - accepts = setOf(SampleStore::class) + val registry = TestRegistry( + actionTypes = mapOf(TestAction::class to listOf(TestAction::class, Any::class)) ) - MiniRuntime.loadRegistries = { emptyList() } - MiniRuntime.loadLegacyRegistry = { legacyRegistry } - Mini.link(dispatcher, store).close() + Mini.link(registry, dispatcher, store).close() - dispatcher.actionTypeMap[TestAction::class] `should be equal to` listOf(TestAction::class) - legacyRegistry.subscriptionCount `should be equal to` 1 + dispatcher.actionTypeMap `should be equal to` existingActionMap } private class TestRegistry( - override val actionTypes: Map, List>>, - private val accepts: Set> + override val actionTypes: Map, List>> ) : MiniRegistry { var subscriptionCount = 0 diff --git a/mini-processor-ksp-test/build.gradle.kts b/mini-processor-ksp-test/build.gradle.kts index 4b020a5..da8dccf 100644 --- a/mini-processor-ksp-test/build.gradle.kts +++ b/mini-processor-ksp-test/build.gradle.kts @@ -19,6 +19,10 @@ plugins { alias(libs.plugins.kotlin.ksp) } +ksp { + arg("mini.registryName", "processor_ksp_test") +} + dependencies { implementation(project(":mini-common")) ksp(project(":mini-processor")) diff --git a/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt index c96697c..e0e3fee 100644 --- a/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt +++ b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt @@ -19,6 +19,7 @@ package mini.ksptest import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini +import mini.codegen.Mini_Generated_processor_ksp_test import org.amshove.kluent.`should be equal to` import org.junit.Test @@ -26,7 +27,7 @@ internal class KspReducersStoreTest { private val store = KspReducersStore() private val dispatcher = Dispatcher().apply { - Mini.link(this, listOf(store)) + Mini.link(Mini_Generated_processor_ksp_test(), this, listOf(store)) } @Test diff --git a/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt index f1b1313..f295c54 100644 --- a/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt +++ b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt @@ -19,11 +19,14 @@ package mini.multiregistry import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini +import mini.codegen.Mini_Generated_processor_ksp_test +import mini.codegen.Mini_Generated_processor_test import mini.ksptest.KspAnyAction import mini.ksptest.KspReducersStore import mini.test.AnyAction import mini.test.ReducersStore import org.amshove.kluent.`should be equal to` +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -31,21 +34,26 @@ internal class MultiRegistryIntegrationTest { private val kaptStore = ReducersStore() private val kspStore = KspReducersStore() - private val dispatcher = Dispatcher().apply { - Mini.link(this, listOf(kaptStore, kspStore)) + private val kaptDispatcher = Dispatcher().apply { + Mini.link(Mini_Generated_processor_test(), this, listOf(kaptStore)) + } + private val kspDispatcher = Dispatcher().apply { + Mini.link(Mini_Generated_processor_ksp_test(), this, listOf(kspStore)) } @Test - fun `action type map merges registries from different modules`() { - assertTrue(dispatcher.actionTypeMap.containsKey(AnyAction::class)) - assertTrue(dispatcher.actionTypeMap.containsKey(KspAnyAction::class)) + fun `explicit local registries keep action maps isolated across modules`() { + assertTrue(kaptDispatcher.actionTypeMap.containsKey(AnyAction::class)) + assertFalse(kaptDispatcher.actionTypeMap.containsKey(KspAnyAction::class)) + assertTrue(kspDispatcher.actionTypeMap.containsKey(KspAnyAction::class)) + assertFalse(kspDispatcher.actionTypeMap.containsKey(AnyAction::class)) } @Test - fun `reducers from multiple generated registries are invoked on one dispatcher`() { + fun `generated registries from different modules coexist without collisions`() { runBlocking { - dispatcher.dispatch(AnyAction("kapt-changed")) - dispatcher.dispatch(KspAnyAction("ksp-changed")) + kaptDispatcher.dispatch(AnyAction("kapt-changed")) + kspDispatcher.dispatch(KspAnyAction("ksp-changed")) kaptStore.state.value `should be equal to` "kapt-changed" kspStore.state.value `should be equal to` "ksp-changed" diff --git a/mini-processor-reducer-only-test/build.gradle.kts b/mini-processor-reducer-only-test/build.gradle.kts index 63df44c..897266e 100644 --- a/mini-processor-reducer-only-test/build.gradle.kts +++ b/mini-processor-reducer-only-test/build.gradle.kts @@ -19,6 +19,12 @@ plugins { alias(libs.plugins.kotlin.kapt) } +kapt { + arguments { + arg("mini.registryName", "reducer_only_test") + } +} + dependencies { implementation(project(":mini-common")) implementation(project(":mini-processor-test")) diff --git a/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt index 3ab8784..0255f35 100644 --- a/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt +++ b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt @@ -19,6 +19,7 @@ package mini.reduceronly import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini +import mini.codegen.Mini_Generated_reducer_only_test import mini.test.AnyAction import org.amshove.kluent.`should be equal to` import org.junit.Test @@ -27,7 +28,7 @@ internal class ReducerOnlyStoreTest { private val store = ReducerOnlyStore() private val dispatcher = Dispatcher().apply { - Mini.link(this, listOf(store)) + Mini.link(Mini_Generated_reducer_only_test(), this, listOf(store)) } @Test diff --git a/mini-processor-test/build.gradle.kts b/mini-processor-test/build.gradle.kts index edcd12d..72a1f54 100644 --- a/mini-processor-test/build.gradle.kts +++ b/mini-processor-test/build.gradle.kts @@ -19,6 +19,12 @@ plugins { alias(libs.plugins.kotlin.kapt) } +kapt { + arguments { + arg("mini.registryName", "processor_test") + } +} + dependencies { implementation(project(":mini-common")) kapt(project(":mini-processor")) diff --git a/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt b/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt index 11c39eb..5f97ef7 100644 --- a/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt +++ b/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt @@ -19,6 +19,7 @@ package mini.test import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini +import mini.codegen.Mini_Generated_processor_test import org.amshove.kluent.`should equal` import org.junit.Test @@ -26,7 +27,7 @@ internal class ReducersStoreTest { private val store = ReducersStore() private val dispatcher = Dispatcher().apply { - Mini.link(this, listOf(store)) + Mini.link(Mini_Generated_processor_test(), this, listOf(store)) } @Test @@ -44,4 +45,4 @@ internal class ReducersStoreTest { store.state.value.`should equal`("changed") } } -} \ No newline at end of file +} diff --git a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt index c8b6318..4364ec1 100644 --- a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt +++ b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt @@ -25,10 +25,13 @@ import mini.processor.common.MINI_REGISTRY_NAME_OPTION import mini.processor.common.actions.ActionTypesGenerator import mini.processor.common.getContainerBuilders import mini.processor.common.reducers.ReducersGenerator +import mini.processor.kapt.isSuspending import mini.processor.kapt.actions.KaptActionTypesGeneratorDelegate import mini.processor.kapt.reducers.KaptReducersGeneratorDelegate import javax.annotation.processing.ProcessingEnvironment import javax.annotation.processing.RoundEnvironment +import javax.lang.model.element.Element +import javax.lang.model.element.ExecutableElement import javax.lang.model.SourceVersion class Processor { @@ -57,10 +60,11 @@ class Processor { .map { elementUtils.getPackageOf(it).qualifiedName.toString() } .distinct() - val (containerFile, container, className) = getContainerBuilders(registryName, packageNames) + val (containerFile, container, _) = getContainerBuilders(registryName, packageNames) + val referencedActionElements = reducerActionElements(roundReducers) try { - ActionTypesGenerator(KaptActionTypesGeneratorDelegate(roundActions)).generate(container) + ActionTypesGenerator(KaptActionTypesGeneratorDelegate(roundActions + referencedActionElements)).generate(container) ReducersGenerator(KaptReducersGeneratorDelegate(roundReducers)).generate(container) } catch (e: Throwable) { if (e !is ProcessorException) { @@ -75,8 +79,22 @@ class Processor { .addType(container.build()) .build() .writeToFile(sourceElements = ((roundActions + roundReducers).toTypedArray())) - writeRegistryServiceFile(className.canonicalName, *((roundActions + roundReducers).toTypedArray())) return true } + + private fun reducerActionElements(reducers: Set): Set { + return reducers + .filterIsInstance() + .mapNotNull { reducer -> + val parameters = if (reducer.isSuspending()) reducer.parameters.dropLast(1) else reducer.parameters + val actionIndex = when (parameters.size) { + 1 -> 0 + 2 -> 1 + else -> return@mapNotNull null + } + typeUtils.asElement(parameters[actionIndex].asType()) + } + .toSet() + } } diff --git a/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt b/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt index 433d416..784b2cc 100644 --- a/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt +++ b/mini-processor/src/main/java/mini/processor/kapt/ProcessorUtils.kt @@ -89,18 +89,6 @@ fun FileSpec.writeToFile(vararg sourceElements: Element) { openWriter.close() } -fun writeRegistryServiceFile(registryClassName: String, vararg sourceElements: Element) { - val serviceFileObject = env.filer.createResource( - StandardLocation.CLASS_OUTPUT, - "", - "META-INF/services/mini.MiniRegistry", - *sourceElements - ) - serviceFileObject.openWriter().use { writer -> - writer.write("$registryClassName\n") - } -} - /** * Map [java.lang.Object] to [Any] */ diff --git a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt index f0db1eb..f417f7e 100644 --- a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt +++ b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt @@ -1,12 +1,12 @@ package mini.processor.ksp import com.google.devtools.ksp.processing.CodeGenerator -import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.squareup.kotlinpoet.ksp.writeTo import mini.Action import mini.Reducer @@ -42,10 +42,11 @@ class MiniSymbolProcessor( .map { it.packageName.asString() } .distinct() - val (containerFile, container, className) = getContainerBuilders(registryName, packageNames) + val (containerFile, container, _) = getContainerBuilders(registryName, packageNames) + val referencedActionSymbols = reducerActionDeclarations(reducerSymbols) try { - ActionTypesGenerator(KspActionTypesGeneratorDelegate(actionSymbols.asSequence())).generate(container) + ActionTypesGenerator(KspActionTypesGeneratorDelegate((actionSymbols + referencedActionSymbols).asSequence())).generate(container) ReducersGenerator(KspReducersGeneratorDelegate(reducerSymbols.asSequence())).generate(container) } catch (e: Throwable) { if (e !is ProcessorException) { @@ -64,14 +65,22 @@ class MiniSymbolProcessor( aggregating = true, originatingKSFiles = originatingKsFiles ) - codeGenerator.createNewFileByPath( - Dependencies(aggregating = true, *originatingKsFiles.toTypedArray()), - "META-INF/services/mini.MiniRegistry", - "" - ).writer().use { writer -> - writer.write("${className.canonicalName}\n") - } return emptyList() } + + private fun reducerActionDeclarations(reducerSymbols: List): List { + return reducerSymbols + .filterIsInstance() + .mapNotNull { reducer -> + val parameters = reducer.parameters + val actionIndex = when (parameters.size) { + 1 -> 0 + 2 -> 1 + else -> return@mapNotNull null + } + parameters[actionIndex].type.resolve().declaration as? KSClassDeclaration + } + .distinctBy { it.qualifiedName?.asString() ?: it.simpleName.asString() } + } } From f0ca7ef1e59d11ea0496b4508b6a9dbc2a902521 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 10:13:39 +0200 Subject: [PATCH 06/20] refactor: Migrate to module-local bootstrap and require explicit registry linking --- README.md | 45 +++++++---------- app/build.gradle.kts | 4 ++ .../sample/MultiRegistrySampleActivity.kt | 50 ++++++------------- .../android/sample/StoreSampleActivity.kt | 4 +- .../android/sample/ViewModelSampleActivity.kt | 4 +- sample-counter-feature/build.gradle.kts | 7 +++ .../android/sample/counter/CounterFeature.kt | 28 +++++++++++ sample-message-feature/build.gradle.kts | 5 ++ .../android/sample/message/MessageFeature.kt | 32 ++++++++++++ 9 files changed, 115 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index d10bc46..c16ad76 100644 --- a/README.md +++ b/README.md @@ -260,9 +260,9 @@ Given the example `Store`s and `Action`s explained before, the workflow would be 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`. -- `MultiRegistrySampleActivity` class uses two feature modules that generate separate registries and are linked by one host `Dispatcher`. +- `MultiRegistrySampleActivity` class shows two feature modules running with separate local Mini runtimes inside the same host app. -For a reviewer-facing multi-registry example, see the JVM integration test module `mini-processor-multiregistry-test`, which loads generated registries from both KAPT and KSP modules on the same classpath. +For a JVM example of isolated coexistence, see `mini-processor-multiregistry-test`, which validates that KAPT and KSP generated registries can coexist on the same classpath without sharing runtime state. For a visual Android demonstration, run the sample app and open `MultiRegistrySampleActivity`. ## How to use @@ -272,9 +272,10 @@ You'll need to add the following snippet to the class that initializes your appl ```kotlin val stores = listOf>() // Here you'll set-up you store list, you can retrieve it using your preferred DI framework val dispatcher = Dispatcher() // Create a new dispatcher +val registry = Mini_Generated_app() // Generated MiniRegistry for this module // Initialize Mini -storeSubscriptions = Mini.link(dispatcher, stores) +storeSubscriptions = Mini.link(registry, dispatcher, stores) stores.forEach { store -> store.initialize() } @@ -293,40 +294,30 @@ As soon as you do this, you'll have Mini up and running. You'll then need to dec Mini now generates one registry per consumer module instead of relying on a single global generated class. - Modules with Mini reducers generate their own `MiniRegistry` implementation. -- Registries are discovered automatically at runtime when you call `Mini.link(...)`. +- Registries are used explicitly by the module that owns them. - Reducer-only modules still generate registries even when they do not declare local `@Action` classes. -- If no new registries are found, Mini falls back to the legacy single generated registry for backward compatibility. +- Registries are not discovered or merged automatically across modules. This allows multiple consumer modules to coexist on the same classpath without generated class collisions. ### Bootstrap patterns -There are two valid ways to bootstrap Mini in an app that uses multiple modules: - -#### Host-global bootstrap -Use this when a host app owns the shared `Dispatcher` and the set of stores. - -```kotlin -val dispatcher = Dispatcher() -val stores = listOf(featureStoreA, featureStoreB) - -val storeSubscriptions = Mini.link(dispatcher, stores) -stores.forEach { it.initialize() } -``` - -This is the most common choice when all modules participate in one shared runtime. +Mini bootstrap is module-local. Each module that uses Mini creates its own `Dispatcher`, its own stores, and links them with its own generated registry. #### Module-local bootstrap -Use this when a feature module is intentionally isolated and owns its own `Dispatcher`, stores, and DI graph. +Use this inside the module that owns the Mini runtime. ```kotlin val dispatcher = Dispatcher() val featureStore = FeatureStore(featureController) +val registry = Mini_Generated_feature() -val storeSubscriptions = Mini.link(dispatcher, listOf(featureStore)) +val storeSubscriptions = Mini.link(registry, dispatcher, listOf(featureStore)) featureStore.initialize() ``` -This is useful for reusable modules or self-contained appcomponents that are embedded in a host app without sharing the host runtime. +This is useful for reusable modules or self-contained appcomponents embedded in a host app without exposing Mini as part of their public API. + +If two modules use Mini in the same app, they should own separate registries and separate runtime state. Integration between those modules should happen through normal module APIs, not through a shared Mini bootstrap. ### Registry naming By default, Mini derives a stable generated registry name from the packages that contain the annotated elements in the module. If you want an explicit name, provide the `mini.registryName` processor option. @@ -349,7 +340,7 @@ ksp { } ``` -Use `mini.registryName` when you want a readable, predictable generated class name. Leave it unset when the stable fallback naming is sufficient. +Use `mini.registryName` when you want a readable, predictable generated class name that you can import explicitly in module bootstrap code. Leave it unset when the stable fallback naming is sufficient. ## Advanced usages ### Kotlin Flow Utils @@ -515,8 +506,8 @@ kapt.use.worker.api=true org.gradle.caching=true ``` -## How to verify this change -The multi-registry implementation can be verified inside this repository without relying on an external host app: +## Verification +The isolated local-registry model can be verified inside this repository without relying on an external host app: ```bash ./gradlew :mini-common:test @@ -526,9 +517,9 @@ The multi-registry implementation can be verified inside this repository without ./gradlew :mini-processor-multiregistry-test:test ``` -The `mini-processor-multiregistry-test` module is the smallest reviewer-facing example that demonstrates generated registries from different modules coexisting on the same classpath. +The `mini-processor-multiregistry-test` module is the smallest in-repo example that demonstrates generated registries from different modules coexisting on the same classpath with isolated runtime state. -If you want to inspect the same idea in a running Android sample, launch the `:app` module and open `MultiRegistrySampleActivity`, which links two feature modules backed by separate generated registries. +If you want to inspect the same idea in a running Android sample, launch the `:app` module and open `MultiRegistrySampleActivity`, which uses two feature modules that each own their own local Mini runtime. ## Known issues ### KSP gotchas diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4128cbb..766410d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,6 +22,10 @@ plugins { alias(libs.plugins.convention.androidApp) } +ksp { + arg("mini.registryName", "app_sample") +} + android { namespace = "mini.android.sample" diff --git a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt index 1ad8643..deceb0d 100644 --- a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt @@ -35,39 +35,22 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import mini.Dispatcher -import mini.LoggerMiddleware -import mini.Mini -import mini.flow import mini.android.sample.counter.CounterState -import mini.android.sample.counter.CounterStore -import mini.android.sample.counter.IncrementCounterAction -import mini.android.sample.message.AdvanceMessageAction +import mini.android.sample.counter.CounterFeatureRuntime import mini.android.sample.message.MessageState -import mini.android.sample.message.MessageStore -import mini.android.sample.message.SetMessageAction +import mini.android.sample.message.MessageFeatureRuntime import mini.android.sample.ui.theme.AppTheme import timber.log.Timber -import java.io.Closeable class MultiRegistrySampleActivity : AppCompatActivity() { - private val dispatcher = Dispatcher() - private val counterStore = CounterStore() - private val messageStore = MessageStore() - private lateinit var storeSubscriptions: Closeable + private val counterFeature = CounterFeatureRuntime() + private val messageFeature = MessageFeatureRuntime() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - storeSubscriptions = Mini.link(dispatcher, listOf(counterStore, messageStore)) - dispatcher.addMiddleware( - LoggerMiddleware(listOf(counterStore, messageStore), logger = { _, tag, msg -> - Timber.tag(tag).d(msg) - }) - ) - counterStore.initialize() - messageStore.initialize() + Timber.tag("MiniSample").d("Counter and message features run with isolated local Mini runtimes") setContent { AppTheme { @@ -77,19 +60,16 @@ class MultiRegistrySampleActivity : AppCompatActivity() { } override fun onDestroy() { - if (this::storeSubscriptions.isInitialized) { - storeSubscriptions.close() - } - counterStore.close() - messageStore.close() + counterFeature.close() + messageFeature.close() super.onDestroy() } @Composable private fun MultiRegistrySampleScreen() { val coroutineScope = rememberCoroutineScope() - val counterState by counterStore.flow().collectAsState(initial = CounterState()) - val messageState by messageStore.flow().collectAsState(initial = MessageState()) + val counterState by counterFeature.flow().collectAsState(initial = CounterState()) + val messageState by messageFeature.flow().collectAsState(initial = MessageState()) Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Column( @@ -103,9 +83,9 @@ class MultiRegistrySampleActivity : AppCompatActivity() { Text("Counter feature state: ${counterState.count}") Spacer(modifier = Modifier.height(12.dp)) Button(onClick = { - dispatcher.dispatchOn(IncrementCounterAction(), coroutineScope) + counterFeature.increment(coroutineScope) }) { - Text("Dispatch action to counter module") + Text("Run counter feature") } Spacer(modifier = Modifier.height(32.dp)) @@ -113,16 +93,16 @@ class MultiRegistrySampleActivity : AppCompatActivity() { Text("Message feature state: ${messageState.text}") Spacer(modifier = Modifier.height(12.dp)) Button(onClick = { - dispatcher.dispatchOn(AdvanceMessageAction(), coroutineScope) + messageFeature.advance(coroutineScope) }) { - Text("Dispatch action to message module") + Text("Advance message feature") } Spacer(modifier = Modifier.height(12.dp)) Button(onClick = { - dispatcher.dispatchOn(SetMessageAction("custom-message"), coroutineScope) + messageFeature.setMessage(coroutineScope, "custom-message") }) { - Text("Dispatch custom message action") + Text("Set custom message") } } } diff --git a/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt b/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt index 4ceca41..3033178 100644 --- a/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt @@ -39,13 +39,15 @@ import kotlinx.coroutines.launch import mini.* import mini.android.FluxActivity import mini.android.sample.ui.theme.AppTheme +import mini.codegen.Mini_Generated_app_sample private val dispatcher = Dispatcher() +private val appRegistry = Mini_Generated_app_sample() class MainStore : Store() { init { - Mini.link(dispatcher, this).track() + Mini.link(appRegistry, dispatcher, this).track() } @Reducer diff --git a/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt b/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt index a6df393..67ceab3 100644 --- a/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt @@ -39,8 +39,10 @@ import mini.* import mini.android.FluxActivity import mini.android.FluxStoreViewModel import mini.android.sample.ui.theme.AppTheme +import mini.codegen.Mini_Generated_app_sample private val dispatcher = Dispatcher() +private val appRegistry = Mini_Generated_app_sample() class MainViewModelReducer : NestedStateContainer() { @@ -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) { diff --git a/sample-counter-feature/build.gradle.kts b/sample-counter-feature/build.gradle.kts index ecdf63e..a41ef6d 100644 --- a/sample-counter-feature/build.gradle.kts +++ b/sample-counter-feature/build.gradle.kts @@ -21,6 +21,12 @@ plugins { alias(libs.plugins.convention.androidLib) } +kapt { + arguments { + arg("mini.registryName", "counter_feature") + } +} + android { namespace = "mini.android.sample.counter" @@ -46,4 +52,5 @@ dependencies { kapt(project(":mini-processor")) implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) } diff --git a/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt b/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt index 3b3071b..8e40523 100644 --- a/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt +++ b/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt @@ -16,10 +16,16 @@ package mini.android.sample.counter +import kotlinx.coroutines.CoroutineScope +import mini.Dispatcher +import mini.Mini import mini.Action import mini.Reducer import mini.State import mini.Store +import mini.codegen.Mini_Generated_counter_feature +import mini.flow +import java.io.Closeable data class CounterState(val count: Int = 0) : State @@ -32,3 +38,25 @@ class CounterStore : Store() { return state.copy(count = state.count + action.amount) } } + +class CounterFeatureRuntime : Closeable { + private val registry = Mini_Generated_counter_feature() + private val dispatcher = Dispatcher() + private val store = CounterStore() + private val subscriptions = Mini.link(registry, dispatcher, store) + + init { + store.initialize() + } + + fun flow() = store.flow() + + fun increment(scope: CoroutineScope, amount: Int = 1) { + dispatcher.dispatchOn(IncrementCounterAction(amount), scope) + } + + override fun close() { + subscriptions.close() + store.close() + } +} diff --git a/sample-message-feature/build.gradle.kts b/sample-message-feature/build.gradle.kts index f04f817..5d32b61 100644 --- a/sample-message-feature/build.gradle.kts +++ b/sample-message-feature/build.gradle.kts @@ -21,6 +21,10 @@ plugins { alias(libs.plugins.convention.androidLib) } +ksp { + arg("mini.registryName", "message_feature") +} + android { namespace = "mini.android.sample.message" @@ -46,4 +50,5 @@ dependencies { ksp(project(":mini-processor")) implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) } diff --git a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt index 68a972b..f579d4a 100644 --- a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt +++ b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt @@ -16,10 +16,16 @@ package mini.android.sample.message +import kotlinx.coroutines.CoroutineScope +import mini.Dispatcher +import mini.Mini import mini.Action import mini.Reducer import mini.State import mini.Store +import mini.codegen.Mini_Generated_message_feature +import mini.flow +import java.io.Closeable data class MessageState( val text: String = "idle", @@ -47,3 +53,29 @@ class MessageStore : Store() { ) } } + +class MessageFeatureRuntime : Closeable { + private val registry = Mini_Generated_message_feature() + private val dispatcher = Dispatcher() + private val store = MessageStore() + private val subscriptions = Mini.link(registry, dispatcher, store) + + init { + store.initialize() + } + + fun flow() = store.flow() + + fun advance(scope: CoroutineScope, prefix: String = "message") { + dispatcher.dispatchOn(AdvanceMessageAction(prefix), scope) + } + + fun setMessage(scope: CoroutineScope, value: String) { + dispatcher.dispatchOn(SetMessageAction(value), scope) + } + + override fun close() { + subscriptions.close() + store.close() + } +} From e1f9c96e96cd94a0ce101a3791e4edeedf18aefb Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 10:19:35 +0200 Subject: [PATCH 07/20] refactor: Remove Timber dependency and replace with standard Android Log --- app/build.gradle.kts | 1 - .../kotlin/mini/android/sample/MultiRegistrySampleActivity.kt | 4 ++-- gradle/libs.versions.toml | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 766410d..b8aee54 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,7 +91,6 @@ dependencies { implementation(libs.bundles.compose) implementation(libs.androidx.activity) implementation(libs.bundles.androidx.lifecycle) - implementation(libs.timber) // Test testImplementation(libs.junit) diff --git a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt index deceb0d..393744b 100644 --- a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt @@ -16,6 +16,7 @@ package mini.android.sample +import android.util.Log import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity @@ -40,7 +41,6 @@ import mini.android.sample.counter.CounterFeatureRuntime import mini.android.sample.message.MessageState import mini.android.sample.message.MessageFeatureRuntime import mini.android.sample.ui.theme.AppTheme -import timber.log.Timber class MultiRegistrySampleActivity : AppCompatActivity() { @@ -50,7 +50,7 @@ class MultiRegistrySampleActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Timber.tag("MiniSample").d("Counter and message features run with isolated local Mini runtimes") + Log.d("MiniSample", "Counter and message features run with isolated local Mini runtimes") setContent { AppTheme { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25585d2..c763015 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,6 @@ incap = "1.0.0" espresso = "3.6.1" kluent = "1.72" -timber = "5.0.1" androidGradlePlugin = "8.5.0" @@ -90,7 +89,6 @@ espresso = { group = "androidx.test.espresso", name = "espresso-core", version.r junit = { group = "junit", name = "junit", version = "4.13.2" } kluent = { group = "org.amshove.kluent", name = "kluent", version.ref = "kluent" } -timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From f069ef33e5019e21ec1545fd3ecd38cff9e3f499 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 10:22:34 +0200 Subject: [PATCH 08/20] chore: Update copyright headers to 2026 --- app/build.gradle.kts | 2 +- app/src/main/kotlin/mini/android/sample/MainActivity.kt | 2 +- .../kotlin/mini/android/sample/MultiRegistrySampleActivity.kt | 2 +- app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt | 2 +- .../main/kotlin/mini/android/sample/ViewModelSampleActivity.kt | 2 +- mini-common/src/main/java/mini/MiniRegistry.kt | 2 +- mini-common/src/test/kotlin/mini/MiniTest.kt | 2 +- mini-processor-ksp-test/build.gradle.kts | 2 +- .../src/main/java/mini/ksptest/KspAnyAction.kt | 2 +- .../src/main/java/mini/ksptest/KspBasicState.kt | 2 +- .../src/main/java/mini/ksptest/KspReducersStore.kt | 2 +- .../src/test/java/mini/ksptest/KspReducersStoreTest.kt | 2 +- mini-processor-multiregistry-test/build.gradle.kts | 2 +- .../java/mini/multiregistry/MultiRegistryIntegrationTest.kt | 2 +- mini-processor-reducer-only-test/build.gradle.kts | 2 +- .../src/main/java/mini/reduceronly/ReducerOnlyState.kt | 2 +- .../src/main/java/mini/reduceronly/ReducerOnlyStore.kt | 2 +- .../src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt | 2 +- mini-processor-test/build.gradle.kts | 2 +- sample-counter-feature/build.gradle.kts | 2 +- .../main/kotlin/mini/android/sample/counter/CounterFeature.kt | 2 +- sample-message-feature/build.gradle.kts | 2 +- .../main/kotlin/mini/android/sample/message/MessageFeature.kt | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b8aee54..1402706 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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. diff --git a/app/src/main/kotlin/mini/android/sample/MainActivity.kt b/app/src/main/kotlin/mini/android/sample/MainActivity.kt index 183053a..02be129 100644 --- a/app/src/main/kotlin/mini/android/sample/MainActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/MainActivity.kt @@ -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. diff --git a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt index 393744b..ea80e02 100644 --- a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt @@ -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. diff --git a/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt b/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt index 3033178..51cfdb5 100644 --- a/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt @@ -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. diff --git a/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt b/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt index 67ceab3..b9725a7 100644 --- a/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt @@ -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. diff --git a/mini-common/src/main/java/mini/MiniRegistry.kt b/mini-common/src/main/java/mini/MiniRegistry.kt index ca13236..abc6b6f 100644 --- a/mini-common/src/main/java/mini/MiniRegistry.kt +++ b/mini-common/src/main/java/mini/MiniRegistry.kt @@ -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. diff --git a/mini-common/src/test/kotlin/mini/MiniTest.kt b/mini-common/src/test/kotlin/mini/MiniTest.kt index e542b4a..735140c 100644 --- a/mini-common/src/test/kotlin/mini/MiniTest.kt +++ b/mini-common/src/test/kotlin/mini/MiniTest.kt @@ -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. diff --git a/mini-processor-ksp-test/build.gradle.kts b/mini-processor-ksp-test/build.gradle.kts index da8dccf..0e51c92 100644 --- a/mini-processor-ksp-test/build.gradle.kts +++ b/mini-processor-ksp-test/build.gradle.kts @@ -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. diff --git a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt index e1d5f90..13cea2a 100644 --- a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt +++ b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspAnyAction.kt @@ -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. diff --git a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt index 09415cd..238334a 100644 --- a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt +++ b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspBasicState.kt @@ -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. diff --git a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt index 8dc94fe..a5292f3 100644 --- a/mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt +++ b/mini-processor-ksp-test/src/main/java/mini/ksptest/KspReducersStore.kt @@ -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. diff --git a/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt index e0e3fee..4613a2f 100644 --- a/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt +++ b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt @@ -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. diff --git a/mini-processor-multiregistry-test/build.gradle.kts b/mini-processor-multiregistry-test/build.gradle.kts index be42abf..c7b1d49 100644 --- a/mini-processor-multiregistry-test/build.gradle.kts +++ b/mini-processor-multiregistry-test/build.gradle.kts @@ -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. diff --git a/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt index f295c54..4b6ae5b 100644 --- a/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt +++ b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt @@ -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. diff --git a/mini-processor-reducer-only-test/build.gradle.kts b/mini-processor-reducer-only-test/build.gradle.kts index 897266e..61e5c36 100644 --- a/mini-processor-reducer-only-test/build.gradle.kts +++ b/mini-processor-reducer-only-test/build.gradle.kts @@ -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. diff --git a/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt index 27d2b88..c523396 100644 --- a/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt +++ b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyState.kt @@ -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. diff --git a/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt index cfb9e90..7fc6157 100644 --- a/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt +++ b/mini-processor-reducer-only-test/src/main/java/mini/reduceronly/ReducerOnlyStore.kt @@ -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. diff --git a/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt index 0255f35..aab7897 100644 --- a/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt +++ b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt @@ -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. diff --git a/mini-processor-test/build.gradle.kts b/mini-processor-test/build.gradle.kts index 72a1f54..08da749 100644 --- a/mini-processor-test/build.gradle.kts +++ b/mini-processor-test/build.gradle.kts @@ -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. diff --git a/sample-counter-feature/build.gradle.kts b/sample-counter-feature/build.gradle.kts index a41ef6d..a4ad8a5 100644 --- a/sample-counter-feature/build.gradle.kts +++ b/sample-counter-feature/build.gradle.kts @@ -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. diff --git a/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt b/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt index 8e40523..fdb0b23 100644 --- a/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt +++ b/sample-counter-feature/src/main/kotlin/mini/android/sample/counter/CounterFeature.kt @@ -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. diff --git a/sample-message-feature/build.gradle.kts b/sample-message-feature/build.gradle.kts index 5d32b61..d1958f9 100644 --- a/sample-message-feature/build.gradle.kts +++ b/sample-message-feature/build.gradle.kts @@ -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. diff --git a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt index f579d4a..a48f2b7 100644 --- a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt +++ b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt @@ -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. From 13683b00efea59b887fee6c22280e06978a6c309 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 10:57:46 +0200 Subject: [PATCH 09/20] refactor: Change generated registry naming to use package segments instead of suffixes --- README.md | 6 +++--- .../kotlin/mini/android/sample/StoreSampleActivity.kt | 4 ++-- .../mini/android/sample/ViewModelSampleActivity.kt | 4 ++-- .../src/test/java/mini/ksptest/KspReducersStoreTest.kt | 4 ++-- .../mini/multiregistry/MultiRegistryIntegrationTest.kt | 8 ++++---- .../test/java/mini/reduceronly/ReducerOnlyStoreTest.kt | 4 ++-- .../src/test/java/mini/test/ReducersStoreTest.kt | 4 ++-- .../main/java/mini/processor/common/ContainerBuilders.kt | 9 ++++++--- .../kotlin/mini/android/sample/counter/CounterFeature.kt | 4 ++-- .../kotlin/mini/android/sample/message/MessageFeature.kt | 4 ++-- 10 files changed, 27 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c16ad76..0df4c5f 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ You'll need to add the following snippet to the class that initializes your appl ```kotlin val stores = listOf>() // Here you'll set-up you store list, you can retrieve it using your preferred DI framework val dispatcher = Dispatcher() // Create a new dispatcher -val registry = Mini_Generated_app() // Generated MiniRegistry for this module +val registry = mini.codegen.app.Mini_Generated() // Generated MiniRegistry for this module // Initialize Mini storeSubscriptions = Mini.link(registry, dispatcher, stores) @@ -309,7 +309,7 @@ Use this inside the module that owns the Mini runtime. ```kotlin val dispatcher = Dispatcher() val featureStore = FeatureStore(featureController) -val registry = Mini_Generated_feature() +val registry = mini.codegen.feature.Mini_Generated() val storeSubscriptions = Mini.link(registry, dispatcher, listOf(featureStore)) featureStore.initialize() @@ -340,7 +340,7 @@ ksp { } ``` -Use `mini.registryName` when you want a readable, predictable generated class name that you can import explicitly in module bootstrap code. Leave it unset when the stable fallback naming is sufficient. +Use `mini.registryName` when you want a readable, predictable generated package segment that you can import explicitly in module bootstrap code. Leave it unset when the stable fallback naming is sufficient. ## Advanced usages ### Kotlin Flow Utils diff --git a/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt b/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt index 51cfdb5..101237e 100644 --- a/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/StoreSampleActivity.kt @@ -39,10 +39,10 @@ import kotlinx.coroutines.launch import mini.* import mini.android.FluxActivity import mini.android.sample.ui.theme.AppTheme -import mini.codegen.Mini_Generated_app_sample +import mini.codegen.app_sample.Mini_Generated private val dispatcher = Dispatcher() -private val appRegistry = Mini_Generated_app_sample() +private val appRegistry = Mini_Generated() class MainStore : Store() { diff --git a/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt b/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt index b9725a7..36d83e1 100644 --- a/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/ViewModelSampleActivity.kt @@ -39,10 +39,10 @@ import mini.* import mini.android.FluxActivity import mini.android.FluxStoreViewModel import mini.android.sample.ui.theme.AppTheme -import mini.codegen.Mini_Generated_app_sample +import mini.codegen.app_sample.Mini_Generated private val dispatcher = Dispatcher() -private val appRegistry = Mini_Generated_app_sample() +private val appRegistry = Mini_Generated() class MainViewModelReducer : NestedStateContainer() { diff --git a/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt index 4613a2f..8ae375e 100644 --- a/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt +++ b/mini-processor-ksp-test/src/test/java/mini/ksptest/KspReducersStoreTest.kt @@ -19,7 +19,7 @@ package mini.ksptest import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini -import mini.codegen.Mini_Generated_processor_ksp_test +import mini.codegen.processor_ksp_test.Mini_Generated import org.amshove.kluent.`should be equal to` import org.junit.Test @@ -27,7 +27,7 @@ internal class KspReducersStoreTest { private val store = KspReducersStore() private val dispatcher = Dispatcher().apply { - Mini.link(Mini_Generated_processor_ksp_test(), this, listOf(store)) + Mini.link(Mini_Generated(), this, listOf(store)) } @Test diff --git a/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt index 4b6ae5b..0e87fd8 100644 --- a/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt +++ b/mini-processor-multiregistry-test/src/test/java/mini/multiregistry/MultiRegistryIntegrationTest.kt @@ -19,8 +19,8 @@ package mini.multiregistry import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini -import mini.codegen.Mini_Generated_processor_ksp_test -import mini.codegen.Mini_Generated_processor_test +import mini.codegen.processor_ksp_test.Mini_Generated as KspMiniGenerated +import mini.codegen.processor_test.Mini_Generated as KaptMiniGenerated import mini.ksptest.KspAnyAction import mini.ksptest.KspReducersStore import mini.test.AnyAction @@ -35,10 +35,10 @@ internal class MultiRegistryIntegrationTest { private val kaptStore = ReducersStore() private val kspStore = KspReducersStore() private val kaptDispatcher = Dispatcher().apply { - Mini.link(Mini_Generated_processor_test(), this, listOf(kaptStore)) + Mini.link(KaptMiniGenerated(), this, listOf(kaptStore)) } private val kspDispatcher = Dispatcher().apply { - Mini.link(Mini_Generated_processor_ksp_test(), this, listOf(kspStore)) + Mini.link(KspMiniGenerated(), this, listOf(kspStore)) } @Test diff --git a/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt index aab7897..5565067 100644 --- a/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt +++ b/mini-processor-reducer-only-test/src/test/java/mini/reduceronly/ReducerOnlyStoreTest.kt @@ -19,7 +19,7 @@ package mini.reduceronly import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini -import mini.codegen.Mini_Generated_reducer_only_test +import mini.codegen.reducer_only_test.Mini_Generated import mini.test.AnyAction import org.amshove.kluent.`should be equal to` import org.junit.Test @@ -28,7 +28,7 @@ internal class ReducerOnlyStoreTest { private val store = ReducerOnlyStore() private val dispatcher = Dispatcher().apply { - Mini.link(Mini_Generated_reducer_only_test(), this, listOf(store)) + Mini.link(Mini_Generated(), this, listOf(store)) } @Test diff --git a/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt b/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt index 5f97ef7..676308c 100644 --- a/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt +++ b/mini-processor-test/src/test/java/mini/test/ReducersStoreTest.kt @@ -19,7 +19,7 @@ package mini.test import kotlinx.coroutines.runBlocking import mini.Dispatcher import mini.Mini -import mini.codegen.Mini_Generated_processor_test +import mini.codegen.processor_test.Mini_Generated import org.amshove.kluent.`should equal` import org.junit.Test @@ -27,7 +27,7 @@ internal class ReducersStoreTest { private val store = ReducersStore() private val dispatcher = Dispatcher().apply { - Mini.link(Mini_Generated_processor_test(), this, listOf(store)) + Mini.link(Mini_Generated(), this, listOf(store)) } @Test diff --git a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt index 7cb32e2..d6ad2d5 100644 --- a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt +++ b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt @@ -24,7 +24,7 @@ import mini.Mini const val MINI_REGISTRY_NAME_OPTION = "mini.registryName" const val MINI_REGISTRY_PACKAGE_NAME = "mini.codegen" -private const val GENERATED_REGISTRY_PREFIX = "Mini_Generated_" +private const val GENERATED_REGISTRY_SIMPLE_NAME = "Mini_Generated" data class ContainerBuilders( val fileSpecBuilder: FileSpec.Builder, @@ -43,7 +43,7 @@ fun getContainerBuilders(registryName: String?, packageNames: Iterable): } fun generatedRegistryClassName(registryName: String?, packageNames: Iterable): ClassName { - val suffix = registryName + val registryPackageSegment = registryName ?.trim() ?.takeIf { it.isNotEmpty() } ?.let(::sanitizeRegistryName) @@ -55,7 +55,10 @@ fun generatedRegistryClassName(registryName: String?, packageNames: Iterable() { } class CounterFeatureRuntime : Closeable { - private val registry = Mini_Generated_counter_feature() + private val registry = Mini_Generated() private val dispatcher = Dispatcher() private val store = CounterStore() private val subscriptions = Mini.link(registry, dispatcher, store) diff --git a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt index a48f2b7..fb7ab03 100644 --- a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt +++ b/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt @@ -23,7 +23,7 @@ import mini.Action import mini.Reducer import mini.State import mini.Store -import mini.codegen.Mini_Generated_message_feature +import mini.codegen.message_feature.Mini_Generated import mini.flow import java.io.Closeable @@ -55,7 +55,7 @@ class MessageStore : Store() { } class MessageFeatureRuntime : Closeable { - private val registry = Mini_Generated_message_feature() + private val registry = Mini_Generated() private val dispatcher = Dispatcher() private val store = MessageStore() private val subscriptions = Mini.link(registry, dispatcher, store) From ddbf316ea49f4114f3e1811bc30e7ac609d36832 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 11:29:53 +0200 Subject: [PATCH 10/20] refactor: Migrate sample-counter-feature from KAPT to KSP --- sample-counter-feature/build.gradle.kts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/sample-counter-feature/build.gradle.kts b/sample-counter-feature/build.gradle.kts index a4ad8a5..08973f4 100644 --- a/sample-counter-feature/build.gradle.kts +++ b/sample-counter-feature/build.gradle.kts @@ -17,14 +17,12 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.kotlin.ksp) alias(libs.plugins.convention.androidLib) } -kapt { - arguments { - arg("mini.registryName", "counter_feature") - } +ksp { + arg("mini.registryName", "counter_feature") } android { @@ -49,7 +47,7 @@ android { dependencies { implementation(project(":mini-common")) - kapt(project(":mini-processor")) + ksp(project(":mini-processor")) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) From 8429e96bb4aebccdf246acef228b1d0054798ee9 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 11:35:44 +0200 Subject: [PATCH 11/20] docs: Update README to clarify multi-registry examples and add test commands --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0df4c5f..fbaed6b 100644 --- a/README.md +++ b/README.md @@ -262,8 +262,8 @@ You can execute the sample in the `app` package. It contains two different sampl - `ViewModelSampleActivity` class uses a `ViewModel` as a `StateContainer`. - `MultiRegistrySampleActivity` class shows two feature modules running with separate local Mini runtimes inside the same host app. -For a JVM example of isolated coexistence, see `mini-processor-multiregistry-test`, which validates that KAPT and KSP generated registries can coexist on the same classpath without sharing runtime state. -For a visual Android demonstration, run the sample app and open `MultiRegistrySampleActivity`. +The repository also includes `mini-processor-multiregistry-test`, a small JVM example that validates isolated coexistence for generated registries coming from different modules. +To see the same module-local runtime model in the Android sample app, open `MultiRegistrySampleActivity`. ## How to use ### Setting up Mini @@ -507,7 +507,7 @@ org.gradle.caching=true ``` ## Verification -The isolated local-registry model can be verified inside this repository without relying on an external host app: +You can verify the repository with these commands: ```bash ./gradlew :mini-common:test @@ -515,11 +515,12 @@ The isolated local-registry model can be verified inside this repository without ./gradlew :mini-processor-ksp-test:test ./gradlew :mini-processor-reducer-only-test:test ./gradlew :mini-processor-multiregistry-test:test +./gradlew test ``` -The `mini-processor-multiregistry-test` module is the smallest in-repo example that demonstrates generated registries from different modules coexisting on the same classpath with isolated runtime state. +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. -If you want to inspect the same idea in a running Android sample, launch the `:app` module and open `MultiRegistrySampleActivity`, which uses two feature modules that each own their own local Mini runtime. +The Android sample app also includes `MultiRegistrySampleActivity`, which uses two feature modules that each own their own local Mini runtime. ## Known issues ### KSP gotchas From 0340926d1c33a3011b3fa7441e279c006ba426e9 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 12:28:31 +0200 Subject: [PATCH 12/20] refactor: Simplify multi-registry sample and isolate message feature into a separate consumer project --- README.md | 6 +-- app/build.gradle.kts | 1 - app/src/main/AndroidManifest.xml | 4 +- ...ity.kt => CounterFeatureSampleActivity.kt} | 33 ++----------- .../mini/android/sample/MainActivity.kt | 14 +++--- app/src/main/res/values/strings.xml | 2 +- .../src/main/AndroidManifest.xml | 2 - .../isolated-consumer}/.gitignore | 0 samples/isolated-consumer/app/.gitignore | 1 + .../isolated-consumer/app/build.gradle.kts | 49 +++++++++++++++++++ .../main/kotlin/sample/consumer/app/Main.kt | 27 ++++++++++ .../app/ExternalSampleIntegrationTest.kt | 38 ++++++++++++++ samples/isolated-consumer/build.gradle.kts | 19 +++++++ .../message-feature/.gitignore | 1 + .../message-feature}/build.gradle.kts | 37 ++++++-------- .../consumer}/message/MessageFeature.kt | 26 +++++----- samples/isolated-consumer/settings.gradle.kts | 39 +++++++++++++++ settings.gradle.kts | 1 - 18 files changed, 217 insertions(+), 83 deletions(-) rename app/src/main/kotlin/mini/android/sample/{MultiRegistrySampleActivity.kt => CounterFeatureSampleActivity.kt} (67%) delete mode 100644 sample-message-feature/src/main/AndroidManifest.xml rename {sample-message-feature => samples/isolated-consumer}/.gitignore (100%) create mode 100644 samples/isolated-consumer/app/.gitignore create mode 100644 samples/isolated-consumer/app/build.gradle.kts create mode 100644 samples/isolated-consumer/app/src/main/kotlin/sample/consumer/app/Main.kt create mode 100644 samples/isolated-consumer/app/src/test/kotlin/sample/consumer/app/ExternalSampleIntegrationTest.kt create mode 100644 samples/isolated-consumer/build.gradle.kts create mode 100644 samples/isolated-consumer/message-feature/.gitignore rename {sample-message-feature => samples/isolated-consumer/message-feature}/build.gradle.kts (59%) rename {sample-message-feature/src/main/kotlin/mini/android/sample => samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer}/message/MessageFeature.kt (75%) create mode 100644 samples/isolated-consumer/settings.gradle.kts diff --git a/README.md b/README.md index fbaed6b..d9d78bc 100644 --- a/README.md +++ b/README.md @@ -260,10 +260,10 @@ Given the example `Store`s and `Action`s explained before, the workflow would be 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`. -- `MultiRegistrySampleActivity` class shows two feature modules running with separate local Mini runtimes inside the same host app. +- `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. -To see the same module-local runtime model in the Android sample app, open `MultiRegistrySampleActivity`. +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 @@ -520,7 +520,7 @@ You can verify the repository with these commands: 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 `MultiRegistrySampleActivity`, which uses two feature modules that each own their own local Mini runtime. +The Android sample app also includes `CounterFeatureSampleActivity`, which uses a feature module that owns its own local Mini runtime. ## Known issues ### KSP gotchas diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1402706..a6bb3dc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,7 +74,6 @@ dependencies { implementation(project(":mini-android")) implementation(project(":mini-kodein-android")) implementation(project(":sample-counter-feature")) - implementation(project(":sample-message-feature")) // kapt(project(":mini-processor")) ksp(project(":mini-processor")) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 23fcbf4..e459e82 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,9 +48,9 @@ android:theme="@style/AppTheme" /> diff --git a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt b/app/src/main/kotlin/mini/android/sample/CounterFeatureSampleActivity.kt similarity index 67% rename from app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt rename to app/src/main/kotlin/mini/android/sample/CounterFeatureSampleActivity.kt index ea80e02..5a4320a 100644 --- a/app/src/main/kotlin/mini/android/sample/MultiRegistrySampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/CounterFeatureSampleActivity.kt @@ -22,9 +22,7 @@ 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.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Scaffold @@ -38,38 +36,33 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import mini.android.sample.counter.CounterState import mini.android.sample.counter.CounterFeatureRuntime -import mini.android.sample.message.MessageState -import mini.android.sample.message.MessageFeatureRuntime import mini.android.sample.ui.theme.AppTheme -class MultiRegistrySampleActivity : AppCompatActivity() { +class CounterFeatureSampleActivity : AppCompatActivity() { private val counterFeature = CounterFeatureRuntime() - private val messageFeature = MessageFeatureRuntime() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Log.d("MiniSample", "Counter and message features run with isolated local Mini runtimes") + Log.d("MiniSample", "Counter feature runs with its own local Mini runtime inside the main sample app") setContent { AppTheme { - MultiRegistrySampleScreen() + CounterFeatureSampleScreen() } } } override fun onDestroy() { counterFeature.close() - messageFeature.close() super.onDestroy() } @Composable - private fun MultiRegistrySampleScreen() { + private fun CounterFeatureSampleScreen() { val coroutineScope = rememberCoroutineScope() val counterState by counterFeature.flow().collectAsState(initial = CounterState()) - val messageState by messageFeature.flow().collectAsState(initial = MessageState()) Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Column( @@ -81,29 +74,11 @@ class MultiRegistrySampleActivity : AppCompatActivity() { verticalArrangement = Arrangement.Center ) { Text("Counter feature state: ${counterState.count}") - Spacer(modifier = Modifier.height(12.dp)) Button(onClick = { counterFeature.increment(coroutineScope) }) { Text("Run counter feature") } - - Spacer(modifier = Modifier.height(32.dp)) - - Text("Message feature state: ${messageState.text}") - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = { - messageFeature.advance(coroutineScope) - }) { - Text("Advance message feature") - } - - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = { - messageFeature.setMessage(coroutineScope, "custom-message") - }) { - Text("Set custom message") - } } } } diff --git a/app/src/main/kotlin/mini/android/sample/MainActivity.kt b/app/src/main/kotlin/mini/android/sample/MainActivity.kt index 02be129..938fd85 100644 --- a/app/src/main/kotlin/mini/android/sample/MainActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/MainActivity.kt @@ -56,8 +56,8 @@ class MainActivity : AppCompatActivity() { startActivity(this) } }, - onGoToMultiRegistrySampleClicked = { - Intent(this, MultiRegistrySampleActivity::class.java).apply { + onGoToFeatureRuntimeSampleClicked = { + Intent(this, CounterFeatureSampleActivity::class.java).apply { startActivity(this) } } @@ -71,7 +71,7 @@ class MainActivity : AppCompatActivity() { private fun MainScreen(modifier: Modifier = Modifier, onGoToStoreSampleClicked: () -> Unit = {}, onGoToViewModelSampleClicked: () -> Unit = {}, - onGoToMultiRegistrySampleClicked: () -> Unit = {}) { + onGoToFeatureRuntimeSampleClicked: () -> Unit = {}) { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MainContent( modifier = modifier @@ -79,7 +79,7 @@ private fun MainScreen(modifier: Modifier = Modifier, .padding(innerPadding), onGoToStoreSampleClicked = onGoToStoreSampleClicked, onGoToViewModelSampleClicked = onGoToViewModelSampleClicked, - onGoToMultiRegistrySampleClicked = onGoToMultiRegistrySampleClicked + onGoToFeatureRuntimeSampleClicked = onGoToFeatureRuntimeSampleClicked ) } } @@ -89,7 +89,7 @@ private fun MainContent( modifier: Modifier = Modifier, onGoToStoreSampleClicked: () -> Unit = {}, onGoToViewModelSampleClicked: () -> Unit = {}, - onGoToMultiRegistrySampleClicked: () -> Unit = {} + onGoToFeatureRuntimeSampleClicked: () -> Unit = {} ) { Column( modifier = modifier.fillMaxSize(), @@ -102,8 +102,8 @@ private fun MainContent( Button(onClick = onGoToViewModelSampleClicked) { Text("Go to ViewModel sample") } - Button(onClick = onGoToMultiRegistrySampleClicked) { - Text("Go to Multi-registry sample") + Button(onClick = onGoToFeatureRuntimeSampleClicked) { + Text("Go to Counter feature sample") } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4e6253..3ff9f21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,5 +20,5 @@ Mini Sample Store Sample View Model Sample - Multi-Registry Sample + Counter Feature Sample diff --git a/sample-message-feature/src/main/AndroidManifest.xml b/sample-message-feature/src/main/AndroidManifest.xml deleted file mode 100644 index 8072ee0..0000000 --- a/sample-message-feature/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/sample-message-feature/.gitignore b/samples/isolated-consumer/.gitignore similarity index 100% rename from sample-message-feature/.gitignore rename to samples/isolated-consumer/.gitignore diff --git a/samples/isolated-consumer/app/.gitignore b/samples/isolated-consumer/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/samples/isolated-consumer/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/samples/isolated-consumer/app/build.gradle.kts b/samples/isolated-consumer/app/build.gradle.kts new file mode 100644 index 0000000..e3152aa --- /dev/null +++ b/samples/isolated-consumer/app/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * 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. + */ + +plugins { + alias(libs.plugins.kotlin.jvm) + application +} + +dependencies { + implementation(project(":mini-common")) + implementation(project(":message-feature")) + implementation(libs.kotlinx.coroutines.core) + + testImplementation(libs.junit) + testImplementation(libs.kluent) + testImplementation(libs.kotlinx.coroutines.core) +} + +application { + mainClass.set("sample.consumer.app.MainKt") +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = libs.versions.java.sdk.get() + } +} + +java { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) +} + +kotlin { + jvmToolchain(libs.versions.java.sdk.get().toInt()) +} diff --git a/samples/isolated-consumer/app/src/main/kotlin/sample/consumer/app/Main.kt b/samples/isolated-consumer/app/src/main/kotlin/sample/consumer/app/Main.kt new file mode 100644 index 0000000..d2c6639 --- /dev/null +++ b/samples/isolated-consumer/app/src/main/kotlin/sample/consumer/app/Main.kt @@ -0,0 +1,27 @@ +/* + * 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 sample.consumer.app + +import kotlinx.coroutines.runBlocking +import sample.consumer.message.MessageFeatureRuntime + +fun main() = runBlocking { + MessageFeatureRuntime().use { runtime -> + runtime.advance() + println(runtime.state) + } +} diff --git a/samples/isolated-consumer/app/src/test/kotlin/sample/consumer/app/ExternalSampleIntegrationTest.kt b/samples/isolated-consumer/app/src/test/kotlin/sample/consumer/app/ExternalSampleIntegrationTest.kt new file mode 100644 index 0000000..48601a7 --- /dev/null +++ b/samples/isolated-consumer/app/src/test/kotlin/sample/consumer/app/ExternalSampleIntegrationTest.kt @@ -0,0 +1,38 @@ +/* + * 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 sample.consumer.app + +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.`should be equal to` +import org.junit.Test +import sample.consumer.message.MessageFeatureRuntime + +class ExternalSampleIntegrationTest { + + @Test + fun `external consumer can use message feature with its own local mini runtime`() { + runBlocking { + MessageFeatureRuntime().use { runtime -> + runtime.advance() + runtime.state.text `should be equal to` "message-1" + + runtime.setMessage("external-consumer") + runtime.state.text `should be equal to` "external-consumer" + } + } + } +} diff --git a/samples/isolated-consumer/build.gradle.kts b/samples/isolated-consumer/build.gradle.kts new file mode 100644 index 0000000..a489e77 --- /dev/null +++ b/samples/isolated-consumer/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +plugins { + base +} diff --git a/samples/isolated-consumer/message-feature/.gitignore b/samples/isolated-consumer/message-feature/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/samples/isolated-consumer/message-feature/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample-message-feature/build.gradle.kts b/samples/isolated-consumer/message-feature/build.gradle.kts similarity index 59% rename from sample-message-feature/build.gradle.kts rename to samples/isolated-consumer/message-feature/build.gradle.kts index d1958f9..05a61c1 100644 --- a/sample-message-feature/build.gradle.kts +++ b/samples/isolated-consumer/message-feature/build.gradle.kts @@ -15,40 +15,33 @@ */ plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.ksp) - alias(libs.plugins.convention.androidLib) } ksp { - arg("mini.registryName", "message_feature") + arg("mini.registryName", "external_message_feature") } -android { - namespace = "mini.android.sample.message" - - compileSdk = libs.versions.android.compileSdk.get().toInt() - buildToolsVersion = libs.versions.android.buildTools.get() - - defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() - } +dependencies { + implementation(project(":mini-common")) + ksp(project(":mini-processor")) - compileOptions { - sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) - targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) - } + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) +} +tasks.withType().configureEach { kotlinOptions { jvmTarget = libs.versions.java.sdk.get() } } -dependencies { - implementation(project(":mini-common")) - ksp(project(":mini-processor")) +java { + sourceCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.java.sdk.get()) +} - implementation(libs.kotlin.stdlib) - implementation(libs.kotlinx.coroutines.core) +kotlin { + jvmToolchain(libs.versions.java.sdk.get().toInt()) } diff --git a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt b/samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer/message/MessageFeature.kt similarity index 75% rename from sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt rename to samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer/message/MessageFeature.kt index fb7ab03..f6a78fe 100644 --- a/sample-message-feature/src/main/kotlin/mini/android/sample/message/MessageFeature.kt +++ b/samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer/message/MessageFeature.kt @@ -14,17 +14,15 @@ * limitations under the License. */ -package mini.android.sample.message +package sample.consumer.message -import kotlinx.coroutines.CoroutineScope +import mini.Action import mini.Dispatcher import mini.Mini -import mini.Action import mini.Reducer import mini.State import mini.Store -import mini.codegen.message_feature.Mini_Generated -import mini.flow +import mini.codegen.external_message_feature.Mini_Generated import java.io.Closeable data class MessageState( @@ -47,10 +45,7 @@ class MessageStore : Store() { @Reducer fun advanceMessage(state: MessageState, action: AdvanceMessageAction): MessageState { val nextVersion = state.version + 1 - return state.copy( - text = "${action.prefix}-$nextVersion", - version = nextVersion - ) + return state.copy(text = "${action.prefix}-$nextVersion", version = nextVersion) } } @@ -60,18 +55,19 @@ class MessageFeatureRuntime : Closeable { private val store = MessageStore() private val subscriptions = Mini.link(registry, dispatcher, store) + val state: MessageState + get() = store.state + init { store.initialize() } - fun flow() = store.flow() - - fun advance(scope: CoroutineScope, prefix: String = "message") { - dispatcher.dispatchOn(AdvanceMessageAction(prefix), scope) + suspend fun advance(prefix: String = "message") { + dispatcher.dispatch(AdvanceMessageAction(prefix)) } - fun setMessage(scope: CoroutineScope, value: String) { - dispatcher.dispatchOn(SetMessageAction(value), scope) + suspend fun setMessage(value: String) { + dispatcher.dispatch(SetMessageAction(value)) } override fun close() { diff --git a/samples/isolated-consumer/settings.gradle.kts b/samples/isolated-consumer/settings.gradle.kts new file mode 100644 index 0000000..137ffa1 --- /dev/null +++ b/samples/isolated-consumer/settings.gradle.kts @@ -0,0 +1,39 @@ +@file:Suppress("UnstableApiUsage") + +rootProject.name = "isolated-consumer" + +include(":app") +include(":message-feature") +include(":mini-common") +include(":mini-processor") + +project(":mini-common").projectDir = file("../../mini-common") +project(":mini-processor").projectDir = file("../../mini-processor") + +includeBuild("../../convention-plugins") + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven { url = java.net.URI("https://jitpack.io") } + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven { url = java.net.URI("https://jitpack.io") } + } + + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a048d88..6bd98a3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,6 @@ include(":app") include(":sample-counter-feature") -include(":sample-message-feature") include(":mini-processor") include(":mini-common") include(":mini-android") From 1134731e2547593c295c587abbc53abd5991f484 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 8 Jun 2026 14:58:32 +0200 Subject: [PATCH 13/20] feat: Integrate isolated consumer message feature into the sample application --- app/build.gradle.kts | 1 + .../sample/CounterFeatureSampleActivity.kt | 24 ++++++++++++++++++- .../sample/consumer/message/MessageFeature.kt | 3 +++ settings.gradle.kts | 3 +++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6bb3dc..0f44871 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,6 +74,7 @@ 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")) diff --git a/app/src/main/kotlin/mini/android/sample/CounterFeatureSampleActivity.kt b/app/src/main/kotlin/mini/android/sample/CounterFeatureSampleActivity.kt index 5a4320a..dbd99be 100644 --- a/app/src/main/kotlin/mini/android/sample/CounterFeatureSampleActivity.kt +++ b/app/src/main/kotlin/mini/android/sample/CounterFeatureSampleActivity.kt @@ -34,18 +34,21 @@ 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 runs with its own local Mini runtime inside the main sample app") + Log.d("MiniSample", "Counter feature and external message feature run with separate local Mini runtimes") setContent { AppTheme { @@ -56,6 +59,7 @@ class CounterFeatureSampleActivity : AppCompatActivity() { override fun onDestroy() { counterFeature.close() + messageFeature.close() super.onDestroy() } @@ -63,6 +67,7 @@ class CounterFeatureSampleActivity : AppCompatActivity() { 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( @@ -79,6 +84,23 @@ class CounterFeatureSampleActivity : AppCompatActivity() { }) { 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") + } } } } diff --git a/samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer/message/MessageFeature.kt b/samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer/message/MessageFeature.kt index f6a78fe..106102a 100644 --- a/samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer/message/MessageFeature.kt +++ b/samples/isolated-consumer/message-feature/src/main/kotlin/sample/consumer/message/MessageFeature.kt @@ -23,6 +23,7 @@ import mini.Reducer import mini.State import mini.Store import mini.codegen.external_message_feature.Mini_Generated +import mini.flow import java.io.Closeable data class MessageState( @@ -58,6 +59,8 @@ class MessageFeatureRuntime : Closeable { val state: MessageState get() = store.state + fun flow() = store.flow() + init { store.initialize() } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6bd98a3..9e5247c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,7 @@ include(":app") include(":sample-counter-feature") +include(":isolated-consumer-message-feature") include(":mini-processor") include(":mini-common") include(":mini-android") @@ -30,6 +31,8 @@ include(":mini-kodein-android") include(":mini-kodein-android-compose") include(":mini-testing") +project(":isolated-consumer-message-feature").projectDir = file("samples/isolated-consumer/message-feature") + // Modules to add as composite builds includeBuild("convention-plugins") From 1335d9ea2391875155fdbaa9ce0c1fe124af26ba Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 15 Jun 2026 11:02:23 +0200 Subject: [PATCH 14/20] refactor: Simplify generated registry class naming and remove package hashing fallback --- README.md | 8 ++- .../processor/common/ContainerBuilders.kt | 35 ++++-------- .../java/mini/processor/kapt/Processor.kt | 6 +- .../mini/processor/ksp/MiniSymbolProcessor.kt | 7 +-- .../processor/common/ContainerBuildersTest.kt | 55 +++++++++++++++++++ 5 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 mini-processor/src/test/java/mini/processor/common/ContainerBuildersTest.kt diff --git a/README.md b/README.md index d9d78bc..a1d6181 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,9 @@ This is useful for reusable modules or self-contained appcomponents embedded in If two modules use Mini in the same app, they should own separate registries and separate runtime state. Integration between those modules should happen through normal module APIs, not through a shared Mini bootstrap. ### Registry naming -By default, Mini derives a stable generated registry name from the packages that contain the annotated elements in the module. If you want an explicit name, provide the `mini.registryName` processor option. +Without extra configuration, Mini generates `mini.codegen.Mini_Generated` for the module. + +That default is stable and works well when only one Mini-enabled module is present on the classpath. If your app or library setup includes more than one Mini-enabled module, configure `mini.registryName` in each one so every generated registry gets its own package. KAPT example: @@ -340,7 +342,9 @@ ksp { } ``` -Use `mini.registryName` when you want a readable, predictable generated package segment that you can import explicitly in module bootstrap code. Leave it unset when the stable fallback naming is sufficient. +Use `mini.registryName` when you want a readable, predictable generated package segment that you can import explicitly in module bootstrap code. + +If multiple modules generate the default `mini.codegen.Mini_Generated`, your build will fail with a duplicate class error. In that case, assign a distinct `mini.registryName` to each Mini-enabled module. ## Advanced usages ### Kotlin Flow Utils diff --git a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt index d6ad2d5..5a3e0c7 100644 --- a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt +++ b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt @@ -32,8 +32,8 @@ data class ContainerBuilders( val className: ClassName ) -fun getContainerBuilders(registryName: String?, packageNames: Iterable): ContainerBuilders { - val containerClassName = generatedRegistryClassName(registryName, packageNames) +fun getContainerBuilders(registryName: String?): ContainerBuilders { + val containerClassName = generatedRegistryClassName(registryName) val containerFile = FileSpec.builder(containerClassName.packageName, containerClassName.simpleName) val container = TypeSpec.classBuilder(containerClassName) @@ -42,35 +42,22 @@ fun getContainerBuilders(registryName: String?, packageNames: Iterable): return ContainerBuilders(containerFile, container, containerClassName) } -fun generatedRegistryClassName(registryName: String?, packageNames: Iterable): ClassName { +fun generatedRegistryClassName(registryName: String?): ClassName { val registryPackageSegment = registryName ?.trim() ?.takeIf { it.isNotEmpty() } ?.let(::sanitizeRegistryName) - ?: packageNames - .map(::sanitizePackageName) - .filter { it.isNotEmpty() } - .sorted() - .joinToString("_") - .takeIf { it.isNotEmpty() } - ?.let(::shortHash) - ?: "default" + ?: "" - // Keep the generated class name stable and move uniqueness into a module-specific - // package segment so each module owns its own generated namespace. - return ClassName("$MINI_REGISTRY_PACKAGE_NAME.$registryPackageSegment", GENERATED_REGISTRY_SIMPLE_NAME) + val packageName = if (registryPackageSegment.isEmpty()) { + MINI_REGISTRY_PACKAGE_NAME + } else { + "$MINI_REGISTRY_PACKAGE_NAME.$registryPackageSegment" + } + + return ClassName(packageName, GENERATED_REGISTRY_SIMPLE_NAME) } private fun sanitizeRegistryName(name: String): String { return name.replace(Regex("[^A-Za-z0-9_]"), "_") } - -private fun sanitizePackageName(packageName: String): String { - return packageName.replace('.', '_') -} - -private fun shortHash(value: String): String { - return value.encodeToByteArray().fold(0x811c9dc5.toInt()) { acc, byte -> - (acc xor byte.toInt()) * 16777619 - }.toUInt().toString(16) -} diff --git a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt index 4364ec1..9364651 100644 --- a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt +++ b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt @@ -56,11 +56,7 @@ class Processor { if (roundActions.isEmpty() && roundReducers.isEmpty()) return false val registryName = env.options[MINI_REGISTRY_NAME_OPTION] - val packageNames = (roundActions + roundReducers) - .map { elementUtils.getPackageOf(it).qualifiedName.toString() } - .distinct() - - val (containerFile, container, _) = getContainerBuilders(registryName, packageNames) + val (containerFile, container, _) = getContainerBuilders(registryName) val referencedActionElements = reducerActionElements(roundReducers) try { diff --git a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt index f417f7e..106f485 100644 --- a/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt +++ b/mini-processor/src/main/java/mini/processor/ksp/MiniSymbolProcessor.kt @@ -37,12 +37,7 @@ class MiniSymbolProcessor( if (actionSymbols.isEmpty() && reducerSymbols.isEmpty()) return emptyList() - val packageNames = (actionSymbols + reducerSymbols) - .filterIsInstance() - .map { it.packageName.asString() } - .distinct() - - val (containerFile, container, _) = getContainerBuilders(registryName, packageNames) + val (containerFile, container, _) = getContainerBuilders(registryName) val referencedActionSymbols = reducerActionDeclarations(reducerSymbols) try { diff --git a/mini-processor/src/test/java/mini/processor/common/ContainerBuildersTest.kt b/mini-processor/src/test/java/mini/processor/common/ContainerBuildersTest.kt new file mode 100644 index 0000000..59b9f1b --- /dev/null +++ b/mini-processor/src/test/java/mini/processor/common/ContainerBuildersTest.kt @@ -0,0 +1,55 @@ +/* + * 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.processor.common + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ContainerBuildersTest { + + @Test + fun `uses stable default registry package when name is missing`() { + val className = generatedRegistryClassName(null) + + assertEquals("mini.codegen", className.packageName) + assertEquals("Mini_Generated", className.simpleName) + } + + @Test + fun `uses stable default registry package when name is blank`() { + val className = generatedRegistryClassName(" ") + + assertEquals("mini.codegen", className.packageName) + assertEquals("Mini_Generated", className.simpleName) + } + + @Test + fun `uses explicit registry name as package segment`() { + val className = generatedRegistryClassName("feature") + + assertEquals("mini.codegen.feature", className.packageName) + assertEquals("Mini_Generated", className.simpleName) + } + + @Test + fun `sanitizes explicit registry name`() { + val className = generatedRegistryClassName("my-feature name") + + assertEquals("mini.codegen.my_feature_name", className.packageName) + assertEquals("Mini_Generated", className.simpleName) + } +} From 6ffc356a861c4caa78409fdb48ebbc69f1d1c631 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 15 Jun 2026 12:29:23 +0200 Subject: [PATCH 15/20] Fix: Mini.kt By: nicolasmertanen --- mini-common/src/main/java/mini/Mini.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mini-common/src/main/java/mini/Mini.kt b/mini-common/src/main/java/mini/Mini.kt index eb41ad3..768a043 100644 --- a/mini-common/src/main/java/mini/Mini.kt +++ b/mini-common/src/main/java/mini/Mini.kt @@ -46,10 +46,15 @@ abstract class Mini : MiniRegistry { containers: Iterable>): Closeable { ensureDispatcherInitialized(registry, dispatcher) val c = CompositeCloseable() - containers.forEach { container -> - c.add(registry.subscribe(dispatcher, container)) + try { + containers.forEach { container -> + c.add(registry.subscribe(dispatcher, container)) + } + return c + } catch (e: Throwable) { + c.close() + throw e } - return c } private fun ensureDispatcherInitialized(registry: MiniRegistry, dispatcher: Dispatcher) { From d2d58087e901dc213745feb1220350747180ecc0 Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 15 Jun 2026 12:37:08 +0200 Subject: [PATCH 16/20] Fix: Processor.kt By: nicolasmertanen --- mini-processor/src/main/java/mini/processor/kapt/Processor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt index 9364651..7477885 100644 --- a/mini-processor/src/main/java/mini/processor/kapt/Processor.kt +++ b/mini-processor/src/main/java/mini/processor/kapt/Processor.kt @@ -91,6 +91,7 @@ class Processor { } typeUtils.asElement(parameters[actionIndex].asType()) } + .distinctBy { it.toString() } .toSet() } } From 88b8ffd390fb1518cd80ce94d4628699f6bcdb2f Mon Sep 17 00:00:00 2001 From: finxo Date: Mon, 15 Jun 2026 13:55:16 +0200 Subject: [PATCH 17/20] Fix: ContainerBuilders.kt By: DaniAguion --- .../src/main/java/mini/processor/common/ContainerBuilders.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt index 5a3e0c7..eb2e0a1 100644 --- a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt +++ b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt @@ -59,5 +59,6 @@ fun generatedRegistryClassName(registryName: String?): ClassName { } private fun sanitizeRegistryName(name: String): String { - return name.replace(Regex("[^A-Za-z0-9_]"), "_") + val sanitized = name.replace(Regex("[^A-Za-z0-9_]"), "_") + return if (sanitized.first().isDigit()) "_$sanitized" else sanitized } From c558103c0af03a97399502c7b5606688897aa26f Mon Sep 17 00:00:00 2001 From: finxo Date: Tue, 16 Jun 2026 12:29:38 +0200 Subject: [PATCH 18/20] Fix: README.md By: adriangl --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1d6181..b7cfd2b 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ You'll need to add the following snippet to the class that initializes your appl ```kotlin val stores = listOf>() // Here you'll set-up you store list, you can retrieve it using your preferred DI framework val dispatcher = Dispatcher() // Create a new dispatcher -val registry = mini.codegen.app.Mini_Generated() // Generated MiniRegistry for this module +val registry = mini.codegen.Mini_Generated() // Generated MiniRegistry for this module // Initialize Mini storeSubscriptions = Mini.link(registry, dispatcher, stores) From cc962cc98f92e56c90890973ed4e5cb23769e3dc Mon Sep 17 00:00:00 2001 From: finxo Date: Tue, 16 Jun 2026 12:32:06 +0200 Subject: [PATCH 19/20] Fix: README.md By: adriangl --- README.md | 47 +++++++---------------------------------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index b7cfd2b..7d7f9e0 100644 --- a/README.md +++ b/README.md @@ -290,42 +290,14 @@ dispatcher.addMiddleware( 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. -### Multi-module registry model -Mini now generates one registry per consumer module instead of relying on a single global generated class. - -- Modules with Mini reducers generate their own `MiniRegistry` implementation. -- Registries are used explicitly by the module that owns them. -- Reducer-only modules still generate registries even when they do not declare local `@Action` classes. -- Registries are not discovered or merged automatically across modules. - -This allows multiple consumer modules to coexist on the same classpath without generated class collisions. - -### Bootstrap patterns -Mini bootstrap is module-local. Each module that uses Mini creates its own `Dispatcher`, its own stores, and links them with its own generated registry. - -#### Module-local bootstrap -Use this inside the module that owns the Mini runtime. - -```kotlin -val dispatcher = Dispatcher() -val featureStore = FeatureStore(featureController) -val registry = mini.codegen.feature.Mini_Generated() - -val storeSubscriptions = Mini.link(registry, dispatcher, listOf(featureStore)) -featureStore.initialize() -``` - -This is useful for reusable modules or self-contained appcomponents embedded in a host app without exposing Mini as part of their public API. - -If two modules use Mini in the same app, they should own separate registries and separate runtime state. Integration between those modules should happen through normal module APIs, not through a shared Mini bootstrap. - -### Registry naming -Without extra configuration, Mini generates `mini.codegen.Mini_Generated` for the module. +## Advanced usages -That default is stable and works well when only one Mini-enabled module is present on the classpath. If your app or library setup includes more than one Mini-enabled module, configure `mini.registryName` in each one so every generated registry gets its own package. +### 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. -KAPT example: +If more than one module uses Mini, assign a distinct `mini.registryName` to each to avoid duplicate class errors: +KAPT: ```kotlin kapt { arguments { @@ -334,19 +306,14 @@ kapt { } ``` -KSP example: - +KSP: ```kotlin ksp { arg("mini.registryName", "feature") } ``` -Use `mini.registryName` when you want a readable, predictable generated package segment that you can import explicitly in module bootstrap code. - -If multiple modules generate the default `mini.codegen.Mini_Generated`, your build will fail with a duplicate class error. In that case, assign a distinct `mini.registryName` to each Mini-enabled module. - -## Advanced usages +The generated registry will be placed under `mini.codegen..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. From 99a904f6da165770d66936bc1f0ae7b24575801b Mon Sep 17 00:00:00 2001 From: finxo Date: Tue, 16 Jun 2026 12:32:57 +0200 Subject: [PATCH 20/20] Fix: ContainerBuilders.kt By: adriangl --- .../src/main/java/mini/processor/common/ContainerBuilders.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt index eb2e0a1..c819c85 100644 --- a/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt +++ b/mini-processor/src/main/java/mini/processor/common/ContainerBuilders.kt @@ -59,6 +59,6 @@ fun generatedRegistryClassName(registryName: String?): ClassName { } private fun sanitizeRegistryName(name: String): String { - val sanitized = name.replace(Regex("[^A-Za-z0-9_]"), "_") + val sanitized = name.lowercase().replace(Regex("[^a-z0-9_]"), "_") return if (sanitized.first().isDigit()) "_$sanitized" else sanitized }