Can I use Timber Logger on my java library? - android

I add the Timber dependency to my Java Core Library Module build.gradle file:
implementation 'com.jakewharton.timber:timber:4.6.0'
Although it did not give an error when gradle synchronizes, I cannot see or use Timber class in the Core Library.

Timber has a dependency on android.util.Log so it cannot be used in a pure Java module.
Decoupling the library from Android has been proposed but the creator of the library has decided against it. https://github.com/JakeWharton/timber/pull/63
The 5.0.0-SNAPSHOT version of Timber now supports usage in Java modules by using the jdk artifact.
<dependency>
<groupId>com.jakewharton.timber</groupId>
<artifactId>timber-jdk</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>

If anyone looking at Timber (Without android dependency) for mutli-module android project. Use timber-jdk like below.
In project's build.gradle file.
allprojects {
repositories {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}
}
In individual module build.gradle file (Example: Domain or remote module)
implementation "com.jakewharton.timber:timber-jdk:5.0.0-SNAPSHOT"
With this you should be able to implement Timber without android framework dependency

If you have multi-module project, you can use Timber in pure Java/Kotlin with a tiny bit of abstraction involved, no external libraries.
Example with Kotlin and Koin:
In pure kotlin module create a big imposter, Timber.kt:
interface ILogger {
fun d(message: String)
fun e(message: String)
fun e(throwable: Throwable, message: String)
fun i(message: String)
}
object Timber: ILogger, KoinComponent {
private val logger: ILogger by inject()
override fun d(message: String) = logger.d(message)
override fun e(message: String) = logger.e(message)
override fun e(throwable: Throwable, message: String) = logger.e(throwable, message)
override fun i(message: String) = logger.i(message)
}
In app module create TimberLogger.kt:
import timber.log.Timber
class TimberLogger : ILogger {
override fun d(message: String) = Timber.d(message) // this is real timber this time
override fun e(message: String) = Timber.e(message)
override fun e(throwable: Throwable, message: String) = Timber.e(throwable, message)
override fun i(message: String) = Timber.i(message)
}
In app module, inject TimberLogger implementation into your fake Timber:
val appModule = module {
single<ILogger> { TimberLogger() }
}
Now you can simply call Timber.d("message") statically from anywhere.
If you have 2+ pure modules to use Timber in, consider creating Utils module and include it in rest of them, so it's available everywhere.

Try Arbor: Timber like logging implementation for Kotlin Multiplatform.
https://github.com/ToxicBakery/Arbor

Related

Dagger 2 doesn't generate classes after project build

I have added the following lines to the gradle file
plugins {
...
id 'kotlin-kapt'
}
dependencies {
...
implementation 'com.google.dagger:dagger:2.28.3'
annotationProcessor 'com.google.dagger:dagger-compiler:2.28.3'
implementation 'com.google.dagger:dagger-android-support:2.28.3' // if you use the support libraries
}
I create a factory ViewModel
class MainViewModelFactory #Inject constructor(private val mainViewModel: MainViewModel) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return mainViewModel as T
}
throw IllegalArgumentException("Unknown class name")
}
}
Then I call (naturally with the help of retrofit services)
private val viewModel: MainViewModel by viewModels {
DaggerAppComponent.create().mainViewModelFactory()
}
According to several sources of information, after building the project, I expect an error that the DaggerAppComponent class was not created, after which, I also expect that it is created automatically and I can add it using import. However, this does not happen. What can be wrong?
According to several sources of information, after building the project, I expect an error that the DaggerAppComponent class was not created, after which, I also expect that it is created automatically and I can add it using import. However, this does not happen. What can be wrong?

Dependency injection in Android Library

I'm working on Android library that other apps will use it.
This library will not have any activity, but it will have Fragments, VM, domain etc.
So far on my apps i worked with Dagger2, and i'm not sure how it will work in library.
Anyone have experience with it? or maybe someone can recommend other library to use for that case (koin?)?
Thanks
Koin is far more easy to use. You can also get rid of annotations and their handling. Suppose we have a class name Helper and needs to be access from different locations.
Implementation Steps:
a) Add Dependency in build.gradle(app).
implementation "io.insert-koin:koin-android:3.3.0"
b) Create a class extend it with KoinComponent
class DIComponent : KoinComponent {
// Utils
val helper by inject<Helper>()
}
c) Initialize Koin by passing it modules in App class
class MainApplication : Application(){
override fun onCreate() {
super.onCreate()
startKoin{
androidLogger()
androidContext(this#MainApplication)
modules(appModule)
}
}
private val appModule = module {
single { Helper() }
}
}
d) Now, to use this class in project (activity/fragment)
class MainActivity : AppCompatActivity() {
private val diComponent = DIComponent()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diComponent.helper.yourFunction()
}
}

Android Compose ui test Unresolved reference

I am trying to write a compose UI test code.
my ui code:
#Composable
fun MyScreen(
vm: MyViewModel,
) {
...
}
and viewModel code:
#HiltViewModel
class MyViewModel #Inject constructor(
private val myUsecase: MyUsecase,
) : ViewModel() {
...
}
MyViewModel has a dependency MyUsecase
The MyUsecase is an interface and is located in :core:domain-api module.
Actually, the implement class is MyUsecaseImpl and is located in :core:domain module.
To write an UI test code for MyScreen composable function, I wrote a fake class: FakeMyUsecase.
class FakeMyUsecase: MyUsecase {
override suspend fun doSomething(): Result<Unit> {
return Result.success(Unit)
}
}
And I tried to write an test code like:
class SignInScreenTest {
#get:Rule
val composeTestRule = createComposeRule()
// Android studio cannot find the FakeMyUsecase
private val signIn: SignIn = FakeMyUsecase()
...
}
Android studio shows the compile error to me and a hint to fix it.
The hint is guiding me that Add dependency on module 'core.domain.androidTest'.
I clicked to follow the hint and the dependency was added in my build.gradle located in app module.
But the compile error doesn't disappear.
How can I fix it?
Here is my app module build.gradle:
dependencies {
implementation project(':core:domain')
implementation project(':core:domain-api')
}

