/Covid19Tracker

Covid19 Tracker is a sample Android application focused on displaying statistics using graphs and adding a lot of tests. From a technical point of view, it has an Offline-First approach and uses the Single Source of Truth principle. Also, it has been built making use of a huge database and Flow streams with the aim of achieving the best performance.

Primary LanguageKotlinOtherNOASSERTION

Covid19 Tracker


Introduction

Covid19 Tracker is a sample Android application focused on displaying statistics using graphs and adding tests, tests and more tests (> 1.000). From a technical point of view, it has an Offline-First approach and uses the Single Source of Truth (SSOT) principle. Also, it has been built making use of a huge database and Flow streams with the aim of achieving the best performance. However, it is important to point out that certain technical decisions have been made only for me to have an opportunity to practice new Android concepts.

You can download the app here.

Technical summary

  • Offline-First: The offline-first apps, while still requiring a connection to the servers, don't need a constant internet connection. The data from servers is downloaded to the user's device and can still be accessed offline.
  • Single Source of Truth (SSOT): It is the practice of structuring information models and associated schemata such that every data element is stored exactly once. You can have an offline app and be sure your data always use one source and that is your database.
  • Model-View-ViewModel (MVVM): It is a software architectural pattern that facilitates the separation of the development of the graphical user interface (without using DataBinding). Also, there are Screen States to handle the different states in the UI.
  • Android Architecture Components: Collection of libraries that help you design robust, testable, and maintainable apps.
    • LiveData: Data objects that notify views when the underlying database changes.
    • ViewModel: Stores UI-related data that isn't destroyed on UI changes.
    • ViewBinding: Generates a binding class for each XML layout file present in that module and allows you to more easily write code that interacts with views.
    • Room: The library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite.
      • DatabaseView: This annotation allows you to encapsulate a query into a class. Room refers to these query-backed classes as views, and they behave the same as simple data objects when used in a DAO.
    • WorkManager: The WorkManager API makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or device restarts.
    • Navigation: This component helps you implement navigation.
    • Custom classes:
      • QueueLiveData: This custom LiveData class will deliver values even when they are posted very quickly one after another. It solves the issue of losing values when several new ones are posted very quickly.
      • KeepStateNavigator: This class is to keep state in fragment navigator.
  • Flow: A cold asynchronous data stream that sequentially emits values and completes normally or with an exception.
    • zip: Zips values from the current flow (this) with other flow using provided transform function applied to each pair of values. The resulting flow completes as soon as one of the flows completes and cancel is called on the remaining flow.
    • combine: Returns a Flow whose values are generated with transform function by combining the most recently emitted values by each flow.
    • flatMapMerge: Transforms elements emitted by the original flow by applying transform, that returns another flow, and then merging and flattening these flows. This operator calls transform sequentially and then merges the resulting flows with a concurrency limit on the number of concurrently collected flows.
  • StateFlow: A SharedFlow that represents a read-only state with a single updatable data value that emits updates to the value to its collectors. A state flow is a hot flow because its active instance exists independently of the presence of collectors.
  • Testing: Unit, instrumentation and UI tests have been implemented. There are more than 1.000 tests using different frameworks, libraries, patterns, techniques, etc.
    • Mockito-Kotlin: A small library that provides helper functions to work with Mockito in Kotlin.
    • Mockk: Provides DSL to mock behavior. Built from zero to fit Kotlin language. Supports named parameters, object mocks, coroutines and extension function mocking.
    • Espresso: Writing concise, beautiful, and reliable Android UI tests.
    • Screen Robot Pattern: This pattern fits with Espresso and allows to create clear and understandable tests.
    • Kakao: Nice and simple DSL for Espresso in Kotlin.
    • Barista: Barista makes developing UI test faster, easier and more predictable. Built on top of Espresso, it provides a simple and discoverable API, removing most of the boilerplate and verbosity of common Espresso tasks.
    • Robolectric: Robolectric lets you run your tests on your workstation, or on your continuous integration environment in a regular JVM, without an emulator. Running tests on an Android emulator or device is slow!
    • Kotest: Kotest is a flexible and comprehensive testing tool for Kotlin with multiplatform support.
    • Turbine: Turbine is a small testing library for kotlinx.coroutines flow.
    • Flow-Test-Observer: Library inspired by TestSubscriber from RxJava. Works with both cold/finite and hot/infinite flow.
    • MockWebServer: A scriptable web server for testing HTTP clients.
    • Google Truth: Truth makes your test assertions and failure messages more readable.
  • Arrow: It is a library for Typed Functional Programming in Kotlin.
    • Either: Represents the presence of either a Left value or a Right value. By convention, most functional programming libraries choose Left as the exceptional case and Right as the success value.
    • IO: It is used to represent operations that can be executed lazily, and are capable of failing, generally with exceptions. This means that code wrapped inside IO will not throw exceptions until it is run, and those exceptions can be captured inside IO for the user to check. In this project, it has only been used in the UpdateDatabaseWorker worker to build concurrent API calls.
  • Koin: Dependency Injection Framework (Kotlin)
  • Moshi & Moshi Converter: A modern JSON library for Kotlin and Java. The converter uses Moshi for serialization to and from JSON.
  • Detekt: A static code analysis tool for the Kotlin programming language. It operates on the abstract syntax tree provided by the Kotlin compiler.
  • Kotlin Gradle DSL: Gradle's Kotlin DSL provides an alternative syntax to the traditional Groovy DSL with an enhanced editing experience in supported IDEs, with superior content assist, refactoring, documentation, and more.
  • Remal check dependency update: Plugin that provides task for discovering dependency updates.
  • GitHub Actions: Automate, customize, and execute your software development workflows right in your repository. Discover, create, and share actions to perform any job, including CI/CD, and combine actions in a completely customized workflow.

