@ExperimentalCorroutinesApi

Realtime Stockwatcher with an MVI pattern, LiveData & a “Serverless” RESTful API

A "Realtime" StateFlow Stockwatcher UI with a "Serverless" RESTful API Developed for the purpose of this Stockwatcher, hosted on the Google Cloud Platform to simulate realtime stock market prices using Retrofit as a REST client.

Written by Marc Farssac on April 24th 2021

MVI Compose Stockwatcher with own realtime RESTful API

This is the first of three articles around a realtime Stockwatcher, implementing reasonable alternative approaches, analysing their results with different tools, methods and techniques and finally extracting conclusions and lessons learnt. Also I will keep working with Jetpack compose and the MVI-Architecture reactive pattern (as explained in this blog post), experimental API and use the repository pattern to access a data source from an own developed "Serverless" RESTful API. This simulates realtime Stock Market values. The project is architected around Clean Code. Last, making use of the Profiler, logs, different API version and Android Studio built in tools I will understand network and App performance that may a source of improvement. This is an ongoing project and the content of this article might be updated at a latter time.

Objectives

  • Write a Clean Code Architecture project

  • Use Google Architecture components with a MVI

  • Use Jetpack compose with inmutable UI

  • Use a repository pattern to encapsulate data access

  • Develop a RESTful API

  • Use corroutines & suspend functions

  • Use logs to prove performance & assumptions

  • Work with LiveData & Flow and their use cases

  • Use Profiler to prove memory & network performance

  • Compare bytecode of two development alternatives

  • Benchmark backends located in Europe & the USA

  • Compare REST API performance, advantages & costs.

  • Test the App, the backend & the corroutines in a synchronous way.


  • Done -> Explained in this article

  • On going -> Detailed in a coming article

  • Pending -> Dealt in a coming article

Architecture & Clean Code

A good architecture allows better testability and maintability. In this project, code will be as clean as possible and although the architecture may not follow the most academic structure, this one allows this project in the most practical way. As we will see, this architecture allowed me to change the RESTful API without impacting the rest of the App but it also would allow to migrate from this MVI to a MVVM in a smooth maner.

The project Presentation layer (interacting with the UI) is where the Activity, Composables & theme and screens are found).

Composables of the UI get their state updated with every data base observed change at the time that send event to the upper layer to add new stocks to watch to the list. More information about this UI pattern can be found in this article I wrote a few days ago.

State propagates from top to bottom, events from bottom to top

Unidirectional data flow

State propagates from top to bottom, events from bottom to top

The Use Case layer handles the purpose of the project. In our use case we get data from the repository (pattern) -only-, but if for example we wanted to show a notification informing the user that within 50m there is a bank to purchase stocks, we would modify this layer or use case.

In the domain we have our business model and is were the business logic happens. Since we don't have business logic (we read data and display it), our domain layer is almost anectodotic.

The Data layer implemented with a Repository pattern is the one able to decide where to find the information and centralizing data access in one place (the StockRepository class) that persists objects, decoupling the business logic (domain layer) and the data access layers (this one). By using it we are hidding how the data is eventually stored or retrieved to and from the data store (see graph 2 below) which depends on the persisted data model and a remote backend data source.

More over, both are injected to the repository model, meaning that it implements an interface which in turns isolates it from the implementation of our local Data Access Object and the Network Service accessing our RESTful API.

Repository pattern

Using a repository we hide how the data is stored and retrieved, which depends on a the persisted data model and a remote backend data source.

In general terms we could say that each layer depends only on the layer one level below. But because we are using the MVI

Testing the backend & its behaviour from the Android client

Junit Tests are on both ends. One one side on the backend, to test the functionality of the API and on the other, on the Android client, to test that the API works as expected from the mobile App.It is not uncommon that different teams work in different parts of the project and that changes in the backend could "break" the client.

API Junit Test

Mobile App Backend Test

Three IDE's: IntelliJ & Android Studio

  • IntelliJ: Development of the backend

  • Android Studio 4.1.2 - Junit4 Tests in the Corroutines Scope

  • Android Studio Arctic Fox 2020.3.1 Canary 15 - JetPack Compose & Instrumental Tests

LiveData, a lifeCycle aware Data holder

The repository stores backend data to the local database which when observed by the UI through the ViewModel updates the Stockwatcher list and their values. Every time after a user inputs a new stock, this generates and event that goes up to the ViewModel that through the useCase requests it to the repository. If this stock is not in our database (it will not be) this is read from the backend and stored locally. This in turns results in stored LiveData being observed by the UI through the ViewModel (and actually, updating the UI). Other than the user inputing a new value, the app uses the Network service to get it from the backend API and update the values of the stocks in the local database and also being displayed.

