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

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.

Mirosław Stanek

October 05, 2018
Tweet

More Decks by Mirosław Stanek

Other Decks in Programming

Transcript

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

    x7 x8 team’s capacity
  2. 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
  3. 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
  4. 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!!!
  5. x1 x2 y2 z1 x3 y1 z2 z3 team’s capacity

    (same as previous) Release Once a week
  6. 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!
  7. 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
  8. 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!
  9. 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
  10. x1 x2 y2 z1 x3 y1 z2 z3 x1 x2

    x5 x6 x3 x4 x7 x8 vs
  11. 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
  12. 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
  13. 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”
  14. Manual tests Others What’s new Build apk Internal distrib Internal

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

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

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

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

    apk distribute notify INTERNAL BUILD Coding Testing and review
  19. 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 { } } } }
  20. 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
  21. Run all tests Create report Send report to slack Nightly

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

    request Update translations Fetch translation sfrom API lint checks
  23. 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' } } }
  24. Coding Testing and review x2 x3 x1 Release candidate Internal

    build RC distrib auto x Rollout auto +1-2 days
  25. 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
  26. @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
  27. @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());
 }
  28. 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
  29. Android instrumentation test template Init state Activity System under testing

    Fake data (e.g. json file) AVD manager Mock objects
  30. @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(); } }
  31. @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
  32. 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
  33. 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 ✔ ✔ ✔
  34. 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 ✔ ✔ ✔
  35. 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
  36. System under test Mocks INTEGRATION TESTING View Stores Navigator API

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

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

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

    API View logic Use cases Repositories, Managers Database
  40. ARCHITECTURE PATTERNS • MVP, MVVM • App Architecture Components •

    Others (see Android Blueprints) github.com/googlesamples/
 android-architecture
  41. 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
  42. @Inject ActivityPresenter presenter; @Inject AnimationUtils animationUtils; @Inject ListAdapter listAdapter; @Override

    protected void onCreate(Bundle state) { // LOGIC THAT MATTERS TO YOU } WITH DAGGER
  43. 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
  44. 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); } }
  45. @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… }
  46. @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
  47. 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; } }
  48. 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
  49. Objects graph CODE MODULES ApiModule DataModule UtilsModule UserManager CountriesRepo CurrenciesRepo

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

    = { AppModule.class, DataModule.class, ApiModule.class, UtilsModule.class, } ) public interface AppComponent { }
  51. 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); } }
  52. Objects graph (AppComponent) FEATURE MODULES CountriesModule PaymentModule CardsModule PaymentManager PaymentConfigRepo

    PaymentApiService CountriesManager CountriesRepository CountriesConfig CountriesApi CardsManager CardsRepository CardsEncryption CardsTokeniseApi more…
  53. @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 { }
  54. MULTI-FEATURE APP SupportFeature App ChatModule FAQModule Support logic PaymentFeature CardsModule

    Payments logic PaymentModule Base Shared features … CountriesFeature CurrenciesFeature UserFactsFeature … …
  55. 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
  56. 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
  57. THE REALITY SupportFeature App Base Shared features Still App… Countries

    Feature UserFacts Feature … PaymentFeature … Transactions
 Feature Still App…
  58. 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:
  59. Every employee on the assembly line has a responsibility to

    push a big red button that stops everything whenever they notice a defect
  60. THEY TELL We don’t have time do it right YOU

    ASK Do you have time to do it twice?
  61. @froger_mcs [email protected] medium.com/@froger_mcs Link to this presentation will be there

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