# How GSY GitHub App Uses DataStore for Persisting User Preferences in Android

> Learn how GSY GitHub App Compose uses Android DataStore for type-safe, reactive storage of user preferences like authentication and language settings. Enhance your app with real-time UI updates.

- Repository: [Shuyu Guo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose)
- Tags: how-to-guide
- Published: 2026-02-26

---

**The GSY GitHub App Compose uses Android DataStore (Preferences) to persist authentication credentials and app language settings through type-safe key-value storage, exposing reactive flows for real-time UI updates and interceptor injection.**

The [carguo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose) repository demonstrates a production-grade implementation of DataStore for persisting user preferences in a Jetpack Compose application. Unlike legacy SharedPreferences, DataStore provides coroutine-safe, transactional storage with built-in type safety that survives process restarts.

## Core Architecture of DataStore Implementation

The application separates concerns through interface abstraction and dependency injection, ensuring the UI and network layers remain decoupled from storage implementation details.

### UserPreferencesDataStore and Interface Abstraction

Concrete storage logic resides in [`UserPreferencesDataStore.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/UserPreferencesDataStore.kt), which implements the `IUserPreferencesDataStore` interface defined in [`IUserPreferencesDataStore.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/IUserPreferencesDataStore.kt). This abstraction exposes three critical preference keys as `stringPreferencesKey` instances: **auth token**, **username**, and **user ID**.

The interface provides reactive access through `Flow<String?>` streams and suspend functions for persistence:

- `saveAuthToken(token: String)`
- `saveUsername(username: String)`
- `saveUserId(userId: String)`
- `clearAll()`
- `authToken: Flow<String?>`
- `username: Flow<String?>`
- `userId: Flow<String?>`

According to the source code in [`core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/UserPreferencesDataStore.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/UserPreferencesDataStore.kt), writes occur through `context.dataStore.edit { preferences -> ... }`, which atomically updates the underlying `preferences.pb` file.

### Dependency Injection with DataStoreModule

The [`DataStoreModule.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/DataStoreModule.kt) file in `core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/di/` binds the concrete implementation to the interface using Hilt:

```kotlin
@Module
@InstallIn(SingletonComponent::class)
abstract class DataStoreModule {
    @Binds
    abstract fun bindUserPreferencesDataStore(
        impl: UserPreferencesDataStore
    ): IUserPreferencesDataStore
}

```

This module also provides a singleton `LanguageDataStore`, demonstrating how the app scales DataStore usage beyond authentication data.

## Reading and Writing Preferences

The repository layer handles credential persistence after successful authentication, while network interceptors consume these preferences to inject authorization headers.

### Persisting Credentials After Login

In [`data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/UserRepository.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/UserRepository.kt), the `UserRepository` persists OAuth or token-based credentials immediately after validation:

```kotlin
suspend fun login(token: String): Result<User> {
    val user = apiService.getAuthenticatedUser("token $token")
    preferencesDataStore.saveAuthToken(token)        // ← persisted
    preferencesDataStore.saveUsername(user.login)   // ← persisted
    preferencesDataStore.saveUserId(user.id.toString())
    return Result.success(user)
}

```

Logout operations call `preferencesDataStore.clearAll()`, which atomically removes all stored keys. The repository exposes login state through a derived flow:

```kotlin
fun isLoggedIn(): Flow<Boolean> =
    preferencesDataStore.authToken.map { it?.isNotEmpty() == true }

```

### Reactive Token Consumption in Network Layer

The `TokenInterceptor` in [`core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/interceptor/TokenInterceptor.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/interceptor/TokenInterceptor.kt) demonstrates how to bridge DataStore's asynchronous API with synchronous interceptor requirements.

The interceptor maintains an in-memory `MutableStateFlow` cache that updates from the DataStore flow:

```kotlin
init {
    interceptorScope.launch {
        userPreferencesDataStore.authToken.collect { newToken ->
            _token.value = newToken
        }
    }
}

```

When intercepting requests, it falls back to synchronous reading if the cache is empty:

```kotlin
override fun intercept(chain: Interceptor.Chain): Response {
    var currentToken = _token.value
    if (currentToken == null) {
        currentToken = runBlocking { userPreferencesDataStore.authToken.first() }
        _token.value = currentToken
    }
    currentToken?.let {
        request = request.newBuilder()
            .header("Authorization", "token $it")
            .build()
    }
    return chain.proceed(request)
}

```

## Language Preferences with DataStore

Beyond authentication, the app uses DataStore for UI configuration through [`LanguageDataStore.kt`](https://github.com/carguo/gsygithubappcompose/blob/main/LanguageDataStore.kt) in `core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/`.

This component persists the selected `app_language` using the same patterns as user credentials:

```kotlin
suspend fun setAppLanguage(lang: AppLanguage) {
    languageDataStore.saveAppLanguage(lang)   // writes to DataStore
}

```

The `LanguageDataStore` exposes `appLanguage` as `Flow<AppLanguage>`, allowing the UI layer to react to locale changes without manual polling.

## Summary

- **Interface abstraction** decouples storage implementation from consumers through `IUserPreferencesDataStore`.
- **Reactive streams** provide real-time updates via Kotlin Flow, eliminating manual synchronization between components.
- **Atomic transactions** ensure data consistency through `DataStore.edit { }` blocks that write to `preferences.pb`.
- **Synchronous fallback** using `runBlocking { flow.first() }` bridges async DataStore with synchronous network interceptor requirements.
- **Scalable pattern** demonstrated by parallel implementations for authentication (`UserPreferencesDataStore`) and configuration (`LanguageDataStore`).

## Frequently Asked Questions

### What is DataStore and why does GSY GitHub App use it instead of SharedPreferences?

DataStore is a Jetpack library providing type-safe, coroutine-aware key-value storage using protocol buffers. The GSY GitHub App uses it because it guarantees **type safety** (preventing ClassCastException), supports **atomic transactions** (all-or-nothing writes), and integrates natively with **Kotlin coroutines** and Flow. Unlike SharedPreferences, DataStore migrates data asynchronously and never blocks the UI thread on I/O operations.

### How does the app handle authentication token persistence across process restarts?

The `UserRepository` persists the token immediately after successful OAuth validation using `preferencesDataStore.saveAuthToken(token)`. This writes to the DataStore file on disk via `context.dataStore.edit`. Upon process restart, the `TokenInterceptor` subscribes to `userPreferencesDataStore.authToken` as a Flow, maintaining an in-memory `MutableStateFlow` cache that updates automatically when stored values change.

### Can DataStore be used synchronously when immediate values are needed?

While DataStore is designed for asynchronous access, the `TokenInterceptor` demonstrates a safe synchronous fallback pattern: `runBlocking { userPreferencesDataStore.authToken.first() }`. This blocks the current thread only during the first read if the in-memory cache is empty, ensuring HTTP headers can be injected immediately while maintaining the reactive architecture for subsequent updates.

### How does the app separate user preferences from other settings like language?

The codebase uses **separate DataStore instances** managed through distinct classes. `UserPreferencesDataStore` handles authentication data (token, username, ID) while `LanguageDataStore` manages UI configuration (`app_language`). Both implement similar patterns—exposing Flows and suspend functions—but inject separately via `DataStoreModule`, preventing key collisions and allowing independent testing and lifecycle management.