At ruumi we support farmers with rotational grazing by providing actionable insights based on state of the art tech, satellites, and machine learning.

We are listening to our users, and the need for an offline-enabled native mobile app is a recurring topic. That’s why we are building an app ready to use on the ground and out in the fields to complement our existing web app.

In the following we’ll outline design decisions we are making and give you an overview over modern Android app design and architecture. We’ll then continue with showing you how to adapt an imperative map to a declarative user interface.

To skip ahead and have a look at the declarative map wrapper, see this gist.

Modern Android App Architecture

Building an offline-enabled app for our farmers, we scoped it out to

  • fetch and upload entities like fields from and to our backend, respectively
  • cache reads and writes, to support farmers in the field with limited connectivity
  • follow modern mobile development with Kotlin, Jetpack, and Compose

The following shows the high-level MVVM (model / view / view model) architecture

        ┌────────────┐
        │ View       │
        │            │
        └──────┬─────┘
               │
        ┌──────▼─────┐
        │ View model │
        │            │
        └──────┬─────┘
               │
        ┌──────▼─────┐
        │ Repository │
    ┌───┤            ├───┐
    │   └────────────┘   │
    │                    │
┌───▼───┐            ┌───▼─────┐
│ Model │            │ Service │
│       │            │         │
└───────┘            └─────────┘

where

  • the view is responsible for displaying and styling data: the declarative user interface with composables (components) reacting to state changes
  • the view model manages and transforms all data required in the view across the app’s lifecycle, e.g. across screen rotations and suspend events
  • the repository acts as a data abstraction layer on top of the offline cache and the network service that is our backend, depending on connectivity
  • the model persists data in a local sqlite database to cache reads and writes on device, to reduce network requests and to make the app work without connectivity
  • the service interacts with our backend by means of sending and receiving entities via HTTP and converting entities from and to JSON, respectively

The architecture provides clean abstractions, separation of concerns, and allows our users to seamlessly move from a Wi-Fi network out to their fields where connectivity is limited or non-existent.

Imperative, Meet Declarative

For the view we are building declarative user interfaces with Compose, reacting to state changes from the view model (e.g. new fields in the database).

Here is an example composable (component) re-rendering when the name changes and propagating the login event up the hieararchy of composables to act upon

@Composable
fun LoginCard(name: String, onLogin: () -> Unit) {
  Column {
    Text("Welcome $name")
    LoginForm(onLogin)
  }
}

Declarative user interfaces allow us to align the mental model between Compose on Android and our web app written in Typescript/React/Redux, making it easy for frontend developers to jump into mobile.

One challenge we were facing was integrating a map view to allow users to see their fields and our insights from a bird’s eye view and smoothly interact with it.

Map frameworks come from the imperative world

map.jumpTo(location)
map.setCamera(camera)

In contrast, we want to adapt the imperative map to a declarative pattern, so that

  • the map is a stateless component, where its appearance is only based on the properties passed in from parent composables (components), and
  • the map’s properties are the single source of truth throughout the application; this allows us to align the location-based user experience across screens

We ended up writing a declarative map wrapper; in Compose we then use it like

val (viewport, setViewport) = remember { mutableStateOf(initialViewport) }

Map(viewport = viewport, onViewportChange = setViewport)

where

  • the viewport is the single source of truth, controlling the map’s appearance, and
  • events from user interaction move up the composable (component) hierarchy

With the declarative map in place, we can have the map (in the view) automatically react to viewport changes (in the view model). And with Kotlin’s coroutines and flows, we can hook up the viewport to database changes to e.g. set the initial viewport to the bounding box of all fields.

Declarative Map Starter

We provide the declarative map starter in

Instead of creating a declarative map wrapper similar to react-map-gl, we’d love to see upstream support. Follow upstream for tracking progress on the Mapbox Map.

About The Author

Daniel works on all things tech at ruumi. In the last weeks he has been focusing on working on the foundations of our mobile app for Android.

Want to leave feedback? Reach out at hello@ruumi.io ❤️