The Journey Towards A Platform Agnostic Codebase

779556c070fdab99dc2e1c842dcf28b2?s=47 Alex Styl
October 11, 2017

The Journey Towards A Platform Agnostic Codebase

Sharing the same codebase across different platforms is the number one dream to many developers. With Kotlin being the new official language for Android and it being already used on other platforms such as the web (and eventually iOS), it is the perfect time to talk about sharing code across platforms.

In this talk Alex will go through the steps he is following to achieve the cross-platform dream in his side project, Memento Calendar, from start to end of a feature. We are going to see how to structure your project so that your business logic is separated from platform specific code, how to write platform agnostic features and how to test them.

779556c070fdab99dc2e1c842dcf28b2?s=128

Alex Styl

October 11, 2017
Tweet

Transcript

  1. The journey towards a 
 platform-agnostic codebase Alexandros Stylianidis @alexstyl

  2. None
  3. None
  4. https://blog.jetbrains.com/kotlin/2017/04/kotlinnative-tech-preview-kotlin-without-a-vm/

  5. Sharing code across platforms writing business logic once, reuse anywhere

    Only have to rewrite UI or any platform specific components
  6. Memento Calendar Contacts Events Namedays

  7. Project Structure

  8. memento module Business Logic Anything unique to Memento No *.android

    packages Goal: 100% Kotlin
  9. android module All components of an Android Application (Manifest, Activities,

    Android APIs) Unique features of the Android Depends on the memento module
  10. android_common module android_wear module android_mobile module

  11. Architecture

  12. MVP Model View Presenter

  13. Model memento module
 View Models
 no knowledge of Android
 describes

    information to be displayed
  14. View (interface) memento module
 describes different states of the UI

    (i.e show data, show loading, show error)
 it is not aware of the platform specific interfaces (such as Lists or TextViews)
  15. View (implementation) android module
 Implemented by Activities (or fragments) 


    Responsible of displaying the data or update the looks of the screen
  16. Presenter memento module
 describes the feature without exposing the platform

    specific components
  17. Separating business logic 
 from Platform

  18. import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 public

    class ContactsProvider {
 
 private final Map<Integer, ContactsProviderSource> sources;
 
 ContactsProvider(Map<Integer, ContactsProviderSource> sources) {
 this.sources = sources;
 }
 
 public Contacts getContacts(List<Long> contactIds, @ContactSource int source) {
 if (sources.containsKey(source)) {
 return sources.get(source).queryContacts(contactIds);
 }
 throw new IllegalArgumentException("Unknown source type: " + source);
 }
 
 public Contact getContact(long contactID, @ContactSource int source) throws ContactNotFoundException {
 if (sources.containsKey(source)) {
 return sources.get(source).getOrCreateContact(contactID);
 }
 throw new IllegalArgumentException("Unknown source type: " + source);
 
 }
 
 public List<Contact> getAllContacts() {
 List<Contact> contacts = new ArrayList<>();
 for (ContactsProviderSource providerSource : sources.values()) {

  19. interface ContactsProviderSource {
 Contact getOrCreateContact(long contactID) throws ContactNotFoundException;
 
 Contacts

    queryContacts(List<Long> contactIds);
 
 Contacts getAllContacts();
 }
  20. internal class AndroidContactsProviderSource(private val cache: ContactCache,
 private val factory: AndroidContactFactory)

    : ContactsProviderSource {
 
 @Throws(ContactNotFoundException::class)
 override fun getOrCreateContact(contactID: Long): Contact {
 var deviceContact = cache.getContact(contactID)
 if (deviceContact == null) {
 deviceContact = factory.createContactWithId(contactID)
 cache.addContact(deviceContact)
 }
 return deviceContact
 }
 
 override fun getAllContacts(): Contacts {
 val allContacts = factory.getAllContacts()
 cache.evictAll()
 cache.addContacts(allContacts)
 return allContacts
 }
 
 override fun queryContacts(contactIds: List<Long>): Contacts {
 val contacts = factory.queryContacts(contactIds)
 cache.addContacts(contacts)
 return contacts
 }
 }
  21. System Resources

  22. Respecting platforms resource system
 Keep translations of each platform

  23. public interface Strings {
 String getString(@StringRes int id);
 
 String

    getString(@StringRes int id, Object... formatArgs);
 
 String getQuantityString(@PluralsRes int id, int size);
 }
  24. public interface Strings {
 String getString(@StringRes int id);
 
 String

    getString(@StringRes int id, Object... formatArgs);
 
 String getQuantityString(@PluralsRes int id, int size);
 }
  25. public interface Strings {
 String getString(@StringRes int id);
 
 String

    getString(@StringRes int id, Object... formatArgs);
 
 String getQuantityString(@PluralsRes int id, int size);
 } Id of what?
 R is Android specific
  26. @Before
 public void setUp() {
 when(mockResources.getString(R.string.nameday)).thenReturn("Nameday");
 when(mockResources.getString(R.string.birthday)).thenReturn("Birthday");
 when(mockResources.getString(R.string.turns_age, 10)).thenReturn("Turns 10");


    } What if we want a different number?
  27. interface Strings {
 fun viewConversation(): String
 fun facebookMessenger(): String
 fun

    nameOf(starSign: StarSign): String
 fun turnsAge(age: Int): String
 fun inviteFriend(): String
 fun todaysNamedays(numberOfNamedays: Int): String
 fun donateAmount(amount: String): String
 fun eventOnDate(eventLabel: String, dateLabel: String): String
 fun appName(): String
 fun shareText(): String
 fun today(): String
 fun tomorrow(): String
 fun todayCelebrateTwo(nameOne: String, nameTwo: String): String
 fun todayCelebrateMany(name: String, numberLeft: Int): String
 fun nameOfEvent(event: EventType): String
 fun postOnFacebook(): String
 fun facebook(): String
 }
  28. class AndroidStrings(private val resources: Resources) : Strings { 
 override

    fun postOnFacebook(): String = resources.getString(R.string.Post_on_Facebook)
 
 override fun facebook(): String = resources.getString(R.string.Facebook)
 
 override fun facebookMessenger(): String = resources.getString(R.string.facebook_messenger)
 
 override fun viewConversation(): String = resources.getString(R.string.View_conversation)
 
 override fun nameOf(starSign: StarSign): String = when (starSign) {
 StarSign.AQUARIUS -> resources.getString(R.string.starsigns_aquarius)
 StarSign.PISCES -> resources.getString(R.string.starsigns_pisces)
 StarSign.ARIES -> resources.getString(R.string.starsigns_aries)
 StarSign.TAURUS -> resources.getString(R.string.starsigns_taurus)
 StarSign.GEMINI -> resources.getString(R.string.starsigns_gemini)
 StarSign.CANCER -> resources.getString(R.string.starsigns_cancer)
 StarSign.LEO -> resources.getString(R.string.starsigns_leo)
 StarSign.VIRGO -> resources.getString(R.string.starsigns_virgo)
 StarSign.LIBRA -> resources.getString(R.string.starsigns_libra)
 StarSign.SCORPIO -> resources.getString(R.string.starsigns_scorpio)
 StarSign.SAGITTARIUS -> resources.getString(R.string.starsigns_sagittarius)
 StarSign.CAPRICORN -> resources.getString(R.string.starsigns_capricorn)
 }
 
 override fun turnsAge(age: Int): String = resources.getString(R.string.turns_age, age);
 
 override fun inviteFriend(): String = resources.getString(R.string.Invite_friend)
 
 override fun todaysNamedays(numberOfNamedays: Int): String =
  29. class JavaStrings : Strings {
 
 override fun postOnFacebook(): String

    = "Post on Facebook"
 
 override fun facebook(): String = "Facebook"
 
 override fun viewConversation(): String = "View Conversations"
 
 override fun facebookMessenger(): String = "Messenger"
 
 override fun nameOf(starSign: StarSign): String = when (starSign) {
 StarSign.AQUARIUS -> "Aquarius"
 StarSign.PISCES -> "Pisces"
 StarSign.ARIES -> "Aries"
 StarSign.TAURUS -> "Taurus"
 StarSign.GEMINI -> "Gemini"
 StarSign.CANCER -> "Cancer"
 StarSign.LEO -> "Leo"
 StarSign.VIRGO -> "Virgo"
 StarSign.LIBRA -> "Libra"
 StarSign.SCORPIO -> "Scorpio"
 StarSign.SAGITTARIUS -> "Sagittarius"
 StarSign.CAPRICORN -> "Capricorn"
 }
 
 override fun turnsAge(age: Int): String = "Turns " + age
 
 override fun inviteFriend(): String = "Invite Friend"
 
 Not having to mock a thing!
  30. Create your own API language that works best on your

    domain.
 Do not just blindly wrap the system into interfaces
  31. Upcoming Events List Writing features

  32. Turns 27 Berta Birthday Μανόλης Turns 22 Chris Birthday Αγγέλα

  33. What information do I need to 
 present back to

    the user?
  34. Turns 27 Berta Birthday Μανόλης Turns 22 Chris Birthday Αγγέλα

    Turns 27 Berta Birthday Μανόλης Turns 22 Chris Birthday Αγγέλα Turns 27 Berta Birthday Μανόλης Turns 22 Chris Birthday Αγγέλα
  35. Turns 27 Berta Birthday Μανόλης Turns 27 Berta Birthday Μανόλης

    Turns 22 Chris Birthday Αγγέλα DateHeader UpcomingContactEvent
  36. Turns 27 Berta Birthday Μανόλης Turns 22 Chris Birthday Αγγέλα

    data class DateHeaderViewModel(val date: String) : UpcomingRowViewModel
  37. Turns 27 Berta Birthday Μανόλης Turns 22 Chris Birthday Αγγέλα

    data class UpcomingContactEventViewModel(val contact: Contact,
 val contactName: String,
 val eventLabel: String,
 @param:ColorInt val eventColor: Int,
 val backgroundVariant: Int,
 val contactImagePath: URI)
 : UpcomingRowViewModel
  38. public interface {
 
 void showLoading();
 
 void display(List<UpcomingContactEventViewModel> events);


    
 void askForContactPermission();
 } UpcomingListMVPView
  39. @RunWith(MockitoJUnitRunner::class)
 class UpcomingEventsPresenterTest {
 
 private val STARTING_DATE = Date.on(1,

    Months.APRIL, 2017)
 
 private val mockView = Mockito.mock(UpcomingListMVPView::class.java)
 private val mockPermissions = Mockito.mock(ContactPermissionRequest::class.java)
 private val mockEventsMonitor = Mockito.mock(UpcomingEventsSettingsMonitor::class.java)
 private val mockProvider = Mockito.mock(IUpcomingEventsProvider::class.java)
 
 private lateinit var upcomingEventsPresenter: UpcomingEventsPresenter
 
 @Before
 fun setUp() {
 upcomingEventsPresenter = UpcomingEventsPresenter(...)
 }
 
 @Test
 fun whenStartPresentingWithoutPermission_askForPermission() {
 Mockito.`when`(mockPermissions.permissionIsPresent()).thenReturn(false)
 
 upcomingEventsPresenter.startPresentingInto(mockView)
 
 Mockito.verify(mockView).askForContactPermission()
 }
 
 @Test
 fun whenStartPresentingWithPermission_showLoading() {
 Mockito.`when`(mockPermissions.permissionIsPresent()).thenReturn(true)
 
 upcomingEventsPresenter.startPresentingInto(mockView)
 
 Mockito.verify(mockView).showLoading()

  40. 
 @Test
 fun whenStartPresentingWithPermission_showEventsAfterDoneLoading() {
 val theDate = Date.on(1, Months.MARCH,

    2017)
 val expectedEvents = arrayListOf<UpcomingRowViewModel>()
 Mockito.`when`(mockProvider.calculateEventsBetween(TimePeriod.aYearFrom(theDate))).thenReturn(expectedEvents)
 Mockito.`when`(mockPermissions.permissionIsPresent()).thenReturn(true)
 
 upcomingEventsPresenter.startPresentingInto(mockView)
 
 Mockito.verify(mockView).showLoading()
 Mockito.verify(mockView).display(expectedEvents)
 }
 
 @Test
 fun whenEventPreferencesAreUpdated_thenUpdatedEventsArePushedToTheView() {
 val initialEvents = arrayListOf<UpcomingRowViewModel>()
 Mockito.`when`(mockProvider.calculateEventsBetween(TimePeriod.aYearFrom(STARTING_DATE))).thenReturn(initialEvents)
 Mockito.`when`(mockPermissions.permissionIsPresent()).thenReturn(true)
 
 upcomingEventsPresenter.startPresentingInto(mockView)
 
 val updatedEvents = arrayListOf<UpcomingRowViewModel>(DateHeaderViewModel("February 2017"))
 Mockito.`when`(mockProvider.calculateEventsBetween(TimePeriod.aYearFrom(STARTING_DATE))).thenReturn(updatedEvents)
 peopleEventsObserver.onChange(false)
 
 Mockito.verify(mockView, Times(1)).display(initialEvents)
 Mockito.verify(mockView, Times(1)).display(updatedEvents)
 
 }
 }

  41. internal class UpcomingEventsPresenter(private val firstDay: Date,
 private val permissions: ContactPermissionRequest,


    private val eventsProvider: UpcomingEventsProvider,
 private val workScheduler: Scheduler,
 private val resultScheduler: Scheduler) {
 
 private val TRIGGER = 1
 private val subject = PublishSubject.create<Int>()
 private var disposable: Disposable? = null
 
 fun startPresentingInto(view: UpcomingListMVPView) {
 disposable =
 subject
 .doOnSubscribe { view.showLoading() }
 .observeOn(workScheduler)
 .map { eventsProvider.calculateEventsBetween(TimePeriod.aYearFrom(firstDay)) }
 .observeOn(resultScheduler)
 .subscribe {
 upcomingRowViewModels ->
 view.display(upcomingRowViewModels)
 }
 if (permissions.permissionIsPresent()) {
 refreshEvents()
 } else {
 view.askForContactPermission()
 }
 }
 
 fun refreshEvents() {
 subject.onNext(TRIGGER)
 }
 
 fun stopPresenting() {

  42. public interface {
 
 void showLoading();
 
 void display(List<UpcomingContactEventViewModel> events);


    
 void askForContactPermission();
 } UpcomingListMVPView
  43. public class UpcomingEventsFragment extends MementoFragment implements {
 
 private ViewGroup

    root;
 private ProgressBar progressBar;
 private TextView emptyView;
 private RecyclerView upcomingList;
 
 private UpcomingEventsPresenter presenter; 
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 presenter = new UpcomingEventsPresenter(…); // initialisations here
 }
 
 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 View view = inflater.inflate(R.layout.fragment_upcoming_events, container, false);
 // all view related things here
 root = Views.findById(view, R.id.root);
 progressBar = Views.findById(view, R.id.upcoming_events_progress);
 emptyView = Views.findById(view, R.id.upcoming_events_emptyview);
 upcomingList = Views.findById(view, R.id.upcoming_events_list);
 
 return view;
 } 
 @Override
 UpcomingListMVPView
  44. 
 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

    {
 View view = inflater.inflate(R.layout.fragment_upcoming_events, container, false);
 // all view related things here
 root = Views.findById(view, R.id.root);
 progressBar = Views.findById(view, R.id.upcoming_events_progress);
 emptyView = Views.findById(view, R.id.upcoming_events_emptyview);
 upcomingList = Views.findById(view, R.id.upcoming_events_list);
 
 return view;
 } 
 @Override
 public void onActivityCreated(@Nullable Bundle savedInstanceState) {
 super.onActivityCreated(savedInstanceState);
 presenter.startPresentingInto(this);
 }
 
 @Override
 public void showLoading() {
 progressBar.setVisibility(View.VISIBLE);
 upcomingList.setVisibility(View.GONE);
 emptyView.setVisibility(View.GONE);
 }
 
 @Override
 public void display(List<UpcomingRowViewModel> events) {
 TransitionManager.beginDelayedTransition(root);
 
 progressBar.setVisibility(View.GONE);
 adapter.displayUpcomingEvents(events);
 

  45. 
 @Override
 public void display(List<UpcomingRowViewModel> events) {
 TransitionManager.beginDelayedTransition(root);
 
 progressBar.setVisibility(View.GONE);


    adapter.displayUpcomingEvents(events);
 
 if (events.size() > 0) {
 upcomingList.setVisibility(View.VISIBLE);
 emptyView.setVisibility(View.GONE);
 } else {
 upcomingList.setVisibility(View.GONE);
 emptyView.setVisibility(View.VISIBLE);
 }
 }
 
 
 @Override
 public void askForContactPermission() {
 permissions.requestForPermission();
 }
 
 @Override
 public void onDestroy() {
 super.onDestroy();
 presenter.stopPresenting();
 }
 
 }
  46. Future steps 100% business logic in memento module
 Transpile into

    JavaScript
 Use Electron for a desktop version
  47. None
  48. Thanks for listening Questions? Alexandros Stylianidis @alexstyl