Skip to content

Let each feature declare its own dependency injection module - #87

Merged
AhmadKharfan merged 1 commit into
developfrom
feat/feature-api-modules
Jul 30, 2026
Merged

Let each feature declare its own dependency injection module#87
AhmadKharfan merged 1 commit into
developfrom
feat/feature-api-modules

Conversation

@AhmadKharfan

Copy link
Copy Markdown
Owner

Why

Every Koin definition in the app lived in :app/di, including each feature's ViewModels. That meant :app had to name concrete classes from every feature module — HubViewModel, EditorViewModel, GitAuthSettingsViewModel and the rest — so it could register them.

This is the specific mistake both reference architectures make. Novix binds every XxxFeatureApi to its XxxFeatureApiImpl in app/di/ApiModule.kt, and MENA does the same in composeApp/di/apiModule.kt. The consequence in both is identical: the app module keeps a hard compile dependency on every implementation, and no implementation class can ever be internal.

It also blocks what comes next here. Giving each feature an api module only decouples anything if :app stops needing the implementation — and while :app registers the ViewModels, it needs the implementation regardless of how many contracts exist.

What changed

Each feature now declares what it contributes to the graph:

  • onboardingModule — its two ViewModels and the OnboardingRepository binding
  • settingsModule — five settings ViewModels
  • projectsModule — hub, open, folder-picker and create-project
  • terminalFeatureModule — the terminal ViewModel
  • editorModule — the editor and AI-chat ViewModels, both parameterised by project id

:app/di/KoinModules.kt drops from naming fourteen ViewModels to aggregating a list. What remains in it is genuinely app-scoped: NetworkMonitor.

The OnboardingRepository binding moved to the onboarding feature, where its implementation already lived.

Behavior

No behavior change. The same definitions, declared by the module that owns them.

Scope

The :data:* bindings stay in :app for now. They bind :domain interfaces to :data implementations, which is legitimately the composition root's job — a data module should not have to know it is being bound.

This does not yet remove :app's dependency on the feature implementations; :app still calls each feature's nav graph. Removing that is the point of the api modules that follow, and this change is what makes them able to.

Tests

  • 723 tests passing, 0 failures.
  • KoinModuleGraphTest is the real gate here: it runs Koin's verify() across the assembled graph, so a definition lost in the move fails at test time rather than on launch. It caught exactly this class of mistake earlier in the migration when GradleProjectInspector had no binding.

Verification

./gradlew test detekt verifyModuleBoundaries :app:assembleDebug — BUILD SUCCESSFUL. Boundary report: 0 baseline entries, 0 violations.

Verified on an emulator, resolving a ViewModel from four different feature modules in one session:

  • Hub (HubViewModel)
  • Settings root (SettingsRootViewModel)
  • Settings → Git & GitHub (GitAuthSettingsViewModel)
  • Create project (CreateProjectViewModel)

No NoBeanDefFoundException, InstanceCreationException or crash in logcat.

@AhmadKharfan
AhmadKharfan merged commit 541982b into develop Jul 30, 2026
3 checks passed
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.

1 participant