building maintainable app
TRANSCRIPT
Building maintainable app with MVP and Dagger2KRISTIJAN JURKOVIĆ ANDROID TEAM LEAD @ INFINUM
We're an independent design & development agency.
INFINUM
• 90 people in 3 offices
• 15 android developers
• hundreds of projects
OUR BIGGEST ISSUES?
“Sometimes when you fill a vacuum, it still sucks.”
― Dennis Ritchie
MVP TO THE RESCUE?
PROGRAM TO INTERFACES NOT IMPLEMENTATIONS
ModelPresenterView
LoginActivity LoginPresenterImpl LoginInteractorImpl
LoginView LoginPresenter LoginInteractor
showLoading() hideLoading() setUsernameError() setPasswordError()
showLoading() hideLoading() setUsernameError() setPasswordError()
login(username, pass)
loginPresenter loginView loginInteractor
login(username, pass) login(username, pass, listener)
login(username, pass, listener)
VIEW PRESENTER MODEL
public interface HomeView { ...}
public interface HomePresenter { ...}
public interface CurrencyInteractor { ...}
VIEW
PRESENTER
MODEL
public interface HomeView { ...}
VIEW
public class HomeActivity extends BaseActivity implements HomeView {
// this is an interface HomePresenter presenter;
...}
public interface HomePresenter { ...}
PRESENTER
public class HomePresenterImpl implements HomePresenter {
// interface private HomeView view;
// and another interface private CurrencyInteractor interactor;
public HomePresenterImpl(HomeView view, CurrencyInteractor interactor) {
this.view = view; this.interactor = interactor; }
... }
public interface CurrencyInteractor { ...}
MODEL
public class CurrencyInteractorImpl implements CurrencyInteractor {
... }
HOW SHOULD I GET MY CONTENT?
public interface HomeView {void showCurrencies(List<Currency> currencies);
}
public interface HomePresenter { void loadCurrencyList();}
public interface CurrencyInteractor { void getCurrencyList(CurrencyListener listener);}
public class HomeActivity extends BaseActivity implements HomeView {
private void init() { presenter = new HomePresenterImpl(this,
new CurrencyInteractorImpl()); presenter.getCurrencyList(); }
@Override public void showCurrencies(List<Currency> currencies) { // display data }
}
public class HomePresenterImpl implements HomePresenter {
...@Override public void loadCurrencyList() { interactor.getCurrencyList(...); }
}
public class CurrencyInteractorImpl implements CurrencyInteractor {
... @Override public void getCurrencyList(
CurrencyListener listener) {
// do API/DB call // return result with listener }
}
VIEW SHOULDN’T CREATE ITS DEPENDENCIES
DEPENDENCY INJECTION
JSR 330
• 5 annotations - @Named, @Inject, @Qualifier, @Scope,
@Singleton
• 1 interface - Provider<T>
DAGGER2 TO THE RESCUE
DAGGER 2
• @Module, @Provides, @Component, @Subcomponent,
ScopedProvider
• Injection into Fields, Constructors, Methods
• Each @Inject has to have its @Provides
APP COMPONENT
HOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
APP COMPONENT
HOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
@Modulepublic class ApiModule {
@Provides @Singleton public ApiService provideApiService(
OkHttpClient client, BaseUrl endpoint, Converter.Factory converter) {
return RestUtils.createApiService(
client, endpoint, converter, ApiService.class);
}}
@Modulepublic class ApiModule {
@Provides @Singleton public ApiService provideApiService(
OkHttpClient client, BaseUrl endpoint, Converter.Factory converter) {
return RestUtils.createApiService(
client, endpoint, converter, ApiService.class);
}}
@Modulepublic class ApiModule {
@Provides @Singleton public ApiService provideApiService(
OkHttpClient client, BaseUrl endpoint, Converter.Factory converter) {
return RestUtils.createApiService(
client, endpoint, converter, ApiService.class);
}}
@Modulepublic class GsonConverterModule {
@Provides @Singleton public Converter.Factory
provideConverter(Gson gson) {
return GsonConverterFactory.create(gson); }}
APP COMPONENT
HOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
EXECUTORS MODULE
@Component(modules = { HostModule.class, GsonConverterModule.class, ClientModule.class, LoggerModule.class, ExecutorsModule.class, ApiModule.class, GsonModule.class})@Singletonpublic interface AppComponent {}
public class MyApplication extends Application {
protected AppComponent appComponent;
protected void init() { appComponent = DaggerAppComponent.create(); }}
HOW CAN WE REUSE THAT IN OUR ACTIVITIES?
public class HomeActivity extends BaseActivity implements HomeView {
private void init() { presenter = new HomePresenterImpl(this,
new CurrencyInteractorImpl()); presenter.getCurrencyList(); }
@Override public void showCurrencies(List<Currency> currencies) { // display data }
}
• Inject presenter into view
• Inject view and interactor into presenter
@Modulepublic class HomeModule {
private HomeView view; public HomeModule(HomeView view) { this.view = view; }
@Provides public HomeView provideView() { return view; }
@Provides public HomePresenter providePresenter(HomePresenterImpl presenter) { return presenter; }
@Provides public CurrencyInteractor provideInteractor(
CurrencyInteractorImpl interactor) { return interactor; }}
@Modulepublic class HomeModule {
private HomeView view; public HomeModule(HomeView view) { this.view = view; }
@Provides public HomeView provideView() { return view; }
@Provides public HomePresenter providePresenter(HomePresenterImpl presenter) { return presenter; }
@Provides public CurrencyInteractor provideInteractor(
CurrencyInteractorImpl interactor) { return interactor; }}
@Modulepublic class HomeModule {
private HomeView view; public HomeModule(HomeView view) { this.view = view; }
@Provides public HomeView provideView() { return view; }
@Provides public HomePresenter providePresenter(HomePresenterImpl presenter) { return presenter; }
@Provides public CurrencyInteractor provideInteractor(
CurrencyInteractorImpl interactor) { return interactor; }}
public class CurrencyInteractorImpl implements CurrencyInteractor {
@Injectpublic CurrencyInteractorImpl(ApiService service) {
}}
public class CurrencyInteractorImpl implements CurrencyInteractor {
@Injectpublic CurrencyInteractorImpl(ApiService service) {
}}
@Modulepublic class HomeModule {
private HomeView view; public HomeModule(HomeView view) { this.view = view; }
@Provides public HomeView provideView() { return view; }
@Provides public HomePresenter providePresenter(HomePresenterImpl presenter) { return presenter; }
@Provides public CurrencyInteractor provideInteractor(
CurrencyInteractorImpl interactor) { return interactor; }}
public class HomePresenterImpl implements HomePresenter {
@Inject public HomePresenterImpl(HomeView view,
CurrencyInteractor interactor) {
this.view = view; this.interactor = interactor; }}
public class HomeActivity extends BaseActivity implements HomeView {
@Inject HomePresenter presenter;
}
@Subcomponent(modules = HomeModule.class)public interface HomeComponent { void inject(HomeActivity activity);}
WHAT’S THAT “SUBCOMPONENT” THING
YOU MENTIONED?
APP COMPONENT
HOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
HOME MODULE
HOMECOMPONENT
EXECUTORS MODULE
@Component(modules = { ...})@Singletonpublic interface AppComponent {
HomeComponent plus(HomeModule module);}
public abstract class BaseActivity extends AppCompatActivity {
Override protected void onCreate(Bundle savedInstanceState) { ... injectDependencies(MyApplication.getAppComponent()); }
protected abstract void injectDependencies(AppComponent appComponent);
}
public class HomeActivity extends BaseActivity implements HomeView {
protected void injectDependencies(AppComponent appComponent) {
appComponent.plus(new HomeModule(this)).inject(this);
}}
APP COMPONENT
HOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
HOME MODULE
HOMECOMPONENTSESSIONCOMPONENT
SESSION MODULE
EXECUTORS MODULE
SATISFACTION LEVEL 9001
“If you don’t like testing your product, most likely your customers won’t like to test it
either.”
APP COMPONENT
HOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
EXECUTORS MODULE
APP COMPONENT
HOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
EXECUTORS MODULE
APPTESTCOMPONENT
MOCKHOST MODULE
CONVERTER MODULE
CLIENT MODULE
LOGGER MODULE
API MODULE
GSON MODULE
SYNC EXECUTORS
MODULE
@Component(modules = { ... MockHostModule.class, SynchronousExecutorsModule.class, ...})@Singletonpublic interface AppTestComponent extends AppComponent {
void inject(MyTestApplication app);}
@Component(modules = { ... MockHostModule.class, SynchronousExecutorsModule.class, ...})@Singletonpublic interface AppTestComponent extends AppComponent {
void inject(MyTestApplication app);}
@Component(modules = { ... MockHostModule.class, SynchronousExecutorsModule.class, ...})@Singletonpublic interface AppTestComponent extends AppComponent {
void inject(MyTestApplication app);}
public class MyTestApplication extends MyApplication implements TestLifecycleApplication {
@Override protected void init() { appComponent = DaggerAppTestComponent.create(); }}
protected void enqueueResponse(String filename) { String body = ResourceUtils.readFromFile(filename); MockResponse mockResponse =
new MockResponse().setBody(body).setResponseCode(HttpURLConnection.HTTP_OK);
mockWebServer.enqueue(mockResponse);}
@Overridepublic void setup() throws Exception { super.setup(); controller = Robolectric
.buildActivity(MockActivity.class)
.create()
.start() .resume() .visible();
fragment = DashboardDrivingModeFragment.newInstance();
controller.get().getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment, null) .commit(); ButterKnife.bind(this, fragment.getView());}
@Testpublic void testEmptyStateNotVisible() { enqueueResponse(“rest-currency-response.json”); btnCurrencyList.performClick(); assertThat(emptyView).isNotVisible();}
THINGS TO REMEMBER
• Orientation change
THINGS TO REMEMBER
• Dagger2 is a powerful tool - make good use of it
• Save yourselves from regression bugs
REFERENCES
• http://antonioleiva.com/mvp-android/
• https://medium.com/@czyrux/presenter-surviving-
orientation-changes-with-
loaders-6da6d86ffbbf#.xou7c71uz
• http://frogermcs.github.io/dependency-injection-with-
dagger-2-custom-scopes/
• https://www.youtube.com/watch?v=oK_XtfXPkqw
Any questions? KRISTIJ[email protected] @KJURKOVIC
Visit infinum.co or find us on social networks:
infinum.co infinumco infinumco infinum