Share
Embed
The story of the Android app
Presentation given on Mobiconf 2016 about building Azimo Android app. To make both - product and engineering team happy.
More about presentation: http://2016.mobiconf.org/speakers#Miroslaw_Stanek
Useful materials:
Dagger2Recipes, example projects of Dagger 2 showed on presentation are available on my Github account: https://github.com/frogermcs
Mirosław Stanek
October 07, 2016
Transcript
104 slides
-
Michael Kent, Azimo CEO “We have no way of knowing
how people will interact with technology, but what we DO know is that it will be different from how they do it today and very different again five years after that.... Change being the only constant” -
5 months development 6 months development (with tests) 1 month
new features/adjustments -
5 months development 6 months development (with tests) 1 month
new features/adjustments 2 months manual testing + bug fixes (x) iterations -
New version 1 month each new version in 2015 7
months 1st app version -
New version New version New version New version New version
1 month Now each new version in 2015 1 week -
‣ Slow ‣ Boring ‣ Not cool… MANUAL TESTING ‣
Slow ‣ Boring ‣ Not cool… -
public boolean shouldAskForAppRate() { if (!isSuccessfulFlow()) return false; if
(isUsingAppLongEnough()) return true; return false; } -
@Test public void testShouldShowRateBoxOnlyUnderRightConditions() { when(appDataMock.getAppInstallTime()).thenReturn(DATE_7_DAYS_AGO); when(rateBoxReminderMock.isDelayed()).thenReturn(false); assertEquals(rateBoxManager.shouldAskForAppRate(), true);
when(appDataMock.getAppInstallTime()).thenReturn(DATE_TODAY); when(rateBoxReminderMock.isDelayed()).thenReturn(true); assertEquals(rateBoxManager.shouldAskForAppRate(), false); } -
‣ Android Studio + JUnit ‣ Hamcrest ‣ Mockito ‣
Unit testing = development BASIC TOOLSET ‣ Android Studio + JUnit ‣ Hamcrest ‣ Mockito ‣ Unit testing = development -
@RunWith(RobolectricTestRunner.class) public class MyActivityTest { @Test public void clickingButton_shouldChangeResultsViewText()
throws Exception { MyActivity activity = Robolectric.setupActivity(MyActivity.class); Button button = (Button) activity.findViewById(R.id.button); TextView results = (TextView) activity.findViewById(R.id.results); button.performClick(); assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!"); } } -
public class MyActivityPresenterTest { @Test public void clickingButton_shouldChangeResultsViewText() throws
Exception { presenter.onButtonClick(); verify(activity).changeResultsViewText(eq(“Robolectric Rocks!”)); } } -
• Basic Model-View-Presenter architecture: • Data Binding Library • Clean
Architecture • Dagger2 for Dependency Injection • uses RxJava for concurrency and data layer abstraction github.com/googlesamples/android-architecture -
github.com/googlesamples/android-architecture • Basic Model-View-Presenter architecture: • Data Binding Library •
Clean Architecture • Dagger2 for Dependency Injection • RxJava for concurrency and data layer abstraction -
public class SplashActivity extends BaseActivity { private SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle bundle) { super.onCreate(savedInstanceState); RestAdapter.Builder restAdapterBuilder = new RestAdapter.Builder() .setClient(HttpClient.getInstance()) .setEndpoint(getString(R.string.endpoint)); RestAdapter restAdapter = restAdapterBuilder.build(); ApiService apiService = restAdapter.create(ApiService.class); UserManager userManager = new UserManager(apiService); presenter = new SplashActivityPresenter(this, Validator.getInstance(), userManager); } } -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } -
SplashActivityModule SplashActivity SplashActivityPresenter AppModule Application Validator ApiModule OkhttpClient RestAdapter GithubApiService
UserManager SplashActivtity SplashActivityPresenter @Inject SplashActivtityPresenter SplashActivity Validator UserManager @Inject Initialization Usage Initialization/usage separation -
SplashActivityModule SplashActivity SplashActivityPresenter AppModule Application Validator ApiModule OkhttpClient RestAdapter GithubApiService
UserManager SplashActivtity SplashActivityPresenter @Inject SplashActivtityPresenter SplashActivity Validator UserManager @Inject Initialization/usage separation Initialization Usage -
@Module public class GithubApiModule { @Provides @Singleton public OkHttpClient
provideOkHttpClient() { return new OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build(); } @Provides @Singleton public Retrofit provideRestAdapter(Application app, OkHttpClient okHttpClient) { return new Retrofit.Builder().client(okHttpClient) .baseUrl(application.getString(R.string.endpoint)) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); } @Provides @Singleton public GithubApiService provideGithubApiService(Retrofit restAdapter) { return restAdapter.create(GithubApiService.class); } } -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } -
public class SplashActivity extends BaseActivity { @Inject SplashActivityPresenter presenter;
@Inject Provider<AnalyticsEvent> analyticsEventProvider; @Inject Lazy<ValidatioNErrorManager> lazyValidationErrorManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSplashActivityComponent().inject(this); } } -
public class SplashActivity { @Inject Validator validator; //...
} SplashActivtity Validator DIRECT INJECTION -
LAZY INJECTION public class SplashActivity { @Inject Lazy<Validator> lazyValidator;
//... } SplashActivtity Validator Lazy<Validator> 1st get() call nth get() call get() SplashActivtity Dependencies Graph Validator Lazy<Validator> Same instance like in 1st call … -
PROVIDER INJECTION 1st get() call nth get() call … get()
(instance 1) SplashActivtity Dependencies Graph Validator Provider<Validator> Validator Validator Provider<Validator> get() (instance n) SplashActivtity Dependencies Graph Validator New instance every time public class SplashActivity { @Inject Provider<Validator> validatorProvider; //... } -
@Module public class AnalyticsModule { @Provides @Singleton public Observable<Analytics> analyticsObservable(final
Lazy<Analytics> analyticsLazy) { return Observable.defer(new Func0<Observable<Analytics>>() { @Override public Observable<Analytics> call() { return Observable.just(analyticsLazy.get()); } }); } } @Inject Observable<Analytics> analytics; -
Lazy injection app launch time optimizations Launch time without lazy
loading: 650ms SplashActivity Crashlytics 200ms Mixpanel Google Analytics 100ms Rest Client 150ms 200ms -
Lazy injection app launch time optimizations Launch time without lazy
loading: 650ms SplashActivity Crashlytics 200ms Mixpanel Google Analytics 100ms Rest Client 150ms 200ms SplashActivity Crashlytics 200ms Mixpanel Google Analytics 100ms Rest Client 150ms 200ms Launch time with lazy loading: 100ms +550ms UI thread blocking later Those will be Lazy-loaded -
Observable injection app launch time optimizations SplashActivity Crashlytics 100ms Launch
time: 100ms Rest of dependencies are loaded in background thread Background thread 200ms Mixpanel Google Analytics Rest Client 150ms 200ms -
public class RepositoriesListActivity extends BaseActivity { @Bind(R.id.rvRepositories) RecyclerView rvRepositories; @Inject
RepositoriesListActivityPresenter presenter; private RepositoriesListAdapter repositoriesListAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activty_repositories_list); ButterKnife.bind(this); setupRepositoriesListView(); } private void setupRepositoriesListView() { rvRepositories.setLayoutManager(new LinearLayoutManager(this)); repositoriesListAdapter = new RepositoriesListAdapter(this); rvRepositories.setAdapter(repositoriesListAdapter); } //... } -
public class RepositoriesListActivity extends BaseActivity { @Bind(R.id.rvRepositories) RecyclerView rvRepositories; @Inject
RepositoriesListActivityPresenter presenter; private RepositoriesListAdapter repositoriesListAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activty_repositories_list); ButterKnife.bind(this); setupRepositoriesListView(); } private void setupRepositoriesListView() { rvRepositories.setLayoutManager(new LinearLayoutManager(this)); repositoriesListAdapter = new RepositoriesListAdapter(this); rvRepositories.setAdapter(repositoriesListAdapter); } //... } -
public class RepositoriesListActivity extends BaseActivity { @Bind(R.id.rvRepositories) RecyclerView rvRepositories;
@Inject RepositoriesListActivityPresenter presenter; @Inject RepositoriesListAdapter repositoriesListAdapter; @Inject LinearLayoutManager linearLayoutManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_repositories_list); ButterKnife.bind(this); setupRepositoriesListView(); presenter.loadRepositories(); } private void setupRepositoriesListView() { rvRepositories.setAdapter(repositoriesListAdapter); rvRepositories.setLayoutManager(linearLayoutManager); } //... } -
public class RepositoriesListAdapter extends RecyclerView.Adapter { //... @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final RecyclerView.ViewHolder viewHolder = null; if (viewType == Repository.TYPE_NORMAL) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_normal, parent, false); viewHolder = new RepositoryViewHolderNormal(); } else if (viewType == Repository.TYPE_BIG) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_big, parent, false); viewHolder = new RepositoryViewHolderBig(view); } else if (viewType == Repository.TYPE_FEATURED) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_featured, parent, false); viewHolder = new RepositoryViewHolderFeatured(view);- } return viewHolder; } @Override public int getItemViewType(int position) { Repository repository = repositories.get(position); if (repository.stargazers_count > 500) { if (repository.forks_count > 100) { return Repository.TYPE_FEATURED; } return Repository.TYPE_BIG; } return Repository.TYPE_NORMAL; } //... } -
public class RepositoriesListAdapter extends RecyclerView.Adapter { private Map<Integer, RepositoriesListViewHolderFactory> viewHolderFactories;
//… @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return viewHolderFactories.get(viewType).createViewHolder(parent); } @Override public int getItemViewType(int position) { Repository repository = repositories.get(position); if (repository.stargazers_count > 500) { if (repository.forks_count > 100) { return Repository.TYPE_FEATURED; } return Repository.TYPE_BIG; } return Repository.TYPE_NORMAL; } //… } -
public class RepositoriesListAdapter extends RecyclerView.Adapter { private Map<Integer, RepositoriesListViewHolderFactory> viewHolderFactories;
//… @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return viewHolderFactories.get(viewType).createViewHolder(parent); } @Override public int getItemViewType(int position) { Repository repository = repositories.get(position); if (repository.stargazers_count > 500) { if (repository.forks_count > 100) { return Repository.TYPE_FEATURED; } return Repository.TYPE_BIG; } return Repository.TYPE_NORMAL; } //… } -
@Module public class RepositoriesListActivityModule extends BaseActivityModule<RepositoriesListActivity> { //…
@Provides @IntoMap @IntKey(Repository.TYPE_BIG) RepositoriesListViewHolderFactory provideViewHolderBigFactory() { return new RepositoryViewHolderBigFactory(); } @Provides @IntoMap @IntKey(Repository.TYPE_NORMAL) RepositoriesListViewHolderFactory provideViewHolderNormalFactory() { return new RepositoryViewHolderNormalFactory(); } @Provides @IntoMap @IntKey(Repository.TYPE_FEATURED) RepositoriesListViewHolderFactory provideViewHolderFeaturedFactory() { return new RepositoryViewHolderFeaturedFactory(); } } MULTIBINDING -
@Module public class RepositoriesListActivityModule extends BaseActivityModule<RepositoriesListActivity> { //…
@Provides @IntoMap @IntKey(Repository.TYPE_BIG) RepositoriesListViewHolderFactory provideViewHolderBigFactory() { return new RepositoryViewHolderBigFactory(); } @Provides @IntoMap @IntKey(Repository.TYPE_NORMAL) RepositoriesListViewHolderFactory provideViewHolderNormalFactory() { return new RepositoryViewHolderNormalFactory(); } @Provides @IntoMap @IntKey(Repository.TYPE_FEATURED) RepositoriesListViewHolderFactory provideViewHolderFeaturedFactory() { return new RepositoryViewHolderFeaturedFactory(); } } -
DAGGER 2 • Compile time generation and validation • Custom
scopes • Speed and memory management -
Dagger2Recipes https://github.com/frogermcs • UserScope • Async injection • MultiBinding +
Autofactory • Example app: Github client -
build_internalDebug - build most recent dev version, API >= 21,
no pro guard, debug panel build_releaseCandidate - build release candidate for testers, API >= 18, proguard, debug panel build_productionRelease - build production app, API >= 16, proguard -
Lane build_productionRelease build_internalDebug branch: master build flavor: release supply •
Sign apk • Update store description • Push to: alpha/beta/release