Understanding Areas in the Dagger 2

I have an error related to scope in dagger 2, and I'm trying to figure out how I can solve it.

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

 class Company { List<Employee> employees; } 

The CompaniesViewModel class contains the companies and the selected (or null ):

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

CompaniesActivity has a link 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)); } } 

The EmployeesViewModel class contains employees and the selected (or null ):

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

EmployeesActivity has a link 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 the selected Employee from the view model and show its details:

  class EmployeeDetailActivity extends Activity { @Inject EmployeesViewModel viewModel; @Override protected void onCreate(Bundle b) { //more stuff getComponent().inject(this); showEmployeeDetail(viewModel.selected); // NullPointerException } } 

I get a NullPointerException because the EmployeesViewModel instance in EmployeesActivity does not match the EmployeeDetailActivity , and in the second the 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; } } 

Please note that CompaniesViewModel is singleton ( @Singleton ), but EmployeesViewModel not, because it needs to be recreated every time a user selects a company (the list of employees will contain other elements).

I could set EmployeesViewModel company employees every time a user selects a company, rather than creating a new instance. But I would like CompaniesViewModel be immutable.

How can i solve this? Any recommendations would be appreciated.

+6
source share
3 answers

Unfortunately, I think that in this case you are abusing the DI infrastructure, and the problems that you encounter are “smells of code” - these problems suggest that you are doing something wrong.

DI structures must be used to inject critical dependencies (co-authoring objects) into top-level components, and the logic that performs these injections must be completely independent of the business logic of your application.

At first glance, everything looks great - you use the Dagger to introduce CompaniesViewModel and EmployeesViewModel in Activity . It could be good (although I wouldn’t do that) if it were real “Objects”. However, in your case, these are “Data Structures” (so you want them to be immutable).

This distinction between objects and data structures is not trivial, but very important. This blog post describes it very well.

Now, if you try to implement Data Structures using the DI infrastructure, you will ultimately turn the structure into the "data provider" of the application, thereby delegating part of the business functions to it. For example: it seems that the EmployeesViewModel not dependent on CompaniesViewModel , but this is a “lie” - the code in the @Provides method binds them together logically, thus “hiding” the dependency. A good “rule of thumb” in this context is that if the DI code depends on the implementation details of the injected objects (for example, calling methods, accessing fields, etc.), this usually indicates an insufficient separation of problems.

Two specific recommendations:

  • Do not mix business logic with DI logic. In your case, do not enter data structures, but enter objects that either provide access to data (bad) or reveal the required functionality when abstracting data (better).
  • I think your attempt to share the View-Model between multiple screens is not very reliable. It would be better to have a separate View-Model instance for each screen. If you need to "split" the state between the screens, then depending on the specific requirements, you can do this using 1) Intent extras 2) Global object 3) General prefs 4) SQLite
+6
source

According to this article about custom areas:

http://frogermcs.imtqy.com/dependency-injection-with-dagger-2-custom-scopes/

On a short scale, we are given "local singletones" who live as long as the region itself.

Just to be clear, @Dagger 2 lacks @ActivityScope or @ApplicationScope annotations. Its just the usual use of custom areas. Only the @Singleton area is accessible by default (provided by Java itself), and the point does not use the area enough (!), And you need to take care of the component that contains this area. This means keeping the link to it inside the Application class and reusing it when the Activity changes.

 public class GithubClientApplication extends Application { private AppComponent appComponent; private UserComponent userComponent; //... public UserComponent createUserComponent(User user) { userComponent = appComponent.plus(new UserModule(user)); return userComponent; } public void releaseUserComponent() { userComponent = null; } //... } 

You can see this sample project:

http://github.com/mmirhoseini/marvel

and this article:

https://hackernoon.com/yet-another-mvp-article-part-1-lets-get-to-know-the-project-d3fd553b3e21

to learn more about MVP and how the dagger area works.

+3
source

There are a couple of issues here that are only indirectly related to Dagger 2 areas.

First, the fact that you used the term "ViewModel" suggests that you are trying to use MVVM . One of the characteristic features of MVVM is the separation of layers. However, your code has not reached any separation between the model and the presentation model.

Let's look at this model definition from Eric Evans:

A domain model is a system of abstractions that describes individual aspects of the sphere of knowledge, influence, or activity (domain). 2

Here, your area of ​​expertise is the company and its employees. Looking at your EmployeesViewModel , it contains at least one field, which is probably better isolated in the model layer.

 class EmployeesViewModel { List<Employee> employees; //model layer Employee selected; } 

It may just be a bad choice of name, but I think your intention is to create the right view models, so any answer to this question should solve this. Although the choice is related to the view, the class does not really qualify as an abstraction of the view. The actual review model would probably somehow fit the way Employee is displayed on the screen. Say you have a "name" and a "date of birth" TextViews. Then the view model will expose to methods that provide text, visibility, color, etc. For these TextViews.

Secondly, what you are proposing is using (Singleton) Dagger 2 to communicate between activities. You want the company selected in CompaniesActivity to be transferred to EmployeesActivity , and the employee selected to EmployeesActivity to be transferred to EmployeeDetailActivity . You ask how to achieve this by forcing them to use the same common global object.

Although this may be technically possible with Dagger 2, the correct approach in Android for exchanging data between actions is to use intentions rather than shared objects. The answers to this question are a really good explanation for this point.

Here is the suggested solution: It is not clear what you are doing to get the List<Company> . Maybe you are getting from dB, maybe you are getting from cached web request. Be that as it may, encapsulate this in a CompaniesRepository object. Similarly for EmployeesRepository .

So you will have something like:

 public abstract class EmployeesRepository { List<Employee> getAll(); Employee get(int id); int getId(Employee employee); } 

Do something similar for the CompaniesRepository class. These two search classes can be single-point and can be initialized in your module.

 @Module class MainModule { @Provides @Singleton public CompaniesRepository(Dependency1 dependency1) { //TODO: code you need to generate the companies retrieval object } @Provides @Singleton public EmployeesRepository(Dependency2 dependency2) { //TODO: code you need to generate the employees retrieval object } } 

Your EmployeesActivity now looks something like this:

 class EmployeesActivity extends Activity { @Inject CompaniesRepository companiesRepository; @Inject EmployeesRepository employeesRepository; @Override protected void onCreate(Bundle b) { //more stuff getComponent().inject(this); //retrieve the id of the company selected in the previous activity //and use that to get the company model int selectedCompanyId = b.getIntExtra(BUNDLE_COMPANY_ID, -1); //TODO: handle case where no company id has been passed into the activity Company selectedCompany = companiesRepository.get(selectedCompanyId); showEmployees(selectedCompany.getEmployees); } //more stuff private onEmployeeSelected(Employee emp) { int selectedEmployeeId = employeesRepository.getId(emp); Intent employeeDetail = new Intent(); employeeDetail.putExtra(BUNDLE_EMPLOYEE_ID, selectedEmployeeId); startActivity(employeeDetail)); } } 

Extend this example with two other steps, and you will come closer to the standard architecture for the Android application, and you will use Dagger 2 without mixing your model layer, view level, etc.

+1
source

Source: https://habr.com/ru/post/1014364/


All Articles