Avoid activity leak using scope dagger 2 - android

First to start with the my project architecture, I am using MVP and Dagger 2 for dependency injection.
I have been exploring scopes in dagger and my question is to better understand scope in context with Activity.
I have an activity(view) leak through presenter despite using activity scope.
As I am new to dagger and I feel I am missing something.
I am assuming that scope should handle my view to null when activity is destroyed(though right now do not understand how it will).Is my assumption right? if yes what I am doing wrong, else is it possible to avoid view leak using dagger? I know about the detachView approach, just curious if we can achieve the same thing using dagger 2.
P.S: I came to know about the leak through leakCanary.
Following is my code
LoginActivity.class
public class LoginActivity extends BaseActivity implements LoginContract.View {
private static final String TAG = "LoginActivity";
#Inject
LoginPresenter presenter;
private LoginComponent loginComponent;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
createComponent();
initViews();
}
private void createComponent() {
loginComponent = ((MyApplication) getApplication()).getRepositoryComponent()
.COMPONENT(new LoginPresenterModule(this));
loginComponent.inject(this);
}
#Override
protected void onDestroy() {
super.onDestroy();
loginComponent = null;
}
LoginPresenterModule.class
#Module
public class LoginPresenterModule {
private final LoginContract.View view;
public LoginPresenterModule(LoginContract.View view) {
this.view = view;
}
#Provides
#ActivityScoped
public LoginContract.View providesView(){
return view;
}
}
LoginComponent.class
#ActivityScoped
#Subcomponent(modules = LoginPresenterModule.class)
public interface LoginComponent {
void inject(LoginActivity loginActivity);
}
LoginPresenter.class
#ActivityScoped
public class LoginPresenter implements LoginContract.Presenter {
private static final String TAG = "LoginPresenter";
private LoginContract.View view;
private DataRespository dataRespository;
#Inject
LoginPresenter(LoginContract.View view, DataRespository dataRespository) {
this.view = view;
this.dataRespository = dataRespository;
}
#Override
public void initTest(String testNo) {
view.showProgressIndicator();
dataRespository.sendTest(testNo, new DataSource.onResponseCallback<Void>() {
#Override
public void onSuccess(Void obj) {
Log.d(TAG, "onSuccess: ");
}
#Override
public void onError(#NotNull ErrorWrapper error) {
Log.d(TAG, "onError: ");
}
});
}
#Override
public void start() {
}
}
DataRespositoryComponent.class
#ApplicationScoped
#Component(dependencies = ApplicationComponent.class,modules =
DataRespositoryModule.class)
public interface DataRepositoryComponent {
LoginComponent COMPONENT(LoginPresenterModule loginPresenterModule);
}
Basically, view is leaked while making network call.
My leakcanary stack:

This activity leaking has nothing to do with Dagger nor can Dagger help to prevent it.
The problem here lies with dataRespository.sendTest(..anonymousCallback..) where you add a callback to receive a result.
Anonymous classes as well as non-static inner classes will keep a reference to their enclosing object. In your case the callback keeps a reference to the presenter, which keeps a reference to the view, which keeps a reference to the Activity (this is what LeakCanary shows).
Since the callback is alive until it receives a response or an error, if your Activity were to be destroyed before your callback finishes, it will leak.
To fix your issue you need to either stop or unregister your callbacks, or remove the reference to the Activity. In this case it would probably be enough to set view = null in your presenter when the Activity gets destroyed to prevent the Activity from leaking.
Just make sure to check if the view is null before accessing it in your callback.

Related

Android MVVM - How to reference Activity in ViewModel

