Dagger 2 - 两个提供相同接口的provide方法

46

假设我有:

public interface Shape  {}


public class Rectangle implements Shape {

}

public class Circle implements Shape {

}

我有一个ApplicationModule,需要提供RecCircle的实例:

@Module
public class ApplicationModule {
    private Shape rec;
    private Shape circle;

    public ApplicationModule() {
        rec = new Rectangle();
        circle= new Circle ();
    }

    @Provides
    public Shape provideRectangle() {
        return rec ;
    }

    @Provides
    public Shape provideCircle() {
        return circle;
    }
}

ApplicationComponent

@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
    Shape provideRectangle();
}

以目前的代码状态,它无法编译。报错信息如下:

Error:(33, 20) error: Shape is bound multiple times.

我认为这是有道理的,因为组件正在尝试查找一个Shape实例,但它找到了两个,所以不知道该返回哪一个。

我的问题是:我应该如何处理这个问题?


1
你可以使用@Qualifier来区分不同的类型。这里是一个简短的例子,展示如何使用这个注解。此外,这个SO问题可能会有所帮助。 - QBrute
1
@ofek-agmon 当您使用 DI 概念时,请勿在构造函数定义中使用 new,而是提供一个方法来提供新对象,并将这些对象注入到相应的构造函数中。 - silentsudo
谢谢你的回答。@sector11,你能给我指一下示例代码让我看看你的意思吗? - Ofek Agmon
4个回答

66

我最近在这篇帖子中回答了类似这样的问题:

Dagger 2:使用@Named获取同一对象的多个实例时出现错误

您需要在您的模块中使用 @Named("someName"),就像这样:

@Module
public class ApplicationModule {
private Shape rec;
private Shape circle;

public ApplicationModule() {
    rec = new Rectangle();
    circle= new Circle ();
}

@Provides
 @Named("rect")
public Shape provideRectangle() {
    return rec ;
}

@Provides
 @Named("circle")
public Shape provideCircle() {
    return circle;
}

}

然后在需要注入它们的任何地方,只需编写

@Inject
@Named("rect")
 Shape objRect;

很有趣,但是在 Kotlin 中你需要用不同的方式进行注入:

@field:[Inject Named("rect")]
lateinit var objRect: Shape

所以显然这段代码编译没有问题,但是注入实际上并没有起作用。我在这里开了一个新的问题,请您看一下,感激不尽。http://stackoverflow.com/q/40139428/1662033 - Ofek Agmon
Kotlin这部分起了作用。当我以传统的方式应用注解时,代码无法编译。 - Devansh Maurya

17

@Qualifier注解是区分具有相同类型的不同实例或注入请求的正确方法。主用户指南页面有一个完整的关于它们的部分

@Qualifier @Retention(RUNTIME)
public interface Parallelogram {} /* name is up to you */

// In your Module:
@Provides @Parallelogram
public Shape provideRectangle() {
    return rec ;
}

// In your other injected types:
@Inject @Parallelogram Shape parallelogramShape;
// or
@Inject @Parallelogram Provider<Shape> parallelogramShapeProvider;

// In your Component:
@Parallelogram Shape provideRectangle();

顺带一提:虽然我同意sector11的观点,认为你不应该在注入类型中使用new,但如果需要,在模块中调用new正是正确的地方。除了添加限定符注释外,我认为你的模块看起来非常合适。


编辑关于使用@Named与自定义限定符注释的比较:

  • @Named是一个内置的@Qualifier注释,就像我上面创建的那个注释一样。对于简单的情况,它很有效,但由于绑定只是一个字符串,因此您在检测有效键或自动完成键时将无法从IDE获得太多帮助。
  • 与Named的字符串参数一样,自定义限定符注释可以具有字符串、基元、枚举或类文字属性。对于枚举,IDE通常可以自动完成有效值。
  • @Named和自定义限定符注释可以通过在组件方法上指定注释(如我上面所做的@Parallelogram)的方式完全相同地从注释中访问。

