0

Here's my code, which I based on some old tutorial found on the internet. There really should be some examples on the main site of Dagger 2, I found it really difficult to understand how to implement all this.

It's really a lot of work to get such a simple app to run. I have two questions:

Do I have to call DaggerLoggerComponent in every class I want to get some components like my Logger class?

Also how can I make the scope of the Logger class a singleton? Right now every button click creates a new logger instance.

Probably I dont understand some underlying concepts, I've only used dependency injection in Spring before and all of this seems strange to me.

public class MainActivity extends AppCompatActivity {

    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init(){
        button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoggerComponent component = DaggerLoggerComponent.builder().loggerModule(new LoggerModule()).build();
                component.getLogger().log("Hello!",MainActivity.this);
            }
        });
    }

}


public class Logger {

    private static int i = 0;

    public Logger(){
        i++;
    }

    public static int getI() {
        return i;
    }

    public void log(String text, Context context){
        Toast.makeText(context,text+" "+i,Toast.LENGTH_SHORT).show();
    }
}


@Singleton
@Component(modules={LoggerModule.class})
public interface LoggerComponent {

    Logger getLogger();

}


@Module
public class LoggerModule {
    @Provides
    @Singleton
    Logger provideLogger(){
        return new Logger();
    }
}
Greyshack
  • 1,901
  • 7
  • 29
  • 48

2 Answers2

2

The answer is

public class MainActivity extends AppCompatActivity {
    @OnClick(R.id.button) //ButterKnife
    public void onClickButton() {
        logger.log("Hello!");
    }

    @Inject
    Logger logger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Injector.INSTANCE.getApplicationComponent().inject(this);
        ButterKnife.bind(this);
    }

    @Override
    protected void onDestroy() {
        ButterKnife.unbind(this);
        super.onDestroy();
    }
}

public class Logger {
    private static int i = 0;

    private CustomApplication customApplication;

    public Logger(CustomApplication application) {
        this.customApplication = application;
        i++;
    }

    public static int getI() {
        return i;
    }

    public void log(String text){
        Toast.makeText(customApplication, text + " " + i,Toast.LENGTH_SHORT).show();
    }
}


public interface LoggerComponent {
    Logger logger();
}

@Module
public class ApplicationModule {
    private CustomApplication customApplication;

    public ApplicationModule(CustomApplication customApplication) {
        this.customApplication = customApplication;
    }

    @Provides
    public CustomApplication customApplication() {
        return customApplication;
    }
}

@Module
public class LoggerModule {
    @Provides
    @Singleton
    Logger provideLogger(){
        return new Logger();
    }
}


@Singleton
@Component(modules={LoggerModule.class, ApplicationModule.class})
public interface ApplicationComponent extends LoggerComponent {
    CustomApplication customApplication();

    void inject(MainActivity mainActivity);
}

public class CustomApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Injector.INSTANCE.initializeApplicationComponent(this);
    }
}

public enum Injector {
    INSTANCE;

    private ApplicationComponent applicationComponent;

    public ApplicationComponent getApplicationComponent() {
        return applicationComponent;
    }

    void initializeApplicationComponent(CustomApplication customApplication) {
        this.applicationComponent = DaggerApplicationComponent.builder()
            .applicationModule(new ApplicationModule(customApplication))
            .build();
    }
}

This is currently our Dagger2 architecture.

EDIT: This is from our actual code for Retrofit stuff from our application we're making:

public interface RecordingService {    
    ScheduledRecordsXML getScheduledRecords(long userId)
            throws ServerErrorException;
}

public class RecordingServiceImpl
        implements RecordingService {

    private static final String TAG = RecordingServiceImpl.class.getSimpleName();

    private RetrofitRecordingService retrofitRecordingService;

    public RecordingServiceImpl(RetrofitRecordingService retrofitRecordingService) {
        this.retrofitRecordingService = retrofitRecordingService;
    }

    @Override
    public ScheduledRecordsXML getScheduledRecords(long userId)
            throws ServerErrorException {
        try {
            return retrofitRecordingService.getScheduledPrograms(String.valueOf(userId));
        } catch(RetrofitError retrofitError) {
            Log.e(TAG, "Error occurred in downloading XML file.", retrofitError);
            throw new ServerErrorException(retrofitError);
        }
    }
}

@Module
public class NetworkClientModule {
    @Provides
    @Singleton
    public OkHttpClient okHttpClient() {
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.interceptors().add(new HeaderInterceptor());
        return okHttpClient;
    }
}

