Closed luna-vulpo closed 3 years ago
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))
}
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)
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.
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?
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
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 https://github.com/InsertKoinIO/koin/issues/380#issue-414254115
Thanks!
@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:
Data
and Domain
, and it's and Android module.Domain
, implements repositories, and it's an Android library 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
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: https://github.com/android10/Android-CleanArchitecture/issues/136
@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.
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
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
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
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.
@KimSeongHeon If I have a UserInfoBean map to UserInfoModel, where should I put it? domain layer need know both of them?
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:
Also:
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/