谢谢你的回答。我会阅读限定符注解方面的内容,但Amir Ziarati的回答不好吗?我刚刚添加了@Named("rect"),它非常好用,但我认为它只在使用@Inject注释进行注入时起作用,并且在组件中创建提供方法时不起作用,例如:Shape provideCircle();, Shape provideRect();,然后你可以这样做:Circle = myComponent.provideCircle();。我认为在这种情况下@Named("rect")会有问题。你认为呢? - Ofek Agmon
1
@Ofek 对于“Amir Ziarati的答案也不好吗”,这是一个公正的回答;我的回答没有涉及他的问题,因为当我发布我的回答时,他的回答还不存在。我已经编辑了我的答案,以解决与@Named的差异,但简而言之,它们都应该可以正常工作。自定义限定符需要一些额外的代码,但也可以提供更多的IDE帮助和类型安全性。 - Jeff Bowman

10

我认为在Module的构造函数中使用new运算符不是一个好主意。这将在初始化对象图时(即调用new ApplicationModule()时)创建每个提供的对象的实例,而不是在Dagger第一次需要该对象时创建它们。在这种情况下(只有两个对象),可能可以忽略不计,但在大型项目中,这可能会导致应用程序启动时出现瓶颈。相反,我将遵循@sector11的建议,在@Provides注解的方法中实例化对象。

至于提供两个相同类型的对象,@Jeff和@Amir都是正确的。您可以使用提供的@Named()限定符,或者像这样创建自己的限定符:

@Qualifier @Retention(RetentionPolicy.RUNTIME)
public @interface RectangleShape {}

@Qualifier @Retention(RetentionPolicy.RUNTIME)
public @interface CircleShape {}

那么你的 ApplicationModule 应该像这样:

@Module
public class ApplicationModule {

    @Provides @RectangleShape // @Named("rectangle")
    public Shape provideRectangle() {
        return new Rectangle();
    }

    @Provides @CircleShape // @Named("circle")
    public Shape provideCircle() {
        return new Circle();
    }

}

使用这个,你可以像这样将这些对象注入到你的类中:

@Inject @RectangleShape /* @Named("rectangle") */ public Shape mRectangle;
@Inject @CircleShape /* @Named("circle") */ public Shape mCircle;
如果您需要在没有@Inject注释的情况下提供您的Shape类的实例,则可以在Component类中这样做:
@Component(modules = { ApplicationModule.class })
public interface ApplicationComponent {

    void inject(MyApplication application);

    @RectangleShape // @Named("rectangle")
    Shape getRectangle();

    @CircleShape // @Named("circle")
    Shape getCircle();

}

这些方法将提供由@Provides注解方法提供的每个类的相同实例。


我理解你所说的关于在@Provides方法中创建类而不是在构造函数中创建以避免瓶颈的方法。但是,例如,我还有一个SharedPrefs类,它是在ApplicationModule中创建的,并且我在多个类中使用applicationComponent.provdieSharedPrefs();->因此,如果我在@ Provides方法中使用new ->我将拥有同一类的许多实例。 - Ofek Agmon
@OfekAgmon 你提供给Dagger的“Component”是一个“接口”,在其中你可以放置getter方法以提供该方式下你类的实例。因此,如果你在你的“ApplicationComponent”中创建了一个名为“getSharedPreferences()”的方法,它返回一个“SharedPreferences”的实例,那么Dagger将始终从你的“ApplicationModule”提供相同的“SharedPreferences”实例。没有必要在“@Provides”注释方法之外创建“SharedPreferences”的实例。 - Bryan

4

除了@Named和自定义限定符(在其他回答中展示),您还可以使用带有enum参数的自定义限定符:

// Definition

@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ShapeType {
  ShapeTypeEnum value(); /* default ShapeTypeEnum.RECTANGLE; */
}

public enum ShapeTypeEnum {
  RECTANGLE, CIRCLE
}

// Usage

@Provides @ShapeType(ShapeTypeEnum.RECTANGLE)
public Shape provideRectangle() {
    return new Rectangle();
}

@Inject @ShapeType(ShapeTypeEnum.RECTANGLE) Shape rectangle;

这是@Named和自定义限定符(每个实现需要一个文件)之间的混合体,它既容易出错又无法自动完成。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接