Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use koin in multi-module android project (clean arch)? #472

Closed
luna-vulpo opened this issue May 29, 2019 · 14 comments
Closed

How to use koin in multi-module android project (clean arch)? #472

luna-vulpo opened this issue May 29, 2019 · 14 comments

Comments

@luna-vulpo
Copy link

Can you provide some hint (or maybe example or sample) how to proper use Koin in project which use modules to divide into layers according to clean arch.

So the dependences between modules (layer) should be:

  • appPresentation depends on domain
  • domain depends on nothing
  • data module depends on doman

Also:

  • domain is pure kotlin
  • domain has repo interfaces which are implemented in data module
  • domain has SomethingUseCase classes which are used in appPresentation module

Now I pass about data module to appPresenation (it depends also on data) bacause I cannot figure out how to expose *UseCase classes without providing implementations of *repos

Similar question is here but I cannot figure out solution:
https://www.reddit.com/r/androiddev/comments/akmawy/multimodule_clean_architecture_project_make_koin/

@SimonHaenle2
Copy link

SimonHaenle2 commented May 29, 2019

Hi,
we recently did this in our project. The trick is to not provide an Implementation of your repo, but rather an Interface of the repository, which is defined in the "domain" layer. In this case, your UseCase remains uncoupled of the actual implementation of the repo and you could easily switch the dataSource.

You can then "provide" the Repo as follows:

single { PoiRepository(get()) as IPoiRepository }

Our UseCase (we named it Interactor) looks like this:

    factory { GetPoisInteractor(get(), get(), get()) }

And the class itsself:

class GetPoisInteractor(
    subscribeScheduler: ISubscribeScheduler,
    postExecutionThread: IObserveScheduler,
    private val poiRepository: IPoiRepository
)...

We also have it like this:
We have one "Injection" object in each layer so we have an "AppInjection" object which holds the appModule, a "DomainInjection" object which holds the domainModule and a "DataInjection" object which holds the dataModule.
In the application just start koin like this:

startKoin {
            androidContext(this@Application)
            modules(listOf(appModule, domainModule, dataMdoule))
        } 

@luna-vulpo
Copy link
Author

luna-vulpo commented May 29, 2019

thank you a lot @SimonHaenle2 for your response.
I already have that trick: domain has repo interfaces which are implemented in data module.

I don't how to answer to that question: If class PoiRepository is in data module then how it is accessible in koin module which is in appPresentation module? or where you put that module?

So it seems that I miss something. I have only 3 modules. (Maybe I should have additional global app module?)
file: appPresentation/build.gradle has:

dependencies {
    implementation project(':domain')
   // I want to remove below dependency. 
    implementation project(':data') 
}

file: domain/build.gradle has:

dependencies {
   // only pure java/kotlin libs
}

file: data/build.gradle has:

dependencies {
   implementation project(':domain')
}

And main application class is in appPresentation module and look like this:
class App : Application() {

override fun onCreate() {
    super.onCreate()
     startKoin{
        androidLogger()
        androidContext(this@App)
        modules(listOf(appModule, useCaseModule, repoModule, networkModule))
    }
}

Module with Repo is in appPresentation module and looks like this:

val repoModule = module {
    single<FooBarRepo> { FooBarRepoImpl(get(), get()) }
}

but here is the problem: I have compile error here because the definition of FooBarRepoImpl is not known in appPresentation (and that should be and I want to achieve)

@SimonHaenle2
Copy link

SimonHaenle2 commented May 29, 2019

Presentation has aceess to Domain AND Data. That's correct and in my opinion does not violate the clean architecture. So FooBarRepoImpl in my opinion belongs to data module and should be there.

@luna-vulpo
Copy link
Author

luna-vulpo commented May 30, 2019

Are you sure? when project has such configuration then classes and functions form data module are accessible in appPresentation module. I think it shouldn't happen. appPresentation should not have an idea that data module exists at all.

Maybe should be a extra module which would bind all? Or separated configuration of koin in data module and pass to that module "app start event" which starts koin for data module?

@SimonHaenle2
Copy link

Found a pretty nice tutorial that explains whole project setup here:
https://proandroiddev.com/creating-clean-architecture-multi-project-mvp-app-34d753a187ad

Also found many other tutorials as well, in all of them presentation has access to domain AND data

@pablodeafsapps
Copy link

pablodeafsapps commented Jun 4, 2019

I do have a similar implementation to the one described above, and everything is working like a charm so far. However, I am finding many issues when dealing with instrumented tests. In my case, presentation, domain, and data are libraries/modules of the main project (app). The popular app module is simply used for general purposes, such as configuring and starting Koin, for example. However, it seems Koin is not working well when undertaking instrumented tests (UI tests in my case) on a module, presentation for example.

This is part of my test:

@LargeTest
class LoginActivityTest : KoinTest {