Dagger2 + ActivityInjection + AndroidXTest/Espresso/RoboElectric in library project

I am working on android library module and I want to test the standalone activity in my module. I was following the article https://medium.com/androiddevelopers/write-once-run-everywhere-tests-on-android-88adb2ba20c5 to use roboelectric and androidx test with espresso. I recently introduced dagger 2 to my library project.
With that my Activity looks like this:
class XYZLibBaseActivity : AppCompatActivity(){
#Inject
lateinit var resourceProvider: ResourceProvider
override fun onCreate(savedInstanceState: Bundle?) {
//creating the dagger component
DaggerXYZLibComponent.factory().create(application).inject(this)
super.onCreate(savedInstanceState)
}
}
My component declaration is
#Component(modules = [ResourceProviderModule::class])
interface XYZLibComponent{
#Component.Factory
interface Factory{
fun create(#BindsInstance application: Application):XYZLibComponent
}
fun inject(xyzLibBaseActivity: XYZLibBaseActivity)
}
and dagger module is
#Module
class ResourceProviderModule {
#Provides
fun provideResourceProvider(application: Application): ResourceProvider{
return ResourceProviderImpl(application.applicationContext)
}
}
This works perfectly fine and I don't want the underlying application to use dagger 2.
Now I wan to test my activity without depending on the underlying application or application class. How can I inject mock ResourceProvider in the activity?
One of many options is
create 2 flavors in your gradle config: real and mock
in both flavors, define a boolean buildConfigField flag
In your provideResourceProvider, return a corresponding implementation based on the flag's value

How to use Koin in multiple module?

There are two modules in my android project, app module and lib module.
Both these two modules need Koin for D.I., so I call startKoin in MyApplication class in app module, and IninKointContentProvider in lib module as below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, modules1)
}
}
// lib module
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
startKoin(context.applicationContext, modules2)
return true
}
}
Then app crashed and shown this message
Caused by: org.koin.error.BeanOverrideException: Try to override definition with Single [class='android.content.Context'], but override is not allowed. Use 'override' option in your definition or module.
I guess startKoin can be called only one time.
The solution I found is merging two koin modules then calling startKoin in MyApplication, but I don't like it. Lib module may be imported by other android project which doesn't use koin, in that case, I think calling startKoin in InitKoinContentProvider is better.
Any solution for this problem?? Thanks!
I found the best solution inspired by #laalto's answer, thanks!
Upgrade to koin 2.0, then use KoinApplication and customized KoinComponent to create a isolated koin context, it can let lib module using koin without any initializing call by app module, still start koin in ContentProvider. The whole code may like below.
// app module
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this#MyApplication)
modules(module{
viewModel { MainViewModel() }
})
}
}
}
class MainActivity: AppCompactActivity() {
private val viewModel: MainViewModel by viewModel()
}
// lib module
internal object MyKoinContext {
lateinit var koinApplication: KoinApplication
}
interface MyKoinComponent : KoinComponent {
override fun getKoin(): Koin {
return MyKoinContext.koinApplication.koin
}
}
class InitKoinContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
MyKoinContext.koinApplication = koinApplication {
androidContext(context.applicationContext)
modules(module{
viewModel { FooViewModel() }
})
}
return true
}
}
class FooActivity: AppCompactActivity(), MyKoinComponent {
private val viewModel: FooViewModel by viewModel()
}
Ref:
https://insert-koin.io/docs/2.0/documentation/reference/index.html#_koin_context_isolation
In your library modules, use loadKoinModules() to load the module-specific koin modules. Docs.
You need to have run startKoin() prior to that, so the init order with content providers can be a little tricky.
To init extra koin modules on other project modules and get no duplicate loading issues (e.g. pressing home, than coming back to the activity), go to your module declaration file:
val myModule = module {
single { MyRepository(get()) }
viewModel { MyViewModel(get()) }
}
private val loadKoinModules by lazy {
loadKoinModules(myModule)
}
fun inject() = loadKoinModules
Then on your view:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inject()
}
TL;DR
Use single/factory methods with override param set to true when providing your dependencies that are overriding those provided by the modules loaded before.
single<Manager>(override = true) { TestManager() }
I have faced a similar issue when I tried to override one of the dependencies for UI test purposes.
When I setup in Application.onCreate():
startKoin {
module {
single { Printer() }
}
}
and then in before method of test:
loadKoinModules(module {
single<Printer> { TestPrinter() }
})
I get a Runtime exception during the test:
org.koin.core.error.DefinitionOverrideException: Already existing definition or try to override an existing one
And the solution is to show Koin that you are intentionally overriding that dependency by using override param of single function like that:
loadKoinModules(module {
single<Printer>(override = true) { TestPrinter() }
})
By design startKoin is meant to be called from the Application class. You can provide a parameter in the lib whether to call startKoin or not. But I doubt that including such things as Koin in libs is a good practice. What if an application already includes Koin, but of different version?
This way worked perfect for me:
#KoinExperimentalAPI
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
}
private fun initKoin() {
startKoin {
androidLogger()
androidContext(this#MainApplication)
}
loadKoinModules(
myFeatureModule
)
}
}
And define you module in your feature as usual:
val myFeatureModule = module {
factory<...> { ...() }
single { ...() }
viewModel { ...(get() }
}

Categories

Resources