广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何构建Dagger2组件并干净地注入Android Activity?

在Android应用中引入Dagger2之后,最常见的困局不是不会写注解,而是组件划分混乱导致依赖图失控。Activity作为系统实例化的组件,无法通过构造函数注入,只能借助字段注入或AndroidInjector机制。如果组件构建方式不合理,要么全局单例塞进Activity生命周期造成泄漏,要么每次创建重复对象拖慢启动。透彻理解组件作用域与注入时机,是搭建可维护依赖框架的前提。

如何构建Dagger2组件并干净地注入Android Activity?

Dagger2组件构建的核心思路:划分作用域与生命周期

组件是Dagger2中依赖图的入口,通过@Component注解定义。一个组件可以依赖模块提供实例,也可以通过dependencies或@Subcomponent关联其他组件。在Android中通常至少需要一个Application级别的组件,例如AppComponent,用来提供全局单例服务,如网络客户端、数据库、SharedPreferences等。这些对象生命周期与应用一致,用@Singleton标注,能够保证整个应用运行期间只创建一次。

Activity级别的依赖不能直接放在AppComponent中,因为那样会变成应用级单例,Activity销毁后对象仍被持有,造成内存泄漏。正确做法是定义ActivityComponent,作为AppComponent的子组件,或者使用独立的组件并通过依赖关系共享。子组件继承父组件的依赖,还能定义Activity特有的作用域注解,例如@PerActivity,保证同一Activity内注入的对象在Activity生命周期内复用。需要给子组件添加模块提供Activity相关依赖,比如Presenter或ViewModel工厂。

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    ApiService apiService();
    DatabaseHelper databaseHelper();
    ActivityComponent.Builder activityComponentBuilder();
}
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {}
@PerActivity
@Subcomponent(modules = ActivityModule.class)
public interface ActivityComponent {
    void inject(MainActivity activity);
    @Subcomponent.Builder
    interface Builder {
        @BindsInstance
        Builder activity(Activity activity);
        ActivityComponent build();
    }
}
@Module
public class ActivityModule {
    @Provides
    @PerActivity
    MainPresenter provideMainPresenter(ApiService apiService, Activity activity) {
        return new MainPresenter(activity, apiService);
    }
}

这种分层结构的好处非常直接:全局服务不会被Activity持有引用,Activity销毁后子组件随之释放,减少内存泄漏风险。自定义@Scope本身是元注解,不包含实际逻辑,但Dagger2通过它保证在组件实例存活期间只创建一个带该作用域的依赖。例如MainPresenter在同一个Activity内注入多次,也只会返回同一个实例。另外,@Subcomponent让子组件可以继承父组件已经暴露的依赖,不需要反复声明,降低了组件之间的耦合。

与@Subcomponent不同,@Component(dependencies = ...)要求父组件显式暴露对象,子组件构建时手动传入依赖。这种方式虽然灵活,但在Activity场景下会增加不少样板代码,而且每次创建都要手动维护父组件引用。相比之下,@Subcomponent通过父组件的工厂方法创建子组件,生命周期更加清晰,更适合Activity这种频繁创建和销毁的组件。

Activity注入的两种方式:字段注入与构造函数注入的选择

构造函数注入是Dagger2最推荐的方式,但Activity由系统通过反射创建,无法在构造阶段提供依赖,因此只能使用字段注入。字段注入的核心是在Activity中声明@Inject成员变量,然后在onCreate里调用component.inject(this)完成依赖赋值。这种方式简单直接,但每个Activity都要手动调用一次注入逻辑,样板代码较多。

public class MainActivity extends AppCompatActivity {
    @Inject
    MainPresenter presenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((MyApplication) getApplication()).getAppComponent()
            .activityComponentBuilder()
            .activity(this)
            .build()
            .inject(this);
        presenter.loadData();
    }
}

