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. Sharing code across platforms writing business logic once, reuse anywhere

    Only have to rewrite UI or any platform specific components
  2. android module All components of an Android Application (Manifest, Activities,

    Android APIs) Unique features of the Android Depends on the memento module
  3. 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)
  4. View (implementation) android module
 Implemented by Activities (or fragments) 


    Responsible of displaying the data or update the looks of the screen
  5. 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()) {

  6. 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
 }
 }
  7. public interface Strings {
 String getString(@StringRes int id);
 
 String

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

    getString(@StringRes int id, Object... formatArgs);
 
 String getQuantityString(@PluralsRes int id, int size);
 }
  9. 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
  10. 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
 }
  11. 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 =
  12. 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!
  13. Create your own API language that works best on your

    domain.
 Do not just blindly wrap the system into interfaces
  14. Turns 27 Berta Birthday Μανόλης Turns 22 Chris Birthday Αγγέλα

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

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

    data class DateHeaderViewModel(val date: String) : UpcomingRowViewModel
  17. 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
  18. @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()

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

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

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

  23. 
 @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();
 }
 
 }
  24. Future steps 100% business logic in memento module
 Transpile into

    JavaScript
 Use Electron for a desktop version