MVVM architecture,
this is my View (Activity):
private MyApp app;
private MainActivityVM viewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
app = (MyApp) this.getApplication();
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
MainActivityVM.Factory factory = new MainActivityVM.Factory(app);
final MainActivityVM model = ViewModelProviders.of(this, factory)
.get(MainActivityVM.class);
viewModel = model;
binding.setVm(viewModel);
viewModel.onCreate();
and View Model:
public class MainActivityVM extends AndroidViewModel implements ViewModel {
public MainActivityVM(#NonNull Application application) {
super(application);
}
#Override public void onCreate() {
model = new MyService();
model.getData(); /* <-- how do i pass the activity here? */
}
#Override public void onPause() { }
#Override public void onResume() { }
#Override public void onDestroy() { }
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
public Factory(#NonNull Application application) {
mApplication = application;
}
#Override
public <T extends android.arch.lifecycle.ViewModel> T create(Class<T> modelClass) {
return (T) new MainActivityVM(mApplication);
}
}
}
and Model:
public class myService{
public getData(){
if(permissionacquired(){
getdata()
}else{
requestPermission();
}
}
private void requestPermission() {
PermissionKey permKey = new PermissionKey(HealthConstants.StepCount.HEALTH_DATA_TYPE, PermissionType.READ);
HealthPermissionManager pmsManager = new HealthPermissionManager(mStore);
try {
// Show user permission UI for allowing user to change options
/* BELOW CODE REQUIRE Activity reference to PASS */
pmsManager.requestPermissions(Collections.singleton(permKey), MainActivity.this).setResultListener(result -> {
/* ABOVE CODE REQUIRE Activity reference to PASS */
Log.d(APP_TAG, "Permission callback is received.");
Map<PermissionKey, Boolean> resultMap = result.getResultMap();
if (resultMap.containsValue(Boolean.FALSE)) {
updateStepCountView("");
showPermissionAlarmDialog();
} else {
// Get the current step count and display it
mReporter.start(mStepCountObserver);
}
});
} catch (Exception e) { Log.e(APP_TAG, "Permission setting fails.", e); }
}
}
EDIT: if you see my request permission in my Model, the API require activity to be pass - how can i pass activity reference to the request permission?
I have a get permission method that comes from Model. this get permission method from my service provider require activity e.g. requestPermission(Activity)
so in my ModelView, i have the model object which is the dataService from another source.
then, how I can reference Activity in my ViewModel so I can call: model.requestPermission(Activity); in my ViewModel?
understanding from here that:
Caution: A ViewModel must never reference a view, Lifecycle, or any
class that may hold a reference to the activity context.
As long as you require permission in onCreate() method you can just move logic with permission request into the activity, and pass request result into viewModel.
In my case I also added Activity into ViewModel for permissions and strings, but it's not a good idea. When I disabled location permission in one Fragment, an application crashed, because it restarted, then restored FragmentManager with fragment stack and later started MainActivity. So ViewModel got location status too early (in constructor) and threw an exception. But when I moved getting location status to a function, then the application restarted normally.
So, using Dagger, you can write something like:
AppModule:
#JvmStatic
#Provides
fun provideActivity(app: MainApplication): AppCompatActivity = app.mainActivity
In MainApplication hold mainActivity and in MainActivity set in onCreate:
application.mainActivity = this
In onDestroy:
application.mainActivity = null
In any ViewModel add:
class SomeViewModel #Inject constructor(
private val activity: Provider<AppCompatActivity>
)
Then use it: activity.get().getString(R.string.some_string).

Cant inject classes using Dagger on Android

I am beggining with Dagger, I am using 1.2 version of it, and I have the following scenario:
Module:
#Module(injects = {
AuthenticationService.class
})
public class ServiceModule {
#Provides
AuthenticationService provideAuthenticationService() {
return ServiceFactory.buildService(AuthenticationService.class);
}
}
On my Application class I create the ObjectGraph:
public class FoxyRastreabilidadeApplication extends Application {
private static FoxyRastreabilidadeApplication singleton;
#Override
public void onCreate() {
super.onCreate();
createObjectGraph();
singleton = this;
}
private void createObjectGraph() {
ObjectGraph.create(ServiceModule.class);
}
}
and finally, at my LoginActivity, I try to inject my AuthenticationService:
public class LoginActivity extends Activity implements LoaderCallbacks<Cursor> {
private UserLoginTask mAuthTask = null;
#Inject
AuthenticationService authenticationService;
}
At this point, when I try to access my AuthenticationService instance it is always null, meaning it wasnt injected at all, I debugged my provider method to be sure of it, so, the question is, am I missing something? If so, what is it?
You need to hold on to the ObjectGraph and ask it to inject your objects directly.
Add an instance variable to your custom Application subclass to hold on to the created ObjectGraph:
this.objectGraph = ObjectGraph.create(ServiceModule.class);
Add a convenience method to your Application to do injection:
public void inject(Object object) {
this.objectGraph.inject(object);
}
Call that method wherever you need injection to happen, for example in your Activity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((MyApplication)getApplication()).inject(this);
...
}

