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. THE STORY OF
    THE APP
    1

    View full-size slide

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

    View full-size slide

  3. SHIP FAST
    =
    LEARN FAST

    View full-size slide

  4. VISION DATA

    ?

    View full-size slide

  5. 1 year goal
    ?
    ? ?
    ?

    View full-size slide

  6. 1 year goal
    3 months goal

    View full-size slide

  7. 1 year goal
    3 months goal

    View full-size slide

  8. 1 year goal
    Progress

    View full-size slide

  9. 1 year goal
    our achievement

    View full-size slide

  10. TESTING
    AUTOMATIONS

    View full-size slide

  11. 5 months

    development
    6 months

    development
    (with tests)

    View full-size slide

  12. 5 months

    development
    6 months

    development
    (with tests)
    1 month
    new features/adjustments

    View full-size slide

  13. 5 months

    development
    6 months

    development
    (with tests)
    1 month
    new features/adjustments
    2 months
    manual testing + bug fixes
    (x) iterations

    View full-size slide

  14. New

    version
    1 month
    each new
    version
    in 2015
    7 months
    1st app
    version

    View full-size slide

  15. New

    version
    New

    version
    New

    version
    New

    version
    New

    version
    1 month
    Now
    each new
    version
    in 2015
    1 week

    View full-size slide

  16. TEST MANUALLY
    ONLY ONCE

    View full-size slide

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

    View full-size slide

  18. public boolean shouldAskForAppRate() {

    if (!isSuccessfulFlow())

    return false;


    if (isUsingAppLongEnough())

    return true;


    return false;

    }

    View full-size slide

  19. @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);

    }

    View full-size slide

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

    View full-size slide

  21. IF WRITING TESTS
    IS HARD, THAT
    MIGHT BE A BUG

    View full-size slide

  22. TEST LOGIC,
    NOT SDK

    View full-size slide

  23. Activity
    XML business
    logic

    View full-size slide

  24. VIEW LOGIC
    ANDROID SDK JAVA
    Activity
    XML business
    logic

    View full-size slide

  25. @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!");

    }

    }

    View full-size slide

  26. public class MyActivityPresenterTest {


    @Test

    public void clickingButton_shouldChangeResultsViewText() throws Exception {

    presenter.onButtonClick();

    verify(activity).changeResultsViewText(eq(“Robolectric Rocks!”));

    }

    }

    View full-size slide

  27. github.com/googlesamples/android-architecture

    View full-size slide

  28. • 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

    View full-size slide

  29. 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

    View full-size slide

  30. DEPENDENCY
    INJECTION

    View full-size slide

  31. UserManager
    UserManager
    RestClient
    UserStore
    UserManager
    UserManager
    RestClient
    UserStore
    RestClient
    UserStore

    View full-size slide

  32. UserManager
    UserManager
    RestClient
    UserStore
    RestClient
    UserStore
    SQLite
    SharedPreferences
    UserStore
    Mock object
    UserStore

    View full-size slide

  33. 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);

    }

    }

    View full-size slide

  34. *Or any other DI framework
    DAGGER 2

    View full-size slide

  35. public class SplashActivity extends BaseActivity {


    @Inject

    SplashActivityPresenter presenter;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    getSplashActivityComponent().inject(this);

    }

    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  38. @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);

    }

    }

    View full-size slide

  39. public class SplashActivity extends BaseActivity {


    @Inject

    SplashActivityPresenter presenter;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    getSplashActivityComponent().inject(this);

    }

    }

    View full-size slide

  40. public class SplashActivity extends BaseActivity {


    @Inject

    SplashActivityPresenter presenter;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    getSplashActivityComponent().inject(this);

    }

    }

    View full-size slide

  41. public class SplashActivity extends BaseActivity {


    @Inject

    SplashActivityPresenter presenter;

    @Inject

    Provider analyticsEventProvider;
    @Inject

    Lazy lazyValidationErrorManager;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    getSplashActivityComponent().inject(this);

    }

    }

    View full-size slide

  42. public class SplashActivity {


    @Inject

    Validator validator;


    //...

    }
    SplashActivtity
    Validator
    DIRECT INJECTION

    View full-size slide

  43. LAZY INJECTION
    public class SplashActivity {


    @Inject

    Lazy lazyValidator;


    //...

    }
    SplashActivtity
    Validator
    Lazy
    1st get() call
    nth get() call
    get()
    SplashActivtity
    Dependencies
    Graph Validator
    Lazy
    Same instance like
    in 1st call

    View full-size slide

  44. PROVIDER INJECTION
    1st get() call
    nth get() call

    get() (instance 1) SplashActivtity
    Dependencies
    Graph
    Validator
    Provider
    Validator
    Validator
    Provider
    get() (instance n) SplashActivtity
    Dependencies
    Graph
    Validator
    New instance
    every time
    public class SplashActivity {


    @Inject

    Provider validatorProvider;


    //...

    }

    View full-size slide

  45. @Inject

    Observable analytics;

    View full-size slide

  46. @Module

    public class AnalyticsModule {
    @Provides

    @Singleton

    public Observable analyticsObservable(final Lazy analyticsLazy) {

    return Observable.defer(new Func0>() {

    @Override

    public Observable call() {

    return Observable.just(analyticsLazy.get());

    }

    });

    }
    }
    @Inject

    Observable analytics;

    View full-size slide

  47. Lazy injection

    app launch time optimizations
    Launch time without lazy loading: 650ms
    SplashActivity
    Crashlytics
    200ms
    Mixpanel Google
    Analytics
    100ms
    Rest
    Client
    150ms 200ms

    View full-size slide

  48. 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

    View full-size slide

  49. 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

    View full-size slide

  50. 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);

    }


    //...

    }

    View full-size slide

  51. 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);

    }


    //...

    }

    View full-size slide

  52. 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);

    }

    //...
    }

    View full-size slide

  53. 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;

    }

    //...

    }

    View full-size slide

  54. public class RepositoriesListAdapter extends RecyclerView.Adapter {

    private Map 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;

    }

    //…

    }

    View full-size slide

  55. public class RepositoriesListAdapter extends RecyclerView.Adapter {

    private Map 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;

    }

    //…

    }

    View full-size slide

  56. @Module

    public class RepositoriesListActivityModule extends
    BaseActivityModule {


    //…


    @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

    View full-size slide

  57. @Module

    public class RepositoriesListActivityModule extends
    BaseActivityModule {


    //…


    @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();

    }

    }

    View full-size slide

  58. DAGGER 2
    • Compile time generation and validation
    • Custom scopes
    • Speed and memory management

    View full-size slide

  59. Dagger2Recipes
    https://github.com/frogermcs
    • UserScope
    • Async injection
    • MultiBinding + Autofactory
    • Example app: Github client

    View full-size slide

  60. THE MOST
    MINIMALISTIC APP

    View full-size slide

  61. The only trusted person in team

    View full-size slide

  62. 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

    View full-size slide

  63. Lane
    build_internalDebug
    clean assemble
    branch: develop
    build flavor: internalDebug

    View full-size slide

  64. Lane
    build_internalDebug
    clean unit tests
    assemble

    View full-size slide

  65. Lane
    build_internalDebug
    clean unit tests
    assemble coverage

    View full-size slide

  66. Lane
    build_internalDebug
    clean unit tests
    assemble coverage

    View full-size slide

  67. Lane
    build_internalDebug
    clean unit tests lint
    assemble coverage

    View full-size slide

  68. Lane
    build_internalDebug
    clean unit tests lint
    assemble coverage

    View full-size slide

  69. Lane
    build_internalDebug
    clean unit tests lint
    assemble coverage

    View full-size slide

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

    View full-size slide

  71. Lane
    build_releaseCandidate
    build_internalDebug
    upload:
    Crashlytics Beta

    View full-size slide

  72. Lane
    build_releaseCandidate
    build_internalDebug
    upload:
    Crashlytics Beta

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  77. Lane
    build_productionRelease
    build_internalDebug
    branch: master
    build flavor: release
    supply
    • Sign apk
    • Update store description
    • Push to: alpha/beta/release

    View full-size slide

  78. Other lanes
    Automation_EndToEnd_CompleteTestSuite

    View full-size slide

  79. Other lanes
    Automation_EndToEnd_CompleteTestSuite

    View full-size slide

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

    View full-size slide

  81. Our job is to make software
    that works*
    *In a long term

    View full-size slide