Was testing the following code. For brevity:
Class ActivityA {
Val aViewModel: AViewModel by viewModel()
Fun onCreate(){
val id = ….
getKoin().setProperty(“id’”, id)
loadKoinModules(aModule)
}
}
And in my modules.kt definitions:
Val aModule = modules {
viewModel { AViewModel(getProperty(“id”))} //works
}
//For ActivityB
Val bModule = modules {
viewModel { BViewModel(getProperty(“id”)} // Caused by: org.koin.core.error.MissingPropertyException: Property 'Id' not found
}
Why do I get this error when trying to create the BViewModel instance. I would have thought that getKoin() would be the same Koin Instance retrieved. But it seems to work only within the same Module definition loaded. I can’t get the property ‘id’ for module B. I have to do the same in ActivityA’s onCreate() for ActivityB.
Any explanation or links regarding this would be appreciated or do I have to use something like Koins' Scope feature for this?
Many Thanks
Related
My project has multi modules, and i am using koin. I have HomeActivity which is in home module and AskQuestionFragment which is in feature module. I need to show AskQuestionFragment in HomeActivity, so i try to inject fragment with koin.
factory (named("askFragment")) { AskQuestionFragment() }
then in HomeActivity i write this
private val fragmentAsk by inject<Fragment>(named("askFragment"))
override fun onCreate(...) {
val pagerAdapter = PagerAdapter(fragmentAsk, fragmentOther, fragmentOther2, fragmentBlablabla)
viewPager.adapter = pagerAdapter
}
It gives me error Caused by: org.koin.core.error.NoBeanDefFoundException: No definition found for class:'androidx.fragment.app.Fragment' & qualifier:'askFragment'. Check your definitions!.
For testing purpose, i tried to change
factory (named("askFragment")) { AskQuestionFragment() } to be factory (named("askFragment")) { 74521647256142765412 }
And it run well, so i think koin is not support fragment injection
How to solve it?
Your issue is that you're trying to inject the subtype "Fragment" but your bean definition is for your class "AskQuestionFragment".
Change your declaration like so:
private val fragmentAsk by inject<AskQuestionFragment>(named("askFragment"))
Alternatively you can declare your type like so:
private val fragmentAsk: AskQuestionFragment by inject(named("askFragment"))
In Arrow Kt Documentation on Dependency Injection, the dependency is defined at the "Edge of the World" or in Android could be an Activity or a Fragment. So the given example is as follow:
import Api.*
class SettingsActivity: Activity {
val deps = FetcherDependencies(Either.monadError(), ActivityApiService(this))
override fun onResume() {
val id = deps.createId("1234")
user.text =
id.fix().map { it.toString() }.getOrElse { "" }
friends.text =
deps.getUserFriends(id).fix().getOrElse { emptyList() }.joinToString()
}
}
But now I'm thinking how could the SettingsActivity in the example could be unit tested? Since the dependency is created within the activity, it could no longer be changed for testing?
When using some other Dependency Injection library, this dependency definition is create outside of the class it will be used on. For example in Dagger, a Module class is created to define how the objects (dependencies) are created and an #Inject is used to "inject" the dependency defined inside the module. So now when unit testing the Activity, I just have to define a different module or manually set the value of the dependency to a mock object.
In Dagger you would create a Mock or Test class that you would #Inject instead of ActivityApiService. It is the same here.
Instead of:
class ActivityApiService(val ctx: Context) {
fun createId(): String = doOtherThing(ctx)
}
You do
interface ActivityApiService {
fun createId(): String
}
and now you have 2 implementations, one for prod
class ActivityApiServiceImpl(val ctx: Context): ActivityApiService {
override fun createId(): Unit = doOtherThing(ctx)
}
and another for testing
fun testBla() {
val api = object: ActivityApiService {
override fun createId(): String = "4321"
}
val deps = FetcherDependencies(Either.monadError(), api)
deps.createId("1234") shouldBe "4321"
}
or even use Mockito or a similar tool to create an ActivityApiService.
I have a couple of articles on how to decouple and unitest outside the Android framework that aren't Arrow-related. Check 'Headless development in Fully Reactive Apps' and the related project https://github.com/pakoito/FunctionalAndroidReference.
If your dependency graph becomes too entangled and you'd like some compile-time magic to create those dependencies, you can always create a local class in tests and #Inject the constructor there. The point is to decouple from things that aren't unitestable, like the whole Android framework :D
I have an interface WordsDataSource using which I have implemented two concrete classes namely WordsLocalDataSource that deals with local database and another WordsRemoteDataSource that deals with manipulating data online on the server. The problem is when I try to inject the two classes in repository class using abstract class name WordsDataSource like
DefaultWordsRepository(
private val wordsRemoteDataSource: WordsDataSource,
private val wordsLocalDataSource: WordsDataSource) {
And adding dependencies in Application class like
class WordsApplication : Application(), KodeinAware {
override val kodein = Kodein.lazy {
import(androidXModule(this#WordsApplication))
bind() from singleton { WordsDatabase.getInstance(instance()) }
bind<WordsDao>() with singleton { instance<WordsDatabase>().wordsDao() }
bind() from singleton { WordsLocalDataSource(instance()) }
bind() from singleton { WordsRemoteDataSource() }
bind<WordsRepository>() with singleton { DefaultWordsRepository(instance(), instance()) }
bind() from provider { ViewModelFactory(instance()) }
}
Then upon running the app I encounter the following issue in the logcat
org.kodein.di.Kodein$NotFoundException: 2 bindings found that match bind<WordsDataSource>() with ?<WordsFragment>().? { ? }:
bind<WordsLocalDataSource>() with singleton { WordsLocalDataSource }
bind<WordsRemoteDataSource>() with singleton { WordsRemoteDataSource }
I have tried the workaround for this by simply declaring the variables by their respective concrete class names like
DefaultWordsRepository(
private val wordsRemoteDataSource: WordsRemoteDataSource,
private val wordsLocalDataSource: WordsLocalDataSource) {
But still want to know whether or not is there any way to resolve the issue.
I am using the following dependencies for kodein
implementation "org.kodein.di:kodein-di-generic-jvm:6.3.3"
implementation "org.kodein.di:kodein-di-framework-android-x:6.3.3"
You have done it the right way by writing the explicit types:
DefaultWordsRepository(
private val wordsRemoteDataSource: WordsRemoteDataSource,
private val wordsLocalDataSource: WordsLocalDataSource)
When working with sub-types we cannot know what kind of implementation to choose. Writing
DefaultWordsRepository(
private val wordsRemoteDataSource: WordsDataSource,
private val wordsLocalDataSource: WordsDataSource)
Doesn't cannot find if you want both sub-types or twice the WordsRemoteDataSource or WordsLocalDataSource. Thus, you need to explicit define your types. Even, we could put WordsRemoteDataSource in the property wordsLocalDataSource, as we cannot rely on variable names.
I try to create scope for a feature. I define a module like this.
val appModule = module {
scope(named("ARTIST_SCOPE")) {
scoped {
ArtistRepository(get())
}
scoped {
GetArtistsUseCase(get())
}
viewModel { ArtistViewModel(get()) }
}
}
My goal is to make ArtistRepository, GetArtistUseCase, and ArtistViewModel only accessible inside Artist Feature.
In my activity
class ArtistActivity : AppCompatActivity() {
private val artistScope = getKoin().createScope("artistScope", named("ARTIST_SCOPE"))
private val viewModel: ArtistViewModel by artistScope.viewModel(this)
...
}
My problem is when I leave this activity and return back to it.
I got this error.
org.koin.core.error.ScopeAlreadyCreatedException: A scope with id 'artistScope' already exists. Reuse or close it.
enter code here
How to reuse the existing scope?
or Am I implement the scope in the right way?
You want to use getOrCreateScope(). This will get an existing instance if you have one that isn't closed with the same scopeId, or it will create a new instance if it needs to.
I have a feeling this is a simple misunderstanding of how Dagger works. Alas I'm not able to find the issue. I have a few GotIt like cards that I want to use all over. To do this I'm using a model which includes the icon, body text, action text, and action on-click handler. Various activities use different adapters to represent the state. For now I'm focusing on a card telling the user when they're missing location permissions. What I'd like to do is create a module which provides the various cards, what I have is (using Kotlin):
#Module
class GotItCardModule {
#Provides #Singleton
#Named(Manifest.permission.ACCESS_FINE_LOCATION)
fun provideLocationGotItCard(application: Application): GotItViewHolder.GotItCard {
val icon = ResourcesCompat.getDrawable(application.resources,
R.drawable.ic_location_off_black_24dp, null)?.apply {
DrawableCompat.setTint(this, Color.WHITE)
}
return GotItViewHolder.GotItCard(
iconDrawable = icon,
bodyText = application.getString(R.string.location_permission_gotit_body),
primaryButtonText = application.getString(R.string.location_permission_gotit_action_primary),
primaryButtonCallback = View.OnClickListener { v ->
(v.context as? Activity)?.let { activity ->
ActivityCompat.requestPermissions(activity,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
RequestCode.Permission.ACCESS_FINE_LOCATION)
}
})
}
}
I've updated my AppComponent to include GotItCardModule::class and my MainActivity with:
#Inject #Named(Manifest.permission.ACCESS_FINE_LOCATION)
lateinit var locationGotItCard: GotItViewHolder.GotItCard
When I build I get an error:
Error:(6, 1) error: [dagger.android.AndroidInjector.inject(T)] com.sample.test.adapters.holders.GotItViewHolder.GotItCard cannot be provided without an #Inject constructor or from an #Provides- or #Produces-annotated method.
What I don't understand is that I have an #Provides and I included this module at the very top level. Why can't it resolve this?
UPDATE:
If I remove #Named everything works (I just can't have more than one GotItCard). I also tried replacing the string with a hard-coded string. I still get the same error.