De RoboGuice a Dagger 2 – Parte 4 (Extra, otros problemas)
Esta es la cuarta parte de los tres artículos anteriores sobre cómo realizar la migración del inyector de dependencias RoboGuice a Dagger 2. Puedes leer el resto de artículos aquí:- Introducción
- Parte 1: Conversión de RoboGuice a Dagger 2
- Parte 2: Sobrecarga de módulos
- Parte 3: Dependencias cíclicas
LayoutInflater y Context inyectados
Uno de los problemas que surgió durante la migración fue el hecho de que algunas vistas perdían la fuente que había sido asignada mediante la librería Calligraphy. Este problema está relacionado con elLayoutInflater
utilizado para inflar las vistas. Calligraphy funciona de la siguiente manera: se inicia Calligraphy en la clase aplicación y se sobrescribe el método attachBaseContext()
en todas las activities para pasarle un CalligraphyContextWrapper
:
1 2 3 4 5 6 7 8 9 10 |
public class Aplicacion extends Application { public void onCreate() { super.onCreate(); CalligraphyConfig.initDefault(new CalligraphyConfig.Builder() .setDefaultFontPath("fonts/miFuente.ttf") .setFontAttrId(R.attr.nombreAtributoParaFuente) .build()); } } |
1 2 3 4 5 6 7 |
public abstract class BaseActivity extends AppCompatActivity { @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)); } } |
TextViews
y EditTexts
utilicen la fuente miFuente.ttf
o puedan usar otra indicándola en el atributo nombreAtributoParaFuente
.
Es decir, que tanto el layout de la activity como el de cualquier vista que inflemos utilizando un LayoutInflater
obtenido mediante el contexto de esta misma activity utilizarán esa fuente.
El problema
El problema surge cuando inyectas elLayoutInflater
. RoboGuice te permite inyectar determinadas clases del sistema operativo sin necesidad de hacerlas inyectables tú mismo. Una de ellas es LayoutInflater
. No sé cómo obtiene la instancia, pero la cuestión es que la crea mediante un contexto que ya ha sido envuelto en en el CalligraphyContextWrapper
. De esta forma, puedes usarlo sin problema ninguno y los TextViews
y EditTexts
se cargarán con su fuente correspondiente.
Así, este ejemplo con RoboGuice funciona correctamente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class MemoAdapter extends RecyclerView.Adapter { @Inject LayoutInflater mLayoutInflater; @Inject Context mContext; public MemoAdapter(List memos, Context context) { RoboGuice.getInjector(context).injectMembers(this); [...] } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new MemoHolder(parent); } @Override public void onBindViewHolder(MemoHolder holder, int position) { holder.bindView(position); } public class MemoHolder extends RecyclerView.ViewHolder { private TextView mName; public MemoHolder(ViewGroup parent) { super(mLayoutInflater.inflate(R.layout.memo_layout, parent, false)); } @Override public void bindView(int position) { mName = (TextView) itemView.findViewById(R.id.memo_name); mName.setText(position); } } } |
LayoutInflater
a menos que lo hagamos inyectable nosotros mismos, y en dicho caso, para obtenerlo solo podríamos utilizar el contexto de la aplicación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Module public class AppModule { private Context mContext; public AppModule(Context context) { mContext = context; } @Provides Context provideContext() { return mContext; } @Provides public LayoutInflater(Context context) { return LayoutInflater.from(context); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Aplicacion extends Application { private static AppComponente appComponent; public void onCreate() { super.onCreate(); CalligraphyConfig.initDefault(new CalligraphyConfig.Builder() .setDefaultFontPath("fonts/miFuente.ttf") .setFontAttrId(R.attr.nombreAtributoParaFuente) .build()); this appComponent = DaggerAppComponent().builder() .appModule(new AppModule(this)) .build } public static AppComponent getAppComponent() { return appComponent; } } |
LayoutInflater
que no ha sido creado con un contexto envuelto en el CalligraphyContextWrapper
, por lo que si lo usas, la vista que infles no tendrá aplicada esta fuente ni en sus TextViews
ni EditTexts
.
Solución
La solución es tan simple como detectar dónde estás inflando vistas con unLayoutInflater
inyectado y pasarle en su lugar el contexto de la activity y obtenerlo a partir de este:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class MemoAdapter extends RecyclerView.Adapter { private Context mContext; public MemoAdapter(Context context, List memos) { this.mContext = context; [...] } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new MemoHolder(parent); } @Override public void onBindViewHolder(MemoHolder holder, int position) { holder.bindView(position); } public class MemoHolder extends RecyclerView.ViewHolder { private TextView mName; public MemoHolder(ViewGroup parent) { super(LayoutInflater.from(mContext).inflate(R.layout.memo_layout, parent, false)); } @Override public void bindView(int position) { mName = (TextView) itemView.findViewById(R.id.memo_name); mName.setText(position); } } } |
mContext
ya no es inyectado. Si lo hubiera sido, habríamos estado en las mismas, ya que estaríamos utilizando un contexto que verdaderamente es la clase aplicación, el cual no tiene aplicado el CalligraphyContextWrapper
.
Inyección en clases genéricas
Una limitación de Dagger es que no se puede inicializar la inyección en una clase genérica. Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MiClase<T> { private OtroCampo mOtroCampo; @Inject CampoInyectado mCampoInyectado; @Inject public MiClase(OtroCampo otroCampo) { DIManager.getAppComponent().inject(this); mOtroCampo = otroCampo; } } |
1 2 3 4 5 6 |
@Component(modules = AppModule.class) public class AppComponent { void inject(MiClase miClase); } |
1 |
error: [Dagger/MembersInjection] Cannot inject members into raw type es.codicatados.temasrelacionados.MiClase |
OtroCampo
que viene en el constructor ni la dependencia de CampoInyectado
porque no acepta que se pueda inyectar nada en MiClase
.
Solución
Para solucionar esto, lo primero que tenemos que hacer es eliminar el métodoinject(MiClase miClase)
del componente, que es quien provoca este mensaje:
1 2 3 4 5 6 |
@Component(includes = AppModule.class) public interface AppComponent { void inject(MiClase miClase); // Elimina esta línea } |
1 2 3 4 5 6 7 8 9 10 11 |
public class MiClase<T> { private CampoInyectado mCampoInyectado; private OtroCampo mOtroCampo; @Inject public MiClass() { mCampoInyectado = DIManager.getAppComponent().getCampoInyectado(); mOtroCampo = DIManager.getAppComponent().getOtroCampo(); } } |
1 2 3 4 5 6 7 |
@Component(includes = AppModule.class) public interface AppComponent { CampoInyectable getCampoInyetable(); OtroCampo getOtroCampo(); } |
CampoInyectable
y OtroCampo
tienen que ser inyectables, mediante un método provider o mediante la anotación @Inject
en sus constructores.