Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding scopes in Dagger 2

I have an scope-related error in Dagger 2 and I'm trying to understand how I can solve it.

I have a CompaniesActivity that shows companies. When the user selects an item, selected company's employees are shown in EmployeesActivity. When the user selects an employee, her detail is shown in EmployeeDetailActivity.

class Company {
    List<Employee> employees;
}

Class CompaniesViewModel contains the companies and the selected one (or null):

class CompaniesViewModel {
    List<Company> companies;
    Company selected;
}

CompaniesActivity has a reference to CompaniesViewModel:

class CompaniesActivity extends Activity {

    @Inject
    CompaniesViewModel viewModel;

    @Override
    protected void onCreate(Bundle b) {
        //more stuff
        getComponent().inject(this);
        showCompanies(viewModel.companies);
    }

    //more stuff

    private onCompanySelected(Company company) {
        viewModel.selected = company;
        startActivity(new Intent(this, EmployeesActivity.class));
    }

}

Class EmployeesViewModel contains the employees and the selected one (or null):

class EmployeesViewModel {
    List<Employee> employees;
    Employee selected;
}

EmployeesActivity has a reference to EmployeesViewModel:

  class EmployeesActivity extends Activity {

        @Inject
        EmployeesViewModel viewModel;

        @Override
        protected void onCreate(Bundle b) {
            //more stuff
            getComponent().inject(this);
            showEmployees(viewModel.employees);
        }

        //more stuff

        private onEmployeeSelected(Employee emp) {
            viewModel.selected = emp;
            startActivity(new Intent(this, EmployeeDetailActivity.class));
        }

    }

Finally, in EmployeeDetailActivity, I get selected Employee from view model and show her detail:

  class EmployeeDetailActivity extends Activity {

        @Inject
        EmployeesViewModel viewModel;

        @Override
        protected void onCreate(Bundle b) {
            //more stuff
            getComponent().inject(this);
            showEmployeeDetail(viewModel.selected); // NullPointerException
        }
    }

I get NullPointerException because EmployeesViewModel instance in EmployeesActivity is not the same as the EmployeeDetailActivity and, in the second one, viewModel.selected is null.

This is my dagger module:

@Module
class MainModule {

    @Provides
    @Singleton
    public CompaniesViewModel providesCompaniesViewModel() {
        CompaniesViewModel cvm = new CompaniesViewModel();
        cvm.companies = getCompanies();
        return cvm;
    }

    @Provides
    public EmployeesViewModel providesEmployeesViewModel(CompaniesViewModel cvm) {
        EmployeesViewModel evm = new EmployeesViewModel();    
        evm.employees = cvm.selected.employees;
        return evm;
    }

}

Note that CompaniesViewModel is singleton (@Singleton) but EmployeesViewModel is not, because it has to be recreated each time user selects a company (employees list will contain other items).

I could set the company's employees to EmployeesViewModel each time user selects a company, instead of create a new instance. But I would like CompaniesViewModel to be immutable.

How can I solve this? Any advise will be appreciated.

like image 739
Héctor Avatar asked Jan 24 '17 09:01

Héctor


People also ask

What are scopes in Dagger 2?

Dagger 2 provides @Scope as a mechanism to handle scoping. Scoping allows you to “preserve” the object instance and provide it as a “local singleton” for the duration of the scoped component. In the last tutorial, we discussed a special scope called @Singleton.

How does the custom scope work in Dagger?

A scope is an annotations class with specific additional annotations: @Scope and @Retention. @Scope annotation is provided by Dagger library to define custom scopes. In our example, we create two scopes: @ActivityScope (for activities) and @FragmentScope (for fragments).

What are subcomponents in Dagger?

Subcomponent are components that is like an extension to its parent component. Partition the dependencies into different compartments. Avoid the parent component to have too many dependencies bound to it. Subcomponent and its parent component have different scope (of lifespan).

What does inject mean in Dagger?

With the @Inject annotation on the constructor, we instruct Dagger that an object of this class can be injected into other objects. Dagger automatically calls this constructor, if an instance of this class is requested.


1 Answers

Unfortunately, I think that you abuse DI framework in this case, and the issues that you encounter are "code smells" - these issues hint that you're doing something wrong.

DI frameworks should be used in order to inject critical dependencies (collaborator objects) into top level components, and the logic that performs these injections should be totally independent of the business logic of your application.

From the first sight everything looks fine - you use Dagger in order to inject CompaniesViewModel and EmployeesViewModel into Activity. This could have been fine (though I wouldn't do it this way) if these were real "Objects". However, in your case, these are "Data Structures" (therefore you want them to be immutable).

This distinction between Objects and Data Structures is not trivial, but very important. This blog post summarizes it pretty well.

Now, if you try to inject Data Structures using DI framework, you ultimately turn the framework into "data provider" of the application, thus delegating part of the business functionality into it. For example: it looks like EmployeesViewModel is independent of CompaniesViewModel, but it is a "lie" - the code in @Provides method ties them together logically, thus "hiding" the dependency. Good "rule of thumb" in this context is that if DI code depends on implementation details of the injected objects (e.g. calls methods, accesses fields, etc.) - it is usually an indication of insufficient separation of concerns.

Two specific recommendations:

  1. Don't mix business logic with DI logic. In your case - don't inject data structures, but inject objects that either provide access to the data (bad), or expose the required functionality while abstracting the data (better).
  2. I think that your attempt of sharing a View-Model between multiple screens is not a very robust design. It would be better to have a separate instance of View-Model for each screen. If you need to "share" state between screens, then, depending on the specific requirements, you could do this with 1) Intent extras 2) Global object 3) Shared prefs 4) SQLite
like image 91
Vasiliy Avatar answered Sep 28 '22 06:09

Vasiliy