Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The story of the Android app

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
Tweet

More Decks by Mirosław Stanek

Other Decks in Technology

Transcript

  1. 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”
  2. <<<

  3. vs

  4. <

  5. 5 months
 development 6 months
 development (with tests) 1 month

    new features/adjustments 2 months manual testing + bug fixes (x) iterations
  6. public boolean shouldAskForAppRate() {
 if (!isSuccessfulFlow())
 return false;
 
 if

    (isUsingAppLongEnough())
 return true;
 
 return false;
 }
  7. @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);
 }
  8. ‣ Android Studio + JUnit ‣ Hamcrest ‣ Mockito ‣

    Unit testing = development BASIC TOOLSET ‣ Android Studio + JUnit ‣ Hamcrest ‣ Mockito ‣ Unit testing = development
  9. ?

  10. @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!");
 }
 }
  11. public class MyActivityPresenterTest {
 
 @Test
 public void clickingButton_shouldChangeResultsViewText() throws

    Exception {
 presenter.onButtonClick();
 verify(activity).changeResultsViewText(eq(“Robolectric Rocks!”));
 }
 }
  12. • 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
  13. 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
  14. 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);
 }
 }
  15. public class SplashActivity extends BaseActivity {
 
 @Inject
 SplashActivityPresenter presenter;


    
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 getSplashActivityComponent().inject(this);
 }
 }
  16. SplashActivityModule SplashActivity SplashActivityPresenter AppModule Application Validator ApiModule OkhttpClient RestAdapter GithubApiService

    UserManager SplashActivtity SplashActivityPresenter @Inject SplashActivtityPresenter SplashActivity Validator UserManager @Inject Initialization Usage Initialization/usage separation
  17. SplashActivityModule SplashActivity SplashActivityPresenter AppModule Application Validator ApiModule OkhttpClient RestAdapter GithubApiService

    UserManager SplashActivtity SplashActivityPresenter @Inject SplashActivtityPresenter SplashActivity Validator UserManager @Inject Initialization/usage separation Initialization Usage
  18. @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);
 }
 }
  19. public class SplashActivity extends BaseActivity {
 
 @Inject
 SplashActivityPresenter presenter;


    
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 getSplashActivityComponent().inject(this);
 }
 }
  20. public class SplashActivity extends BaseActivity {
 
 @Inject
 SplashActivityPresenter presenter;


    
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 getSplashActivityComponent().inject(this);
 }
 }
  21. 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);
 }
 }
  22. 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 …
  23. 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;
 
 //...
 }
  24. @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;
  25. Lazy injection
 app launch time optimizations Launch time without lazy

    loading: 650ms SplashActivity Crashlytics 200ms Mixpanel Google Analytics 100ms Rest Client 150ms 200ms
  26. 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
  27. 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
  28. 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);
 }
 
 //...
 }
  29. 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);
 }
 
 //...
 }
  30. 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);
 }
 //... }
  31. 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;
 }
 //...
 }
  32. 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;
 } 
 //… 
 }
  33. 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;
 } 
 //… 
 }
  34. @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
  35. @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();
 }
 }
  36. 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
  37. Lane build_productionRelease build_internalDebug branch: master build flavor: release supply •

    Sign apk • Update store description • Push to: alpha/beta/release