Dagger can't inject a Presenter in my Activities

I'm refactoring an Android app to an MVP architecture, using Dagger 1.
My code is more or less the following:
public interface View {
Presenter getPresenter();
}
public abstract class Presenter {
// Dagger 1 can't have an abstract class without injectable members... :(
#Inject
Application dont_need_this;
protected View view;
public void takeView(View view) { ... }
}
The field in Presenter is not pretty, but what can we do?
Next, I have an Activity that's also a View.
public abstract class ActivityView extends Activity implements View {
#Inject
protected Presenter presenter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ObjectGraph scope;
// Looks for an "scoped" ObjectGraph hold in an retained fragment
if (/* fragment is null */) {
// This is not a rotation but the first time the activity is
// launched
MVPApplication app = (MVPApplication) getApplication();
ObjectGraph scope = app.createScope(this);
// Store in the fragment
}
scope.inject(this);
// setContentView, etc.
presenter.takeView(this);
}
}
All seems good for now...
public abstract class MVPApplication extends Application {
private ObjectGraph objectGraph;
#Override
public void onCreate() {
super.onCreate();
// Root scope
// createRootModules is defined in the subclass
objectGraph = ObjectGraph.create(createRootModules());
objectGraph.inject(this);
}
public abstract ObjectGraph createScope(View view);
}
With this, my application subclass has to:
define createRootModules() to instantiate every module in the root scope
check in createScope() the class of the concrete ActivityView, essentially using a HashMap<Class<? extends View>, Module> and do...
return objectGraph.plus(new ModuleForThisActivityViewClass());
For example, I have an CollectionActivityView. My app tries to...
return objectGraph.plus(new CollectionModule());
And I have the binding for this particular Presenter in this module:
#Module(addsTo = MyAppModule.class,
injects = {CollectionActivityView.class, CollectionPresenter.class}, complete=false)
public class CollectionModule {
#Provides
#Singleton
public Presenter providePresenter(CollectionPresenter presenter) {
return presenter;
}
}
But when doing the 'plus()` this error happens:
ComponentInfo{com.mycompany.myapp/com.mycompany.myapp.view.CollectionActivityView}:
java.lang.IllegalStateException: Unable to create binding for
com.mycompany.myapp.mvp.presenter.Presenter
...
Caused by: java.lang.IllegalStateException: Unable to create binding for
com.mycompany.myapp.mvp.presenter.Presenter
at dagger.internal.Linker.linkRequested(Linker.java:147)
at dagger.internal.Linker.linkAll(Linker.java:109)
at dagger.ObjectGraph$DaggerObjectGraph.linkEverything(ObjectGraph.java:244)
at dagger.ObjectGraph$DaggerObjectGraph.plus(ObjectGraph.java:203)
Dagger is trying to resolve the dangling dependency of the superclass Presenter in ActivityView before plusing the ObjectGraph with the new Module, who is in charge of providing the Presenter.
Is there a way to avoid this? Am I doing something terribly wrong? Do you have any suggestions for doing this properly?
Looking at it again, it was my fault.
I don't know why I did it (probably trying to avoid tagging the module as incomplete or library) but MyAppModule declared it was injecting CollectionActivityView, so the Presenter dependency had to be resolved in that module before plussing the other. Removed that from the annotation in MyAppModule and everything worked as intended.