手动调用inject虽然直白,但每个Activity都要重复获取Application、构建组件、调用注入,容易忘记。为了消除这些模板代码,Dagger2提供了AndroidInjection和AndroidInjector,结合@ContributesAndroidInjector可以自动完成注入。需要引入dagger-android与dagger-android-support依赖,在Application中实现HasAndroidInjector或HasActivityInjector接口,并通过DispatchingAndroidInjector统一分发。Activity只需在super.onCreate()之前调用AndroidInjection.inject(this)即可。

@Singleton
@Component(modules = {
    AndroidInjectionModule.class,
    AppModule.class,
    ActivityBindingModule.class
})
public interface AppComponent {
    void inject(MyApplication application);
}
@Module
public abstract class ActivityBindingModule {
    @ContributesAndroidInjector(modules = MainActivityModule.class)
    abstract MainActivity bindMainActivity();
}
public class MainActivity extends AppCompatActivity {
    @Inject
    MainPresenter presenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        presenter.loadData();
    }
}

两种方式各有适用场景。手动构建组件的优点是依赖图可见、调试直观,适合小型项目或刚接触Dagger2的开发者;缺点则是Activity必须了解组件构建细节,代码重复。AndroidInjector方案解耦了Activity与具体组件,Activity只依赖AndroidInjection静态调用,便于测试和模块化,同时减少了大量的注入模板。不过AndroidInjector多了一层抽象,编译错误或运行时的依赖缺失可能更难以定位。如果项目规模较大且Activity较多,推荐使用@ContributesAndroidInjector;如果项目简单,手动构建组件加字段注入完全够用。

最佳实践:避免常见错误与性能优化

作用域不匹配是Dagger2使用中最典型的错误之一。如果某个依赖被@Singleton标注,却由ActivityComponent提供,Dagger2编译时会直接报错,因为应用级作用域不能依赖短生命周期组件。反过来,如果Activity依赖被AppComponent提供,会变成全局单例,导致Activity被静态引用无法回收。必须严格根据生命周期选择作用域,不要图省事把不同层级的依赖混在同一个组件中。

另一个常见错误是在Activity中保存Component引用。把ActivityComponent存为Activity成员变量,会在Activity销毁后继续持有依赖图,可能导致Activity无法被垃圾回收。正确做法是每次注入使用父组件的工厂方法临时创建子组件,或者依赖AndroidInjector自动管理。这样可以保证子组件的生命周期与Activity一致,不会意外延长对象存活时间。

@Subcomponent(modules = ActivityModule.class)
@PerActivity
public interface ActivityComponent {
    @Subcomponent.Builder
    interface Builder {
        @BindsInstance
        Builder activity(Activity activity);
        Builder intent(Intent intent);
        ActivityComponent build();
    }
    void inject(MainActivity activity);
}

使用@BindsInstance可以避免在Module中编写构造参数传递逻辑,让组件构建过程更加简洁。例如Activity实例或Intent参数都可以通过@BindsInstance直接绑定到依赖图中,不需要额外的@Module提供方法。这样减少了模块类的数量,也让依赖来源更加明确。在Application中构建子组件时,直接调用activityComponentBuilder().activity(this).intent(getIntent()).build()即可完成注入。

性能优化方面,尽量把无状态依赖设计为无作用域,每次注入时新建实例,避免长时间持有。对于创建成本高的对象如Retrofit、OkHttpClient,使用@Singleton集中管理。还可以使用Lazy<T>或Provider<T>延迟加载,减少初始化开销。例如Presenter中依赖HeavyService时,可以注入Provider<HeavyService>,在真正使用时才获取。但要注意Provider方法不要在循环中频繁调用,避免创建重复实例。测试性方面,使用@Component.Builder可以替换真实模块,注入Mock依赖,让单元测试和UI测试都更加容易编写。

合理的组件构建和注入方式能让项目在功能膨胀时保持结构清晰,核心是理解作用域与生命周期的对应关系。Activity应当只依赖注入、不持有组件引用,并借助AndroidInjector减少样板代码。这些实践落地后,Dagger2才能真正帮助你管理依赖,而不是变成另一种负担。

广告位 · 文章底部(后台-模块管理替换此区域)