@Module(includes = {NetworkClientModule.class})
public class ServiceModule {
    @Provides
    @Singleton
    public RecordingService recordingService(OkHttpClient okHttpClient, Persister persister, AppConfig appConfig) {
        return new RecordingServiceImpl(
                new RestAdapter.Builder().setEndpoint(appConfig.getServerEndpoint())
                        .setConverter(new SimpleXMLConverter(persister))
                        .setClient(new OkClient(okHttpClient))
                        .setLogLevel(RestAdapter.LogLevel.NONE)
                        .build()
                        .create(RetrofitRecordingService.class));
    }

    //...
}

public interface RetrofitRecordingService {
    @GET("/getScheduledPrograms")
    ScheduledRecordsXML getScheduledPrograms(@Query("UserID") String userId);
}

public interface ServiceComponent {
    RecordingService RecordingService();

    //...
}

public interface AppDomainComponent
        extends InteractorComponent, ServiceComponent, ManagerComponent, ParserComponent {
}

@Singleton
@Component(modules = {
        //...
        InteractorModule.class,
        ManagerModule.class,
        ServiceModule.class,
        ParserModule.class
    //...
})
public interface ApplicationComponent
        extends AppContextComponent, AppDataComponent, AppDomainComponent, AppUtilsComponent, AppPresentationComponent {
    void inject(DashboardActivity dashboardActivity);
    //...
}
Community
  • 1
  • 1
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • Great answer, thanks. Is there some reference how to do those things or somewhere to read about how it all works? I'd like to understand it better – Greyshack Aug 28 '15 at 22:44
  • 1
    Honestly, the "reference" is the official documentation which is just terrible, I learned it from a tutorial, example codes, my previous usage of Dagger1 (based on Android Bootstrap), and using it in the project we're making at work (and figuring out that for example, you need to provide the scope for the module provider methods as well to get scoped providers). Other than that, check the `Dagger-2` tag here on Stack Overflow, I've answered a whole bunch of stuff based on my experience with it so far - although I haven't written about the *right* way of doing mocking yet. – EpicPandaForce Aug 29 '15 at 08:34
  • I recommend the stuff written about Dagger2 on this blog: http://frogermcs.github.io/ – EpicPandaForce Aug 29 '15 at 08:35
  • I've seen your answers and they are really helpful, will definitely check them out in more details, thanks a lot! – Greyshack Aug 29 '15 at 09:20
  • 1
    No prob :) Just make sure you know that I'm no one official or anything, and I learn new things about Dagger2 and other things as we progress onward :P So always have your critical eye up. I've been thinking a lot about how to do proper mocking, but I wouldn't want to need to parametrize every module with a specific "provider" instance for test and production (you can't extend modules.. -_-).... so yeah, if you ever figure out the right way of mocking, let me know! It's probably something to do with inheritance of provider and injector methods for a "real" and a "test" component. – EpicPandaForce Aug 29 '15 at 09:27
  • Do I understand it correctly that for every activity I need to add an additional inject method to the ApplicationComponent? Also if I wanted to use Retrofit, is it enough to add a provision method to the application module and the get method to some component, either to application component or to a new one, but then the ApplicationComponent would need to extend the new one as well. Is that correct? – Greyshack Aug 29 '15 at 13:58
  • 1
    Technically, what **we** do is that we have a `___Component` interface for every module, but in reality, they are just interfaces to store the provision methods. They aren't actual components. There is only one component in ours, but you can also add subscoped components if you like (a component for the activity and have your own activity-scoped modules, for example). So quick answers, `I need to add an additional inject method to the ApplicationComponent` **yes**, for the second half, you just need to add a provider method to the module (don't forget the scope) and provision to component. Yes – EpicPandaForce Aug 29 '15 at 14:40
  • 1
    Anyways, I added some code from our app to show what we did for Retrofit related stuff. – EpicPandaForce Aug 29 '15 at 14:48
  • 1
    Thanks that helps! You're great :) – Greyshack Aug 29 '15 at 16:07
  • Also about this enum Injector class, it could have been a normal singleton class just as well right? Why did you choose it to be an enum? – Greyshack Aug 29 '15 at 16:33
  • Glad that helped! :) Technically I use enum singletons because http://stackoverflow.com/a/26292956/2413303 – EpicPandaForce Aug 29 '15 at 16:36
  • But yes, I am actually pretty sure the more I think about it that the right way to allow for mocking is to inherit the provision methods and the inject methods from another interface into ApplicationComponent, and then the component in Injector would be swappable for a component that has the right modules specified, not just the specific ApplicationComponent. Either that or the "provider" interface for modules, but I don't like that approach. As I said, we haven't been doing this, but it is a problem we will have to solve - and I think this will be the solution. – EpicPandaForce Aug 29 '15 at 17:02
  • 1
    To be honest I'm not sure I get what you mean, probably too advanced for now :D I have one more question, is it possible to inject into my example logger class context using @Inject annotation? I managed to achieve this by adding Context parameter to LoggerModule provideLogger method, and then added Context to Logger constructor. But that is a lot of manual work tbh – Greyshack Aug 29 '15 at 17:59
  • `using @Inject annotation` yes, but you must add the `inject(Logger logger);` to your `ApplicationComponent`, then call `Injector.INSTANCE.getApplicationComponent().inject(this);` in the constructor of `Logger`. – EpicPandaForce Aug 29 '15 at 18:24
  • Yep I did it like that. What's the difference between that and passing the dependencies through constructor? Which one do you use? – Greyshack Aug 29 '15 at 18:36
  • If you look up "constructor injection vs field injection", you'll see it's a very hot topic with lots of arguments for each side, but I personally prefer field injection because that's what I'm used to from using Spring Framework. If there's one glaring flaw of field injection, it's that you need to specify the implementation class in the inject list, which makes the code a bit more difficult to port from one place to another (I needed a subset of the project and I was like ugh) but I still prefer field to constructor because it makes the modules and classes look cleaner. Personal opinion. – EpicPandaForce Aug 29 '15 at 19:51
  • Well, I wrote on the theoretical solutions of mocking with Dagger2 at the moment: http://stackoverflow.com/a/32298518/2413303 – EpicPandaForce Aug 30 '15 at 16:50
  • Isn't it possible to make an interface for the mocked service, and have two implementations production and mock, and then in the module have two providing methods one returning the mock and one the original with @Named annotation so that we can inject the one we want later into activity ? Also I can't get Named annotation to work, I've only added it to the Module class to the providing method and then in activity to inject annotation and it doesn;t compile. – Greyshack Aug 30 '15 at 21:19
  • It doesnt make sense but it works if I add the same @Named annotation to the component method as well. It sucks because otherwise now I'd have to make two methods in the component as well. I guess I can just switch in the single providing method, if I want to use the Mock I just return new Mock and otherwise return new Production service. – Greyshack Aug 30 '15 at 21:35
  • Yes, that works too... I just didn't want to add the mock stuff to the production stuff! Especially because later on, that just doesn't work right; having to add `@Named()` annotations randomly in your code. – EpicPandaForce Aug 30 '15 at 22:01