Activity graphs and non-found dependency

I'm starting using the dagger, like it pretty much, but now facing some difficulties. My scenario is as follows: there's an activity and a dependency for it. Dependency is injected to the activity, and requires a reference to that activity. Just like this:
public class MainActivity extends BaseActivity {
#Inject ScbeHelper scbeHelper;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
}
}
public class ScbeHelper {
protected static final String TAG = "scbe_helper";
private BaseActivity activityContext;
#Inject
public ScbeHelper(BaseActivity context) {
this.activityContext = context;
}
}
I'm following dagger's example from the github for activity's graphs. So I've created a similar structure in my project. First, the BaseActivity class, from which MainActivity is inherited:
public abstract class BaseActivity extends Activity {
private ObjectGraph activityGraph;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
protoApp application = (protoApp) getApplication();
// Exception happens in next line, inside plus() method
activityGraph = application.getApplicationGraph().plus(getModules().toArray());
// Inject ourselves so subclasses will have dependencies fulfilled when this method returns.
activityGraph.inject(this);
((protoApp)getApplication()).inject(this);
}
protected List<Object> getModules() {
return Arrays.<Object>asList(new ActivityModule(this));
}
public void inject(Object object) {
activityGraph.inject(object);
}
}
And the module:
#Module(injects={MainActivity.class})
public class ActivityModule {
private final BaseActivity activity;
public ActivityModule(BaseActivity activity) {
this.activity = activity;
}
#Provides #Singleton BaseActivity provideActivity() {
return activity;
}
}
Now, the problem: No injectable members on com.example.proto.BaseActivity. Do you want to add an injectable constructor? required by public com.example.proto.ScbeHelper(com.example.proto.BaseActivity)
In other words, provider method ActivityModule.provideActivity() doesn't really provide the instance of BaseActivity for some reason, though in my understanding it's set up correctly. Does anyone see an error in my setup? Am I missing something in dagger's logic?
Thanks in advance!
I'm no Dagger expert, but you have 2 issues:
you have a cyclical dependency: you helper want to have the activity injected, your activity wants to have the helper injected. I don't think Dagger can resolve this
your activity tries to get injected twice, once with the activity-level graph, once with the application-level graph
Here's what I did to get it to work:
in ScbeHelper: remove the #Inject annotation
in BaseActivity: remove ((protoApp)getApplication()).inject(this);
in ActivityModule: remove your provideActivity method (it's not going to be used anymore), and add the following method:
#Provides #Singleton ScbeHelper provideScbeHelper() {
return new ScbeHelper(activity);
}
What this does is it provides your ScbeHelper with the context it needs, but leaves only 1 annotation-driven injection so dagger can resolve it. Hope this helps.

Dagger and dependecies on provided classes