    @get:Rule
    var activityRule = ActivityTestRule(LoginActivity::class.java)

    @Before
    fun setUp() {
        startKoin(listOf(presentationLayerModule))
    }

    @After
    fun tearDown() {
        stopKoin()
    }

    @Test
    fun whenActivityStartsLoginIsDisplayed() {
        onView(withId(R.id.activity_login_tv_title))
            .check(matches(isDisplayed()))
    }
}

The above snippet uses version 1.0.2, although a similar problem arises when upgrading to the recent 2.0.1 version. Basically, the error says something like:


java.lang.IllegalStateException: StandAloneContext Koin instance is null
at org.koin.standalone.StandAloneContext.getKoin(StandAloneContext.kt:68)
at org.koin.android.ext.android.ComponentCallbacksExtKt.getKoin(ComponentCallbacksExt.kt:74)
at org.deafsapps.android.cleanapp.presentationlayer.login.view.ui.LoginActivity$$special$$inlined$inject$1.invoke(ComponentCallbacksExt.kt:146)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at org.deafsapps.android.cleanapp.presentationlayer.login.view.ui.LoginActivity.getLoginPresenter(LoginActivity.kt)
at org.deafsapps.android.cleanapp.presentationlayer.login.view.ui.LoginActivity.onResume(LoginActivity.kt:52)
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1257)
at androidx.test.runner.MonitoringInstrumentation.callActivityOnResume(MonitoringInstrumentation.java:734)
at android.app.Activity.performResume(Activity.java:6076)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2975)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3017)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2392)
at android.app.ActivityThread.access$800(ActivityThread.java:151)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1303)

Contrary, I am not having that problem on single-module applications (module app only), so it is clear to me where the problem is. Moreover, no error or issue arises when using Koin for Unit tests. What am I missing?

I believe this is also related to issue #380 (comment)

Thanks!

@mradzinski
Copy link

mradzinski commented Oct 18, 2019

@luna-vulpo I know this is a bit old, but your clean architecture implementation/concept is a bit off. As some others mentioned here, your Presentation module does need to know of Data and Domain, and that doesn't violate the Clean Architecture injection pattern but enforces it.

Simplifying a bit:

  • Presentation depends on Data and Domain, and it's and Android module.
  • Data depends on Domain, implements repositories, and it's an Android library module.
  • Domain contains repositories definitions, interactors implementations, etc, has no dependencies and it's a pure Kotlin/Java module.

As for DI, given that dependency injection is an architectural pattern that manifests separation of concerns at application level, construction concern should be confined to the main module in your application. Library modules should not depend on dependency injection framework that you’re using.

To get a bit more context on this topic, I recommend reading Mark Seemmann’s post titled DI-Friendly Library. Mark is probably one of the most authoritative experts in the world in context of dependency injection, so you can trust his judgement

@dhaksddj
Copy link

Presentation should NOT know about Data.

That final link you gave is a red-herring as it is only about DI not clean architecture layer dependencies.

I suggest looking at what Uncle Bob actually said or the following issue:
android10/Android-CleanArchitecture#136

