Skip to Content
Technical Articles

Visualizing COVID-19 Data using SAP Fiori Chart Cards on Android

The SAP Cloud Platform SDK for Android includes a Fiori UI library that provides a collection of user interface components.  These components allow developers to easily create applications that follow Android Material Design as well as the SAP Fiori for Android Design Guidelines. In release 3.0 of this SDK, a Chart Cards component was introduced that provides a ChartCardView: a composite UI component that displays a list of Android CardView instances, each of which contains a chart plot as well as related metadata.

Chart%20Cards%20Component%20Demo%20on%20Tablet

Chart Cards View on Phone

Chart%20Cards%20View%20on%20Tablet

Chart Cards View on Tablet

As I worked with some of the initial consumers of the SAP Cloud Platform SDK for Android, I realized that the complexity of the ChartCardView warranted a sample application to demonstrate how it can be utilized in a real-world application for data visualization.

The time of developing this sample application coincided with the COVID-19 pandemic outbreak in July 2020. It occurred to me that we could use a COVID data tracking API service to provide the data source, which could give the sample app a more realistic “look and feel”, not only for end-users of the application, but also for application developers. The sample app was written fully in Kotlin and it followed the latest Android Jetpack guidelines, including its architecture components, data binding, and navigation. The details will briefly be explained in the latter part of this article if you are interested in the technical aspects.

The Chart Cards Sample Application with COVID-19 Data Tracking

The application makes HTTP requests to the COVID Tracking APIs to retrieve US Daily and States Current COVID-19 data.

  • US Daily COVID-19 Data:
 https://api.covidtracking.com/v1/us/daily.json
[
    {
        "date": 20201006,
        "states": 56,
        "positive": 7460634,
        "negative": 97932855,
        "pending": 8680,
        "hospitalizedCurrently": 31346,
        "hospitalizedCumulative": 414461,
        "inIcuCurrently": 6438,
        "inIcuCumulative": 20973,
        "onVentilatorCurrently": 1609,
        "onVentilatorCumulative": 2388,
        "recovered": 2952390,
        "dateChecked": "2020-10-06T00:00:00Z",
        "death": 202675,
        "hospitalized": 414461,
        "totalTestResults": 110226302,
        "lastModified": "2020-10-06T00:00:00Z",
        "total": 0,
        "posNeg": 0,
        "deathIncrease": 634,
        "hospitalizedIncrease": -624,
        "negativeIncrease": 722475,
        "positiveIncrease": 38661,
        "totalTestResultsIncrease": 823419,
        "hash": "521257bd43a9127505142433642b7987cd42eb37"
    },
    ...
]
  • States Current COVID-19 Data
 https://api.covidtracking.com/v1/states/current.json
[
    {
        "date": 20201007,
        "state": "AK",
        "positive": 9861,
        "probableCases": null,
        "negative": 480213,
        "pending": null,
        "totalTestResults": 490074,
        "hospitalizedCurrently": 46,
        "hospitalizedCumulative": null,
        "inIcuCurrently": null,
        "inIcuCumulative": null,
        "onVentilatorCurrently": 6,
        "onVentilatorCumulative": null,
        "recovered": 5626,
        "dataQualityGrade": "A",
        "lastUpdateEt": "10/7/2020 03:59",
        "dateModified": "2020-10-07T03:59:00Z",
        "checkTimeEt": "10/06 23:59",
        "death": 59,
        "hospitalized": null,
        "dateChecked": "2020-10-07T03:59:00Z",
        "totalTestsViral": 490074,
        "positiveTestsViral": 9099,
        "negativeTestsViral": 480682,
        "positiveCasesViral": 9861,
        "deathConfirmed": 59,
        "deathProbable": null,
        "totalTestEncountersViral": null,
        "totalTestsPeopleViral": null,
        "totalTestsAntibody": null,
        "positiveTestsAntibody": null,
        "negativeTestsAntibody": null,
        "totalTestsPeopleAntibody": null,
        "positiveTestsPeopleAntibody": null,
        "negativeTestsPeopleAntibody": null,
        "totalTestsPeopleAntigen": null,
        "positiveTestsPeopleAntigen": null,
        "totalTestsAntigen": null,
        "positiveTestsAntigen": null,
        "fips": "02",
        "positiveIncrease": 274,
        "negativeIncrease": 10426,
        "total": 490074,
        "totalTestResultsSource": "posNeg",
        "totalTestResultsIncrease": 10700,
        "posNeg": 490074,
        "deathIncrease": 1,
        "hospitalizedIncrease": 0,
        "hash": "1a883e2869ef7b3a3c422abb9a41a55364b84ae5",
        "commercialScore": 0,
        "negativeRegularScore": 0,
        "negativeScore": 0,
        "positiveScore": 0,
        "score": 0,
        "grade": ""
    },
    ...
]

