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

Android Architecture Components - A Testable Approach to Android Development

Android Architecture Components - A Testable Approach to Android Development

Developing Android Apps has notoriously been a difficult task. Previously, Google refrained from giving advice as to how you should go about architecting your Android applications and it has been “left it up to the reader” to decide how. This lead to apps having a lot of bloat in their activities and being really difficult to test. This has now changed since Google I/O 2017 where the new Android Architecture components were introduced. The libraries aim to make developing and maintaining your Android apps a lot easier. In this presentation, Rebecca will present the components and dive into detail around the different types of tests you should write (and where) for your newly architected Android application.

Rebecca Franks is an Android Engineer at Over, currently building the Android version of the already popular photo editing iOS app. She is a Google Developer Expert for Android and she enjoys public speaking by frequently speaking at conferences and local meetups. Her blog has been featured multiple times on Android Weekly and she loves travelling the world.

Rebecca Franks

March 27, 2018
Tweet

More Decks by Rebecca Franks

Other Decks in Programming

Transcript

  1. @riggaroo
    Android Architecture
    Components
    A Testable Approach to Android
    Development
    Rebecca Franks

    View Slide

  2. HELLO!
    I am Rebecca Franks
    Android Developer
    Google Developer Expert
    @riggaroo

    View Slide

  3. What are the Android Architecture Components?
    Powerful APIs that
    are testable
    "
    Improve
    app quality

    Address common
    issues

    Best practice
    guidelines
    %

    View Slide

  4. @riggaroo
    Components
    - Room
    - ViewModels
    - LiveData *
    - Lifecycle *
    - Paging Library *
    - …

    View Slide

  5. Architecting an App

    View Slide

  6. bit.ly/android-arch
    bit.ly/android-arch-kotlin

    View Slide

  7. MVVM
    Model, View, ViewModel

    View Slide

  8. MVVM
    View
    ViewModel
    Model

    View Slide

  9. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    MVVM

    View Slide

  10. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    MVVM

    View Slide

  11. Room - SQLite
    Object Mapper

    View Slide

  12. Creating an Entity
    @Entity(tableName = TABLE_NAME)
    public class Event {
    @PrimaryKey(autoGenerate = true)
    private int id;
    private String name;
    private LocalDateTime date;
    public int getId() {
    return id;
    }
    public String getName() {
    return name;
    }
    public LocalDateTime getDate() {
    return date;
    }
    }

    View Slide

  13. Creating a DAO
    @Dao
    public interface EventDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void addEvent(Event event);
    @Query("SELECT * FROM " + Event.TABLE_NAME + " WHERE " + Event.DATE_FIELD + " > :minDate")
    LiveData> getEvents(LocalDateTime minDate);
    }

    View Slide

  14. Creating a Database
    @Database(entities = {Event.class}, version = 1)
    public abstract class EventDatabase extends RoomDatabase {
    public abstract EventDao eventDao();
    }

    View Slide

  15. Create an instance of the DB
    @Provides
    @Singleton
    EventDatabase providesEventDatabase(CountdownApplication context) {
    return Room.databaseBuilder(context, EventDatabase.class,
    “event.db").build();
    }

    View Slide

  16. Model

    View Slide

  17. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    MVVM

    View Slide

  18. Model
    Model layer follows Repository pattern
    ie EventRepository, LoginRepository
    MVVM - Model

    View Slide

  19. Repository

    View Slide

  20. Repository
    Business Logic, Caching
    Plain object
    No Android Dependencies

    View Slide

  21. public class EventRepositoryImpl implements EventRepository {
    @Inject
    EventDao eventDao;
    public EventRepositoryImpl(EventDao eventDao) {
    this.eventDao = eventDao;
    }
    @Override
    public Completable addEvent(Event event) {
    if (event == null){
    return Completable.error(new IllegalArgumentException("Event cannot be null"));
    }
    return Completable.fromAction(() -> eventDao.addEvent(event));
    }
    }

    View Slide

  22. public class EventRepositoryImpl implements EventRepository {
    @Inject
    EventDao eventDao;
    public EventRepositoryImpl(EventDao eventDao) {
    this.eventDao = eventDao;
    }
    @Override
    public Completable addEvent(Event event) {
    if (event == null){
    return Completable.error(new IllegalArgumentException("Event cannot be null"));
    }
    return Completable.fromAction(() -> eventDao.addEvent(event));
    }
    }
    Completable is used when the Observable has to
    do some task without emitting a value.

    View Slide

  23. ViewModel

    View Slide

  24. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    MVVM

    View Slide

  25. ViewModel
    AddEventViewModel, EventListViewModel
    MVVM - ViewModel

    View Slide

  26. What is a ViewModel?
    - The VM in MVVM architecture
    - Store & manage UI-related
    data
    - Data survives config changes
    - No leaks

    View Slide

  27. public class AddEventViewModel extends ViewModel {
    @Inject
    EventRepository eventRepository;
    private String eventName;
    private String eventDescription;
    private LocalDateTime eventDateTime ;
    @Inject
    AddEventViewModel() {
    eventDateTime = LocalDateTime.now();
    }
    void addEvent() {
    Event event = new Event(0, eventName, eventDescription, eventDateTime);
    eventRepository.addEvent(event).observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(Schedulers.io())
    .subscribe(new CompletableObserver() {
    @Override
    public void onSubscribe(Disposable d) {
    }
    Creating your ViewModel

    View Slide

  28. @Inject
    AddEventViewModel() {
    eventDateTime = LocalDateTime.now();
    }
    void addEvent() {
    Event event = new Event(0, eventName, eventDescription, eventDateTime);
    eventRepository.addEvent(event).observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(Schedulers.io())
    .subscribe(new CompletableObserver() {
    @Override
    public void onSubscribe(Disposable d) {
    }
    @Override
    public void onComplete() {
    Log.d(TAG, ”onComplete - successfully added event");
    }
    @Override
    public void onError(Throwable e) {
    Log.d(TAG, e, "onError - add:");
    }
    });
    }

    View Slide

  29. @Inject
    AddEventViewModel() {
    eventDateTime = LocalDateTime.now();
    }
    void addEvent() {
    Event event = new Event(0, eventName, eventDescription, eventDateTime);
    eventRepository.addEvent(event).observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(Schedulers.io())
    .subscribe(new CompletableObserver() {
    @Override
    public void onSubscribe(Disposable d) {
    }
    @Override
    public void onComplete() {
    Log.d(TAG, ”onComplete - successfully added event");
    }
    @Override
    public void onError(Throwable e) {
    Log.d(TAG, e, "onError - add:");
    }
    });
    }

    View Slide

  30. View

    View Slide

  31. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    MVVM

    View Slide

  32. View
    AddEventFragment, EventListFragment
    MVVM - View

    View Slide

  33. public class AddEventFragment extends Fragment {
    private Button buttonAddEvent;
    private AddEventViewModel addEventViewModel;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
    savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_add_event, container, false);
    //..
    setupClickListeners();
    setupViewModel();
    return view;
    }
    private void setupViewModel() {
    addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class);
    }
    private void setupClickListeners() {
    buttonAddEvent.setOnClickListener(v -> {
    addEventViewModel.addEvent();
    getActivity().finish();
    });
    }
    }

    View Slide

  34. public class AddEventFragment extends Fragment {
    private Button buttonAddEvent;
    private AddEventViewModel addEventViewModel;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
    savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_add_event, container, false);
    //..
    setupClickListeners();
    setupViewModel();
    return view;
    }
    private void setupViewModel() {
    addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class);
    }
    private void setupClickListeners() {
    buttonAddEvent.setOnClickListener(v -> {
    addEventViewModel.addEvent();
    getActivity().finish();
    });
    }
    }

    View Slide

  35. public class AddEventFragment extends Fragment {
    private Button buttonAddEvent;
    private AddEventViewModel addEventViewModel;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
    savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_add_event, container, false);
    //..
    setupClickListeners();
    setupViewModel();
    return view;
    }
    private void setupViewModel() {
    addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class);
    }
    private void setupClickListeners() {
    buttonAddEvent.setOnClickListener(v -> {
    addEventViewModel.addEvent();
    getActivity().finish();
    });
    }
    }

    View Slide

  36. public class AddEventFragment extends Fragment {
    private Button buttonAddEvent;
    private AddEventViewModel addEventViewModel;
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
    savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_add_event, container, false);
    //..
    setupClickListeners();
    setupViewModel();
    return view;
    }
    private void setupViewModel() {
    addEventViewModel = ViewModelProviders.of(this).get(AddEventViewModel.class);
    }
    private void setupClickListeners() {
    buttonAddEvent.setOnClickListener(v -> {
    addEventViewModel.addEvent();
    getActivity().finish();
    });
    }
    }

    View Slide

  37. Demo

    View Slide

  38. View Slide

  39. Let’s chat about
    tests…

    View Slide

  40. View Slide

  41. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    JUnit tests
    MVVM

    View Slide

  42. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    Instrumentation Tests
    MVVM

    View Slide

  43. Abstract out Android
    Logic as much as
    possible.
    Write plenty of JUnit
    Tests, not as many UI
    Tests

    View Slide

  44. View Slide

  45. @riggaroo
    JUnit Tests
    - Run on JVM
    - No Android Dependencies
    - Use Mockito - mock out everything
    but current class under test

    View Slide

  46. @riggaroo
    Instrumentation Tests
    - Two Types:
    - Android Unit Test
    - Use Android dependencies
    - No UI
    - UI Test
    - Use Espresso
    - Physical clicks on device
    - Hardly any mocking (maybe HTTP
    responses)

    View Slide

  47. Testing Database
    Queries

    View Slide

  48. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    Testing Room

    View Slide

  49. @RunWith(AndroidJUnit4.class)
    public class EventDaoTest {
    EventDao eventDao;
    EventDatabase eventDatabase;
    @Before
    public void setup() {
    eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
    EventDatabase.class).build();
    eventDao = eventDatabase.eventDao();
    }
    @Test
    public void addEvent_SuccessfullyAddsEvent() throws InterruptedException {
    Event event = generateEventTestData(0, "Wedding");
    eventDao.addEvent(event);
    List eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now()));
    assertEquals(event.getName(), eventRetrieved.get(0).getName());
    }
    }

    View Slide

  50. @RunWith(AndroidJUnit4.class)
    public class EventDaoTest {
    EventDao eventDao;
    EventDatabase eventDatabase;
    @Before
    public void setup() {
    eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
    EventDatabase.class).build();
    eventDao = eventDatabase.eventDao();
    }
    @Test
    public void addEvent_SuccessfullyAddsEvent() throws InterruptedException {
    Event event = generateEventTestData(0, "Wedding");
    eventDao.addEvent(event);
    List eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now()));
    assertEquals(event.getName(), eventRetrieved.get(0).getName());
    }
    }

    View Slide

  51. @RunWith(AndroidJUnit4.class)
    public class EventDaoTest {
    EventDao eventDao;
    EventDatabase eventDatabase;
    @Before
    public void setup() {
    eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
    EventDatabase.class).build();
    eventDao = eventDatabase.eventDao();
    }
    @Test
    public void addEvent_SuccessfullyAddsEvent() throws InterruptedException {
    Event event = generateEventTestData(0, "Wedding");
    eventDao.addEvent(event);
    List eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now()));
    assertEquals(event.getName(), eventRetrieved.get(0).getName());
    }
    }

    View Slide

  52. @RunWith(AndroidJUnit4.class)
    public class EventDaoTest {
    EventDao eventDao;
    EventDatabase eventDatabase;
    @Before
    public void setup() {
    eventDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
    EventDatabase.class).build();
    eventDao = eventDatabase.eventDao();
    }
    @Test
    public void addEvent_SuccessfullyAddsEvent() throws InterruptedException {
    Event event = generateEventTestData(0, "Wedding");
    eventDao.addEvent(event);
    List eventRetrieved = getValue(eventDao.getEvents(LocalDateTime.now()));
    assertEquals(event.getName(), eventRetrieved.get(0).getName());
    }
    }

    View Slide

  53. Testing Models/
    Repositories

    View Slide

  54. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    Model Tests

    View Slide

  55. public class EventRepositoryImplTest {
    @Mock
    private EventDao eventDao;
    @Rule
    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
    private EventRepository eventRepository;
    @Before
    public void setUp() {
    MockitoAnnotations.initMocks(this);
    eventRepository = new EventRepositoryImpl(eventDao);
    }
    @Test
    public void addEvent_TriggersDbAdd(){
    Event event = FakeEventDataGenerator.getFakeEvent();
    eventRepository.addEvent(event).test().onComplete();
    verify(eventDao).addEvent(event);
    }
    }

    View Slide

  56. public class EventRepositoryImplTest {
    @Mock
    private EventDao eventDao;
    @Rule
    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
    private EventRepository eventRepository;
    @Before
    public void setUp() {
    MockitoAnnotations.initMocks(this);
    eventRepository = new EventRepositoryImpl(eventDao);
    }
    @Test
    public void addEvent_TriggersDbAdd(){
    Event event = FakeEventDataGenerator.getFakeEvent();
    eventRepository.addEvent(event).test().onComplete();
    verify(eventDao).addEvent(event);
    }
    }

    View Slide

  57. public class EventRepositoryImplTest {
    @Mock
    private EventDao eventDao;
    @Rule
    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
    private EventRepository eventRepository;
    @Before
    public void setUp() {
    MockitoAnnotations.initMocks(this);
    eventRepository = new EventRepositoryImpl(eventDao);
    }
    @Test
    public void addEvent_TriggersDbAdd(){
    Event event = FakeEventDataGenerator.getFakeEvent();
    eventRepository.addEvent(event).test().onComplete();
    verify(eventDao).addEvent(event);
    }
    }

    View Slide

  58. Testing ViewModels

    View Slide

  59. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    View Model Tests

    View Slide

  60. public class AddEventViewModelTest {
    AddEventViewModel addEventViewModel;
    @Mock
    EventRepository eventRepository;
    @Rule
    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
    @Before
    public void setup() {
    MockitoAnnotations.initMocks(this);
    addEventViewModel = new AddEventViewModel();
    addEventViewModel.eventRepository = eventRepository;
    }
    @Test
    public void addEvent() {
    when(eventRepository.addEvent(any())).thenReturn(Completable.complete());
    addEventViewModel.addEvent();
    verify(eventRepository).addEvent(any());
    }

    View Slide

  61. public class AddEventViewModelTest {
    AddEventViewModel addEventViewModel;
    @Mock
    EventRepository eventRepository;
    @Rule
    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
    @Before
    public void setup() {
    MockitoAnnotations.initMocks(this);
    addEventViewModel = new AddEventViewModel();
    addEventViewModel.eventRepository = eventRepository;
    }
    @Test
    public void addEvent() {
    when(eventRepository.addEvent(any())).thenReturn(Completable.complete());
    addEventViewModel.addEvent();
    verify(eventRepository).addEvent(any());
    }

    View Slide

  62. public class AddEventViewModelTest {
    AddEventViewModel addEventViewModel;
    @Mock
    EventRepository eventRepository;
    @Rule
    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
    @Before
    public void setup() {
    MockitoAnnotations.initMocks(this);
    addEventViewModel = new AddEventViewModel();
    addEventViewModel.eventRepository = eventRepository;
    }
    @Test
    public void addEvent() {
    when(eventRepository.addEvent(any())).thenReturn(Completable.complete());
    addEventViewModel.addEvent();
    verify(eventRepository).addEvent(any());
    }

    View Slide

  63. View Tests ☕

    View Slide

  64. View
    ViewModel
    Model
    Retrofit API
    Room Database
    Web
    Service
    SQLite
    View Tests

    View Slide

  65. Espresso Basic Formula

    View Slide

  66. @RunWith(AndroidJUnit4.class)
    public class AddEventFragmentTest {
    @Rule
    public ActivityTestRule activityTestRule =
    new ActivityTestRule<>(AddEventActivity.class, true, true);
    private AddEventViewModel addEventViewModel;
    @Before
    public void setup(){
    addEventViewModel = mock(AddEventViewModel.class);
    //..
    }
    @Test
    public void addEvent(){
    when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now());
    onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"),
    closeSoftKeyboard());
    onView(withId(R.id.button_set_date)).perform(click());
    onView(withText("OK")).perform(scrollTo(), click());
    onView(withId(R.id.button_add)).perform(click());

    View Slide

  67. public ActivityTestRule activityTestRule =
    new ActivityTestRule<>(AddEventActivity.class, true, true);
    private AddEventViewModel addEventViewModel;
    @Before
    public void setup(){
    addEventViewModel = mock(AddEventViewModel.class);
    //..
    }
    @Test
    public void addEvent(){
    when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now());
    onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"),
    closeSoftKeyboard());
    onView(withId(R.id.button_set_date)).perform(click());
    onView(withText("OK")).perform(scrollTo(), click());
    onView(withId(R.id.button_add)).perform(click());
    }
    }

    View Slide

  68. public ActivityTestRule activityTestRule =
    new ActivityTestRule<>(AddEventActivity.class, true, true);
    private AddEventViewModel addEventViewModel;
    @Before
    public void setup(){
    addEventViewModel = mock(AddEventViewModel.class);
    //..
    }
    @Test
    public void addEvent(){
    when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now());
    onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"),
    closeSoftKeyboard());
    onView(withId(R.id.button_set_date)).perform(click());
    onView(withText("OK")).perform(scrollTo(), click());
    onView(withId(R.id.button_add)).perform(click());
    }
    }

    View Slide

  69. public ActivityTestRule activityTestRule =
    new ActivityTestRule<>(AddEventActivity.class, true, true);
    private AddEventViewModel addEventViewModel;
    @Before
    public void setup(){
    addEventViewModel = mock(AddEventViewModel.class);
    //..
    }
    @Test
    public void addEvent(){
    when(addEventViewModel.getEventDateTime()).thenReturn(LocalDateTime.now());
    onView(withId(R.id.edit_text_title)).perform(typeText("Christmas Day"),
    closeSoftKeyboard());
    onView(withId(R.id.button_set_date)).perform(click());
    onView(withText("OK")).perform(scrollTo(), click());
    onView(withId(R.id.button_add)).perform(click());
    }
    }

    View Slide

  70. Running UI Tests

    View Slide

  71. View Slide

  72. What about Android
    classes or static methods?

    View Slide

  73. JUnit tests
    View
    ViewModel
    Repository
    ResourceProvider
    SharedPrefProvider
    MVVM

    View Slide

  74. Android Unit tests
    View
    ViewModel
    Repository
    ResourceProvider
    SharedPrefProvider
    MVVM

    View Slide

  75. public class ResourceProvider {
    private final Context context;
    public ResourceProvider(Context context){
    this.context = context;
    }
    public String getString(Integer resId){
    return context.getString(resId);
    }
    }

    View Slide

  76. View Slide

  77. Summary
    Encourages
    clean code
    (
    UI Tests
    using Espresso

    Less memory
    leaks and bugs

    Simplifies code

    View Slide

  78. @riggaroo
    Thank you!
    developer.android.com/arch
    Rebecca Franks
    @riggaroo

    View Slide