Share
Embed
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
Transcript
125 slides
-
Anyone who has never made a mistake has never tried
anything new. -Albert Einstein -
Ship fast, Iterate often, Don’t crash, Succeed Fail fast, Fail
often, Fail better -
Release Once a month x1 x2 x5 x6 x3 x4
x7 x8 team’s capacity -
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 -
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 -
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!!! -
x1 x2 y2 z1 x3 y1 z2 z3 team’s capacity
(same as previous) Release Once a week -
Week 1 x1 x2 x1’ x2’ x1 x2 x1’ Dev
QA/Business -
x1’ x2’ x1’ build 1 build 2 Week 1 x1
x2 x1 x2 Dev QA/Business -
x1’ feedback 1 feedback 2 Week 1 x1 x2 x1’
x2’ x1 x2 Dev QA/Business -
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! -
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 -
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! -
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 -
x1 x2 y2 z1 x3 y1 z2 z3 x1 x2
x5 x6 x3 x4 x7 x8 vs -
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 -
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 -
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” -
Manual tests Others What’s new Build apk Internal distrib Internal
build Coding Testing and review x2 x3 x1 Release candidate -
Sign apk Release notes Internal distrib Update assets Store upload
RC distribution Coding Testing and review x2 x3 x1 Release candidate -
+2h x number of loops +2h Coding Testing and review
x2 x3 x1 Release candidate Internal build RC distrib x Rollout +1-2 days -
+2h Coding Testing and review x2 x3 x1 Release candidate
Internal build RC distrib x Rollout +1-2 days +2h x number of loops -
clean the project unit tests tests coverage lint checks assemble
apk distribute notify INTERNAL BUILD Coding Testing and review -
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 { } } } } -
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 -
Run all tests Create report Send report to slack Nightly
tests Setup and run simulators Every 24hrs -
Every 24hrs Update /res/values Remove unused resources create merge
request Update translations Fetch translation sfrom API lint checks -
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' } } } -
Coding Testing and review x2 x3 x1 Release candidate Internal
build RC distrib auto x Rollout auto +1-2 days -
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 -
@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 -
@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 -
@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()); } -
mocks, init config System under test Data and settings Use
cases, presentation logic -
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 -
Android instrumentation test template Fake data (e.g. json file) AVD
manager System under testing Mock objects -
Android instrumentation test template Init state Activity System under testing
Fake data (e.g. json file) AVD manager Mock objects -
@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(); } } -
@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 -
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 -
Automation Test Supervisor Automated test emulator run plugin Check on
github.com/AzimoLabs -
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 ✔ ✔ ✔ -
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 ✔ ✔ ✔ -
Write code that matters 8 Separate logic from SDK /
Code must be testable -
Activity Data Navigation Networking UI View logic ALL-IN-ONE Testing? External
QA, only end-to-end. -
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 -
UNIT TESTING View Stores Navigator API View logic Use cases
Repositories, Managers Database -
View Stores Navigator API View logic Use cases Repositories, Managers
Database UNIT TESTING -
View Stores Navigator API View logic Use cases Repositories, Managers
Database UNIT TESTING -
System under test Mocks INTEGRATION TESTING View Stores Navigator API
View logic Use cases Repositories, Managers Database -
System under test View Stores Navigator API View logic Use
cases Repositories, Managers Database END-TO-END TESTING -
EASY TO “THREAD” Always UI Ensure background thread Always background
View Stores Navigator API View logic Use cases Repositories, Managers Database -
SDK VS LOGIC Android SDK Plain code View Stores Navigator
API View logic Use cases Repositories, Managers Database -
THE BEST ARCHITECTURE The one that makes the developers team
the most productive. -
ARCHITECTURE PATTERNS • MVP, MVVM • App Architecture Components •
Others (see Android Blueprints) github.com/googlesamples/ android-architecture -
@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 -
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 -
@Inject ActivityPresenter presenter; @Inject AnimationUtils animationUtils; @Inject ListAdapter listAdapter; @Override
protected void onCreate(Bundle state) { // LOGIC THAT MATTERS TO YOU } WITH DAGGER -
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 -
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); } } -
@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… } -
@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 -
DAGGER2RECIPES github.com/frogermcs • Async injection • MultiBinding + Autofactory •
Demo app with all-in-one • Multimodule demo app -
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; } } -
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 -
Objects graph CODE MODULES ApiModule DataModule UtilsModule UserManager CountriesRepo CurrenciesRepo
… OKHttpClient ApiRestClient AuthInterceptor LoggingInterceptor CurrencyFormatter ExchangeCalculator … more… -
Objects graph (AppComponent) ApiModule DataModule UtilsModule more… @Singleton @Component( modules
= { AppModule.class, DataModule.class, ApiModule.class, UtilsModule.class, } ) public interface AppComponent { } -
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); } } -
Objects graph (AppComponent) FEATURE MODULES CountriesModule PaymentModule CardsModule PaymentManager PaymentConfigRepo
PaymentApiService CountriesManager CountriesRepository CountriesConfig CountriesApi CardsManager CardsRepository CardsEncryption CardsTokeniseApi more… -
@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 { } -
MULTI-FEATURE APP SupportFeature App ChatModule FAQModule Support logic PaymentFeature CardsModule
Payments logic PaymentModule Base Shared features … CountriesFeature CurrenciesFeature UserFactsFeature … … -
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 -
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 -
THE REALITY SupportFeature App Base Shared features Still App… Countries
Feature UserFacts Feature … PaymentFeature … Transactions Feature Still App… -
PROJECT MODULARIZATION Multi module setup Dagger 2 across modules Unit
testing Tests coverage -
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: -
Every employee on the assembly line has a responsibility to
push a big red button that stops everything whenever they notice a defect -
Release every week + stop the line 40 releases in
last 12 months. -
Release once a week - achieved in 83% Crash free
>99.5% - achieved in 100% -
THEY TELL We don’t have time do it right YOU
ASK Do you have time to do it twice? -
@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 :)