Thursday, 23 August 2018

Architecture 
A project developed by a diverse team needs clear guidelines on how to face common problems. For example, developers need a way to get off the main thread. It makes sense to provide a framework to do this consistently. Also, a good architecture should be hard to break: defining the layers of an app and clearly describing their relationships, avoids mistakes and simplifies code reviews.

We took concepts from Clean Architecture to solve these two problems (layering and background execution). The app is divided into a three layer structure:
Presentation layer (Views and ViewModels)
Domain layer (use cases)
Data layer (repositories, user manager)

The presentation layer cannot talk to the data layer directly. A ViewModel can only get to a repository through one or more use cases. This limitation ensures independence and testability. It also brings a nice opportunity to jump to a background thread: all use cases are executed in the background guaranteeing that no data access happens on the UI thread.

General architecture of the app
Presentation layer: Views + ViewModels + Data Binding
ViewModels provide data to the views via LiveData. The actual UI calls are done with Data Binding, relieving the activities and fragments from boilerplate. We deal with events using an event wrapper, modeled as part of the UI’s state. Read more about this pattern in this blog post.

Domain layer : UseCases
The domain layer revolves around the UseCase class, which went through a lot of iterations. In order to avoid callback hell we decided to use LiveData to expose the results of the UseCases.
By default use cases execute on a DefaultScheduler (a Kotlin object) which can later be modified from tests to run synchronously. We found this easier than dealing with a custom Dagger graph to inject a synchronous scheduler.

Data layer
The app dealt with 3 types of data, considering how often they change:
Static data that never changes: map, agenda, etc.
Data that changes 0–10 times per day: schedule data (sessions, speakers, tags, etc.)
Data that changes constantly even without user interaction: reservations and session starring
An important requirement for the app is offline support. Every piece of data should be available to the user on first run and even with a spotty Wi-Fi connection (we can’t assume perfect coverage at the venue and we should assume that many visitors will turn off their roaming data).
The static data is hard-coded. For example, the agenda repository started as an embedded JSON file but it was so static we translated it to Kotlin for simplicity and performance.
The conference data comes in a relatively large JSON file (around 600Kb uncompressed). An initial version of it was included in the APK to achieve full offline support. The app downloads fresh data from a static URL whenever the user refreshes the schedule or when the app receives a Firebase Cloud Messaging signal with a request to refresh. The conference data is downloaded inside a job managed by a JobScheduler to ensure that the user’s data is used responsibly.
The downloaded JSON is cached using OkHttp so the next time the app is started, the cached version is used instead of the bootstrapped file. This approach relieved us from dealing with files directly.
For user data (reservation, session starring, uploading Firebase tokens, etc.) we used Firestore, which is a NoSQL cloud database. It comes with offline support so we were able to sync user data across Android, web and iOS effortlessly.
Sign in support was implemented with Firebase Authentication. An AuthStateListener indicates when the current user has changed (from logged out to logged in, for example) using a LiveData observable.


No comments:

Post a Comment