From 5759ab3738e7346607a4611b905858853cc3a6fd Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Tue, 19 May 2026 17:08:37 +0530 Subject: [PATCH] Add dependency wiring, token storage, and app entry points - AppDependencies: manual DI container with lazy singletons - TokenStore: DataStore-backed persistence for server URL, access token, theme preference, and accent color selection - DataStoreFactory: multiplatform DataStore creation - LocalAppDependencies: CompositionLocal for DI access in composables - App.kt: root composable with theme + Coil ImageLoader (Ktor engine) - MainActivity: creates AppDependencies, provides via CompositionLocal - Coil configured with authenticated Ktor HttpClient for private images Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Avinal Kumar --- .../kotlin/com/avinal/memos/MainActivity.kt | 29 +++++++++++ .../commonMain/kotlin/com/avinal/memos/App.kt | 27 +++++++++++ .../com/avinal/memos/AppDependencies.kt | 48 +++++++++++++++++++ .../com/avinal/memos/util/DataStoreFactory.kt | 9 ++++ .../avinal/memos/util/LocalDependencies.kt | 8 ++++ .../com/avinal/memos/util/TokenStore.kt | 48 +++++++++++++++++++ 6 files changed, 169 insertions(+) create mode 100644 androidApp/src/main/kotlin/com/avinal/memos/MainActivity.kt create mode 100644 composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt create mode 100644 composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt create mode 100644 composeApp/src/commonMain/kotlin/com/avinal/memos/util/DataStoreFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/avinal/memos/util/LocalDependencies.kt create mode 100644 composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt diff --git a/androidApp/src/main/kotlin/com/avinal/memos/MainActivity.kt b/androidApp/src/main/kotlin/com/avinal/memos/MainActivity.kt new file mode 100644 index 0000000..73310bc --- /dev/null +++ b/androidApp/src/main/kotlin/com/avinal/memos/MainActivity.kt @@ -0,0 +1,29 @@ +package com.avinal.memos + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.CompositionLocalProvider +import com.avinal.memos.util.LocalAppDependencies + +class MainActivity : ComponentActivity() { + + private val deps by lazy { + AppDependencies( + dataStorePath = filesDir.resolve("memos_prefs.preferences_pb").absolutePath, + platformContext = applicationContext, + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + deps.initialize() + enableEdgeToEdge() + setContent { + CompositionLocalProvider(LocalAppDependencies provides deps) { + App() + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt new file mode 100644 index 0000000..69bc100 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt @@ -0,0 +1,27 @@ +package com.avinal.memos + +import androidx.compose.runtime.Composable +import coil3.ImageLoader +import coil3.compose.setSingletonImageLoaderFactory +import coil3.compose.LocalPlatformContext +import coil3.network.ktor3.KtorNetworkFetcherFactory +import com.avinal.memos.ui.navigation.AppNavHost +import com.avinal.memos.ui.theme.MemosAppTheme +import com.avinal.memos.util.LocalAppDependencies + +@Composable +fun App() { + val deps = LocalAppDependencies.current + + setSingletonImageLoaderFactory { context -> + ImageLoader.Builder(context) + .components { + add(KtorNetworkFetcherFactory(httpClient = deps.httpClient)) + } + .build() + } + + MemosAppTheme { + AppNavHost(deps) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt new file mode 100644 index 0000000..2358341 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt @@ -0,0 +1,48 @@ +package com.avinal.memos + +import com.avinal.memos.api.HttpClientFactory +import com.avinal.memos.api.MemosApiClient +import com.avinal.memos.db.MemosDatabase +import com.avinal.memos.db.createPlatformDatabase +import com.avinal.memos.domain.AuthRepository +import com.avinal.memos.domain.MemoRepository +import com.avinal.memos.util.TokenStore +import com.avinal.memos.util.createDataStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch + +class AppDependencies( + dataStorePath: String, + platformContext: Any, +) { + @Volatile private var cachedToken: String? = null + @Volatile private var cachedServerUrl: String? = null + + private val dataStore = createDataStore(dataStorePath) + val tokenStore = TokenStore(dataStore) + + val database: MemosDatabase by lazy { createPlatformDatabase(platformContext) } + + val httpClient by lazy { + HttpClientFactory.create { cachedToken } + } + + val apiClient: MemosApiClient by lazy { + MemosApiClient( + httpClient = httpClient, + baseUrlProvider = { cachedServerUrl ?: "" }, + ) + } + + val authRepository: AuthRepository by lazy { AuthRepository(apiClient, tokenStore) } + val memoRepository: MemoRepository by lazy { MemoRepository(apiClient, database.memoDao()) } + + fun initialize() { + CoroutineScope(Dispatchers.IO).launch { + launch { tokenStore.accessToken.collect { cachedToken = it } } + launch { tokenStore.serverUrl.collect { cachedServerUrl = it } } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/DataStoreFactory.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/DataStoreFactory.kt new file mode 100644 index 0000000..b85a5a2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/DataStoreFactory.kt @@ -0,0 +1,9 @@ +package com.avinal.memos.util + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import okio.Path.Companion.toPath + +fun createDataStore(path: String): DataStore = + PreferenceDataStoreFactory.createWithPath(produceFile = { path.toPath() }) diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/LocalDependencies.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/LocalDependencies.kt new file mode 100644 index 0000000..5c529ce --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/LocalDependencies.kt @@ -0,0 +1,8 @@ +package com.avinal.memos.util + +import androidx.compose.runtime.staticCompositionLocalOf +import com.avinal.memos.AppDependencies + +val LocalAppDependencies = staticCompositionLocalOf { + error("AppDependencies not provided") +} diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt new file mode 100644 index 0000000..003c7cf --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/util/TokenStore.kt @@ -0,0 +1,48 @@ +package com.avinal.memos.util + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class TokenStore(private val dataStore: DataStore) { + + val serverUrl: Flow = dataStore.data.map { it[KEY_SERVER_URL] } + val accessToken: Flow = dataStore.data.map { it[KEY_ACCESS_TOKEN] } + val theme: Flow = dataStore.data.map { it[KEY_THEME] ?: "DARK" } + val accentColor: Flow = dataStore.data.map { it[KEY_ACCENT] ?: "Cobalt" } + + suspend fun saveCredentials(serverUrl: String, token: String) { + dataStore.edit { prefs -> + prefs[KEY_SERVER_URL] = serverUrl + prefs[KEY_ACCESS_TOKEN] = token + } + } + + suspend fun saveTheme(theme: String) { + dataStore.edit { it[KEY_THEME] = theme } + } + + suspend fun saveAccentColor(name: String) { + dataStore.edit { it[KEY_ACCENT] = name } + } + + suspend fun clear() { + dataStore.edit { + val theme = it[KEY_THEME] + val accent = it[KEY_ACCENT] + it.clear() + if (theme != null) it[KEY_THEME] = theme + if (accent != null) it[KEY_ACCENT] = accent + } + } + + companion object { + private val KEY_SERVER_URL = stringPreferencesKey("server_url") + private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token") + private val KEY_THEME = stringPreferencesKey("app_theme") + private val KEY_ACCENT = stringPreferencesKey("accent_color") + } +}