How to test a fragment that depends on a dagger subcomponent? - android

Note: I am not using Dagger-Android, just Dagger 2.
When I started writing my app I was injecting the fragment through the AppComponent so my integration test worked.
Then I created a subcomponent called MainComponent which lives in MainActivity. In my fragment I was getting the subcomponent using
((MainActivity) getActivity()).mainComponent.inject(this);
Then in my integration tests I would do
FragmentScenario.launchInContainer(RecipesListFragment.class, null, R.style.AppTheme, null)
which throws an error
EmptyFragmentActivity cannot be cast to ...MainActivity
as FragmentScenario launches the fragment in an EmptyFragmentActivity.
I thought that in order to fix this I could remove the dependency on MainActivity to get the component so I used a FragmentFactory and passed in MainComponent as a parameter. But now the test fails because when I create the FragmentScenario I do not have the MainComponent to pass it in the factory.
So is there a way to launch the scenario and still use the MainComponent subcomponent?

Posting what I did in the end to answer Henrique's question. I read a few posts about it but I can't find them now since it was some time ago.
There was no clear answer so I ended up using the dependency on MainActivity to inject since using the FragmentFactory proved too much hassle without many gains.
In the AndroidTest folder I created an empty MainTestActivity that extends MainActivity.
Then in the FragmentTest
ActivityScenario<MainTestActivity> scenario;
#Before
public void setUp() {
scenario = ActivityScenario.launch(MainTestActivity.class).onActivity(activity -> {
MainFragment fragment = new MainFragment();
activity.startActivityFromFragment(fragment, new Intent(activity, MainTestActivity.class), 0);
});
}

Related

Android - Dagger inject Shared ViewModel to Fragment

I need to bind two fragments with shared ViewModel.
Component:
void injectMovieFragment(MovieFragment movieFragment);
void injectMovieCollectionFragment(MovieCollectionFragment movieCollectionFragment);
SharedViewModelModule:
#Provides
MovieSharedViewModel provideMovieSharedViewModel(Fragment fragment) {
return new ViewModelProvider(fragment.requireActivity()).get(MovieSharedViewModel.class);
}
This code cause MissingBinding error on Dagger (cannot get Fragment from MovieFragment/ MovieCollectionFragment).
SharedViewModelModule v2:
#Provides
MovieSharedViewModel provideMovieSharedViewModelToMovieFragment(MovieFragment fragment) {
return new ViewModelProvider(fragment.requireActivity()).get(MovieSharedViewModel.class);
}
#Provides
MovieSharedViewModel provideMovieSharedViewModelToMovieCollectionFragment(MovieCollectionFragment fragment) {
return new ViewModelProvider(fragment.requireActivity()).get(MovieSharedViewModel.class);
}
This code cause DuplicateBindings error on Dagger.
To get sharedViewModel without Dagger2 injection, I simply using:
sharedViewModel = new ViewModelProvider(requireActivity()).get(MovieSharedViewModel.class);
inside onViewCreated of MovieFragment & MovieCollectionFragment classes, and this works well.
How to properly inject this shared ViewModel using Dagger 2?
It would be nice if you could add how you are using injection in your fragment. From the error it seems like you are not injecting the created fragment into your dagger graph.
This is required as fragments are normally created by android and you need to provide the created instance to dagger.
The way I used to do it is like expose my DaggerComponent from my CustomApplication class(You can also expose the component through your activity).
Then in the fragment's onAttach function inject the created fragment like this.
override fun onAttach(context: Context) {
super.onAttach(context)
(context.applicationContext as CustomApplication).applicationComponent.injectMovieFragment(this)
}
This should fix the error. Please provide how you are using injection in fragments for more complete answers.

How to test a Fragment with a ViewModel scoped in a graph

