9 Cards Client Architecture

9 Cards Client Architecture

This is the first in a series of technical articles explaining how 9 Cards works. If you missed our introduction to this new product, catch up here: First Open Source Android Launcher written in Scala released.

Client Architecture:

The first goal of the 9 Cards Client was to create a Functional Architecture over the available imperative Android SDK written in Java, and using different patterns than we are used to in the backend in Scala. First off, let me tell you that it wasn’t easy. While we sometimes changed our architecture in the UI section, the good news is we found a nice way to create a Functional Architecture for Android Apps using Scala, and that’s what we want to show you.

During the development of this project, we avoided using Java libraries commonly used by Android developers like Dagger, RXJava, and others. Instead, we built our project by using a few popular Scala libraries that allowed us to improve our architecture from a functional programming standpoint.

Layers

The first thing we’ll discuss is the overall structure for applications regarding the logical grouping of components into separate layers that communicate with each other.

Our architecture is divided into three layers:

  • Services: first abstraction layer - it provides methods that communicate with services, for example, repository, API, applications installed on the cell phone, wifi status, etc.

  • Processes: our use cases accessing the services layer to create semantic methods for the app, for example, get collections, sync device, add an application, etc.

  • App: for abstracting between Android SDK and our business logic to make the application more testable. For this, we are using a new concept called Jobs where we can combine UI actions and calls to our processes.

9 Cards Architecture

Every service is a small piece of code that performs a specific task. For instance, acquiring information from the apps installed on the phone, calls to the API, store data in a database, and so on.

Every process combines different services to create the use cases. For example, if you want to login a user, you can combine methods from ApiServices and PersistenceService to get information from the backend and store it in the local database.

The App is the only layer that can work with the UI using the Android SDK. This layer contains the Activities, Fragments, and other necessary Android components for the interface. We worked extensively to try and find a better way to make this layer more functional. The main problem is the lack of control when it comes to creating activities; something that’s necessary to the Android Context for accessing phone information such as contacts, applications, widgets, and more. To help us work with views in a functional way, we are using the Macroid library; we’ve also created the Jobs for combining the UI actions and the business logic in our processes layer. We’ll talk more about this later.

Communication between layers

Our architecture is based on two Typelevel libraries: Cats and Monix. To make combining methods in the different layers and Jobs easier, we always return the same type based off of these wonderful libraries. The type is the following:

type TaskService[T] = EitherT[Task, NineCardException, T]

EitherT is provided by Cats and allows the effect of Task to be combined with the fail-fast effect of Scala Either.

Monix provides us with the Task Monad for controlling possibly lazy & asynchronous computations avoiding callbacks.

NineCardException is an ADT for controlling the exceptions in our code and T the type returned.

Every method in the service layer, processes layer, and Jobs in the App layer return TaskService[T] type and, finally, they are called from the activities or views for executing the actions from the UI.

We have two important advantages using this approach:

  • Using the Task of Monix we have asynchronous evaluation, can execute the task easily in the UI Android Thread, or we can cancel the running computation if it’s necessary.
  • We can combine our TaskService in serial or parallel as we like, thanks to the Cats API. For that, let’s show you below how we use Applicative instances with Cats operators for parallelism through the Task non-deterministic instance.

Jobs for combining UI Actions and Business

At the beginning of the project, we worked on a few approaches to producing a functional style App layer. We began to see common patterns that our Android Dev friends were using to translate to Scala, but it was wrong. Finally, we realized that we needed to use the same type for combining the UI Action and business logic in our process layer, so we created Jobs.

We are using the Macroid library for working in the user interface to do things like add information to RecyclerView, animate views, etc. All of these UI transformations are created using Macroid, transforming to our TaskService type, and finally, combining with our process.

The Jobs are created in the companion object of every Activity or Fragment, and the methods contain instructions that we call from our activities or views.

We’re going to show you a simple example of loading widgets in the UI. The method of the Jobs could be:

  def loadWidgets(): TaskService[Unit] =
    for {
      _ <- actions.showLoading()
      widgets <- di.deviceProcess.getWidgets
      _ <- actions.loadWidgets(widgets)
    } yield ()

In the for comprehension, we are combining a common process for showing information in a RecyclerView sequentially. We can run the async task the following way:

Task.fork(job.loadWidgets().value).runAsync()

We’ve included some new methods in our type to help us resolve the async tasks:

job.loadWidgets().resolveAsyncServiceOr(_ => showErrorLoadingWidgets())

The loadWidgets() method is our happy path, and it’s our responsibility to manage the possible problems that can arise during the call. For that, we use another method in the Job for showing the error in the screen.

Finally, we can also compose the methods of different jobs using Applicative in Cats to do parallels calls:

import cats.implicits._
(widgetsJobs.hostWidget(widget) *> widgetsDialogJobs.close()).resolveAsync()

Conclusions

Creating a launcher in Scala was an excellent experience, especially considering we’re a company that uses Scala every day. During this project, the backend team members assisted the client team and vice versa. This teamwork was essential to the development of 9 Cards.

You can find out more at 9Cards.io and comment or contribute on the 9 Cards GitHub and 9 Cards Backend repo or join in on the conversation at the 9 Cards Gitter channel.

blog comments powered by Disqus

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.