I work on an Android project using multiple Project Flavors, and we use Koin to inject the appropriate dependencies based on the current flavor.
We already use the checkModules Gradle task (described here : https://start.insert-koin.io/#/getting-started/testing?id=checking-your-modules) in order to ensure that our dependency tree is valid.
However, it seems that there is a use case missing.
Let's say I want to inject an InterfaceA in my Activity. I would write the following code :
class MyActivity : Activity() {
private val interfaceA_Impl: InterfaceA by inject()
...
}
Koin requires the implementation of InterfaceA to be provided in a module, as such :
val myModule = module {
single<InterfaceA> { MyInterfaceImpl() }
}
In my project, each implementation is "flavor-specific".
My question is :
Is there a way to ensure that ALL by inject targets are valid ? In other words, to ensure that all interfaces that I am trying to inject have valid implementations ? Currently, if an implementation is forgotten, the app crashes during runtime, and I would like to know about it sooner (maybe during unit tests, at the same time checkModules is ran ?)
Thanks a lot !
Related
My Android production is code full of Hilt modules that install various production implementations:
#Module
#InstallIn(ApplicationComponent.class)
public abstract class TimeModule {...}
#Module
#InstallIn(ApplicationComponent.class)
public abstract class DatabaseModule {...}
In all my instrumented tests, I would like to replace those bindings with fakes. My test codebase includes modules that bind fake implementations, but having two modules provide the same class obviously causes compile-time errors.
The Hilt documentation recommends using #UninstallModule(), but that means I'd have to add UninstallModule for every single production module in every single test. That seems like the wrong approach.
How would one normally replace production modules with fake modules? And is there a way to install modules from another module like Guice does, so I could remove #InstallIn from all my production modules and instead simply have one ProductionModule that installs all the individual modules? That would make it easier to just uninstall one module in tests.
How would one normally replace production modules with fake modules?
Probably how it's normally done, is like the documentation said with the UninstallModule annotation. But here is an alternative, which I like to use, using build flavors:
I like to organize my project, so there are mock and live flavors. And there are 3 folders inside my app module: src/main/kotlin with Activities, Fragments etc..., src/mock/kotlin where my fake bindings live, and finally src/live/kotlin where my real production bindings live.
Here's the relevant config from my app level build.gradle.kts:
android {
productFlavors {
flavorDimensions("environment")
register("mock") {
dimension = "environment"
}
register("dev") {
dimension = "environment"
}
register("prod") {
dimension = "environment"
}
sourceSets {
getByName("mock").java.srcDir("src/mock/kotlin")
getByName("dev").java.srcDir("src/live/kotlin")
getByName("prod").java.srcDir("src/live/kotlin")
}
}
}
Project structure overview
Inside the live InteractorModule:
#Module
#InstallIn(ApplicationComponent::class)
abstract class InteractorModule {
#Binds
abstract fun bindTodoInteractor(interactor: TodoInteractorImpl): TodoInteractor
}
Inside the FakeInteractorsModule:
#Module
#InstallIn(ApplicationComponent::class)
abstract class InteractorModule {
#Binds
abstract fun bindTodoInteractor(interactor: TodoInteractorFake): TodoInteractor
}
So now you can use the build variant tab to change between the real and the mock implementations of your interfaces. So if you want to use your fakes inside your instrumentation tests use the mock flavor while running the tests.
One upside of this method, is that by changing the build variant, you can swich between your instrumentation tests using your fakes to using the live implementations. Conversly, you can use your fake implementations inside the actuall application, which can be nice, if you just want to try out the app with mock data.
I hope this helped a bit to at least promote some ideas, on how you can solve this "two implementations for one interface" dilemma!
How to add a debug-mode specific functionality into an Android app which is broken into several SDKs?
Lets say I need to add an OkHttp interceptor to all OkHttp clients to all SDKs. But it has to be added only into the app's debug build. However, when the app is broken into several SDKs, they are published as release builds even if the main app is built in the debug mode. Thus, the debug-specific interceptors inside the SDKs would not be added to the OkHttp clients.
How to overcome this issue? I was thinking to take advantage of dependency injection (Koin) and pass Build type information from the app where Koin is started, but not sure Koin supports this feature.
You can select what build variant is active of each of the modules of your application.
https://developer.android.com/studio/build/build-variants
You can use getAll() in Koin for your purposes.
moduleb:
Domain:
interface Interceptor
class InterceptorFactory(val interceptors: List<Interceptor>) // Here you have a list of all interceptors from all modules
class HeadersInterceptor : Interceptor
Koin:
object BKoin {
val network2: Module
get() = module {
single<HeadersInterceptor>() bind Interceptor::class
single<InterceptorFactory> {
InterceptorFactory(getAll<Interceptor>())
}
}
}
module app:
define your objects
class OtherInterceptor : Interceptor
declare them
object AKoin {
val network1: Module
get() = module {
single<OtherInterceptor>() bind Interceptor::class
}
}
init Koin
startKoin {
val app = modules(
listOf(
BKoin.network2,
AKoin.network1
)
)
Log.d("TUT", "${app.koin.get<InterceptorFactory>().interceptors.map { it::class.simpleName }}")
}
Gradle:
dependencies {
implementation project(':moduleb')
}
I'd like to use system functions like getTimeMillis() which should be a part of kotlin.system: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.system/index.html
But the compiler says such module cannot be imported. The gradle configuration is like this (kotlin multiplatform project):
commonMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.10"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.10.0"
implementation "io.ktor:ktor-client:1.0.0"
implementation "io.ktor:ktor-client-logging:1.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.1.0"
}
Also I cannot find any example of usage or this module.
getTimeMillis() is only available for JVM and Native not for Common and JS.
If you but the call to getTimeMillis() in the source directory of the Native module the compiler can find the function.
If you need the call to be in Common you have to implement a Common wrapper function on your own and implement the wrapper on each platform yourself.
In order to do this create a stub function and a function which uses it in your common module . For example:
expect fun getSystemTimeInMillis(): Long
fun printSystemTimeMillis() {
println("System time in millis: ${getSystemTimeInMillis()}")
}
Then implement that function your platform specific modules. For example in a JVM module like:
actual fun getSystemTimeInMillis() = System.currentTimeMillis()
Or in a native module like:
actual fun getSystemTimeInMillis() = getTimeMillis()
See also: https://github.com/eggeral/kotlin-native-system-package
I implemented Jacoco in my Android project using the following tutorial https://proandroiddev.com/unified-code-coverage-for-android-revisited-44789c9b722f to cater for test coverage in the kotlin classes.
For some unknown reason, it's not reporting coverage for static methods declared under the Companion block.
class Meh {
companion object {
fun test () {
// logic to test
}
}
However if I convert the class to an instance rather than a singleton that I am able to see the coverage completely fine.
Has anyone came across this problem ? and what did you do ?
following tutorial https://proandroiddev.com/unified-code-coverage-for-android-revisited-44789c9b722f
after cloning of example from the same tutorial in its state as of today (HEAD commit)
git clone https://github.com/rafaeltoledo/unified-code-coverage-android.git
cd unified-code-coverage-android
git checkout kotlin-coverage
addition of companion object into MainActivity
class MainActivity : AppCompatActivity() {
+ companion object {
+ fun executed() {
+ }
+
+ fun notExecuted() {
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
+ executed()
start of virtual device Pixel XL with API 28 and target Android 9.0 (Google APIs) in freshly downloaded Android Studio 3.2.1
and execution of
./gradlew jacocoTestReport
following report is produced in directory app/build/reports/jacoco/jacocoTestReport/html/ as expected
Given the amount of factors that influence result (such as versions of all involved components - Android SDK, Device, Kotlin compiler, Gradle, JaCoCo, etc, etc), attempts to guess what is different in your case are IMO counterproductive, and so that the best advice - is to perform very careful comparison of differences between your setup and above example.
Update
As was figured out during comparison by #HeWhoProtects , problem was in
exclusion of **/*$*
that refers to exclusion of class files from analysis. Single source file can compile into multiple class files, e.g. in case of nested classes in Java and exactly in case of companion in Kotlin and in both cases name of class and class file will contain $.
I found the the cause of the problem but not sure why it caused it yet, my excludes rules includes more rules than the one in the tutorial above, in different jacoco tutorial for ignoring autogenerated files, it was suggested to include '**/*$*' as rule, as soon as I removed it, it showed coverage for static methods in kotlin.
My understanding of Jacoco that these rules ignore files and will not show it in the report, and before I made the change, it was showing that this class is covered in the test coverage.... is it weird or am I missing a fundamental thing about how kotlin generates methods or how jacoco excludes rules work ?
Anyway I hope this helps..
I have a homemade library that generates DataMapper classes.
They are generated with #Singleton and #Inject annotations to be able to inject them where i need them.
But where it doesn't work is when Dagger tries to create the dependency tree, this error shows :
:data:kaptGenerateStubsDebugKotlin
e: /Users/me/myproject/data/build/tmp/kapt3/stubs/debug/com/myproject/data/di/DataComponent.java:11: error: [Dagger/MissingBinding] error.NonExistentClass cannot be provided without an #Inject constructor or an #Provides-annotated method.
public abstract com.myproject.domain.repository.ContentRepository contentRepository();
^
error.NonExistentClass is injected at
com.myproject.data.repository.ContentDataRepository.<init>(…, myGeneratedDataMapper, …)
com.myproject.data.repository.ContentDataRepository is injected at
com.myproject.data.di.module.DataModule.contentRepository(contentDataRepository)
com.myproject.domain.repository.ContentRepository is provided at
com.myproject.data.di.DataComponent.contentRepository()
:data:kaptDebugKotlin
:data:kaptDebugKotlin FAILED
Involved classes are :
DataModule (module for dagger)
#Module
class DataModule {
#Provides
#Singleton
fun contentRepository(contentDataRepository: ContentDataRepository): ContentRepository = contentDataRepository
}
DataComponent (component for dagger):
#Singleton
#Component(modules = [DataModule::class])
interface DataComponent {
fun contentRepository(): ContentRepository
}
ContentDataRepository
#Singleton
class ContentDataRepository #Inject constructor(
private val myGeneratedDataMapper: MyGeneratedDataMapper
) : ContentRepository {
...
}
MyGeneratedDataMapper
#Singleton
class MyGeneratedDataMapper #Inject constructor() {
...
}
The thing is, if i disable kapt of dagger dependency in gradle.build, then build, then enable it, then build, it works.
If i do a clean + build, it doesn't work, same error.
I want to make it work in one row.
I don't know if you are using AS3.2 or AS3.3 with androidX artifacts or not but Maybe this is the case with you too.
so when i migrated to androidX artifacts in AS3.2 i got hit with bunch of NonExistentClass errors ends the build with
kaptGenerateStubsDebugKotlin
:data:kaptDebugKotlin
:data:kaptDebugKotlin
I finally found out that it has something to do with Dagger itself and degraded the version from 2.17 to 2.16 now the latest version of Dagger2 is 2.18 which i can't use due to this bug / feature [they forgot about].
Update:
i found the solution and it just came today so here is the issue tracker link:
https://issuetracker.google.com/issues/115738511
so the bug was not in the Dagger but it was with Jetifier and i totally ignored the fact that it was set enabled during migration
here's the solution i copied from the link:
Sorry jetifier beta01 was not binary compatible with alpha10.
We have published beta02 that should fix this issue.
Please try:
buildscript { dependencies {
classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta02' } }
You're probably not going to like my answer but the order is kinda random.
Look at this thread for some more explaining and maybe some more guidance but, if you want to verify you are running first look at Gradle plugins and how to use them