我在Dagger 2中遇到与范围相关的错误,我试图了解如何解决它。
我有一个显示公司的CompaniesActivity
。当用户选择项目时,所选公司的员工将显示在EmployeesActivity
中。当用户选择员工时,她的详细信息显示在EmployeeDetailActivity
。
class Company {
List<Employee> employees;
}
班级CompaniesViewModel
包含公司和所选公司(或null
):
class CompaniesViewModel {
List<Company> companies;
Company selected;
}
CompaniesActivity
引用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));
}
}
班级EmployeesViewModel
包含员工和所选员工(或null
):
class EmployeesViewModel {
List<Employee> employees;
Employee selected;
}
EmployeesActivity
引用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));
}
}
最后,在EmployeeDetailActivity
中,我从视图模型中选择了Employee并显示了她的详细信息:
class EmployeeDetailActivity extends Activity {
@Inject
EmployeesViewModel viewModel;
@Override
protected void onCreate(Bundle b) {
//more stuff
getComponent().inject(this);
showEmployeeDetail(viewModel.selected); // NullPointerException
}
}
我得到NullPointerException
,因为EmployeesViewModel
中的EmployeesActivity
个实例与EmployeeDetailActivity
不同,而在第二个中,viewModel.selected
是{{1} }}
这是我的匕首模块:
null
请注意@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;
}
}
是单身(CompaniesViewModel
),但@Singleton
不是,因为每次用户选择公司时都必须重新创建(员工列表将包含其他项目)。< / p>
每次用户选择公司时,我都可以EmployeesViewModel
公司的员工set
,而不是创建新的实例。但我希望EmployeesViewModel
是不可变的。
我该如何解决这个问题?任何建议将不胜感激。
答案 0 :(得分:9)
不幸的是,我认为你在这种情况下滥用DI框架,而你遇到的问题是&#34;代码闻起来&#34; - 这些问题暗示你做错了什么。
应使用DI框架将关键依赖项(协作者对象)注入顶级组件,执行这些注入的逻辑应完全独立于应用程序的业务逻辑。
从第一眼看,一切都很好 - 你使用Dagger将CompaniesViewModel
和EmployeesViewModel
注入Activity
。这可能没问题(虽然我不会这样做)如果这些是真实的&#34;对象&#34;。但是,在您的情况下,这些是&#34;数据结构&#34; (因此你希望它们是不可变的)。
对象和数据结构之间的区别并非易事,但非常重要。 This blog post很好地总结了它。
现在,如果您尝试使用DI框架注入数据结构,最终将框架转换为数据提供程序&#34;应用程序,因此将部分业务功能委托给它。例如:看起来EmployeesViewModel
独立于CompaniesViewModel
,但它是一个&#34;谎言&#34; - @Provides
方法中的代码在逻辑上将它们联系在一起,因此&#34;隐藏&#34;依赖。好的&#34;经验法则&#34;在这种情况下,如果DI代码依赖于注入对象的实现细节(例如调用方法,访问字段等) - 它通常表示关注点分离不足。
两项具体建议:
答案 1 :(得分:4)
根据这篇关于自定义范围的文章:
http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/
短距离为我们提供“本地单身人士”,其范围与范围本身一样长。
为了清楚起见 - Dagger 2中默认没有提供@ActivityScope或@ApplicationScope注释。这只是自定义范围的最常见用法。默认情况下只有@Singleton范围可用(由Java本身提供),并且使用范围是不够的(!),您必须处理包含该范围的组件。这意味着在Application类中保留对它的引用,并在Activity更改时重用它。
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;
}
//...
}
您可以查看此示例项目:
http://github.com/mmirhoseini/marvel
和本文:
https://hackernoon.com/yet-another-mvp-article-part-1-lets-get-to-know-the-project-d3fd553b3e21
更加熟悉MVP并了解匕首的作用范围。
答案 2 :(得分:1)
这里有几个问题,只与Dagger 2范围有关。
首先,你使用了术语&#34; ViewModel&#34;建议您尝试使用MVVM架构。 MVVM的一个显着特征是层的分离。但是,您的代码未实现模型和视图模型之间的任何分离。
让我们来看看Eric Evans的模型定义:
域模型是一个抽象系统,描述知识,影响或活动领域(域)的选定方面。2
在这里,您的知识领域是公司及其员工。查看您的EmployeesViewModel
,它至少包含一个可能在模型层中更好地隔离的字段。
class EmployeesViewModel {
List<Employee> employees; //model layer
Employee selected;
}
也许这仅仅是一个不幸的名称选择,但我认为你的目的是创建适当的视图模型,所以对这个问题的任何答案都应该解决这个问题。虽然选择与视图相关联,但该类并不具备视图的抽象的条件。真实的视图模型可能会以某种方式匹配员工在屏幕上显示的方式。让我们说你有&#34;名字&#34;和&#34;出生日期&#34; TextViews。然后,视图模型将公开为这些TextView提供文本,可见性,颜色等的方法。
其次,您提议的是使用(单例)Dagger 2范围在活动之间进行通信。您希望将CompaniesActivity
中选定的公司与EmployeesActivity
和EmployeesActivity
中选择的员工进行沟通,以便与EmployeeDetailActivity
进行沟通。
您正在询问如何通过使它们使用相同的共享全局对象来实现此目的。
虽然使用Dagger 2在技术上可能是这样,但Android中用于在Activities之间进行通信的正确方法是使用意图而不是共享对象。 this question的答案是对这一点的非常好的解释。
这是一个建议的解决方案:
目前尚不清楚你在做什么获取 List<Company>
。也许你是从数据库中获取的,也许你是从缓存的Web请求获得的。无论是什么,都将其封装在一个对象CompaniesRepository
中。同样适用于EmployeesRepository
。
所以你会有类似的东西:
public abstract class EmployeesRepository {
List<Employee> getAll();
Employee get(int id);
int getId(Employee employee);
}
为CompaniesRepository
课做类似的事情。这两个检索类可以是单例,并在模块中初始化。
@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
}
}
您的EmployeesActivity现在看起来像这样:
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));
}
}
将此示例扩展到您的其他两个活动,您将接近Android应用的标准架构,您将使用Dagger 2而不会混合您的模型层,视图层等。