android - que - Entendiendo alcances en Dagger 2
dagger kotlin (3)
Hay un par de problemas aquí y que están relacionados oblicuamente con los alcances de Dagger 2.
En primer lugar, el hecho de que haya utilizado el término "ViewModel" sugiere que está tratando de utilizar la arquitectura MVVM . Una de las características principales de MVVM es la separación de capas. Sin embargo, su código no ha logrado ninguna separación entre el modelo y el modelo de visualización.
Echemos un vistazo a esta definición de modelo de Eric Evans:
Un modelo de dominio es un sistema de abstracciones que describe aspectos seleccionados de una esfera de conocimiento, influencia o actividad (un dominio). 2
Aquí su esfera de conocimiento es una empresa y sus empleados. Al mirar su EmployeesViewModel
, contiene al menos un campo que probablemente esté mejor aislado en la capa del modelo.
class EmployeesViewModel {
List<Employee> employees; //model layer
Employee selected;
}
Tal vez sea simplemente una desafortunada elección de nombre, pero creo que su intención es crear modelos de visualización adecuados, por lo que cualquier respuesta a esta pregunta debería abordar eso. Si bien la selección está asociada a la vista, la clase realmente no califica como una abstracción de la vista. Un modelo de vista real probablemente coincidiría de alguna manera con la forma en que el Empleado se muestra en la pantalla. Supongamos que tiene TextViews de "nombre" y "fecha de nacimiento". Luego, un modelo de vista expondría los métodos que proporcionan el texto, la visibilidad, el color, etc. para esos TextViews.
En segundo lugar, lo que está proponiendo es utilizar (unísono) Dagger 2 ámbitos para comunicarse entre Actividades. Desea que la empresa seleccionada en CompaniesActivity
se comunique a EmployeesActivity
y que el empleado seleccionado en EmployeesActivity
se comunique a EmployeeDetailActivity
. Usted está preguntando cómo lograr esto al hacer que todos usen el mismo objeto global compartido.
Si bien esto puede ser técnicamente posible con Dagger 2, el enfoque correcto en Android para comunicarse entre Actividades es usar intents, en lugar de objetos compartidos. Las respuestas a esta pregunta son una muy buena explicación de este punto.
Aquí hay una solución propuesta: no está claro lo que está haciendo para obtener realmente la List<Company>
. Tal vez esté obteniendo de un archivo db, tal vez lo esté recibiendo de un pedido web en caché. Sea lo que sea, encapsular esto en un objeto CompaniesRepository
. Del mismo modo para EmployeesRepository
.
Entonces tendrás algo como:
public abstract class EmployeesRepository {
List<Employee> getAll();
Employee get(int id);
int getId(Employee employee);
}
Haz algo similar para una clase de CompaniesRepository
. Estas dos clases de recuperación pueden ser simples y se inicializan en su módulo.
@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 ahora se ve más o menos así:
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));
}
}
Extienda este ejemplo a sus otras dos actividades y se acercará a la arquitectura estándar para una aplicación de Android y usará Dagger 2 sin mezclar su capa de modelo, capa de vista, etc.
Tengo un error relacionado con el alcance en Dagger 2 y estoy tratando de entender cómo puedo resolverlo.
Tengo una actividad empresarial que muestra las empresas. Cuando el usuario selecciona un elemento, los empleados de la empresa seleccionada se muestran en EmployeesActivity
. Cuando el usuario selecciona un empleado, su detalle se muestra en EmployeeDetailActivity
.
class Company {
List<Employee> employees;
}
Class CompaniesViewModel
contiene las compañías y el seleccionado (o null
):
class CompaniesViewModel {
List<Company> companies;
Company selected;
}
CompaniesActivity
tiene una referencia a 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
contiene los empleados y el seleccionado (o null
):
class EmployeesViewModel {
List<Employee> employees;
Employee selected;
}
EmployeesActivity
tiene una referencia a 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));
}
}
Finalmente, en EmployeeDetailActivity
, selecciono Employee from view model y le muestro sus detalles:
class EmployeeDetailActivity extends Activity {
@Inject
EmployeesViewModel viewModel;
@Override
protected void onCreate(Bundle b) {
//more stuff
getComponent().inject(this);
showEmployeeDetail(viewModel.selected); // NullPointerException
}
}
Obtengo NullPointerException
porque la instancia EmployeesViewModel
en EmployeesActivity
no es lo mismo que EmployeeDetailActivity
y, en la segunda, viewModel.selected
es null
.
Este es mi módulo daga:
@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;
}
}
Tenga en cuenta que CompaniesViewModel
es singleton ( @Singleton
) pero EmployeesViewModel
no lo es, porque tiene que volver a crearse cada vez que el usuario selecciona una empresa (la lista de empleados contendrá otros elementos).
Podría set
los empleados de la empresa en EmployeesViewModel
cada vez que el usuario selecciona una empresa, en lugar de crear una nueva instancia. Pero me gustaría que CompaniesViewModel
sea inmutable.
¿Como puedo resolver esto? Cualquier consejo será apreciado.
De acuerdo con este artículo sobre ámbitos personalizados:
http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/
En resumen, los alcances nos dan "singletons locales" que viven tanto como el alcance mismo.
Para que quede claro, no hay anotaciones de @ActivityScope o @ApplicationScope proporcionadas por defecto en Dagger 2. Es el uso más común de los ámbitos personalizados. Solo el alcance de @Singleton está disponible de manera predeterminada (proporcionado por Java), y el punto es que usar un alcance no es suficiente (!) Y debe cuidar el componente que contiene ese alcance. Esto significa mantener una referencia dentro de la clase de Aplicación y reutilizarla cuando la Actividad cambie.
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;
}
//...
}
Puede echar un vistazo a este proyecto de muestra:
http://github.com/mmirhoseini/marvel
y este artículo:
https://hackernoon.com/yet-another-mvp-article-part-1-lets-get-to-know-the-project-d3fd553b3e21
familiarizarse con MVP y aprender cómo funciona el alcance daga.
Lamentablemente, creo que en este caso se abusa del marco de DI, y los problemas que encuentra son "olores de código": estos problemas indican que está haciendo algo mal.
Los marcos DI deben usarse para inyectar dependencias críticas (objetos colaboradores) en componentes de nivel superior, y la lógica que realiza estas inyecciones debe ser totalmente independiente de la lógica comercial de su aplicación.
Desde el primer vistazo, todo se ve bien: utiliza Dagger para inyectar CompaniesViewModel
y EmployeesViewModel
en Activity
. Esto podría haber estado bien (aunque no lo haría de esta manera) si se tratara de "Objetos" reales. Sin embargo, en su caso, estas son "estructuras de datos" (por lo tanto, desea que sean inmutables).
Esta distinción entre objetos y estructuras de datos no es trivial, pero es muy importante. Esta publicación en el blog lo resume bastante bien.
Ahora, si intenta inyectar Data Structures usando DI framework, finalmente convertirá el framework en "proveedor de datos" de la aplicación, delegando así parte de la funcionalidad empresarial. Por ejemplo: parece que EmployeesViewModel
es independiente de CompaniesViewModel
, pero es una "mentira": el código en el método @Provides
vincula lógicamente, "ocultando" la dependencia. Una buena "regla general" en este contexto es que si el código DI depende de los detalles de implementación de los objetos inyectados (por ejemplo, métodos de llamada, campos de acceso, etc.), generalmente es una indicación de una separación insuficiente de las preocupaciones.
Dos recomendaciones específicas:
- No mezcle la lógica comercial con la lógica DI. En su caso, no inyecte estructuras de datos, sino inyecte objetos que proporcionen acceso a los datos (incorrectos) o exponga la funcionalidad requerida al tiempo que abstrae los datos (mejor).
- Creo que su intento de compartir un View-Model entre varias pantallas no es un diseño muy robusto. Sería mejor tener una instancia separada de View-Model para cada pantalla. Si necesita "compartir" estado entre pantallas, entonces, dependiendo de los requisitos específicos, puede hacerlo con 1) extras de intención 2) objeto global 3) preferencias compartidas 4) SQLite