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