How gsygithubappcompose Persists User Session Across App Restarts Using Jetpack DataStore
The gsygithubappcompose Android app persists user sessions across restarts using Jetpack DataStore to store authentication tokens, which are automatically restored on app launch by a custom OkHttp interceptor.
The open-source GitHub client application carguo/gsygithubappcompose implements robust user session persistence to maintain authentication state across process terminations and device reboots. By leveraging Jetpack DataStore—Google's modern replacement for SharedPreferences—the app securely stores OAuth tokens and user credentials in persistent storage. This implementation ensures users remain logged in after restarting the app without requiring manual re-authentication.
Storing Credentials in DataStore
The session persistence layer centers on UserPreferencesDataStore, which defines the schema for storing authentication data and handles read/write operations.
Preferences Keys and Storage File
Located in core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/UserPreferencesDataStore.kt (lines 15-38), the DataStore manages three distinct PreferencesKeys: auth_token, username, and user_id. These keys correspond to values stored in a file named user_preferences on the device. The class exposes these values as Kotlin Flow properties (authToken, username, userId), enabling reactive observation of session state changes without blocking the main thread.
Login Flow Implementation
The UserRepository class handles credential persistence through its login and loginWithOAuth methods in data/src/main/java/com/shuyu/gsygithubappcompose/data/repository/UserRepository.kt (lines 34-40). Upon successful authentication, these methods immediately write to the DataStore:
// Login – stores the token in DataStore
suspend fun login(token: String): Result<User> = try {
val user = apiService.getAuthenticatedUser("token $token")
preferencesDataStore.saveAuthToken(token) // ← persisted
preferencesDataStore.saveUsername(user.login)
preferencesDataStore.saveUserId(user.id.toString())
Result.success(user)
} catch (e: Exception) { Result.failure(e) }
This synchronous write operation ensures the token is committed to disk before the login flow completes, guaranteeing persistence across immediate app termination.
Restoring the Session on App Start
The critical mechanism for maintaining session continuity lies in how the app retrieves stored credentials when the process restarts.
TokenInterceptor Initialization
The TokenInterceptor class in core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/interceptor/TokenInterceptor.kt serves as the gatekeeper for all authenticated network requests. During its init block (lines 25-31), it subscribes to the authToken flow from UserPreferencesDataStore, populating an in-memory MutableStateFlow named _token:
// Initialization block subscribing to DataStore changes
init {
scope.launch {
userPreferencesDataStore.authToken.collect { token ->
_token.value = token
}
}
}
Synchronous Fallback for First Request
To handle race conditions where a network request occurs before the asynchronous flow emits a value, the interceptor implements a synchronous fallback mechanism. If _token is null when intercepting a request, it blocking-reads the first value from DataStore using authToken.first() (lines 39-43) and caches it immediately:
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
var currentToken = _token.value
if (currentToken == null) {
// read synchronously from DataStore on first use
currentToken = runBlocking { userPreferencesDataStore.authToken.first() }
_token.value = currentToken
}
// ... add header
}
This ensures that even the very first network request after app startup includes valid authentication credentials.
Authenticating Network Requests
Once restored, the token is applied to every outgoing HTTP call. The interceptor checks the cached _token value and injects the Authorization: token <token> header into the request builder (lines 45-50):
currentToken?.let {
request = request.newBuilder()
.header("Authorization", "token $it")
.build()
}
return chain.proceed(request)
Because the token is maintained in both persistent storage (DataStore) and memory (_token), the app seamlessly maintains authentication state across configuration changes and process restarts.
Clearing the Session on Logout
Session termination requires wiping both persistent storage and memory caches to prevent credential leakage.
The UserRepository.logout() method orchestrates this cleanup by calling preferencesDataStore.clearAll() to remove all three keys from the DataStore, followed by wiping the local Room database (lines 73-76). Additionally, TokenInterceptor provides a clearAuthorization() method (lines 55-60) that explicitly nulls the in-memory _token state:
// Logout – clears persisted session
suspend fun logout() {
preferencesDataStore.clearAll() // removes token, username, user_id
appDatabase.clearAllData()
}
Summary
- Jetpack DataStore serves as the persistent storage mechanism for authentication tokens, replacing legacy SharedPreferences in the
gsygithubappcomposearchitecture. - UserPreferencesDataStore defines three preference keys (
auth_token,username,user_id) backed by the user_preferences file, exposing them as reactive Kotlin Flows for asynchronous access. - TokenInterceptor automatically restores the session on app startup by subscribing to the DataStore flow and implementing a synchronous fallback using
first()for immediate request handling. - UserRepository handles both persistence during login (
saveAuthToken) and complete cleanup during logout (clearAll), ensuring no credential remnants survive session termination.
Frequently Asked Questions
What storage mechanism does gsygithubappcompose use for session persistence?
The application uses Jetpack DataStore, specifically Preferences DataStore, to persist the auth_token, username, and user_id values. This modern storage solution replaces SharedPreferences and provides type safety, asynchronous operations via Kotlin coroutines, and transactional consistency.
How does the app handle the first network request before DataStore loads?
The TokenInterceptor implements a synchronous fallback mechanism using runBlocking to call userPreferencesDataStore.authToken.first(). This blocking read ensures that even if the asynchronous flow hasn't emitted a value yet, the first request can still retrieve the stored token from disk immediately without waiting for the flow collection.
Where is the authentication token physically stored on the device?
The token is stored in a file named user_preferences managed by the DataStore API, located in the app's private data directory. The UserPreferencesDataStore class in core/common/src/main/java/com/shuyu/gsygithubappcompose/core/common/datastore/UserPreferencesDataStore.kt abstracts access to this file using the auth_token preference key (lines 15-20).
How is the session cleared when a user logs out?
Logging out triggers UserRepository.logout(), which invokes preferencesDataStore.clearAll() (lines 73-76) to delete all authentication keys from DataStore, followed by appDatabase.clearAllData() to wipe local caches. The TokenInterceptor also provides clearAuthorization() (lines 55-60) to reset the in-memory _token state, ensuring complete session termination.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →