RuntimeException con Dagger 2 en dispositivos Android 7.0 y Samsung
dagger-2 samsung-mobile (2)
Encontré el mismo problema en mi aplicación y lo resolví usando el siguiente código:
Application app = activity.getApplication();
if(app == null) {
app = (Application)activity.getApplicationContext();
}
En mi consola Google Play veo muchos informes de fallos desde que comencé a usar Dagger 2, pero solo en Android 7.0 y principalmente en dispositivos Samsung, algunos dispositivos Huawai y Motorola y algunos dispositivos Xperia raros:
java.lang.RuntimeException:
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2984)
at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3045)
at android.app.ActivityThread.-wrap14 (ActivityThread.java)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1642)
at android.os.Handler.dispatchMessage (Handler.java:102)
at android.os.Looper.loop (Looper.java:154)
at android.app.ActivityThread.main (ActivityThread.java:6776)
at java.lang.reflect.Method.invoke (Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:1518)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1408)
Caused by: java.lang.RuntimeException:
at dagger.android.AndroidInjection.inject (AndroidInjection.java:48)
at dagger.android.support.DaggerAppCompatActivity.onCreate (DaggerAppCompatActivity.java:43)
at com.package.MainActivity.onCreate (MainActivity.java:83)
at android.app.Activity.performCreate (Activity.java:6956)
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1126)
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2927)
No puedo reproducir el problema ya que no tengo ningún dispositivo afectado a la mano, también parece que no todos los dispositivos de un tipo están afectados, más como un error de inicio aleatorio.
De lo que aprendí a través de la investigación es que lo más probable es que se llame a onCreate de la actividad antes de que la actividad se adjunte realmente a una aplicación. Pero no puedo probar esta afirmación ...
Estoy siguiendo el plan de arquitectura de Google para MVP + Dagger.
Mi clase de aplicación:
public class App extends DaggerApplication {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
AppComponent appComponent = DaggerAppComponent.builder().application(this).build();
appComponent.inject(this);
return appComponent;
}
}
Mi clase MainActivity:
public class MainActivity extends DaggerAppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
Código Daga relevante 2:
DaggerAppCompatActivity: https://github.com/google/dagger/blob/e8d7cd4c29c1316c5bb1cf0737d4f29111fcb1c8/java/dagger/android/support/DaggerAppCompatActivity.java#L42-L45
protected void onCreate(@Nullable Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
}
AndroidInjection: https://github.com/google/dagger/blob/e8d7cd4c29c1316c5bb1cf0737d4f29111fcb1c8/java/dagger/android/AndroidInjection.java#L43-L52
public static void inject(Activity activity) {
checkNotNull(activity, "activity");
Application application = activity.getApplication();
if (!(application instanceof HasActivityInjector)) {
throw new RuntimeException(
String.format(
"%s does not implement %s",
application.getClass().getCanonicalName(),
HasActivityInjector.class.getCanonicalName()));
}
No tengo idea de cómo resolver este bloqueo, pero la cantidad de bloqueos es demasiado importante como para ignorarlo. Dado que mi uso de Dagger 2 funciona perfectamente en todas las demás versiones de dispositivos y dispositivos con Android, asumo que no se debe a la forma en que uso Dagger 2, sino de alguna manera a algunas implementaciones 7.0 específicas del proveedor. Si alguien enfrentó el mismo problema y encontró una solución, por favor, ¡ayúdenme!
Dado que este error me está volviendo loco, presenté una versión de prueba para usuarios de 100k que intentaban entender dónde va todo esto mal.
public abstract class TestDaggerAppCompatActivity extends AppCompatActivity implements HasFragmentInjector, HasSupportFragmentInjector {
@Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;
@Inject DispatchingAndroidInjector<android.app.Fragment> frameworkFragmentInjector;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
inject();
super.onCreate(savedInstanceState);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return supportFragmentInjector;
}
@Override
public AndroidInjector<android.app.Fragment> fragmentInjector() {
return frameworkFragmentInjector;
}
private void inject() {
Application application = getApplication();
if(application == null) {
injectWithNullApplication();
return;
}
if (!(application instanceof HasActivityInjector)) {
injectWithWrongApplication();
return;
}
// Everything seems ok...
injectNow(application);
}
private void injectWithNullApplication() {
Application application = (Application) getApplicationContext();
injectNow(application);
}
private void injectWithWrongApplication() {
Application application = (Application) getApplicationContext();
injectNow(application);
}
private void injectNow(Application application) {
checkNotNull(application, "Application must not be null");
if (!(application instanceof HasActivityInjector)) {
throw new RuntimeException(String.format("%s does not implement %s", application.getClass().getCanonicalName(), HasActivityInjector.class.getCanonicalName()));
}
AndroidInjector<Activity> activityInjector = ((HasActivityInjector) application).activityInjector();
checkNotNull(activityInjector, "%s.activityInjector() returned null", application.getClass().getCanonicalName());
activityInjector.inject(this);
}
}
La actividad se basa en la actividad de Dagger con el código de inyección de Android en línea. Mi idea era que si este problema no se resolvía utilizando ApplicationContext en lugar de getApplication()
los rastros de mi pila deberían detallar qué está pasando:
- Si el problema es causado por
getApplication()
el seguimiento de la pila contendráinjectWithNullApplication()
oinjectWithWrongApplication()
- un NPE lanzado mostraría que
getApplicationContext()
devolvió un valor nulo - una
getApplicationContext()
RuntimeException lanzada mostraría quegetApplicationContext()
no es mi aplicación - si no se lanzara ninguna excepción,
getApplication()
ogetApplicationContext()
devolvieron mi solicitud y no me importaría lo que realmente resolviera el problema
Y aquí está el rastro de la pila:
Caused by: java.lang.RuntimeException:
at com.package.di.TestDaggerAppCompatActivity.inject (TestDaggerAppCompatActivity.java:49)
at com.package.di.TestDaggerAppCompatActivity.onCreate (TestDaggerAppCompatActivity.java:31)
at com.package.MainActivity.onCreate (MainActivity.java:83)
at android.app.Activity.performCreate (Activity.java:6942)
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1126)
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2880)
Por lo tanto, la cláusula if !(application instanceof HasActivityInjector)
en !(application instanceof HasActivityInjector)
inject()
no se redireccionó a injectWithWrongApplication()
pero la misma cláusula if causó la injectNow(Application application)
RuntimeException en injectNow(Application application)
en la misma instancia de aplicación. WTF? Parecía 100 veces mi código, pero si tengo un error ahí, ¡por favor avíseme! De lo contrario, creo que hay algunas cosas realmente extrañas que suceden en algunas implementaciones de proveedores de 7.0 que tal vez no sean solucionables ...
Basándome en las discusiones en https://github.com/google/dagger/issues/748 , también getApplicationContext()
una versión de prueba que solo usa getApplicationContext()
lugar de getApplication()
en todos los componentes de Dagger sin ninguna diferencia.
Mi etiqueta de aplicación de manifiesto
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/SplashScreenTheme"
android:fullBackupContent="false">
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
<meta-data android:name="com.google.android.gms.games.APP_ID" android:value="@string/app_id" />
<meta-data android:name="android.max_aspect" android:value="2.1" />
<activity
android:name="com.package.MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name="com.package.GeneratorService" android:exported="false"/>
</application>
Finalmente, encontré una manera de resolver los bloqueos causados por el uso de Dagger 2 en Android 7.0 para mi aplicación. Tenga en cuenta que esto no resuelve el problema con una aplicación personalizada que no se utiliza correctamente en Android 7.0 . En mi caso, no tenía una lógica importante en mi aplicación personalizada, además de implementar Dagger 2, por lo que simplemente reemplacé la implementación basada en DaggerApplication
con la ApplicationlessInjection
continuación.
Problemas conocidos
- No hay inyección de dependencia en las clases de aplicaciones personalizadas (de todas formas, probablemente no sea una buena idea con las malditas implementaciones de Android 7.0 OEM)
- No todos los componentes de Dagger fueron modificados por mí, solo
DaggerAppCompatActivity
,DaggerIntentService
yDaggerFragment
. Si está utilizando otros componentes comoDaggerDialogFragment
oDaggerBroadcastReceiver
, necesita crear sus propios implementos, pero creo que no debería ser demasiado difícil :)
Implementación
Deja de usar DaggerApplication
. Vuelva a extender su aplicación personalizada desde la Application
estándar o elimínela por completo. Para la inyección de dependencia con Dagger 2 ya no es necesario. Simplemente extienda, por ejemplo, FixedDaggerAppCompatActivity
y FixedDaggerAppCompatActivity
para ir con el Dagger 2 DI para actividades.
Puede notar que todavía estoy pasando el contexto de la ApplicationlessInjection.getInstance()
a ApplicationlessInjection.getInstance()
. La inyección de dependencia en sí no necesita el contexto en absoluto, pero quiero poder inyectar fácilmente el contexto de la aplicación en mis otros componentes y módulos. Y ahí no me importa si el contexto de la aplicación es mi aplicación personalizada o alguna otra cosa loca de Android 7.0, siempre y cuando sea un contexto.
ApplicationlessInjection
public class ApplicationlessInjection
implements
HasActivityInjector,
HasFragmentInjector,
HasSupportFragmentInjector,
HasServiceInjector,
HasBroadcastReceiverInjector,
HasContentProviderInjector {
private static ApplicationlessInjection instance = null;
@Inject DispatchingAndroidInjector<Activity> activityInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector;
@Inject DispatchingAndroidInjector<android.app.Fragment> fragmentInjector;
@Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;
@Inject DispatchingAndroidInjector<Service> serviceInjector;
@Inject DispatchingAndroidInjector<ContentProvider> contentProviderInjector;
public ApplicationlessInjection(Context applicationContext) {
AppComponent appComponent = DaggerAppComponent.builder().context(applicationContext).build();
appComponent.inject(this);
}
@Override
public DispatchingAndroidInjector<Activity> activityInjector() {
return activityInjector;
}
@Override
public DispatchingAndroidInjector<android.app.Fragment> fragmentInjector() {
return fragmentInjector;
}
@Override
public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
return supportFragmentInjector;
}
@Override
public DispatchingAndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
return broadcastReceiverInjector;
}
@Override
public DispatchingAndroidInjector<Service> serviceInjector() {
return serviceInjector;
}
@Override
public AndroidInjector<ContentProvider> contentProviderInjector() {
return contentProviderInjector;
}
public static ApplicationlessInjection getInstance(Context applicationContext) {
if(instance == null) {
synchronized(ApplicationlessInjection.class) {
if (instance == null) {
instance = new ApplicationlessInjection(applicationContext);
}
}
}
return instance;
}
}
FixedDaggerAppCompatActivity
public abstract class FixedDaggerAppCompatActivity extends AppCompatActivity implements HasFragmentInjector, HasSupportFragmentInjector {
@Inject DispatchingAndroidInjector<Fragment> supportFragmentInjector;
@Inject DispatchingAndroidInjector<android.app.Fragment> frameworkFragmentInjector;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
inject();
super.onCreate(savedInstanceState);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return supportFragmentInjector;
}
@Override
public AndroidInjector<android.app.Fragment> fragmentInjector() {
return frameworkFragmentInjector;
}
private void inject() {
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
AndroidInjector<Activity> activityInjector = injection.activityInjector();
if (activityInjector == null) {
throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null");
}
activityInjector.inject(this);
}
}
FixedDaggerFragment
public abstract class FixedDaggerFragment extends Fragment implements HasSupportFragmentInjector {
@Inject DispatchingAndroidInjector<Fragment> childFragmentInjector;
@Override
public void onAttach(Context context) {
inject();
super.onAttach(context);
}
@Override
public AndroidInjector<Fragment> supportFragmentInjector() {
return childFragmentInjector;
}
public void inject() {
HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector();
AndroidInjector<Fragment> fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector();
if (fragmentInjector == null) {
throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName()));
}
fragmentInjector.inject(this);
}
private HasSupportFragmentInjector findHasFragmentInjector() {
Fragment parentFragment = this;
while ((parentFragment = parentFragment.getParentFragment()) != null) {
if (parentFragment instanceof HasSupportFragmentInjector) {
return (HasSupportFragmentInjector) parentFragment;
}
}
Activity activity = getActivity();
if (activity instanceof HasSupportFragmentInjector) {
return (HasSupportFragmentInjector) activity;
}
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext());
if (injection != null) {
return injection;
}
throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName()));
}
}
FixedDaggerIntentService
public abstract class FixedDaggerIntentService extends IntentService {
public FixedDaggerIntentService(String name) {
super(name);
}
@Override
public void onCreate() {
inject();
super.onCreate();
}
private void inject() {
ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext());
AndroidInjector<Service> serviceInjector = injection.serviceInjector();
if (serviceInjector == null) {
throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null");
}
serviceInjector.inject(this);
}
}
Mi AppComponent
@Singleton
@Component(modules = {
AppModule.class,
ActivityBindingModule.class,
AndroidSupportInjectionModule.class
})
public interface AppComponent extends AndroidInjector<ApplicationlessInjection> {
@Override
void inject(ApplicationlessInjection instance);
@Component.Builder
interface Builder {
@BindsInstance
AppComponent.Builder context(Context applicationContext);
AppComponent build();
}
}
Mi módulo de aplicación
@Module
public abstract class AppModule {
@Binds
@ApplicationContext
abstract Context bindContext(Context applicationContext);
}
Y para completar mi anotación @ApplicationContext
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationContext {}
Espero poder ayudar a alguien más con mi código también. Para mí, pude resolver todos los fallos relacionados con la introducción de Dagger 2 y las extrañas versiones de Android 7.0.
Si se necesita más aclaración, házmelo saber!