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

The Journey Towards A Platform Agnostic Codebase

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.

Alex Styl

October 11, 2017
Tweet

More Decks by Alex Styl

Other Decks in Technology

Transcript

  1. The journey towards a 

    platform-agnostic codebase
    Alexandros Stylianidis
    @alexstyl

    View full-size slide

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

    View full-size slide

  3. Sharing code across platforms
    writing business logic once, reuse anywhere
    Only have to rewrite UI or any platform specific components

    View full-size slide

  4. Memento Calendar
    Contacts Events Namedays

    View full-size slide

  5. Project Structure

    View full-size slide

  6. memento module
    Business Logic
    Anything unique to Memento
    No *.android packages
    Goal: 100% Kotlin

    View full-size slide

  7. android module
    All components of an Android Application (Manifest, Activities, Android APIs)
    Unique features of the Android
    Depends on the memento module

    View full-size slide

  8. android_common module
    android_wear module
    android_mobile module

    View full-size slide

  9. Architecture

    View full-size slide

  10. MVP
    Model
    View
    Presenter

    View full-size slide

  11. Model
    memento module

    View Models

    no knowledge of Android

    describes information to be displayed

    View full-size slide

  12. 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)

    View full-size slide

  13. View (implementation)
    android module

    Implemented by Activities (or fragments) 

    Responsible of displaying the data or update the looks of the screen

    View full-size slide

  14. Presenter
    memento module

    describes the feature without exposing the platform specific components

    View full-size slide

  15. Separating business logic 

    from Platform

    View full-size slide

  16. import java.util.ArrayList;

    import java.util.Collections;

    import java.util.List;

    import java.util.Map;


    public class ContactsProvider {


    private final Map sources;


    ContactsProvider(Map sources) {

    this.sources = sources;

    }


    public Contacts getContacts(List 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 getAllContacts() {

    List contacts = new ArrayList<>();

    for (ContactsProviderSource providerSource : sources.values()) {


    View full-size slide

  17. interface ContactsProviderSource {

    Contact getOrCreateContact(long contactID) throws ContactNotFoundException;


    Contacts queryContacts(List contactIds);


    Contacts getAllContacts();

    }

    View full-size slide

  18. 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): Contacts {

    val contacts = factory.queryContacts(contactIds)

    cache.addContacts(contacts)

    return contacts

    }

    }

    View full-size slide

  19. System Resources

    View full-size slide

  20. Respecting platforms resource system

    Keep translations of each platform

    View full-size slide

  21. public interface Strings {

    String getString(@StringRes int id);


    String getString(@StringRes int id, Object... formatArgs);


    String getQuantityString(@PluralsRes int id, int size);

    }

    View full-size slide

  22. public interface Strings {

    String getString(@StringRes int id);


    String getString(@StringRes int id, Object... formatArgs);


    String getQuantityString(@PluralsRes int id, int size);

    }

    View full-size slide

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

    View full-size slide

  24. @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?

    View full-size slide

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

    }

    View full-size slide

  26. 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 =

    View full-size slide

  27. 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!

    View full-size slide

  28. Create your own API language that works best on your domain.

    Do not just blindly wrap the system into interfaces

    View full-size slide

  29. Upcoming Events List
    Writing features

    View full-size slide

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

    View full-size slide

  31. What information do I need to 

    present back to the user?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. public interface {


    void showLoading();


    void display(List events);


    void askForContactPermission();

    }
    UpcomingListMVPView

    View full-size slide

  37. @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()


    View full-size slide


  38. @Test

    fun whenStartPresentingWithPermission_showEventsAfterDoneLoading() {

    val theDate = Date.on(1, Months.MARCH, 2017)

    val expectedEvents = arrayListOf()

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

    Mockito.`when`(mockProvider.calculateEventsBetween(TimePeriod.aYearFrom(STARTING_DATE))).thenReturn(initialEvents)

    Mockito.`when`(mockPermissions.permissionIsPresent()).thenReturn(true)


    upcomingEventsPresenter.startPresentingInto(mockView)


    val updatedEvents = arrayListOf(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)


    }

    }


    View full-size slide

  39. 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()

    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() {


    View full-size slide

  40. public interface {


    void showLoading();


    void display(List events);


    void askForContactPermission();

    }
    UpcomingListMVPView

    View full-size slide

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

    View full-size slide


  42. @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 events) {

    TransitionManager.beginDelayedTransition(root);


    progressBar.setVisibility(View.GONE);

    adapter.displayUpcomingEvents(events);


    View full-size slide


  43. @Override

    public void display(List 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();

    }


    }

    View full-size slide

  44. Future steps
    100% business logic in memento module

    Transpile into JavaScript

    Use Electron for a desktop version

    View full-size slide

  45. Thanks for listening
    Questions?
    Alexandros Stylianidis
    @alexstyl

    View full-size slide