Screens

Country

List

Bar Charts

Line Charts

Pie Charts

Countries & Regions

World

List

Bar Charts

Line Charts

Pie Charts

Others

GIF

Simplified UML Database


Database rows from 2020/01/23 until 2021/02/13

  • country: 194
  • region: 418
  • sub_region: 3.208
  • world_stats: 388
  • country_stats: 74.299
  • region_stats: 158.573
  • sub_region_stats: 1.244.704

Initialize Database

There are three ways to initialize the local database:

  • By default, using the zip file: This file is in the assets folder and its name is covid19-tracker-db.zip. In the MainActivity class, the fileUtils.initDatabase() method unzips this file. After that, the Covid19TrackerDatabase class loads the unzipped file in the .createFromFile(File("${context.filesDir}${File.separator}$DATABASE_NAME")) method.

  • Unzipped file: You need to add this file in the assets folder with the name covid19-tracker-db. In the Covid19TrackerDatabase class you need to replace the line .createFromFile(File("${context.filesDir}${File.separator}$DATABASE_NAME")) with this one .createFromAsset(DATABASE_NAME). Also, in the MainActivity class you need to remove the fileUtils.initDatabase() method.

  • Adding jsons manually: You can manually add the jsons downloaded from https://api.covid19tracking.narrativa.com/api/YYYY-MM-DD. You need to save these files with this format YYYY-MM-DD.json in the assets/data folder. In the Covid19TrackerDatabase class you need to remove the line .createFromFile(File("${context.filesDir}${File.separator}$DATABASE_NAME")) and .createFromAsset(DATABASE_NAME) and add this piece of code:

    .addCallback(object : RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            val request = OneTimeWorkRequestBuilder<PopulateDatabaseWorker>().build()
            WorkManager.getInstance(context).enqueue(request)
        }
    })

    Also, remove the fileUtils.initDatabase() method from MainActivity. The PopulateDatabaseWorker worker is in charge of creating and populating the database. You can choose a date range using the variables START_DATE and END_DATE. I recommend using the emulator to generate the database. After that, in the internal folder data/data /com.jaimegc.covid19tracker/databases you can export the covid19-tracker-db file and zip it it in order for it to be loaded following the first of the three methods explained in this section.

The data for any other day, from the last one in the local database until the current one, will be downloaded automatically using the UpdateDatabaseWorker worker. The data will be updated every 6 hours.

⚠️ WARNING: ⚠️ The data provided and used for the generation of these products comes from the aggregation of different sources, each of which with different update times and frequencies. Additionally, each country has its own accounting criteria, so comparisons of data between countries or regions, and even within them over time, may not be representative of reality. An example is the case of positive cases that depend not only on the spread of the disease but also on the number of tests that are carried out.

Tests, tests and more tests

The tests are a mix of different frameworks, libraries, patterns, techniques, etc. You can see the same tests implemented in a variety of ways.

Unit Tests (712 ✅)

Integration Tests (122 ✅)

UI Tests (259 ✅)

GitHub Actions

You can see the config file here.

Gradle tasks

  • ./gradlew detekt: Code analysis. More information here.
  • ./gradlew checkDependencyUpdates: Check dependency updates.

Credits

Special thanks

Thanks

Contribute

If you want to contribute to this app, you're always welcome! See Contributing Guidelines.

You can improve the code, adding screenshot tests, themes, compose, modularization, etc.

Author

Jaime GC

License

Copyright 2020 Jaime GC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.