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

Bce40bbee247856d407f1d732fda01c0?s=128

Mirosław Stanek

October 07, 2016
Tweet

More Decks by Mirosław Stanek

Other Decks in Technology

Transcript

  1. THE STORY OF THE APP 1

  2. None
  3. 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”
  4. SHIP FAST

  5. SHIP FAST = LEARN FAST

  6. DATA FIRST

  7. None
  8. <<<

  9. None
  10. ➡ ?

  11. vs

  12. <

  13. VISION DATA ⚔ ?

  14. None
  15. 1 year goal

  16. 1 year goal ? ? ? ?

  17. 1 year goal 3 months goal

  18. 1 year goal 3 months goal

  19. 1 year goal Progress

  20. 1 year goal our achievement

  21. = SHIP FAST

  22. TESTING AUTOMATIONS

  23. TESTS 1 CODE

  24. 5 months
 development 6 months
 development (with tests)

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

    new features/adjustments
  26. 5 months
 development 6 months
 development (with tests) 1 month

    new features/adjustments 2 months manual testing + bug fixes (x) iterations
  27. New
 version 1 month each new version in 2015 7

    months 1st app version
  28. New
 version New
 version New
 version New
 version New
 version

    1 month Now each new version in 2015 1 week
  29. TEST MANUALLY ONLY ONCE

  30. ‣ Slow ‣ Boring ‣ Not cool… MANUAL TESTING ‣

    Slow ‣ Boring ‣ Not cool…
  31. public boolean shouldAskForAppRate() {
 if (!isSuccessfulFlow())
 return false;
 
 if

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

    Unit testing = development BASIC TOOLSET ‣ Android Studio + JUnit ‣ Hamcrest ‣ Mockito ‣ Unit testing = development
  34. IF WRITING TESTS IS HARD, THAT MIGHT BE A BUG

  35. TEST LOGIC, NOT SDK

  36. Activity XML business logic

  37. VIEW LOGIC ANDROID SDK JAVA Activity XML business logic

  38. ?

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

    Exception {
 presenter.onButtonClick();
 verify(activity).changeResultsViewText(eq(“Robolectric Rocks!”));
 }
 }
  41. github.com/googlesamples/android-architecture

  42. • 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
  43. 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
  44. DEPENDENCY INJECTION

  45. UserManager UserManager RestClient UserStore UserManager UserManager RestClient UserStore RestClient UserStore

  46. UserManager UserManager RestClient UserStore RestClient UserStore SQLite SharedPreferences UserStore Mock

    object UserStore
  47. 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);
 }
 }
  48. *Or any other DI framework DAGGER 2

  49. public class SplashActivity extends BaseActivity {
 
 @Inject
 SplashActivityPresenter presenter;


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

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

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


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


    
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 getSplashActivityComponent().inject(this);
 }
 }
  55. 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);
 }
 }
  56. public class SplashActivity {
 
 @Inject
 Validator validator;
 
 //...


    } SplashActivtity Validator DIRECT INJECTION
  57. 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 …
  58. 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;
 
 //...
 }
  59. @Inject
 Observable<Analytics> analytics;

  60. @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;
  61. Lazy injection
 app launch time optimizations Launch time without lazy

    loading: 650ms SplashActivity Crashlytics 200ms Mixpanel Google Analytics 100ms Rest Client 150ms 200ms
  62. 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
  63. 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
  64. CLEANER CODE

  65. 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);
 }
 
 //...
 }
  66. 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);
 }
 
 //...
 }
  67. 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);
 }
 //... }
  68. 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;
 }
 //...
 }
  69. 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;
 } 
 //… 
 }
  70. 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;
 } 
 //… 
 }
  71. @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
  72. @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();
 }
 }
  73. DAGGER 2 • Compile time generation and validation • Custom

    scopes • Speed and memory management
  74. Dagger2Recipes https://github.com/frogermcs • UserScope • Async injection • MultiBinding +

    Autofactory • Example app: Github client
  75. AUTOMATION

  76. THE MOST MINIMALISTIC APP

  77. None
  78. None
  79. The only trusted person in team

  80. 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
  81. None
  82. Lane build_internalDebug clean assemble branch: develop build flavor: internalDebug

  83. Lane build_internalDebug clean unit tests assemble

  84. Lane build_internalDebug clean unit tests assemble coverage

  85. Lane build_internalDebug clean unit tests assemble coverage

  86. Lane build_internalDebug clean unit tests lint assemble coverage

  87. Lane build_internalDebug clean unit tests lint assemble coverage

  88. Lane build_internalDebug clean unit tests lint assemble coverage

  89. Lane build_releaseCandidate build_internalDebug branch: release/* build flavor: release

  90. Lane build_releaseCandidate build_internalDebug upload: Crashlytics Beta

  91. Lane build_releaseCandidate build_internalDebug upload: Crashlytics Beta

  92. Lane build_releaseCandidate build_internalDebug upload: Crashlytics Beta notify: Slack

  93. Lane build_releaseCandidate build_internalDebug upload: Crashlytics Beta notify: Slack upload: Nimbledroid

  94. Lane build_releaseCandidate build_internalDebug upload: Crashlytics Beta notify: Slack upload: Nimbledroid

  95. Lane build_productionRelease build_internalDebug branch: master build flavor: release

  96. Lane build_productionRelease build_internalDebug branch: master build flavor: release supply •

    Sign apk • Update store description • Push to: alpha/beta/release
  97. Other lanes Automation_EndToEnd_CompleteTestSuite

  98. Other lanes Automation_EndToEnd_CompleteTestSuite

  99. Other lanes Static_code_analysis (powered by Facebook infer) - http://fbinfer.com/ Automation_EndToEnd_CompleteTestSuite

  100. None
  101. Our job is to make software that works* *In a

    long term
  102. 104 THANKS! froger_mcs mirek@azimo.com