One way to continuously update the stocks values has been using a 2000ms fixedRateTimer in the ViewModel that has accessed the repository for each of the stock symbols in our database. Every time a new symbol was entered through the OutlinedTestField (input box), this was stored locally, its value observed as a State from the MainScreen (let's say, the old xml layout set in the content) and this state, beeing a parameter of the UI @Composables, triggereing an update of the UI. In the timer I have used the list of Stocks stored in the model database to get their values from the Remote Data Source (the RESTful API), update the model and let this update propagate again to UI through the observation of the state of the model LiveData as a StateFlow.

Another way to achieve the same has been done launching a suspended function in the viewmodel scope, which has looped forever.

In a coming article I will look at the bytecode of boths solutions using the Compiled Code with the Decompiler.

As expected, reducing the delay and/or the timer duration to a few milliseconds (10ms) resuts in the UI becoming irresponsive, not being able neither to add new stocks to the list neither nor to remove them.

One last thing worth mentioning is the fact that removing stocks from our local database doesn't propagate fast enough to the viewmodel LiveData when we are updating stock values from the backend. In other words, if I remove a stock while I already called the backend to read its new value, this stock will be stored again in the database and its value propagated again to the UI. Some handling had to be done to ensure that a removed stock was not saved again in the local database except of course that the user inputed it again from the UI input dialog.

Stock price

The stock price has been calculated by addind the ASCII code of each of the symbol characters in module 100. The code belongs to the Google Cloud Function deployed as a RESTful API.

Stock price oscillations

Stock price variation has been calculated using the time in milliseconds to produce a variation of up to +/- 0.5 the original stock price.

Symbol price variations (+/- 0.5)

StockWatcher screenshoot

Coroutines

A coroutine is a light weight thread, like threads can run in parallel, wait for each other and communicate. They execute in some context. In particular we are refreshing the stock values in the viewModel scope, a scope that will cancel automatically all corroutines runing when the ViewModel would be deleted.

.asLiveData()

If our tasks are synchronous we have to use Squence and yield(...) a value to the iterator being built and suspend until the next value is requested. In suspend() functions whe have to use Flow instead and emit(..) the result of some processing. Sometimes, if we want our function to return LiveData() to make it easy for observing from the UI we use the liveData{} builder or we can transform the flow with a map transformation and use the extension .asLiveData() to collect Flow values and emit them to the UI.

.asFlow()

To return multiple asynchronously computed values we use Kotlin Flows. Creates a cold flow that products a single value from the given functional type. Flows are cold streams similar to sequences - the code inside the builder does not run until the flow is collected with the collect() function (without parameters).

map() and flatmap() API's

These are extensions for collection transformations. We use them to transform flows() into liveData{}.

Comercial stock price APIs

At first I created a Free accounts and used both Alpha Vantage API and the Yahoo Finance API but they have many limitations on the free version. A Mocked API created with the Mock API was good to test retrofit but could not be customized to achieve the objectives of this project.

Suspending functions & Retrofit

Suspending functions are at the core of corroutines. They are functions that can be paused and resumed and can execute a long runing operation and wait for it to complete without blocking, in particular, the main UI thread. They run in a corroutine, which can be understood as a lightweight thread; a thread without all the overhead of "former threads" meant to work concurrently - in the best case - in a multiprocessor computer.

In this project we are using suspending functions to write and delete to the local database (in the Data Access Object) and also to get stock prices from the backend API.

Watching the logs I could see that backend readings were happening with a few (less than 10ms) thus pulling the stocks and getting them from a suspend function (Retrofit takes care of it) is working well.

Own Google Cloud Functions Endpoint in central US

To Mock a stock price API I created my own REST API using Cloud Functions, a Google Cloud event-driven, serverless compute platform, which provides automatic scaling, high availability, and fault tolerance with no servers to provision, manage, update or patch.

I created one simple endpoint without authentication for the purpose of this Jetpack compose, corroutines, LiveData and Flow project with a $2 montly budget to avoid unexpected costs. If you give it a try and it is not working, bear it in mind.

REST API Response

The response payload is JSON-encoded and can be reached from https://us-central1-stockwatcher-mfb-cat.cloudfunctions.net/quote?symbol=MFB. with a @Query parameter in the endpoint.

Conclusions

In this first part of the project we have seen an App clean architecture and layers using JetPack Compose on the UI, recieving updates from the upper layers and sending events upstream without having to update the UI as state changes trigger UI updates.

We have also deployed an own REStul API as a Google Cloud function and validated its development process, performance and costs.

We have also seen that stocks removed from the local store could be stored again if we removed them while a call to the remote data source was made.

Last we could measure the time needed to access an own API located in the USA from Europe, see the costs associated with this call and the availability / scalability of this Service from the Google PaaS.


Written by Marc Farssac on April 24th 2021

Senior Android
Architect & Developer

Get in touch

Marc Farssac

Passeig Maragall 350, Baixos
08031 Barcelona
Catalonia, Spain

E-Mail: This email address is being protected from spambots. You need JavaScript enabled to view it.
Mobile: +34 644 764 764
Landline: +34 93 460 86 39

© Marc Farssac. All rights reserved.
Legacy site https://former.mfb.cat.