I am "Daggering" my Android app and I am facing a little problem that I don't know if it's me or framework's fault. If it's me I will be very disappointed by myself :)
I have the following class to provide:
#Singleton
public class XmlService {
private final DataCommitter<XmlWritable> mXmlCommitter;
private final DataReader<XmlPushable> mXmlReader;
private final ConcurrentObjectMonitor mConcObjMonitor;
#Inject
public XmlService(DataCommitter<XmlWritable> xmlCommitter,
DataReader<XmlPushable> xmlProvider,
ConcurrentObjectMonitor concObjMonitor) {
mXmlCommitter = xmlCommitter;
mXmlReader = xmlProvider;
mConcObjMonitor = concObjMonitor;
}
With the following (among the others) class:
public class XmlDataReader implements DataReader<XmlPushable> {
// No Singleton no more.
// Eager Singleton (Therefore it should be made thread safe)
//static XmlDataReader mInstance = new XmlDataReader();
[CUT]
protected XmlPullParser mXmlPullParser;
#Inject
public void XmlDataRead(XmlPullParser xmlPullParser) {
mXmlPullParser = xmlPullParser;
}
This class is referred in my XmlServiceModule like this:
#Module(complete = true)
public class XmlServiceModule {
#Provides DataReader<XmlPushable> provideDataReader(XmlDataReader dataReader) {
return dataReader;
}
}
Now, the question is, is it legal to Inject provided classes? Because I am getting an error with the #Inject on the XmlDataReader ctor: Cannot inject public void XmlDataRead(org.xmlpull.v1.XmlPullParser).
EDIT: sorry guys, there was an error in my sample code, but I will leave it as is, because it can be useful in order to understand Christian's answer below.
Actually, you have an error in your sample code. You have no injectable constructor - you cannot inject an instance method, only a constructor, and that method is never called.
You should have:
public class XmlDataReader implements DataReader<XmlPushable> {
protected final XmlPullParser mXmlPullParser;
#Inject
public XmlDataReader(XmlPullParser xmlPullParser) {
mXmlPullParser = xmlPullParser;
}
}
This way it's a constructor, and will be properly injected.
As to the overall approach, you're in the right direction. You #Provide DataReader<XmlPushable, and effectively bind XmlDataReader to it in your provides method. So far so good. Dagger, when asked for DataReader<XmlPushable> will use that binding, and will effectively pass-through the dependency and provide XmlDataReader to satisfy this.
XmlDataReader has an #Inject annotated constructor, so Dagger's analysis will see it as an "injectable type" so you don't need an #Provides to provide it. As long as you either have an XmlPullParser that either has an #Inject annotated constructor or is provided in an #Provides method on a module, what you've coded here seems good, but is not quite complete.
You don't have any entryPoints listed in your #Module, and you must have a class that will be the first thing you obtain from the ObjectGraph. So this code is reasonable as far as it goes, but incomplete. You need something that injects (through a field or constructor) DataReader<XmlPushable> For example, you might make a FooActivity which does this.
Consider:
public class XmlDataReader implements DataReader<XmlPushable> {
protected XmlPullParser mXmlPullParser;
#Inject public XmlDataReader(XmlPullParser xmlPullParser) {
mXmlPullParser = xmlPullParser;
}
}
#Module(entryPoints = { FooActivity.class, BarActivity.class })
public class XmlServiceModule {
#Provides DataReader<XmlPushable> provideDataReader(XmlDataReader dataReader) {
return dataReader;
}
#Singleton
#Provides XmlPullParserFactory parserFactory() {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
return factory;
}
#Provides XmlPullParser parser(XmlPullParserFactory factory) {
XmlPullParser xpp = factory.newPullParser();
}
}
public class YourApp extends Application {
private ObjectGraph graph;
#Override public void onCreate() {
super.onCreate();
graph = ObjectGraph.get(new ExampleModule(this));
}
public ObjectGraph graph() {
return objectGraph;
}
}
public abstract class BaseActivity extends Activity {
#Override protected void onCreate(Bundle state) {
super.onCreate(state);
((YourApp) getApplication()).objectGraph().inject(this);
}
}
class FooActivity extends BaseActivity {
#Inject DataReader<XmlPushable> reader;
#Override public void onCreate(Bundle bundle) {
super.onCreate(bundle);
// custom Foo setup
}
}
class BarActivity extends BaseActivity {
#Inject DataReader<XmlPushable> reader;
#Override public void onCreate(Bundle bundle) {
super.onCreate(bundle);
// custom Bar setup
}
}
This is probably close to what you want. All things are reachable from an entry point, and all things you depend on are bound. The two Activities are your "entry points" (roots in the object graph). When they are initialized, they ultimately call the ObjectGraph's inject(Object) method on themselves, which causes Dagger to inject any #Inject annotated fields. Those fields are the DataReader<XmlPushable> fields, which is satisfy by XmlDataReader, which is provided with an XmlPullParser, which is provided by using a XmlPullParser. It all flows down the dependency chain.
Small note - #Module(complete=true) is the default. You don't need to specify complete, unless it's incomplete and you specify false

Categories

Resources