I´m using the navGraphViewModels ViewModel scoping on Android, and when I´m implementing the Fragment tests I can´t even start the test.
I´m using mockito to mock the NavigationController and using the documentation suggested aproach with the fragmentScenario. The problem comes when the ViewModel is tryed to be created that throws an Exception because NavController#getBackStackEntry is not mocked and I can´t mock it because NavController is a final class.
How can I test that uses ViewModels which are scoped to a navigation graph?
After a lot of time I found the answer.
You should change the fragment NavController by one that you have created and changed it ViewModelStore. An example of this is found in the test of the Android Source code.
val scenario = launchFragmentInContainer<TestVMFragment>()
navController.setViewModelStore(ViewModelStore())
scenario.onFragment { fragment ->
Navigation.setViewNavController(fragment.requireView(), navController)
}
}

Is Dagger component bound to the object that instantiate it?

I have been using Dagger 2 in my project. I understand that the lifetime of scoped object is the same as the lifetime of component (with the same scope). What about the lifetime of component then?
For example, I have a component:
#MyApp
#Component(modules = {
ApplicationModule.class})
public interface ApplicationComponent {
// Injection methods
void inject(MyApplication mainApplication);
}
I build component by:
public class MyApplication extends Application {
#Override
public void onCreate() {
super.onCreate();
buildApplicationComponent();
mApplicationComponent.inject(this);
}
private void buildApplicationComponent() {
mApplicationComponent = DaggerApplicationComponent.builder()
.applicationModule(new ApplicationModule(this))
.build();
}
}
Currently, the code to build component is in Application class. But
is it so that if I build component in Fragment the ApplicationComponent would have the same lifetime as the fragment & if I execute it in Application class the component would have the same lifetime as the whole application? Or how the lifetime of component is defined?
It is plain Java. You have object no matter where. The object provides you dependencies. If you use the same instance you will have the same dependencies or basically the same "Scope/lifetime". If you create somewhere new object it means new other object\dependencies it can provide so "another Scope/lifetime". If you share the object between different objects (You create it in the Application class, but reuse it in another fragment) you are in the "same Scope/lifetime".
But in general I see only "old way" of using Dagger 2 here. This is how your classes should end up everywhere. No need to find reference to any Components and etc or try to instantiate them on your own or clear them on your own in Fragments or Actvities. You have some code for Dagger 2 in one place and then some Annotations in you classes "which do the real work of your project". No need for complicated "connection" between the two parts and I see here a lot of cases like this which are part of tutorials that date back to 2016-2017...
class Repostory #Inject constructor(
private val dependency1: Dependency1
) {}
class Activity or Fragment {
#Inject lateinit var dependency2: Dependency2
}
This is a great example for right usage of Dagger2.
https://github.com/google/iosched
Here is an article about the app:
https://medium.com/#JoseAlcerreca
Jose Alcerreca is one of the Google Lead Devs responsible for creating the guidelines to write Android apps.
#Module
abstract class ActivityBindingModule {
#ActivityScoped
#ContributesAndroidInjector(modules = [MainActivityModule::class])
internal abstract fun mainActivity(): MainActivity
}
Just try to compile something like this. There is the exaxt same class in the app I gave you. Check the generated code and also the docs. If you see ContributesAndroidInjector there is explained:
Generates an {#link AndroidInjector} for the return type of this method. The injector is
implemented with a {#link dagger.Subcomponent} and will be a child of the {#link dagger.Module}'s component.
If you check the AndroidInjector docs you will see that it is the one that is injectin the Activity. And there it will be the implementation:
#Subcomponent(modules = {MainActivityModule.class})
#ActivityScoped
public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
#Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder<MainActivity> {}
}
So in general these annotations do the magic for you when a new activity starts. They create the Subcomponent, they use it and when the Activity Lifecycle ends the Subcomponent is also dead. Dagger clears the reference. Dagger knows how Activities work or Fragments and etc. There was the old way when you craete the component in the Activity, use it, call inject on your own. Or put some component in the AppClass and then clear it on your onw. But now there is a bigger amount of annotations.

Dagger - Setting a Dynamic Value to SubComponent Builder [duplicate]

This question already has answers here:
Dagger 2.10 Android subcomponents and builders
(2 answers)
Closed 4 years ago.
I want to set a value to my sub-component builder at the time of building it. If simple Dagger 2 is concerned, we can achieve like following:
#UserScope
#Subcomponent(modules = {UserModule.class, ActivityBuilder.class, NewEditQuotationModule.class})
public interface UserComponent {
NewEditQuotationComponent.Builder getNewEditQuotationComponent();
void inject(UserManager userManager);
#Subcomponent.Builder
interface Builder {
#BindsInstance
Builder user(User user);
UserComponent build();
}}
But In case of Dagger Android, Subcomponent and its associated builder is handled by #ContributesAndroidInjector. Dagger Auto generate Subcomponent and its builder even its implementation with the current context.
I want to set some value at the time of building My Dagger Android Subcomponent. I tried by following approach:
#Subcomponent(modules = NewEditQuotationModule.class)
public interface NewEditQuotationComponent extends AndroidInjector<InquiriesEditNewMainActivity> {
#Subcomponent.Builder
abstract class Builder extends AndroidInjector.Builder<InquiriesEditNewMainActivity> {
#BindsInstance
public abstract Builder setName(#Named("My_Name") String name);
}}
#Module
public abstract class NewEditQuotationModule {
#Binds
#IntoMap
#ActivityKey(InquiriesEditNewMainActivity.class)
abstract AndroidInjector.Factory<? extends Activity>
bindInquiriesEditNewMainActivityInjectorFactory(NewEditQuotationComponent.Builder builder);
}
I tired to build it by following way:
AndroidInjector.Builder androidInjector = MyApplication
.getApplication()
.getAppComponent()
.getApiEndPoint()
.getApiEndPointComponent()
.getUserManager()
.getUserComponent()
.getNewEditQuotationComponent().setName("My Name");
androidInjector.seedInstance(this);
androidInjector.build();
But not succeed.
Please let me know
How can I set some value at the time of building my component?
Where am I wrong in the previous approach?
dagger.android works by automatically creating your component in onCreate, and needs to work this way because Android is allowed to create your Activity on its own. Pre-creating your AndroidInjector will not work; that is what's wrong with your previous approach.
As I tried to indicate through a duplicate-question vote, the only way for you to set module instance or #BindsInstance values in dagger.android is by overriding the seedInstance method in the AndroidInjector.Builder that serves as your #Subcomponent.Builder as well. This will let you pull values out of the Activity instance and set them in your Activity graph.
Though you could always stash data in a #Singleton (ApplicationComponent-scoped) object or VM global variable, you are trying to do the right thing by passing data carefully from Activity to Activity without setting global state. I think this is the right approach, but also that your solution probably will not involve constructing an AndroidInjector explicitly. Instead, pass your data through the extras bundle, which is an idiomatic way of transferring parameters and other data into Activities; you can then insert that data into your graph by pulling it out of getIntent().getExtras() on your Activity instance in seedInstance, since getIntent() should be populated by the time onCreate(Bundle) runs.

Referencing the Activity inside its module

How do I use the new AndroidInjector.inject and still be able to provide an Activity instance inside an Activity Module? The Dagger docs don`t make it clear how to archive this.
The use case is the following: I have an Activity Module which provides a Presenter to my Activity, but the Presenter needs a reference to the Activity.
I used to have something like
#Inject Presenter presenter;
public onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((CustomApplication) getApplicationContext())
.getAppComponent()
.plus(new ActivityModule(this));
}
Can someone can point me to a sample that uses AndroidInjector.inject(this) instead and allow the reference of the Activity inside the Dagger 2 module?
Check Dagger 2 Github issue 615
The instance of your Activity is automatically provided, just pass it as a parameter in your module methods.
Example:
#Provides
#ActivityScope
public providePresenter(ActivityA activity) {
return new PresenterA(activity);
}
You'll now be able to abstract simple modules. Your presenter can be constructor injected too.
This actually cutout a lot of code from all my modules.

Categories

Resources