Skip to content

feat: Implement multi-registry support with separate feature modules#131

Open
finxo wants to merge 13 commits into
masterfrom
feat/multi_registry
Open

feat: Implement multi-registry support with separate feature modules#131
finxo wants to merge 13 commits into
masterfrom
feat/multi_registry

Conversation

@finxo

@finxo finxo commented Jun 1, 2026

Copy link
Copy Markdown

PR's key points

  • Introduces multi-registry support allowing separate feature modules to generate their own registries
  • Adds MiniRegistry interface and refactors Mini class to support multiple registries discovery
  • Creates two sample feature modules (sample-counter-feature and sample-message-feature) demonstrating isolated registries
  • Updates documentation with comprehensive multi-module setup guide and bootstrap patterns
  • Adds MultiRegistrySampleActivity to showcase runtime linking of separate feature registries
  • Maintains backward compatibility with legacy single registry approach

How to review this PR?

  1. Start with the documentation updates in README.md to understand the multi-registry concept
  2. Review the new MiniRegistry interface and Mini.link() implementation
  3. Examine the sample feature modules and how they generate separate registries
  4. Test the MultiRegistrySampleActivity in the sample app to see the feature in action
  5. Verify integration tests pass: ./gradlew :mini-processor-multiregistry-test:test

Related Issues (delete if this does not apply)

Definition of Done

  • Tests pass
  • Works with Proguard
  • There is no outcommented or debug code left

@adriangl adriangl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the effort with the PR :). However, I have a very big concern I'd like to discuss: consumer coupling.

The current implementation is built under an assumption: the project's main module imports and configures Mini and modules provide extra stores or actions for the main Mini to link. While this approach may make sense in a multi-module project, if we bundle Mini inside a library, it would require that any module consuming it must:

  1. Be aware that Mini is being used in the library
  2. Know about and explicitly instantiate the library's Store classes in the module
  3. Pass those stores to Mini.link()

This means that if a library uses Mini internally, its consumers are forced to depend on Mini too. That breaks a fundamental principle of library design: implementation details must stay internal. A consumer of a library should just call its public API, not know or care that it uses Mini under the hood.

We have to rethink this approach before we can merge this. The main blocker that currently disallows using it with multiple modules is that we currently create a unique Mini_Generated per project, no matter the modules or libraries that it contains, and that single instance is used by the Mini code to glue everything together. In the multi-module scenario, or lib + consumer that also uses Mini the build doesn't know which Mini_Generated prevails in compilation time and breaks. We should study how to create multiple instances so each module bundles its own Mini and we don't leak it so consumers don't need to implement it to use the module or lib.

One option that comes to my mind is to generate a MiniComponent or something like that that only belongs to the module where Mini is imported, and use that class to link everything inside the module. That way we wouldn't have any issues regarding multi-module conflicts and it would keep the Mini usage as implementation detail of the module or lib, so consumers don't need to implement Mini themselves. We may keep the concept of registries to do this too.

Comment thread gradle/libs.versions.toml Outdated

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" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to add Timber, we can use the standard Android Log class so we keep this unopinionated

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,130 @@
/*
* Copyright 2024 HyperDevs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the template in the .idea/copyright folder to generate this header, I doubt this code was written in 2024 😛.
Check the rest of the new files in the PR so they have the proper, updated copyright header

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +69 to +73
private fun shortHash(value: String): String {
return value.encodeToByteArray().fold(0x811c9dc5.toInt()) { acc, byte ->
(acc xor byte.toInt()) * 16777619
}.toUInt().toString(16)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm... what is this? Can you explain it to me?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13683b0

It was a fallback to name every registry with a unique suffix, but now every Mini_Generated it's called like that and just change the package where it comes from

Comment thread sample-counter-feature/build.gradle.kts Outdated

dependencies {
implementation(project(":mini-common"))
kapt(project(":mini-processor"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ksp for samples, kapt is in maintenance mode, basically: https://developer.android.com/build/migrate-to-ksp

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread README.md Outdated
Comment on lines +518 to +532
## 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.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README should not describe specific change verifications

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import timber.log.Timber
import java.io.Closeable

class MultiRegistrySampleActivity : AppCompatActivity() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make more sense to bundle the multi-module setup as a separate project inside, let's say, a samples folder so we can test this use case in isolation. We can use composite builds there to import the needed Mini modules (or rely on Jitpack dependencies if we want to test with the real dependencies).
That way we can also remove some module overhead from the main Mini project.

@finxo finxo Jun 8, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0340926

Examples has been divided in 2. One in a module within app, and other one outside in a samples' folder. That way I think every possibility of using mini is tested

Comment thread README.md Outdated
Comment on lines +265 to +266
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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentences here are kinda odd, I may rewrite them so they're not focused to a reviewer

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@finxo

finxo commented Jun 8, 2026

Copy link
Copy Markdown
Author

Thanks for the effort with the PR :). However, I have a very big concern I'd like to discuss: consumer coupling.

The current implementation is built under an assumption: the project's main module imports and configures Mini and modules provide extra stores or actions for the main Mini to link. While this approach may make sense in a multi-module project, if we bundle Mini inside a library, it would require that any module consuming it must:

1. Be aware that Mini is being used in the library

2. Know about and explicitly instantiate the library's `Store` classes in the module

3. Pass those stores to `Mini.link()`

This means that if a library uses Mini internally, its consumers are forced to depend on Mini too. That breaks a fundamental principle of library design: implementation details must stay internal. A consumer of a library should just call its public API, not know or care that it uses Mini under the hood.

We have to rethink this approach before we can merge this. The main blocker that currently disallows using it with multiple modules is that we currently create a unique Mini_Generated per project, no matter the modules or libraries that it contains, and that single instance is used by the Mini code to glue everything together. In the multi-module scenario, or lib + consumer that also uses Mini the build doesn't know which Mini_Generated prevails in compilation time and breaks. We should study how to create multiple instances so each module bundles its own Mini and we don't leak it so consumers don't need to implement it to use the module or lib.

One option that comes to my mind is to generate a MiniComponent or something like that that only belongs to the module where Mini is imported, and use that class to link everything inside the module. That way we wouldn't have any issues regarding multi-module conflicts and it would keep the Mini usage as implementation detail of the module or lib, so consumers don't need to implement Mini themselves. We may keep the concept of registries to do this too.

What I have implemented now is a module-local Mini runtime model:

  • each module generates its own local MiniRegistry
  • generated registries are unique per module, so there are no class collisions
  • Mini no longer does global discovery or cross-module composition
  • each module links its own reducers explicitly with its own generated registry
  • a library can keep Mini fully internal to the module and expose only its public API
  • consumers no longer need to know Mini exists, instantiate the module’s stores, or call Mini.link() for that module
    In practice, the generated registry now belongs to the module that owns the Mini wiring, and Mini is no longer treated as a single project-wide glue point. We also validated this in two ways:
  • an in-repo module sample for the internal-module case
  • an external consumer sample under samples/isolated-consumer for the library/consumer case
    So the new model is much closer to the “MiniComponent per module” direction you suggested, while keeping MiniRegistry as the local bootstrap abstraction.

@finxo finxo requested a review from adriangl June 8, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants