Manuel Vicente Vivo

Problem Statement

Imagine that your app provides Fingerprint authentication. Your users will have to enable it manually. They can do it either after they register into your app (Registration Flow) or once they've logged in (In App – Settings).

Fingerprint_screen.png

The logic is the same for both flows but there's specific logic for each one. For example, we want to track different analytics tags depending on the flow we're in.

“Registration Fingerprint enable button pressed”
“In App Fingerprint enable button pressed”

We want to reuse as much as possible. Potentially: Activity, Fragment, Presenter, ...

How would you solve it with Dagger?

Let's take a step back

How would the code look like with just one flow?

If we had one flow, the user would register successfully into the app and we'd show the Fingerprint screen to enable fingerprint authentication.

The logic in the presenter that gets injected by Dagger would be something like this:


public class FingerprintPresenter {

    private final AnalyticsManager analyticsManager;
	
	public FingerprintPresenter(AnalyticsManager analyticsManager) {
	
		this.analyticsManager = analyticsManager;
	}
	
	public void onEnableButtonPressed() {

		analyticsManager.logTag("Registration Fingerprint enable button pressed");
	}
}

Then you'd inject the Presenter with a Module and a Component. Because we just call it from the Registration flow, we could add it to the RegistrationComponent.


@FlowScope
@Component(dependencies = BaseComponent.class, 
			modules = {RegistrationModule.class, FingerprintModule.class})
public interface RegistrationComponent {

	void inject(RegistrationActivity activity);
	void inject(FingerprintActivity activity);
}


@Module
public class FingerprintModule {

	@Provides	
	public FingerprintPresenter providesFingerprintPresenter(AnalyticsManager analyticsManager) {

		return new FingerprintPresenter(analyticsManager);
	}
}

The Activity would look something like this:


public class FingerprintActivity extends AppCompactActivity {
	
	@Inject FingerprintPresenter presenter;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		
		super.onCreate(savedInstanceState);
		
		RegistrationComponent component = DaggerRegistrationComponent.builder()
											.applicationComponent(getAppComponent())
											.build();
											
		component.inject(this);
		
		setContentView(R.layout.activity_fingerprint);
	}
}

Requirements change

Now, our client decides that the Fingerprint screen needs to be in App as well (as part of Settings). And they want us to track the interaction in the different flows so we can compare them:
Do we get more activations during registration or in app?

At some point, a class should know in which flow we are. It makes sense that class is the Activity. As part of the Intent (with an extra), the activity can get the flow it is in. The RegistrationActivity will start the Activity adding a "hey, you're part of the registration flow" extra. The same can be used in the HomeActivity with a "InApp flow" value.


public class FingerprintActivity extends AppCompactActivity {
	
	@Inject FingerprintPresenter presenter;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		
		super.onCreate(savedInstanceState);

		// 0: Registration, 1: In App		
		int flow = getIntent().getIntExtra("FingerprintActivityFlowExtra", 0);
		
		...
	}
}

What can we do to solve the problem?

Solution 1 - Not ideal

Probably, the first thing that comes to your mind is passing the flow in the Presenter's constructor and have something like this:


public class FingerprintPresenter {

	private final AnalyticsManager analyticsManager;
	private final int flow;
	
	public FingerprintPresented(AnalyticsManager analyticsManager, int flow) {
		
		this.analyticsManager = analyticsManager;
		this.flow = flow;
	}
	
	public void onEnableButtonPressed() {

		if (flow == 0) {
			analyticsManager.logTag("Registration Fingerprint enable button pressed");
		} else {
			analyticsManager.logTag("In App Fingerprint enable button pressed");
		}
	}
}

But then, because the value is dynamic, we have to pass it as a parameter in the constructor of the Module.


@Module
public class FingerprintModule {

	int flow;
	
	public FingerprintModule(int flow) {
		
		this.flow = flow;
	}

	@Provides	
	public FingerprintPresenter providesFingerprintPresenter(AnalyticsManager analyticsManager) {

		return new FingerprintPresenter(analyticsManager, flow);
	}
}

And it makes it more difficult to build the Component because we have to create an instance of the Module ourselves.


public class FingerprintActivity extends AppCompactActivity {
	
	@Inject FingerprintPresenter presenter;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		
		super.onCreate(savedInstanceState);
		
		// 0: Registration, 1: In App		
		int flow = getIntent().getIntExtra("FingerprintActivityFlowExtra", 0);

		RegistrationComponent component = DaggerRegistrationComponent.builder()
 									      .applicationComponent(getAppComponent())
										  .fingerprintModule(new FingerprintModule(flow))
									      .build();
											
		component.inject(this);
		
		setContentView(R.layout.activity_fingerprint);
	}
}

This would do it! What are the disadvantages of this option?

  • Each time we want to add a new flow, we have to take a look at all the places we're considering it and add the new behaviour. If instead of just the analytics tracking we have to do more stuff and in more methods, the situation can be unsustainable. So, I'd say it's not very scalable.
  • Our presenter will be full of "If" statements which might involve a huge unit test class as well.
  • The example contains just an Activity and a Presenter. What if we had Fragments? Or more Presenters? Those "ifs" would be all over the place.
  • The method annotated with @Provides in the Module cannot be static because we have to create an instance of the Module. That is going to cause some performance issues: Dagger creates a Provider for each @Provides method. Static Provides methods is something that Dagger is working on. In this way, Dagger can hook directly into your static methods which improves performance (startup time, above all) since it doesn't create that many instances.
  • It doesn't make a lot of sense that even though we're in the “InApp” flow, we're injecting a RegistrationComponent. Although this problem can be fixed if we create a specific component for the Fingerprint feature (it shouldn't be of @FlowScope though).

Solution 2 - A better one

Another option would be creating an abstract class with an abstract method in charge of tracking the tag relative to the flow. We would create a FingerprintInAppPresenter and FingerprintRegistrationPresenter extending both from the abstract class FingerprintPresenter with all the shared logic but the analytics tracking.

Personally, instead of using Inheritance, I'd rather use Composition. I'm sure you would as well: Java doesn't support multiple inheritance, it's easier to test, more flexible, etc.

You can use inheritance (the implementation would be really similar) if you want, but from now on, I'm going to focus on composition.

How can we extract the shared logic out of the Presenter? With an interface.


public interface FingerprintTracker {

	void trackEnableButtonPressed();
}

In this way, we can use the interface in the Presenter. Simple and easy to read and understand.


public class FingerprintPresenter {

	private final FingerprintTracker fingerprintTracker;

	public FingerprintPresenter(FingerprintTracker fingerprintTracker) {
		
		this.fingerprintTracker = fingerprintTracker;
	}

	public void onEnableButtonPressed() {

		fingerprintTracker.trackEnableButtonPressed();
	}
}

Because we have two different behaviours (flows), we need two different classes that implement that interface.


public class FingerprintRegistrationTracker implements FingerprintPresenter {

	private final AnalyticsManager analyticsManager;	

	public FingerprintRegistrationTracker(AnalyticsManager analyticsManager) {
	
		this.analyticsManager = analyticsManager;
	}
	
	@Override
	public class trackEnableButtonPressed() {
	
		analyticsManager.logTag("Registration Fingerprint enable button pressed");
	}
}

public class FingerprintInAppTracker implements FingerprintPresenter {

	private final AnalyticsManager analyticsManager;	

	public FingerprintRegistrationTracker(AnalyticsManager analyticsManager) {
	
		this.analyticsManager = analyticsManager;
	}
	
	@Override
	public class trackEnableButtonPressed() {
	
		analyticsManager.logTag("In App Fingerprint enable button pressed");
	}
}

Now, we need a Module and a Component for each flow. For example, in the InApp flow, the FingerprintInAppModule is going to inject the FingerprintInAppTracker when asked for a FingerprintTracker. If we had a InAppComponent, we could add the FingerprintInAppModule to it. When the feature is used "in App", the FingerprintTracker is going to be of type FingerprintInAppTracker. Let's see that in code.


@Module
public class FingerprintInAppModule {

	@Provides
	public FingerprintTracker providesFingerprintTracker(AnalyticsManager aManager) {
	
		return new FingerprintInAppTracker(aManager);
	}

	@Provides
	public FingerprintPresenter providesFingerprintPresenter(FingerprintTracker tracker) {
	
		return new FingerprintPresenter(tracker);
	}
}

@FlowScope
@Component(dependencies = BaseComponent.class, 
			modules = {HomeModule.class, FingerprintInAppModule.class})
public interface InAppComponent {

	void inject(HomeActivity activity);
	void inject(FingerprintActivity activity);
}

We can do the same for the Registration flow. The RegistrationComponent includes the FingerprintRegistrationModule which is going to inject a FingerprintTracker of type FingerprintRegistrationTracker.


@Module
public class FingerprintRegistrationModule {

	@Provides
	public FingerprintTracker providesFingerprintTracker(AnalyticsManager aManager) {
	
		return new FingerprintRegistrationTracker(aManager);
	}

	@Provides
	public FingerprintPresenter providesFingerprintPresenter(FingerprintTracker tracker) {
	
		return new FingerprintPresenter(tracker);
	}
}


@FlowScope
@Component(dependencies = BaseComponent.class, 
			modules = {HomeModule.class, FingerprintRegistrationModule.class})
public interface RegistrationComponent {

	void inject(RegistrationActivity activity);
	void inject(FingerprintActivity activity);
}

Now we just have to inject the right component in the Activity depending on the flow.


public class FingerprintActivity extends AppCompactActivity {
	
	@Inject FingerprintPresenter presenter;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		
		super.onCreate(savedInstanceState);
		
		int flow = getIntent().getIntExtra("FingerprintActivityFlowExtra", 0);
		if (flow == 0) {
		
			RegistrationComponent component = DaggerRegistrationComponent.builder()
	 									      	.applicationComponent(getAppComponent())
										      	.build();
			component.inject(this);
		} else {

			InAppComponent component = DaggerInAppComponent.builder()
 									      .applicationComponent(getAppComponent())
									      .build();
			component.inject(this);		
		}
		
		setContentView(R.layout.activity_fingerprint);
	}
}

This is my favorite solution so far. Thoughts:

  • The Presenter is really neat. The shared behaviour is in one place and we don't care if it diverges because that's going to be injected externally.
  • If you want to add a new flow, you just have to create the classes implementing the different interfaces you might have and a new Dagger module.
  • The logic to decide how to behave in certain flow is just in one place: when we inject the Component in the Activity.
  • Testing becomes easier with mocking since the different behaviours are injected. We can test each class in isolation and verify the interactions.
  • If instead of having RegistrationComponent and InAppComponent you have a FingerprintComponent, you'd need to create a Component per flow: something like FingerprintInAppComponent and FingerprintRegistrationComponent.
  • Yes, we could refactor this solution even more :) For example, we can create another Module that @Provides the Presenter (which is duplicated code in the two FingerprintFlowModules). We could call it FingerprintModule, for example. In that case, the FingerprintRegistrationModule and FingerprintInAppModule would just provide the RegistrationTracker. We could keep going but I don't want the post to be too long, I'll leave it to you!

Thanks a lot! Let me know what you think.

All the best.

manuel_vicente_vivo.jpeg Manuel VicenteVivo 

Short Bio: Mobile Software Engineer working at Capital One who loves Mobile dev, AI, music and science in general.

Twitter (@manuelvicnt)

Medium (https://medium.com/@manuelvicnt)

Related Search Term(s): Dagger, Tutorials

Create, Design, Develop and Connect at AnDevCon D.C. 2017!

Thoughts? Leave a comment: