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
Related
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()
}
}
I would like to try Hilt DI in the android library.
It is a dependency on another project, with its own submodule. The very first problem I've encountered is the requirement of marking Application with #HiltAndroidApp. Now I do not have anything that extends Application in my library ofc but would like to utilize Hilt and its predefined components.
Is it possible or should I go with Dagger only in such a case? I've found a solution for Dagger, where library dependency injection is made totally independently (the client is unaware of the library's DI): Dagger solution, would love to hear any opinion on that, maybe someone already put a great effort into that issue and can share his insights.
If you're trying to include Hilt in an android library, then you should expect the android app (client of your library) to mark its Application with #HiltAndroidApp.
You should include your whole setup (entry points, modules, dependencies, ... whatever you want to have in your library) in the library module, and make the requirement for the client of the library to use the #HiltAndroidApp to use your library correctly.
You don't need to include #HiltAndroidApp in library module to inject the dependencies in library modules to app module or any dynamic feature modules.
This sample has only core library module, app, and dynamic feature modules. Dynamic feature module implementation is optional.
Result of injecting from core library module to App's Activity and Fragment is as
Project dependency Structure
feature_hilt_camera feature_hilt_photos (Dynamic Feature Modules)
| | |
| ----App----
| |
core(android-library)
In core library module have a dagger module as
#InstallIn(ApplicationComponent::class)
#Module
class CoreModule {
#Singleton
#Provides
fun provideCoreDependency(application: Application) = CoreDependency(application)
#Provides
fun provideCoreActivityDependency(context: Application) = CoreActivityDependency(context)
#Provides
fun provideCoreCameraDependency(): CoreCameraDependency = CoreCameraDependency()
#Provides
fun provideCorePhotoDependency(): CorePhotoDependency = CorePhotoDependency()
#Provides
fun provideAnotherDependency() = AnotherDependency()
}
And inject to Activity as
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
/**
* Injected from [CoreModule] with #Singleton scope
*/
#Inject
lateinit var coreDependency: CoreDependency
/**
* Injected from [CoreModule] with no scope
*/
#Inject
lateinit var coreActivityDependency: CoreActivityDependency
/**
* Injected from [MainActivityModule] with no scope
*/
#Inject
lateinit var toastMaker: ToastMaker
/**
*
* Injected from [MainActivityModule] with #ActivityScoped
* * To inject this there should be #Binds that gets Context from an Application
*/
#Inject
lateinit var mainActivityObject: MainActivityObject
/**
* Injected via constructor injection with no scope
*/
#Inject
lateinit var sensorController: SensorController
/**
* Injected via constructor injection with #Singleton scope
*
* ### Unlike Tutorial 9-2 This can be injected because MainActivity's component does not
* depend on any component with another scope
*/
#Inject
lateinit var singletonObject: SingletonObject
#Inject
lateinit var anotherDependency: AnotherDependency
#SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.tvInfo).text =
"CoreModule #Singleton coreDependency: ${coreDependency.hashCode()}\n" +
"CoreModule no scope coreActivityDependency: ${coreActivityDependency.hashCode()}\n" +
"CoreModule no scope anotherDependency: ${anotherDependency.hashCode()}\n" +
"MainActivityModule #ActivityScoped mainActivityObject: ${mainActivityObject.hashCode()}\n" +
"MainActivityModule no scope toastMaker: ${toastMaker.hashCode()}\n" +
"Constructor no scope sensorController: ${sensorController.hashCode()}\n"
"Constructor #Singleton singletonObject: ${singletonObject.hashCode()}"
}
}
and it's same for HomeFragment which is in app module
#AndroidEntryPoint
class HomeFragment : Fragment() {
/**
* Injected from [CoreModule] with #Singleton scope
*/
#Inject
lateinit var coreDependency: CoreDependency
/**
* Injected from [CoreModule] with no scope
*/
#Inject
lateinit var coreActivityDependency: CoreActivityDependency
#Inject
lateinit var homeFragmentObject: HomeFragmentObject
/**
* This dependency cannot be injected since this fragment's component does not depend on CoreComponent
* unlike Tutorial 9-2 counterpart
*/
#Inject
lateinit var mainActivityObject: MainActivityObject
#Inject
lateinit var fragmentObject: FragmentObject
}
If you also wish to inject to dynamic feature modules you need a provision module in your library module as
/**
* This component is required for adding component to DFM dependencies
*/
#EntryPoint
#InstallIn(ApplicationComponent::class)
interface CoreModuleDependencies {
/*
🔥 Provision methods to provide dependencies to components that depend on this component
*/
fun coreDependency(): CoreDependency
fun coreActivityDependency(): CoreActivityDependency
fun coreCameraDependency(): CoreCameraDependency
fun corePhotoDependency(): CorePhotoDependency
}
and dynamic feature module you will use this interface as dependent component
In camera dynamic feature module have a component like this
#Component(
dependencies = [CoreModuleDependencies::class],
modules = [CameraModule::class]
)
interface CameraComponent {
fun inject(cameraFragment1: CameraFragment1)
fun inject(cameraFragment2: CameraFragment2)
fun inject(cameraActivity: CameraActivity)
#Component.Factory
interface Factory {
fun create(coreComponentDependencies: CoreModuleDependencies,
#BindsInstance application: Application): CameraComponent
}
}
and inject it to your dynamic feature fragment with
private fun initCoreDependentInjection() {
val coreModuleDependencies = EntryPointAccessors.fromApplication(
requireActivity().applicationContext,
CoreModuleDependencies::class.java
)
DaggerCameraComponent.factory().create(
coreModuleDependencies,
requireActivity().application
)
.inject(this)
}
Full sample that in image is here, and you check out implementation for both libraries and dynamic feature modules in this sample project.
It's possible to integrate Hilt into your library, but you will have to handle the case where the app is not a Hilt application. You can handle this case by annotating your Activity/Fragment with #OptionalInject and then checking OptionalInjectCheck#wasInjectedByHilt() to check if the Activity/Fragment was injected by Hilt or not.
#OptionalInject
#AndroidEntryPoint
public final class MyFragment extends Fragment {
...
#Override public void onAttach(Activity activity) {
super.onAttach(activity); // Injection will happen here, but only if the Activity used Hilt
if (!OptionalInjectCheck.wasInjectedByHilt(this)) {
// Get Dagger components the previous way and inject manually
}
}
}
Note that doing this will not make your library simpler (it'll actually get more complex since you need to support both Hilt and non-Hilt applications). The main benefit would be to your clients that use Hilt, since they wouldn't need to do any component/module setup to get your library up and running in their app.
I am developing an Android application using Kotlin. I am also writing integrated tests for my application using Dagger and Expresso. I am now having a problem with mocking a logic. First, let me show you how I mock logic in the code using Dagger for the integrated tests.
I have the Dagger app component class for the test with the following definition
#Singleton
#Component(modules = [ TestAppModule::class ])
interface TestAppComponent : AppComponent //App component is the actual class used for the application
{
fun inject(app: MainActivity)
}
This is the definition of the TestAppModule class
#Module
open class TestAppModule (private val app: Application) {
#Singleton
#Provides
open fun fileIOService(): IFileIOService {
return FakeFileIOService()
}
}
I also have a class called AppModule for the actual Application code with the following definition.
#Module
open class TestAppModule (private val app: Application) {
#Singleton
#Provides
open fun fileIOService(): IFileIOService {
return FakeFileIOService()
}
}
The idea is that the integration tests will use the TestAppModule and so the FakeFileIOService class will be injected. For the actual application, it will use the AppModule and so the FileIOService class will be injected instead. Both the FakeFileIOService class and the FileIOService class will inherit from the IFileIOService interface. The IFileIOService interface will have the following definition.
interface IFileIOService
{
fun saveFile(data: String)
}
The FileIOService class and the FakeIOService class will have their own version of the implementation for the saveFile method. The FakeIOService will have the mock version.
I will have a property for IFileIOService in the MainActivity class for dependency injection.
class MainActivity: AppCompatActivity()
{
#Inject
lateinit var fileIOService: IFileIOService
}
As I mentioned, the FakeFileIOService class will be injected for the integration tests. The code works fine. It uses the fake version for the integration tests and the concrete for the actual application.
But now, I am having a problem where I need to mock the logic of the method using Mockito and get the returned value within the tests.
But, now I am having an issue with mocking the logic using Mokito to test File and getting the mocked value back within the tests.
Following is the implementation of my
fun saveFile(data: String) {
FileOutputStream mockFos = Mockito.mock(FileOutputStream.class);
mockFos.write(data.getBytes());
mockFos.close();
}
Literally, I am trying to mock the file logic using Mockito. Then I am trying to write an integration test something like this.
#get:Rule
var mainActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule<MainActivity>(CameraActivity::class.java, true, false)
#Test
fun fileIsSaveInWhenButtonIsClicked() {
Intents.init()
var targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
var intent: Intent = Intent(targetContext, MainActivity::class.java)
this.mainActivityRule.launchActivity(null)
onView(withId(R.id.button_save_file)).perform(click())
//here I want to assert if the file is saved using Mockito something like this
//verify(mockFos).write(data.getBytes()); the issue is that I cannot get the mockFos instance creatd in the saveFile method of the FakeFileIOService class
Intents.release()
}
See the comment in the test. The issue is that I want to verify if the file is saved using Mockito. But the issue is that I cannot get the mockFos instance created in the saveFile of the FakeIOService class. How can I test it? What would be the best way to test this scenario?
You are not supposed to mock system logic. Writing to a file and closing the file is system logic. You are only supposed to mock your own logic.
And what you should be verifying is that the saveFile method was called. Not that the content of saveMyFile was invoked.
val mock = mock(IFileIOService::class.java)
mainActivity.fileIOService = mock
//do something that in return does fileIOService.saveFile
verify(mock).saveFile(any())
Another option is to spy:
val spy = spy(mainActivity.fileIOService)
//do something that in return does fileIOService.saveFile
verify(spy).saveFile(any())
So up to Dagger 2.11 I've been able to construct TestComponent's and Modules to enable key components to be injected into integration tests. This is great for Api tests and objects with heavy component requirements
Typically I would have code like this:-
class SpotifyApiTest {
lateinit var spotifyApi : SpotifyApi
#Inject set
lateinit var spotifyHelper : SpotifyIOHelper
#Inject set
#Before
fun setup() {
var context = InstrumentationRegistry.getInstrumentation().context
val testAppComponent = DaggerSpotifyTestComponent.builder()
.spotifyApiModule(SpotifyApiModule(context))
.build()
testAppComponent.inject(this)
}
#Test
......
}
N/B remember to add the following to your gradle build file
kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion"
This approach works extremely well up to Dagger 2.11 but after that version Modules with parameterized constructors do not work preventing context being supplied let alone the application. So how can I use the new AndroidInjection() functionality for integration tests with the Dagger 2.16 for example?
I've come up with the following approach to inject the context into the modules for dagger 2.11+ so its a start, I've still no idea how to handle application from the integration test perspective.
#Singleton
#Component(modules = arrayOf(SpotifyAccountsModule::class,SpotifyApiModule::class))
interface SpotifyTestComponent {
fun inject(test: SpotifyApiTest)
fun inject(test: SpotifyAccountApiTest)
#Component.Builder
interface Builder {
#BindsInstance
fun create(context: Context): Builder
fun build(): SpotifyTestComponent
}
}
This will make context available as a parameter for providers and should help others when they reach this particular brick wall.
I am trying to create simple MVP Archtecture app using Dagger 2. I am tying to achieave same result as in this tutorial, but with Kotlin. Here is my code so far.
Presenter:
class MainPresenter #Inject constructor(var view: IMainView): IMainPresenter{
override fun beginMessuring() {
view.toastMessage("Measuring started")
}
override fun stopMessuring() {
view.toastMessage("Measuring stopped")
}
}
View:
class MainActivity : AppCompatActivity(), IMainView {
#Inject lateinit var presenter : MainPresenter
val component: IMainComponent by lazy {
DaggerIMainComponent
.builder()
.mainPresenterModule(MainPresenterModule(this))
.build()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
component.inject(this)
presenter.beginMessuring()
}
override fun toastMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
}
Dagger Module:
#Module
class MainPresenterModule(private val view: IMainView) {
#Provides
fun provideView() = view
}
Dagger Component:
#Component(modules = arrayOf(MainPresenterModule::class))
interface IMainComponent {
fun inject(mainView : IMainActivity)
}
The problem is that I am getting build error which starts with this:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.maciej.spiritlvl/com.example.maciej.spiritlvl.View.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property presenter has not been initialized
PS, my gradle dagger config:
kapt 'com.google.dagger:dagger-compiler:2.9'
mplementation 'com.google.dagger:dagger:2.9'
EDIT:
Changed injected presenter type from IMainView to MainView.
Whenever trying to inject any interface, like in your case IMainPresenter, you need to tell dagger which concrete implementation to use. Dagger has no means of knowing which implementation of that interface you want to 'have' (you might have numerous implementations of that interface).
You did the right thing for the IMainView by adding a #Provides-annotated method to your module. You can do the same for your presenter, but that imho would render the whole point of dagger useless, because you'd have to create the presenter yourself when creating the module.
So I would, instead of injecting the IMainPresenter interface into your activity, inject the concrete implementation MainPresenter. Then you also shouldn't need a #Provides method in your module (for the presenter).