The JSON data is transformed into ChartCardDataModel objects which are displayed in the Chart Cards view from the SAP Fiori UI library. In case the network service is unavailable on the Android device, an offline mode allows retrieval of COVID-19 data from locally pre-downloaded JSON documents. The user interface of this application appears as follows:

US Daily COVID Data in Chart Cards View (Base Card mode)

US%20Daily%20COVID%20Data%20on%20Tablet%20%28Base%20Card%20mode%29

US Daily COVID Data on Tablet (Base Card mode)

 

States Current COVID Data in Chart Cards View (Scrollable Card mode)

States%20Current%20COVID%20Data%20on%20Tablet%20%28Scrollable%20Card%20mode%29

States Current COVID Data on Tablet (Scrollable Card mode)

 

The Chart Cards view supports two different display modes out of the box:

  1. Base Card: orders cards vertically, stacked on top of each other. It is the default display mode if not explicitly set.
  2. Scrollable Card: places cards in a horizontally scrollable collection view.

This sample app demonstrates the Base Card mode for the US Daily COVID Data:

        <com.sap.cloud.mobile.fiori.chartcard.ChartCardView
            android:id="@+id/chartCardView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layoutStyle="baseCard"/>

and the Scrollable Card mode for the States Current COVID Data:

            <com.sap.cloud.mobile.fiori.chartcard.ChartCardView
                android:id="@+id/chartCardViewScrollable"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layoutStyle="scrollableCard"/>

By clicking on one of the Chart Cards, you will be directed to a Card Details view which displays the chart plot in the corresponding Fiori Chart view. For example, a Fiori Line Chart view:

Chart%20Details%20in%20Fiori%20Line%20Chart%20View

Chart Details in Fiori Line Chart View

And another example shows the card details in a Fiori Horizontal Bar Chart view:

Chart%20Details%20in%20Fiori%20Horizontal%20Bar%20Chart%20View

Chart Details in Fiori Horizontal Bar Chart View

Prerequisites

In order to run the sample application on your own development environment after downloading it from the GitHub repository, you will also need to meet the following requirements:

The blog Step by Step with the SAP Cloud Platform SDK for Android contains additional details on how to set up and install the SDK.

There is a dedicated section in the Fiori for Android Design Guidelines which explains the Chart and Chart Card components.

SAP Cloud Platform Mobile Services Documentation provides more technical details on how to use the Chart Cards component in an Android application.

Run the Application

Once you have downloaded this application and installed the SAP Cloud Platform SDK for Android locally on your development machine, open the project in Android Studio.

Select the Downloaded Project to Open

Let the build.gradle sync….

You might need to change the sdkVersion inside the gradle.properties file to match the SDK version you just installed locally, in order to successfully build the application.

# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

sdkVersion=3.1.3

Once successfully built, you may now run the application on an Android emulator or device (running Android API 26 or higher).

The sample app supports both live network and offline modes that can be toggled in the Settings by clicking on the settings_icon icon.

 

The Source Code

This is the more technical part of this article which is written particularly for developers who want to learn a bit more about the implementation details of the sample application.

As mentioned earlier, the sample application incorporates some of the latest Android Jetpack development components, including Architecture Components and Navigation component. That’s why in the project-level build.gradle, you will find the google() repository was added:

allprojects {
    repositories {
        google()
        jcenter()
    }
}

Architecture Components

Following the guide to app architecture by Jetpack, the application consists of modules depicted in this diagram:

Guide%20to%20app%20architecture

App Architecture

 

Source%20Code%20Structure

Source Code Structure

  • Repository: retrieves data from either live APIs or locally stored JSON documents and stores the data into the Room database.
  • View Models: refreshes data from the Repository and composes the data needed to display in the Chart Cards or Chart on the UI.
  • Data Binding: uses LiveData to notify the UI to update its views when underlying data has changed.

The Fragment and Activity code becomes much more compact due to shifting the responsibility of acquiring data to View Models and the Observable nature of LiveData in Data Binding. Because of the lifecycle awareness of ViewModels, the UI data also correctly handles orientation changes. The onCreateView function of BaseCardsFragment appears as follows:

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        setHasOptionsMenu(true)
        binding = FragmentBaseCardsBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.chartCardsViewModel = viewModel
        mChartCardView = binding.root.findViewById(R.id.chartCardView)

        viewModel.covidUsChartCards.observe(viewLifecycleOwner,
            Observer<MutableList<ChartCardDataModel>> {
                if (it.isNotEmpty())
                    setChartCardView(it)
            })

        viewModel.xLabels4UsData.observe(viewLifecycleOwner,
            Observer { if (it.isNotEmpty()) origXLabels = it})

        // Observer for the network error.
        viewModel.eventNetworkError.observe(viewLifecycleOwner, Observer<Boolean> { isNetworkError ->
            if (isNetworkError) onNetworkError()
        })

        return binding.root
    }

Navigation

This sample application implements its entire navigation using the Jetpack Navigation component, including:

  • Navigation graph
  • NavHost
  • NavController

The navigation graph of this app defining the destinations and actions appears as follows:

<?xml version="1.0" encoding="utf-8"?>

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@id/home_view_pager_fragment">

    <fragment
        android:id="@+id/home_view_pager_fragment"
        android:name="com.example.fiorichartcardsapp.HomeViewPagerFragment"
        tools:layout="@layout/fragment_home_view_pager">

       <action
            android:id="@+id/action_view_pager_fragment_to_card_detail_fragment"
            app:destination="@id/card_details_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>

    <fragment
        android:id="@+id/card_details_fragment"
        android:name="com.example.fiorichartcardsapp.CardDetailsFragment"
        android:label="@string/card_details_title"
        tools:layout="@layout/fragment_card_details">
        <argument
            android:name="chartViewData"
            app:argType="com.example.fiorichartcardsapp.domain.DetailChartData"
            />
    </fragment>

    <fragment
        android:id="@+id/settings_dest"
        android:name="com.example.fiorichartcardsapp.SettingsFragment"
        android:label="@string/settings"
        tools:layout="@layout/fragment_settings" />
    <activity
        android:id="@+id/settingsActivity"
        android:name="com.example.fiorichartcardsapp.SettingsActivity"
        android:label="SettingsActivity" />
</navigation>

Throughout the application, the fragment transactions, the Up/Back actions, and the passing of data between fragments are all handled by the navigation component without explicitly writing code in the activity/fragment classes. For example, in the onClick handler of the BaseCardsFragment, a DetailChartData object is prepared and passed to the subsequent CardDetailsFragment as a Safe Args by using the NavController:

   private fun setChartCardView(usChartCards: MutableList<ChartCardDataModel>) {
        val layoutType = binding.chartCardView.layoutType
        (binding.chartCardView).setViewLayoutManager()
        val chartCardViewAdapter = this.activity?.let {
            ChartCardViewAdapter(it, usChartCards, layoutType)
        }
        (binding.chartCardView).setViewAdapter(chartCardViewAdapter)

        mChartCardView?.setItemClickListener(object : FioriItemClickListener {
            override fun onClick(view: View, position: Int) {
                val card = usChartCards[position]
                val chartData = DetailChartData(
                    card.plotType, card.chartCardTitle!!, card.chartCardTimestamp!!,
                    origXLabels, card.plotDataSet!!)
                val action = HomeViewPagerFragmentDirections.actionViewPagerFragmentToCardDetailFragment(chartData)
                findNavController().navigate(action)
            }
        })
    }

Unit Tests and Android Tests

Another benefit of using Architecture Components in the sample application is the ease of writing modulized tests.

Since the layers of the application architecture are well defined and separated, each layer below the UI can be tested separately via unit tests. Kotlin Coroutines also provides more flexibility in testing due to the configurable coroutine scopes (i.e. structured concurrency). To test asynchronous functionality in an app, you could define a TestCoroutineScope for the main coroutine dispatcher to run the coroutine functions.

Due to the separation of data and its presentation, the Android tests also become easier to implement and maintain by implementing test fakes for the data model and the repository.

For more questions or comments…

If you have questions or comments on this sample app (or, better yet, suggestions for improvements or new functionality), please don’t hesitate to leave a comment below.

References

1 Comment
You must be Logged on to comment or reply to a post.