1

Do I have to call DaggerLoggerComponent in every class I want to get some components like my Logger class?

Yes for all classes that created by the system like Application, Activity and Service. but for you own classes, you don't need that. just annotate you constructor with @inject and dagger will provide your dependencies.

Also how can I make the scope of the Logger class a singleton? Right now every button click creates a new logger instance.

Your setup for singleton is correct. but you have to initialize the component one time after the activity is created (onCreate) in order to let dagger to inject all fields. Also you can utilize lazy injection feature if you don't need the Logger object right away.

    @Inject
    Logger logger;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LoggerComponent component = DaggerLoggerComponent.builder().loggerModule(new LoggerModule()).build();

        component.inject(this);

        init();
    }

Then you can access your object without take the reference from the component:

private void init(){
        button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                logger.log("Hello!",MainActivity.this);
            }
        });
    }

In summary: You have to initialize the component in all classes that use field injections.

UPDATE: To do the actual injection, you have to declare inject() method into your component and dagger will automatically implement it. This method will take care of provide any object annotated with @Inject.

sudanix
  • 515
  • 3
  • 9
  • But it's possible to somehow wrap system created classes with dagger? I've seen it somewhere, extending application class etc, but I can't find a good example. Do you know how to do that? Because if I want to have only one instance of a given service, I cant create the DaggerLoggerComponent in every activity I make. Activities should be managed in some kind of container I think – Greyshack Aug 28 '15 at 19:41
  • Also your code doesnt work. @Inject Logger is a nullpointer, you sure it's enough to create a local LoggerComponent in onCreate ? – Greyshack Aug 28 '15 at 21:55
  • I update the answer. I forgot to call inject method on the component. Your component interface should have inject(MainActivity activity) method. – sudanix Aug 28 '15 at 22:03
  • Yep it worked. Still I don't really liike this. The logger component is tightly coupled to MainActivity. I don't really see the profits here of using this library... seems really like a lot of work to do simple things – Greyshack Aug 28 '15 at 22:07
  • Actually, your **real** concern should be that a component's scope is tied to the component instance, meaning your "singleton logger" will be killed and recreated along with the activity that contains the component. – EpicPandaForce Aug 28 '15 at 22:11