← Back to list

Using DataStore for Local Reminder State Instead of Room in Android

How to decide when a simple DataStore-backed state layer is enough for an Android feature and when a full Room database would only add compl

Raylabs · 2026-04-19 05:31 · 36 claps · 4.2 min read paywalled
#android #kotlin #data-stores #room #architecture
Open on Medium ↗
Wiki topics: 📱 · Mobile Development 🏛️ · Architecture

Using DataStore for Local Reminder State Instead of Room in Android

Not every local feature in Android needs a database, even if Room is already sitting there as the obvious answer.

That sounds obvious, but it is surprisingly easy to forget once Room becomes the default persistence reflex. A feature needs local state, so the team reaches for a database before asking whether the feature actually behaves like data that wants a database.

For a reminder workflow, I needed local persistence for:

  • simple settings
  • a few timestamps
  • per-item local actions like reviewed, snoozed, or dismissed

What I did not need was relational querying, joins, indexed table access, or a schema that was trying to grow into a local source of truth.

That is why I picked DataStore.

Start with the actual shape of the data

This decision gets much easier when you describe the real state first and the technology second.

The reminder feature needed two kinds of local persistence:

Small user-scoped settings

  • reminder enabled
  • daily notifications enabled
  • preferred notification hour and minute

Small per-order local state

  • checked timestamp
  • assumed picked-up timestamp
  • dismissed timestamp
  • snoozed-until timestamp

That is not relational data.

It is not query-heavy.

It is not the kind of state that needs Room to become understandable.

It is just local workflow state with a small footprint.

Why Room would have been heavier than necessary

Room is great when the feature actually benefits from:

  • entities and relations
  • indexed queries
  • list filtering at the storage level
  • observable table data
  • migration history that maps to durable structured records

But if the feature only needs a small settings blob and a keyed set of local reminder actions, Room adds a lot of ceremony very quickly:

  • entities
  • DAO interfaces
  • database setup
  • schema considerations
  • testing surface area that is larger than the feature actually requires

That does not make Room bad. It just means the cost should match the problem.

Here, it did not.

The right persistence choice becomes clearer when you compare it against the real behavior the feature needs.

The right persistence choice becomes clearer when you compare it against the real behavior the feature needs.

What DataStore handled well

DataStore fit this problem well for a few practical reasons.

1. The state was local-only

The reminder state was intentionally local-only.

That is an important distinction. The feature did not need to sync “reviewed,” “dismissed,” or “snoozed” actions back into shared order data automatically.

Once you accept that boundary, a lightweight local store becomes much easier to justify.

2. The data shape was small

The settings were tiny, and the per-item reminder state was basically a keyed collection of flags and timestamps.

That kind of state is easy to serialize, restore, and reason about.

3. The feature benefited from simplicity

A small repository backed by DataStore is much easier to reason about than a miniature database stack when the feature does not actually need database behavior.

That simplicity also helps in tests. The repository contract stays focused on feature behavior instead of dragging in a broader storage abstraction than the feature deserves.

What this choice does not solve

This is the part where these comparisons usually get too neat, so I want to be explicit.

Choosing DataStore here does not mean DataStore is better than Room in general.

This approach becomes weaker if the feature needs:

  • complex filtering and querying
  • partial updates across many structured records
  • relational constraints
  • data that should evolve through formal schema migrations
  • strong multi-screen data access patterns that feel naturally table-shaped

If the reminder feature later grows into a richer local domain with more query-heavy behavior, Room could absolutely become the better choice.

That would be fine.

Architecture should fit the problem you actually have, not the future architecture debate you are trying to pre-win.

The most useful decision rule

The rule I would use again is simple:

Use DataStore when the local persistence is:

  • small
  • user-scoped
  • configuration-like
  • map-like
  • not query-heavy
  • not relational

Use Room when the local persistence is:

  • growing into a real data model
  • queried in multiple ways
  • relational or entity-driven
  • likely to require structured migrations

That rule is more useful than “DataStore for simple, Room for complex,” because it ties the choice to behavior instead of vague labels.

A side benefit: the feature stays honest

There was another benefit here that I think matters more than people expect.

Because reminder state stayed in DataStore and stayed local-only, the feature remained honest about what it was doing.

Actions like:

  • reviewed
  • likely picked up
  • dismiss
  • snooze

only cleared local reminder noise. They did not silently rewrite shared order data.

That separation made both the UX and the architecture cleaner:

  • local feature state stayed local
  • source-of-truth order data stayed in the real data flow

I like that pattern for a lot of workflow features. It keeps local convenience from quietly mutating shared truth.

....

override val reminderSettings: Flow<ReminderSettings> = dataStore.data
        .map { preferences ->
            val key = reminderSettingsKey()
            parseSettings(preferences[key])
        }
        .distinctUntilChanged()

override val reminderLocalStates: Flow<Map<String, ReminderLocalState>> = dataStore.data
        .map { preferences ->
            val key = reminderLocalStatesKey()
            parseLocalStates(preferences[key])
        }
        .distinctUntilChanged()

....

override suspend fun markChecked(orderId: String, timestampMillis: Long) {
        updateLocalState(orderId) { current ->
            current.copy(
                checkedAtEpochMillis = timestampMillis,
                assumedPickedUpAtEpochMillis = null,
                dismissedAtEpochMillis = null,
                snoozedUntilEpochMillis = null
            )
        }
    }

....
@OptIn(ExperimentalCoroutinesApi::class)
class ReminderRepositoryImplTest {

    @Test
    fun `reminder settings are isolated per signed in user`() = runTest {
        val userA = mockUser("uid-a")
        val userB = mockUser("uid-b")
        val (repository, authController, _) = createRepository(
            scope = backgroundScope,
            initialUser = userA
        )

        repository.setReminderEnabled(true)
        repository.setDailyNotificationEnabled(true)
        assertEquals(
            ReminderSettings(
                isReminderEnabled = true,
                isDailyNotificationEnabled = true
            ),
            repository.reminderSettings.first()
        )

        authController.currentUser = userB
        assertEquals(ReminderSettings(), repository.reminderSettings.first())

        repository.setDailyNotificationEnabled(true)
        assertEquals(
            ReminderSettings(
                isReminderEnabled = false,
                isDailyNotificationEnabled = true
            ),
            repository.reminderSettings.first()
        )

        authController.currentUser = userA
        assertEquals(
            ReminderSettings(
                isReminderEnabled = true,
                isDailyNotificationEnabled = true
            ),
            repository.reminderSettings.first()
        )
    }

...

The best persistence choice is usually the one that matches the actual shape of the problem.

For this reminder feature, the state was small, local, and not relational. That made DataStore a better fit than Room.

Not because Room is too heavy in general, but because it would have solved problems this feature did not actually have.

And for me, that is usually a good sign I am choosing the right level of architecture.


메타데이터
post_id
edd4981fc3e8
slug
using-datastore-for-local-reminder-state-instead-of-room-in-android-edd4981fc3e8
url
https://medium.com/@raylabs/using-datastore-for-local-reminder-state-instead-of-room-in-android-edd4981fc3e8
canonical_url
https://medium.com/@raylabs/using-datastore-for-local-reminder-state-instead-of-room-in-android-edd4981fc3e8
author_url
https://medium.com/@raylabs
status
ok
fetched_at
2026-07-11 03:47:11