@arnaudgiuliani arnaudgiuliani added this to Identified in Koin Dev Board Dec 17, 2019
@szymonkubisiak
Copy link

szymonkubisiak commented Jan 12, 2020

@luna-vulpo You're right.
The problem here is that architecture deals with theoretical entities, without regarding practical issues. Just like physics homework that starts with "assume no friction", it cannot be implemented directly. The theoretical division into Presentation, Domain and Data is glaringly ignoring the real-world issue: how to tie them all together and present to the operating system as a single app.

In order to do that while staying 100% faithful to the division, (as you've already noticed) you need a fourth module, lets call it app. It will perform the real world chores: OS entry point, inclusion in gradle dependencies and instantiation of the other 3 modules. This lets you maintain hard architectonic barrier between Presentation and Data. The benefit is that nobody can use data classes in ui even by accident.

However, this usually comes at quite high inconvenience costs while benefits are negligible for an experienced team. Therefore, in common practice, the tiny app duties are merged into (comparatively) huge Presentation module. This creates false appearance that tying them all together is inherent duty of Presentation. It's not - but the whole point of designer's job is to know when to sidestep the rules. As long as you maintain some other kind of barrier between ui and data, it's fine. In a perfect world Data should not export any of it's internals - so creating accidental dependency should be impossible anyway.

@LeoDroidCoder
Copy link

Are you sure? when project has such configuration then classes and functions form data module are accessible in appPresentation module. I think it shouldn't happen. appPresentation should not have an idea that data module exists at all.

Maybe should be a extra module which would bind all? Or separated configuration of koin in data module and pass to that module "app start event" which starts koin for data module?

I struggled with the same question.
And I as surprised to find that in Fernando Cejas's (android10) implementation of the Clean Architecture,
there is a dependency to the data module in presentation:
https://github.com/android10/Android-CleanArchitecture/blob/master/presentation/build.gradle

@LeoDroidCoder
Copy link

Presentation should NOT know about Data.

That final link you gave is a red-herring as it is only about DI not clean architecture layer dependencies.

I suggest looking at what Uncle Bob actually said or the following issue:
android10/Android-CleanArchitecture#136

But it DOES know in this example:
https://github.com/android10/Android-CleanArchitecture/blob/master/presentation/build.gradle

@tamimattafi
Copy link

tamimattafi commented Sep 13, 2021

Are you sure? when project has such configuration then classes and functions form data module are accessible in appPresentation module. I think it shouldn't happen. appPresentation should not have an idea that data module exists at all.

Maybe should be a extra module which would bind all? Or separated configuration of koin in data module and pass to that module "app start event" which starts koin for data module?

Correct, I always have an extra module called view or presentation for activities and fragments, and I do my DI in app module, app module contains DI, Application class and Manifest only.

Here's a project that I wrote while learning Koin and other technologies, I'm not sure if I correctly used the Koin library, but the idea on how to separate Presentation and Data is implemented here https://github.com/tamimattafi/just-do-it

@KimSeongHeon
Copy link

How about seperate presentation from app module in project?

I think that

//build.gradle in presentation Module 
dependencies {
   implementation project(':domain')
}
//build.gradle in app Module
dependencies {
   implementation project(':data')
   implementation project(':domain')
   implementation project(':presentation')
}
// App.kt in app Module
class App : Application() {

override fun onCreate() {
    super.onCreate()
     startKoin{
        androidLogger()
        androidContext(this@App)
        modules(listOf(appModule, useCaseModule, repoModule, networkModule))
    }
}

if you seperate app from presentation Module, presentation Module has only one dependency about domain.
As yoy say, domain Module has no other dependencies.
But I think that app Module is forced to have all dependencies because Koin inject specific instance from field.

I don't know what the answer is, but I solved it with this thought.

@est7
Copy link

est7 commented Jun 21, 2024

@KimSeongHeon If I have a UserInfoBean map to UserInfoModel, where should I put it? domain layer need know both of them?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests