Effective mobile engineering to make product successful

Effective mobile engineering to make product successful

Have you ever wondered how the best product ideas succeed? It's not only great timing, product-market fit or marketing activities. It's also about business and engineering cooperation.

In this presentation, I'll talk about our experiences with making great (and bad) ideas happen. How to deliver fast, learn from mistakes and keep your users happy (and crash free!).

I will show how the team of a few engineers can build the app used globally. How we develop and maintain our code, test, deliver, learn and iterate.

Bce40bbee247856d407f1d732fda01c0?s=128

Mirosław Stanek

October 05, 2018
Tweet

Transcript

  1. None
  2. fail fast

  3. Anyone who has never made a mistake has never tried

    anything new. -Albert Einstein
  4. IT BIZ ↔ Me

  5. Fail fast, Fail often, Fail better, Succeed

  6. Ship fast, Iterate often, Don’t crash, Succeed Fail fast, Fail

    often, Fail better
  7. # KPIs

  8. Crash-free users >99.5% Release once a week

  9. New app once a week???

  10. Release Once a month x1 x2 x5 x6 x3 x4

    x7 x8 team’s capacity
  11. x1 x2 x3 x4 x5 x6 x7 x8 build 1

    checking Dev QA/Business build1 build2 Week 1 Week 2 Week 3 “W hat’s wrong, W hat’s ok?” “W hat’s wrong, W hat’s ok?” build 2 checking
  12. x1 x2 x3 x4 x5 x6 x7 x8 build 1

    testing x1’ x2’ x4’ build 2 testing Dev QA/Business build1 build2 Week 1 Week 2 Week 3 “we don’t like x5, x8” “we don’t like x1, x2, x4” x5’ x8’ Week 4
  13. x1 x2 x3 x4 x5 x6 x7 x8 build 1

    testing x1’ x2’ x4’ build 2 testing Dev QA/Business build1 build2 build3 Week 1 Week 2 Week 3 fixes x5’ x8’ x1’ x2’ x4' Week 4 more fixes “checking them now..” x5’ x8’ RELEASE!!!
  14. x1 x2 y2 z1 x3 y1 z2 z3 team’s capacity

    (same as previous) Release Once a week
  15. Week 1 x1 x2 x1’ x2’ x1 x2 x1’ Dev

    QA/Business
  16. x1’ x2’ x1’ build 1 build 2 Week 1 x1

    x2 x1 x2 Dev QA/Business
  17. x1’ feedback 1 feedback 2 Week 1 x1 x2 x1’

    x2’ x1 x2 Dev QA/Business
  18. Week 1 x1 x2 x1’ x2’ Week 2 x3 x1

    x2 x1’ Dev QA/Business x2’ x3 build 4 build 5 release candidate v1 RELEASE!
  19. Week 1 x1 x2 x1’ x2’ Week 2 x3 y1

    y2 x1 x2 x1’ Dev QA/Business x2’ y1 x3 feedback 1 build Week 3
  20. Week 1 x1 x2 x1’ x2’ Week 2 x3 y1

    y2 x1 x2 x1’ Dev QA/Business y1’ z1 z2 Week 3 x2’ y1 y1’ z1 y2 x3 v2 RELEASE!
  21. Week 1 x1 x2 x1’ x2’ Week 2 x3 y1

    y2 x1 x2 x1’ Dev QA/Business y1’ z1 z2 Week 3 x2’ y1 y1’ z1 y2 x3 z3 v3 RELEASE! Week 4 z1’ z2’ z3 z1’ z2’ z2
  22. x1 x2 y2 z1 x3 y1 z2 z3 x1 x2

    x5 x6 x3 x4 x7 x8 vs
  23. x1 x2 y2 z1 x3 y1 z2 z3 x1 x2

    x5 x6 x3 x4 x7 x8 • Same context, reasonable overhead • Different context, a lot to keep in mind How many things to have in mind by engineers Software 
 engineer focus QA 
 engineer focus QA and software
 engineer focus
  24. How many things can go wrong x1 x2 y2 z1

    x3 y1 z2 z3 x1 x2 x5 x6 x3 x4 x7 x8 • Same context, reasonable overhead • less things can break • Different context, a lot to keep in mind • more things can break
  25. Business starts learning from here x1 x2 y2 z1 x3

    y1 z2 z3 x1 x2 x5 x6 x3 x4 x7 x8 • Same context, reasonable overhead • less things can break • business can iterate fast • Different context, a lot to keep in mind • more things can break • business can’t iterate fast “We’ve learned that you don’t have to build this” “We’ve learned that you didn’t have to build this”
  26. SHIP FAST

  27. ? x2 x3 x1

  28. Coding Testing and review x2 x3 x1 Release candidate

  29. ⌚ ??? Coding Testing and review x2 x3 x1 Release

    candidate
  30. Manual tests Others What’s new Build apk Internal distrib Internal

    build Coding Testing and review x2 x3 x1 Release candidate
  31. Sign
 apk Release
 notes Internal
 distrib Update assets Store
 upload

    RC distribution Coding Testing and review x2 x3 x1 Release candidate
  32. +2h x number of loops +2h Coding Testing and review

    x2 x3 x1 Release candidate Internal build RC distrib x Rollout +1-2 days
  33. +2h Coding Testing and review x2 x3 x1 Release candidate

    Internal build RC distrib x Rollout +1-2 days +2h x number of loops
  34. clean the project unit tests tests coverage lint checks assemble

    apk distribute notify INTERNAL BUILD Coding Testing and review
  35. INTERNAL BUILD $ ./gradlew clean testDebugUnitTest testDebugUnitTestCoverage lintStaging assembleStaging crashlyticsUploadDistributionStaging

    clean the project unit tests tests coverage lint checks assemble apk distribute notify pipeline { agent { node { label 'mobile' }} //... stages { stage("Build and distribute debug") { steps { } } } }
  36. New feature branch On demand Create release branch Build and

    distribute RC Create Merge Request Notify reviewer New 
 release candidate Create merge request Bump version code 4 clicks 10 clicks 3 lines of code Push to master Push to Play Store beta Production app quick tests Store release Build, check, distribute apk 30min of clicking Trigger Actions Savings
  37. Run all tests Create report Send report to slack Nightly

    tests Setup and run simulators Every 24hrs
  38. Every 24hrs Update 
 /res/values Remove unused resources create merge

    request Update translations Fetch translation sfrom API lint checks
  39. apply plugin: 'io.fabric' apply plugin: 'jacoco' apply plugin: 'com.getkeepsafe.dexcount' apply

    plugin: 'com.github.triplet.play' def process_language(language): print('Starting processing of: ' + language) is_default_language = language == ApiConfig.DEFAULT_LANGUAGE translations = translations_api.get_translations() if '--android' in sys.argv: parsed_keys = transform_all_translations(translations) android_exporter = ATranslationsExporter(parsed_keys, lang_code) android_exporter.generate_translations() if is_default_language: android_exporter.cleanup_android_new_translations() pipeline { triggers { gitlab(triggerOnPush: true, branchFilterType: 'All') } stage("Build debug") { when { branch 'dev' } steps { sh './gradlew clean assembleDebug --stacktrace' } } }
  40. Coding Testing and review x2 x3 x1 Release candidate Internal

    build RC distrib auto x Rollout auto +1-2 days
  41. * QUALITY ASSURANCE

  42. Key feature can be tested manually only once.

  43. RC QA fast feedback QA Development 3 STAGES OF QA

    Unit tests Lint checks Manual tests Functional tests End-to-end tests Test infra maintenance
  44. LINT CHECKS

  45. UNIT TESTING

  46. Written by devs - Test single units 100% of tests

    must pass ✅
  47. @Test
 public void testInit_whenConditionsMet_thenShouldShowRatingBox() {
 when(rateBoxManager.shouldAskForRate()).thenReturn(true);
 activityPresenter.initView(); verify(activityMock, once()).showRateBox(); }

    >200sec to find this screen/popup <1sec to execute test
  48. @Test
 public void testCriteria_whenAllMet_thenShouldShowRateBox() {
 when(appDataMock.getAppInstallTime()).thenReturn(DATE_7_DAYS_AGO);
 when(rateBoxReminderMock.isDelayed()).thenReturn(false); assertEquals(rateBoxManager.shouldAskForAppRate(), true);
 }

    @Test
 public void testCriteria_whenAppFreshlyInstalled_thenShouldntShowRateBox() { when(appDataMock.getAppInstallTime()).thenReturn(DATE_TODAY);
 when(rateBoxReminderMock.isDelayed()).thenReturn(true);
 assertEquals(rateBoxManager.shouldAskForAppRate(), false);
 } CRITERIA UNIT TESTS
  49. COVERAGE 100% (?)

  50. @Test
 public void testSetGet() {
 assertFalse(model.isSet()); model.setItTo(true);
 assertTrue(model.isSet()); model.setItTo(false); assertFalse(model.isSet());


    } COVERAGE 100% @Test
 public void testConstructor() {
 model = new Model(true);
 assertTrue(model.isSet());
 }
  51. 2 unit tests, 0 integration tests

  52. COVERAGE 50-60% & FAST FORWARD TO QA COVERAGE 100%

  53. INTEGRATION TESTING

  54. Built by QA engineers Run on emulator Hermetic env

  55. Data and settings Use cases, presentation logic

  56. mocks, init config System under test Data and settings Use

    cases, presentation logic
  57. mocks System under test Use cases, presentation logic

  58. mocks Screen by screen flow: - Splash - Login -

    Main Screen - List if items API calls + database operations Screen by screen flow: - Splash - Login - Main Screen - List if items API calls + database operations 5s 5s 5s 0s
  59. Android instrumentation test template AVD manager System under testing

  60. Android instrumentation test template Fake data (e.g. json file) AVD

    manager System under testing Mock objects
  61. Android instrumentation test template Init state Activity System under testing

    Fake data (e.g. json file) AVD manager Mock objects
  62. @RunWith(AndroidJUnit4.class) public class ForceAppUpdate_MockTests extends AzimoTestContainer { @Rule public DaggerMockRule<AzimoAppComponent>

    daggerRule = DaggerMockUtils.defaultDaggerMockRule(); @Mock ApiVersionChecker apiVersionChecker; public void setupLogic() { when(apiVersionChecker.shouldUpdateApplication()).thenReturn(true); } @Test public void testForceAppUpdate_ifAppIsTooOld_shouldShowUpdateAzimoScreen() throws Exception { /* Data mocking (App-State unrelated) */ when(apiVersionChecker.shouldUpdateApplication()).thenReturn(true); /* Config init */ TestLauncher.init() .setting_setInputToUser(TestUsers.getTestUserNoTransfersNoRecipients()) .setting_useStartingPoint(LauncherStartingCondition.USER) .run(); /* Test Start */ appTooOldNavi().waitForEnteredActivity(AppTooOldActivity.class); appTooOldAsserts().assertThat_correctTitleIsDisplayed(); appTooOldAsserts().assertThat_correctDescriptionIsDisplayed(); appTooOldAsserts().assertThat_correctButtonDescriptionIsDisplayed(); } }
  63. @RunWith(AndroidJUnit4.class) public class ForceAppUpdate_MockTests extends AzimoTestContainer { @Rule public DaggerMockRule<AzimoAppComponent>

    daggerRule = DaggerMockUtils.defaultDaggerMockRule(); @Mock ApiVersionChecker apiVersionChecker; public void setupLogic() { when(apiVersionChecker.shouldUpdateApplication()).thenReturn(true); } @Test public void testForceAppUpdate_ifAppIsTooOld_shouldShowUpdateAzimoScreen() throws Exception { /* Data mocking (App-State unrelated) */ when(apiVersionChecker.shouldUpdateApplication()).thenReturn(true); /* Config init */ TestLauncher.init() .setting_setInputToUser(TestUsers.getTestUserNoTransfersNoRecipients()) .setting_useStartingPoint(LauncherStartingCondition.USER) .run(); /* Test Start */ appTooOldNavi().waitForEnteredActivity(AppTooOldActivity.class); appTooOldAsserts().assertThat_correctTitleIsDisplayed(); appTooOldAsserts().assertThat_correctDescriptionIsDisplayed(); appTooOldAsserts().assertThat_correctButtonDescriptionIsDisplayed(); } } Mocking Init state activity test
  64. END-TO-END TESTING

  65. Simulate end user 5 End-to-end features Up to 2 hours

  66. None
  67. How to get there ? 
 Be 1% better every

    release
  68. early 2015 - no unit tests (product launch) late 2015

    - 20% coverage, manual testing 2016 - 40% coverage, started QA engineering 2017 - 60% coverage, core features e2e 2018 - 50-60% coverage, all features e2e someday - “Lint zero” project
  69. Automation Test Supervisor Automated test emulator run plugin Check on

    github.com/AzimoLabs
  70. Testing Fast feedback QA Release candidate Internal build RC distrib

    auto min x Rollout auto asap RC testing x2 x3 x1 Development Coding Code checks ✔ ✔ ✔
  71. Testing Fast feedback QA Release candidate Internal build RC distrib

    auto min x Rollout auto asap RC testing x2 x3 x1 Development Coding Code checks ✔ ✔ ✔
  72. Write code that matters 8 Separate logic from SDK /

    Code must be testable
  73. IF WRITING TESTS IS HARD, THAT MIGHT BE A BUG

  74. Activity Data Navigation Networking UI View logic ALL-IN-ONE Testing? External

    QA, only end-to-end.
  75. Model Presenter View Database Navigation API View logic TESTABLE Data

    logic Unit testing - moderate (a lot to mock, incl. SDK) Integration testing - moderate End-to-end testing - easy Stores
  76. View Stores Navigator API View logic Use cases Repositories, Managers

    5-STARS Database
  77. UNIT TESTING View Stores Navigator API View logic Use cases

    Repositories, Managers Database
  78. View Stores Navigator API View logic Use cases Repositories, Managers

    Database UNIT TESTING
  79. View Stores Navigator API View logic Use cases Repositories, Managers

    Database UNIT TESTING
  80. System under test Mocks INTEGRATION TESTING View Stores Navigator API

    View logic Use cases Repositories, Managers Database
  81. System under test View Stores Navigator API View logic Use

    cases Repositories, Managers Database END-TO-END TESTING
  82. EASY TO “THREAD” Always UI Ensure background thread Always background

    View Stores Navigator API View logic Use cases Repositories, Managers Database
  83. SDK VS LOGIC Android SDK Plain code View Stores Navigator

    API View logic Use cases Repositories, Managers Database
  84. THE BEST ARCHITECTURE The one that makes the developers team

    the most productive.
  85. ARCHITECTURE PATTERNS • MVP, MVVM • App Architecture Components •

    Others (see Android Blueprints) github.com/googlesamples/
 android-architecture
  86. WRITE LOGIC THAT MATTERS! GENERATE EVERYTHING ELSE. 8

  87. @BindView(R.id.tvTitle) TextView tvTitle; @BindColor(R.color.brand) int colorBrand; @BindString(R.string.hello) String strHello; @Override

    protected void onCreate(Bundle state) { // LOGIC THAT MATTERS TO YOU } WITH BUTTERKNIFE
  88. private TextView tvTitle; private String title; private int colorBrand; @Override

    protected void onCreate(Bundle state) { // A LITTLE BIT OF INIT tvTitle = findViewById(R.string.project_id); colorBrand = ContextCompat.getColor(this, R.color.brand); strHello = getString(R.string.strHello); // A LITTLE BIT OF LOGIC } WITHOUT BUTTERKNIFE
  89. @Inject ActivityPresenter presenter; @Inject AnimationUtils animationUtils; @Inject ListAdapter listAdapter; @Override

    protected void onCreate(Bundle state) { // LOGIC THAT MATTERS TO YOU } WITH DAGGER
  90. ActivityPresenter presenter; AnimationUtils animationUtils; ListAdapter listAdapter; @Override protected void onCreate(Bundle

    savedInstanceState) { super.onCreate(savedInstanceState); // A LITTLE BIT OF INIT presenter = new ActivityPresenter(this, /* x other dependencies */); animationUtils = new AnimationUtils(this); listAdapter = new listAdapter(this, /* y other dependencies*/); // A LITTLE BIT OF LOGIC
 // … } WITHOUT DAGGER
  91. MORE DAGGER (INJECT EVERYTHING) @ActivityScope public class SupportAdapter extends SimplestRecyclerViewAdapter<SupportViewModel,

    SupportViewHolder> { public static final int ITEM_VIEW_TYPE_CATEGORY = 0; //…and 11 other types //Factories for all items types private final Map<Integer, SupportViewHolderFactory> vhFactories; @Inject public SupportAdapter(Map<Integer, SupportViewHolderFactory> vhFactories) { this.vhFactories = vhFactories; } @Override public SupportViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { //Take factory for given view type and create ViewHolder return vhFactories.get(viewType).createViewHolder(parent); } }
  92. @Module abstract class BindingsModule { @Binds @IntoMap @IntKey(ITEM_VIEW_TYPE_LOADING) abstract SupportViewHolderFactory

    bindLoadingViewHolderFactory(LoadingViewHolderFactory factory); @Binds @IntoMap @IntKey(ITEM_VIEW_TYPE_ARTICLE) abstract SupportViewHolderFactory bindArticleViewHolderFactory(ArticleViewHolderFactory factory); @Binds @IntoMap @IntKey(ITEM_VIEW_TYPE_CTA_CHAT) abstract SupportViewHolderFactory bindCtaChatViewHolderFactory(CtaChatViewHolderFactory factory); // and all other factories… }
  93. @AutoFactory(implementing = SupportViewHolderFactory.class) public class ArticleViewHolder extends SupportViewHolder { @BindView(R2.id.text)

    TextView text; @BindView(R2.id.divider) View divider; ArticleViewModel viewModel; private final ArticleListener host; public ArticleViewHolder(@Provided LayoutInflater inflater, @Provided ArticleListener host, ViewGroup parent) { super(inflater, parent, R.layout.as_item_support_article); this.host = host; } @Override public void bind(SupportViewModel item) { viewModel = (ArticleViewModel) item; text.setText(viewModel.title); ViewUtils.visibility(divider, !viewModel.last); } @OnClick(R2.id.contentContainer) void itemClicked() { host.articleSelected(viewModel); } } Logic that matters! Code generated for us
  94. DAGGER2RECIPES github.com/frogermcs • Async injection • MultiBinding + Autofactory •

    Demo app with all-in-one • Multimodule demo app
  95. void handleApiErrorResponse(HttpException e) { GeneralErrorResponse errorResponse = errorResponseParser.valueOf(e.response()); switch (errorResponse.genericErrorCode)

    { case ErrorCodes.CODE_401_DEVICE_NOT_AUTHENTICATED: ((BaseActivity) activityCompanion).logoutUser(); break; case ErrorCodes.CODE_403_AUTHENTICATE_ACCOUNT_BLOCKED: ((BaseActivity) activityCompanion).showAccountBlockedError(); break; case ErrorCodes.CODE_422_USER_EMAIL_ALREADY_TAKEN: if (activityCompanion instanceof CreateAccountActivity) { ((CreateAccountActivity) activityCompanion).showEmailAlreadyRegistered(); } break; case ErrorCodes.CODE_404_USER_INVALID_INVIATION: if (activityCompanion instanceof CreateAccountActivity) { ((CreateAccountActivity) activityCompanion).showInvalidInvitation(); } break; default: handleGeneralError(errorResponse, e); break; } }
  96. ERROR HANDLER @AutoHandler public interface SimplerErrorListener { @ErrorCode(CODE_403_AUTHENTICATE_ACCOUNT_BLOCKED) void accountBlocked();

    @ErrorCode(CODE_422_USER_EMAIL_ALREADY_TAKEN) void emailTaken(); @ErrorCode(codes = {"501", "503"}) void serverError(); } Check on github.com/AzimoLabs /Api-Error-Handler
  97. MODULARIZE ⚛

  98. Objects graph CODE MODULES ApiModule DataModule UtilsModule UserManager CountriesRepo CurrenciesRepo

    … OKHttpClient ApiRestClient AuthInterceptor LoggingInterceptor CurrencyFormatter ExchangeCalculator … more…
  99. Objects graph (AppComponent) ApiModule DataModule UtilsModule more… @Singleton @Component( modules

    = { AppModule.class, DataModule.class, ApiModule.class, UtilsModule.class, } ) public interface AppComponent { }
  100. Objects graph (AppComponent) ApiModule DataModule UtilsModule more… @Module public class

    ApiModule { @Provides @Singleton public OkHttpClient provideOkHttpClient() {/**/} @Provides @Singleton public Retrofit provideRestAdapter(OkHttpClient okHttpClient) {/**/} @Provides @Singleton public GithubApiService provideGithubApiService(Retrofit restAdapter) { return restAdapter.create(GithubApiService.class); } }
  101. Objects graph (AppComponent) ApiModule DataModule UtilsModule more… Our DataModule >600

    lines of code
  102. Objects graph (AppComponent) FEATURE MODULES CountriesModule PaymentModule CardsModule PaymentManager PaymentConfigRepo

    PaymentApiService CountriesManager CountriesRepository CountriesConfig CountriesApi CardsManager CardsRepository CardsEncryption CardsTokeniseApi more…
  103. CountriesModule PaymentModule CardsModule DataModule vs > ? @ ? >

    @
  104. @Singleton @Component( modules = { AzimoAppModule.class, DataModule.class, ApiModule.class, AppConfigModule.class, UtilsModule.class,

    BackendModule.class, GcmModule.class, ApiAuthorisationModule.class, ViewMappersModule.class, AnalyticsModule.class, MatchingModule.class, CardsModule.class, FormattersModule.class, TransactionDownloadModule.class, PeopleDownloadModule.class, AppsTrackingModule.class, DocUploaderModule.class, PricingModule.class, ZendeskModule.class, PayinModule.class, CheckoutModule.class, SharedEverywhereModule.class, PaymentModule.class, RateModule.class, EstimatedDeliveryModule.class, ZendeskModule.class, UserFactsModule.class, UserFactsDownloadModule.class, FirebaseCardsApiModule.class } ) public interface AppComponent { }
  105. None
  106. MODULARIZE MORE ⚛ ⚛ ⚛

  107. MULTI-FEATURE APP SupportFeature App ChatModule FAQModule Support logic PaymentFeature CardsModule

    Payments logic PaymentModule Base Shared features … CountriesFeature CurrenciesFeature UserFactsFeature … …
  108. SupportFeature App ChatModule FAQModule Support logic PaymentFeature CardsModule Payments logic

    PaymentModule Base Shared features … CountriesFeature CurrenciesFeature UserFactsFeature … … • Quick compilation • Quick unit tests • Clean structure MULTI-FEATURE APP
  109. SupportFeature App ChatModule FAQModule Support logic PaymentFeature CardsModule Payments logic

    PaymentModule Base Shared features … CountriesFeature CurrenciesFeature UserFactsFeature … … • Configuration 
 complexity • Low solutions 
 maturity • High entry level MULTI-FEATURE APP
  110. THE REALITY SupportFeature App Base Shared features Still App… Countries

    Feature UserFacts Feature … PaymentFeature … Transactions
 Feature Still App…
  111. PROJECT MODULARIZATION Multi module setup

  112. PROJECT MODULARIZATION Multi module setup Dagger 2 across modules

  113. PROJECT MODULARIZATION Multi module setup Dagger 2 across modules Unit

    testing
  114. PROJECT MODULARIZATION Multi module setup Dagger 2 across modules Unit

    testing Tests 
 coverage
  115. PROJECT MODULARIZATION Multi module setup Dagger 2 across modules Unit

    testing Tests 
 coverage github.com/frogermcs/MultiModuleGithubClient End-to-end testing Proguard configuration more in the future… Done during:
  116. ?A PEOPLE

  117. Crash-free users >99.5% Release once a week

  118. !!! Crash-free users >99.5% Release once a week

  119. ✋ STOP THE LINE

  120. Every employee on the assembly line has a responsibility to

    push a big red button that stops everything whenever they notice a defect
  121. Release every week + stop the line 40 releases in

    last 12 months.
  122. Release weeks in last 12 months

  123. Release once a week - achieved in 83% Crash free

    >99.5% - achieved in 100%
  124. THEY TELL We don’t have time do it right YOU

    ASK Do you have time to do it twice?
  125. @froger_mcs mirek@azimo.com medium.com/@froger_mcs Link to this presentation will be there

    More about how we build the technology Ask me anything you want :)