From e4effcf40f6116ca8e9e155ab8374e331581900e Mon Sep 17 00:00:00 2001 From: Garfie Date: Sat, 28 Mar 2026 09:03:14 -0500 Subject: [PATCH] Migrate DI from Hilt to Koin 4.1 --- CLAUDE.md | 10 +- app/CLAUDE.md | 4 +- app/build.gradle.kts | 6 +- app/proguard-rules.pro | 3 + .../librechat/android/LibreChatApplication.kt | 56 +++-- .../com/librechat/android/MainActivity.kt | 8 +- .../librechat/android/navigation/AppModule.kt | 8 + .../android/navigation/LibreChatNavHost.kt | 4 +- .../android/navigation/NavHostViewModel.kt | 5 +- .../android/KoinGraphVerificationTest.kt | 180 +++++++++++++++ build-logic/CLAUDE.md | 8 +- build-logic/convention/build.gradle.kts | 8 +- .../kotlin/AndroidFeatureConventionPlugin.kt | 6 +- ...ugin.kt => AndroidKoinConventionPlugin.kt} | 11 +- build.gradle.kts | 1 - core/common/CLAUDE.md | 9 +- core/common/build.gradle.kts | 2 +- .../android/core/common/di/CommonModule.kt | 26 +++ .../core/common/di/CoroutineScopeModule.kt | 26 --- .../core/common/di/DispatcherModule.kt | 35 --- .../android/core/common/di/KoinQualifiers.kt | 12 + .../common/network/ConnectivityObserver.kt | 8 +- .../common/di/CommonModuleVerificationTest.kt | 16 ++ core/data/CLAUDE.md | 14 +- core/data/build.gradle.kts | 2 +- .../data/datastore/ConfigCacheDataStore.kt | 5 +- .../core/data/datastore/ServerDataStore.kt | 5 +- .../core/data/datastore/SettingsDataStore.kt | 5 +- .../core/data/datastore/ThemeDataStore.kt | 5 +- .../core/data/datastore/TokenDataStore.kt | 13 +- .../android/core/data/di/DataModule.kt | 210 ++++++++---------- .../android/core/data/di/DatabaseModule.kt | 40 ---- .../data/repository/AgentRepositoryImpl.kt | 5 +- .../data/repository/ApiKeyRepositoryImpl.kt | 5 +- .../data/repository/AuthRepositoryImpl.kt | 8 +- .../data/repository/BalanceRepositoryImpl.kt | 5 +- .../data/repository/BannerRepositoryImpl.kt | 5 +- .../data/repository/ChatRepositoryImpl.kt | 8 +- .../data/repository/ConfigRepositoryImpl.kt | 5 +- .../repository/ConversationRepositoryImpl.kt | 5 +- .../data/repository/DraftRepositoryImpl.kt | 9 +- .../data/repository/FileRepositoryImpl.kt | 5 +- .../core/data/repository/KeyRepositoryImpl.kt | 5 +- .../core/data/repository/McpRepositoryImpl.kt | 5 +- .../data/repository/MemoryRepositoryImpl.kt | 5 +- .../data/repository/MessageRepositoryImpl.kt | 5 +- .../data/repository/PresetRepositoryImpl.kt | 5 +- .../data/repository/PromptRepositoryImpl.kt | 5 +- .../data/repository/SearchRepositoryImpl.kt | 5 +- .../data/repository/ShareRepositoryImpl.kt | 5 +- .../data/repository/SpeechRepositoryImpl.kt | 5 +- .../core/data/repository/TagRepositoryImpl.kt | 5 +- .../data/repository/UserRepositoryImpl.kt | 5 +- .../data/di/DataModuleVerificationTest.kt | 45 ++++ core/network/CLAUDE.md | 8 +- core/network/build.gradle.kts | 2 +- .../android/core/network/api/AgentsApi.kt | 3 +- .../android/core/network/api/ApiKeysApi.kt | 3 +- .../android/core/network/api/AuthApi.kt | 3 +- .../android/core/network/api/BalanceApi.kt | 3 +- .../android/core/network/api/BannerApi.kt | 3 +- .../android/core/network/api/ChatApi.kt | 3 +- .../android/core/network/api/ConfigApi.kt | 3 +- .../core/network/api/ConversationsApi.kt | 3 +- .../android/core/network/api/FilesApi.kt | 3 +- .../android/core/network/api/FilesExtApi.kt | 3 +- .../android/core/network/api/KeysApi.kt | 3 +- .../android/core/network/api/McpApi.kt | 3 +- .../android/core/network/api/MemoriesApi.kt | 3 +- .../android/core/network/api/MessagesApi.kt | 3 +- .../android/core/network/api/PresetsApi.kt | 3 +- .../android/core/network/api/PromptsApi.kt | 3 +- .../android/core/network/api/SearchApi.kt | 3 +- .../android/core/network/api/ShareApi.kt | 3 +- .../android/core/network/api/SpeechApi.kt | 3 +- .../android/core/network/api/TagsApi.kt | 3 +- .../android/core/network/api/UserApi.kt | 3 +- .../android/core/network/di/NetworkModule.kt | 184 ++++++++------- .../android/core/network/di/RefreshClient.kt | 7 - .../core/network/di/StreamingClient.kt | 7 - .../android/core/network/sse/SseClient.kt | 3 +- .../di/NetworkModuleVerificationTest.kt | 19 ++ core/ui/CLAUDE.md | 2 +- .../android/feature/agents/di/AgentsModule.kt | 23 ++ .../agents/screen/AgentDetailScreen.kt | 4 +- .../agents/screen/AgentEditorScreen.kt | 4 +- .../agents/screen/AgentMarketplaceScreen.kt | 4 +- .../agents/viewmodel/AgentDetailViewModel.kt | 5 +- .../agents/viewmodel/AgentEditorViewModel.kt | 8 +- .../viewmodel/AgentMarketplaceViewModel.kt | 5 +- .../agents/di/AgentsModuleVerificationTest.kt | 21 ++ .../android/feature/auth/di/AuthModule.kt | 27 +++ .../feature/auth/oauth/OAuthManager.kt | 4 +- .../auth/screen/ForgotPasswordScreen.kt | 4 +- .../feature/auth/screen/LoginScreen.kt | 4 +- .../feature/auth/screen/RegisterScreen.kt | 4 +- .../auth/screen/ResetPasswordScreen.kt | 4 +- .../feature/auth/screen/ServerUrlScreen.kt | 4 +- .../feature/auth/screen/TermsScreen.kt | 4 +- .../feature/auth/screen/TwoFactorScreen.kt | 4 +- .../feature/auth/screen/VerifyEmailScreen.kt | 4 +- .../auth/viewmodel/ForgotPasswordViewModel.kt | 5 +- .../feature/auth/viewmodel/LoginViewModel.kt | 5 +- .../auth/viewmodel/RegisterViewModel.kt | 5 +- .../auth/viewmodel/ResetPasswordViewModel.kt | 5 +- .../auth/viewmodel/ServerUrlViewModel.kt | 5 +- .../feature/auth/viewmodel/TermsViewModel.kt | 5 +- .../auth/viewmodel/TwoFactorViewModel.kt | 5 +- .../auth/viewmodel/VerifyEmailViewModel.kt | 5 +- .../auth/di/AuthModuleVerificationTest.kt | 22 ++ .../android/feature/chat/di/ChatModule.kt | 36 +++ .../chat/prompts/PromptEditorScreen.kt | 4 +- .../chat/prompts/PromptEditorViewModel.kt | 5 +- .../chat/prompts/PromptsLibraryScreen.kt | 4 +- .../feature/chat/prompts/PromptsViewModel.kt | 5 +- .../android/feature/chat/screen/ChatScreen.kt | 4 +- .../feature/chat/viewmodel/ChatViewModel.kt | 9 +- .../chat/di/ChatModuleVerificationTest.kt | 33 +++ .../conversations/di/ConversationsModule.kt | 17 ++ .../export/ConversationExporter.kt | 4 +- .../export/ConversationImporter.kt | 3 +- .../screen/ArchivedConversationsScreen.kt | 4 +- .../screen/ConversationListScreen.kt | 4 +- .../ArchivedConversationsViewModel.kt | 5 +- .../viewmodel/ConversationListViewModel.kt | 5 +- .../di/ConversationsModuleVerificationTest.kt | 25 +++ .../android/feature/files/di/FilesModule.kt | 16 ++ .../feature/files/screen/FilesScreen.kt | 4 +- .../feature/files/viewmodel/FilesViewModel.kt | 8 +- .../files/di/FilesModuleVerificationTest.kt | 19 ++ .../feature/settings/di/SettingsModule.kt | 35 +++ .../settings/navigation/SettingsNavigation.kt | 4 +- .../settings/screen/AccountSettingsScreen.kt | 6 +- .../feature/settings/screen/ApiKeysScreen.kt | 4 +- .../settings/screen/ChatSettingsScreen.kt | 6 +- .../settings/screen/DataSettingsScreen.kt | 6 +- .../settings/screen/GeneralSettingsScreen.kt | 6 +- .../settings/screen/McpServersScreen.kt | 4 +- .../feature/settings/screen/MemoriesScreen.kt | 4 +- .../settings/screen/PresetManagerScreen.kt | 4 +- .../feature/settings/screen/SettingsScreen.kt | 4 +- .../settings/screen/TabbedSettingsScreen.kt | 4 +- .../settings/viewmodel/ApiKeysViewModel.kt | 5 +- .../settings/viewmodel/McpViewModel.kt | 5 +- .../settings/viewmodel/MemoriesViewModel.kt | 5 +- .../viewmodel/PresetManagerViewModel.kt | 5 +- .../settings/viewmodel/SettingsViewModel.kt | 8 +- .../di/SettingsModuleVerificationTest.kt | 31 +++ gradle/libs.versions.toml | 16 +- 149 files changed, 1065 insertions(+), 716 deletions(-) create mode 100644 app/src/main/kotlin/com/librechat/android/navigation/AppModule.kt create mode 100644 app/src/test/kotlin/com/librechat/android/KoinGraphVerificationTest.kt rename build-logic/convention/src/main/kotlin/{AndroidHiltConventionPlugin.kt => AndroidKoinConventionPlugin.kt} (51%) create mode 100644 core/common/src/main/kotlin/com/librechat/android/core/common/di/CommonModule.kt delete mode 100644 core/common/src/main/kotlin/com/librechat/android/core/common/di/CoroutineScopeModule.kt delete mode 100644 core/common/src/main/kotlin/com/librechat/android/core/common/di/DispatcherModule.kt create mode 100644 core/common/src/main/kotlin/com/librechat/android/core/common/di/KoinQualifiers.kt create mode 100644 core/common/src/test/kotlin/com/librechat/android/core/common/di/CommonModuleVerificationTest.kt delete mode 100644 core/data/src/main/kotlin/com/librechat/android/core/data/di/DatabaseModule.kt create mode 100644 core/data/src/test/kotlin/com/librechat/android/core/data/di/DataModuleVerificationTest.kt delete mode 100644 core/network/src/main/kotlin/com/librechat/android/core/network/di/RefreshClient.kt delete mode 100644 core/network/src/main/kotlin/com/librechat/android/core/network/di/StreamingClient.kt create mode 100644 core/network/src/test/kotlin/com/librechat/android/core/network/di/NetworkModuleVerificationTest.kt create mode 100644 feature/agents/src/main/kotlin/com/librechat/android/feature/agents/di/AgentsModule.kt create mode 100644 feature/agents/src/test/kotlin/com/librechat/android/feature/agents/di/AgentsModuleVerificationTest.kt create mode 100644 feature/auth/src/main/kotlin/com/librechat/android/feature/auth/di/AuthModule.kt create mode 100644 feature/auth/src/test/kotlin/com/librechat/android/feature/auth/di/AuthModuleVerificationTest.kt create mode 100644 feature/chat/src/main/kotlin/com/librechat/android/feature/chat/di/ChatModule.kt create mode 100644 feature/chat/src/test/kotlin/com/librechat/android/feature/chat/di/ChatModuleVerificationTest.kt create mode 100644 feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/di/ConversationsModule.kt create mode 100644 feature/conversations/src/test/kotlin/com/librechat/android/feature/conversations/di/ConversationsModuleVerificationTest.kt create mode 100644 feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt create mode 100644 feature/files/src/test/kotlin/com/librechat/android/feature/files/di/FilesModuleVerificationTest.kt create mode 100644 feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt create mode 100644 feature/settings/src/test/kotlin/com/librechat/android/feature/settings/di/SettingsModuleVerificationTest.kt diff --git a/CLAUDE.md b/CLAUDE.md index e3cb454..739452e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ Native Android client for LibreChat. Connects to existing LibreChat backend serv ## Tech Stack - **UI**: Jetpack Compose + Compose Navigation -- **DI**: Hilt +- **DI**: Koin (KMP-ready) - **Network**: Ktor Client (OkHttp engine) - **Serialization**: Kotlinx Serialization - **Local Storage**: Room (cache), DataStore (prefs), EncryptedSharedPreferences (tokens) @@ -39,6 +39,14 @@ Each module has its own `CLAUDE.md` with specific guidance. - Room is a read-through cache; server is source of truth - Custom SSE parser over raw ByteReadChannel (not Ktor SSE plugin) +## Adding a New Feature Module + +1. Create the module directory under `feature/` +2. Apply the `librechat.android.feature` convention plugin in `build.gradle.kts` — this auto-applies Koin + Compose deps +3. Create `di/Module.kt` with a Koin `module { }` containing `viewModelOf(::YourViewModel)` definitions +4. Register the module in `LibreChatApplication.kt`'s `startKoin { modules(...) }` list +5. Use `koinViewModel()` in screen composables to inject ViewModels + ## Backend Quirks - Mutation endpoints wrap body in `arg` field: `{ "arg": { ... } }` diff --git a/app/CLAUDE.md b/app/CLAUDE.md index 52df94d..7c553f4 100644 --- a/app/CLAUDE.md +++ b/app/CLAUDE.md @@ -1,6 +1,6 @@ # App Module -Single Activity architecture. `MainActivity` is the sole entry point, annotated `@AndroidEntryPoint`. +Single Activity architecture. `MainActivity` is the sole entry point. ## Navigation @@ -50,7 +50,7 @@ Conversations are integrated into the drawer body. Agents, Files, and Settings a ## Dependencies This module depends on all `:core:*` and all `:feature:*` modules. -It applies convention plugins: `librechat.android.application`, `librechat.android.compose`, `librechat.android.hilt`. +It applies convention plugins: `librechat.android.application`, `librechat.android.compose`, `librechat.android.koin`. ### Server-Synced Favorites - `NavHostViewModel` exposes `favorites: StateFlow>` and `toggleFavorite()` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 03ad758..13f3eba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id("librechat.android.application") id("librechat.android.compose") - id("librechat.android.hilt") + id("librechat.android.koin") } android { @@ -31,7 +31,9 @@ dependencies { implementation(libs.activity.compose) implementation(libs.navigation.compose) - implementation(libs.hilt.navigation.compose) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.compose.viewmodel.navigation) implementation(libs.compose.material3.wsc) implementation(libs.bundles.lifecycle) implementation(libs.coil.compose) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ef145c1..ad89151 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -15,3 +15,6 @@ # Keep Room entities -keep class com.librechat.android.core.data.db.entity.** { *; } + +# Koin - keep ViewModel constructors for reflection-based instantiation +-keep class * extends androidx.lifecycle.ViewModel { (...); } diff --git a/app/src/main/kotlin/com/librechat/android/LibreChatApplication.kt b/app/src/main/kotlin/com/librechat/android/LibreChatApplication.kt index 43749fb..143430d 100644 --- a/app/src/main/kotlin/com/librechat/android/LibreChatApplication.kt +++ b/app/src/main/kotlin/com/librechat/android/LibreChatApplication.kt @@ -5,37 +5,61 @@ import coil.ImageLoader import coil.ImageLoaderFactory import coil.disk.DiskCache import coil.memory.MemoryCache +import com.librechat.android.core.common.di.commonModule +import com.librechat.android.core.data.di.dataModule import com.librechat.android.core.network.client.TokenManager -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.android.HiltAndroidApp -import dagger.hilt.components.SingletonComponent +import com.librechat.android.core.network.di.networkModule +import com.librechat.android.feature.agents.di.agentsModule +import com.librechat.android.feature.auth.di.authModule +import com.librechat.android.feature.chat.di.chatModule +import com.librechat.android.feature.conversations.di.conversationsModule +import com.librechat.android.feature.files.di.filesModule +import com.librechat.android.feature.settings.di.settingsModule +import com.librechat.android.navigation.appModule import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient +import org.koin.android.ext.android.get +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin import timber.log.Timber -@HiltAndroidApp class LibreChatApplication : Application(), ImageLoaderFactory { - @EntryPoint - @InstallIn(SingletonComponent::class) - interface ImageLoaderEntryPoint { - fun tokenManager(): TokenManager - } - override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } + + try { + startKoin { + if (BuildConfig.DEBUG) { + androidLogger(org.koin.core.logger.Level.DEBUG) + } + androidContext(this@LibreChatApplication) + allowOverride(false) + modules( + commonModule, + networkModule, + dataModule, + appModule, + authModule, + chatModule, + conversationsModule, + settingsModule, + agentsModule, + filesModule, + ) + } + } catch (e: Exception) { + Timber.e(e, "Koin initialization failed") + throw e // Always rethrow — DI failure is unrecoverable + } } override fun newImageLoader(): ImageLoader { - val entryPoint = EntryPointAccessors.fromApplication( - this, ImageLoaderEntryPoint::class.java - ) - val tokenManager = entryPoint.tokenManager() + val tokenManager: TokenManager = get() val okHttpClient = OkHttpClient.Builder() .addInterceptor { chain -> diff --git a/app/src/main/kotlin/com/librechat/android/MainActivity.kt b/app/src/main/kotlin/com/librechat/android/MainActivity.kt index ad51518..5f4a33e 100644 --- a/app/src/main/kotlin/com/librechat/android/MainActivity.kt +++ b/app/src/main/kotlin/com/librechat/android/MainActivity.kt @@ -38,16 +38,14 @@ import com.librechat.android.core.ui.theme.LibreChatTheme import com.librechat.android.feature.chat.ShareIntentConsumer import com.librechat.android.feature.chat.SharedContent import com.librechat.android.navigation.LibreChatNavHost -import dagger.hilt.android.AndroidEntryPoint +import org.koin.android.ext.android.inject import timber.log.Timber -import javax.inject.Inject import androidx.compose.ui.res.stringResource -@AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject lateinit var connectivityObserver: ConnectivityObserver - @Inject lateinit var themeDataStore: ThemeDataStore + private val connectivityObserver: ConnectivityObserver by inject() + private val themeDataStore: ThemeDataStore by inject() private var deepLinkUri by mutableStateOf(null) diff --git a/app/src/main/kotlin/com/librechat/android/navigation/AppModule.kt b/app/src/main/kotlin/com/librechat/android/navigation/AppModule.kt new file mode 100644 index 0000000..b6ad572 --- /dev/null +++ b/app/src/main/kotlin/com/librechat/android/navigation/AppModule.kt @@ -0,0 +1,8 @@ +package com.librechat.android.navigation + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val appModule = module { + viewModelOf(::NavHostViewModel) +} diff --git a/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt b/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt index 280025d..9ddf6d0 100644 --- a/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt +++ b/app/src/main/kotlin/com/librechat/android/navigation/LibreChatNavHost.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.currentBackStackEntryAsState @@ -77,7 +77,7 @@ fun LibreChatNavHost( deepLinkUri: Uri? = null, onDeepLinkConsumed: () -> Unit = {}, shareNavigationTrigger: Int = 0, - navHostViewModel: NavHostViewModel = hiltViewModel(), + navHostViewModel: NavHostViewModel = koinViewModel(), ) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() diff --git a/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt b/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt index 6084cca..982644f 100644 --- a/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt +++ b/app/src/main/kotlin/com/librechat/android/navigation/NavHostViewModel.kt @@ -19,7 +19,6 @@ import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -33,7 +32,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import timber.log.Timber -import javax.inject.Inject /** * Combined UI state for the drawer sidebar. A single emission replaces @@ -59,8 +57,7 @@ data class VersionMismatchState( val backendVersion: String, ) -@HiltViewModel -class NavHostViewModel @Inject constructor( +class NavHostViewModel( private val authRepository: AuthRepository, private val bannerRepository: BannerRepository, private val configRepository: ConfigRepository, diff --git a/app/src/test/kotlin/com/librechat/android/KoinGraphVerificationTest.kt b/app/src/test/kotlin/com/librechat/android/KoinGraphVerificationTest.kt new file mode 100644 index 0000000..1993f1a --- /dev/null +++ b/app/src/test/kotlin/com/librechat/android/KoinGraphVerificationTest.kt @@ -0,0 +1,180 @@ +package com.librechat.android + +import com.librechat.android.core.common.di.commonModule +import com.librechat.android.core.common.network.ConnectivityObserver +import com.librechat.android.core.data.datastore.ConfigCacheDataStore +import com.librechat.android.core.data.datastore.ServerDataStore +import com.librechat.android.core.data.datastore.SettingsDataStore +import com.librechat.android.core.data.datastore.ThemeDataStore +import com.librechat.android.core.data.di.dataModule +import com.librechat.android.core.data.repository.AgentRepository +import com.librechat.android.core.data.repository.ApiKeyRepository +import com.librechat.android.core.data.repository.AuthRepository +import com.librechat.android.core.data.repository.BalanceRepository +import com.librechat.android.core.data.repository.BannerRepository +import com.librechat.android.core.data.repository.ChatRepository +import com.librechat.android.core.data.repository.ConfigRepository +import com.librechat.android.core.data.repository.ConversationRepository +import com.librechat.android.core.data.repository.DraftRepository +import com.librechat.android.core.data.repository.FileRepository +import com.librechat.android.core.data.repository.KeyRepository +import com.librechat.android.core.data.repository.McpRepository +import com.librechat.android.core.data.repository.MemoryRepository +import com.librechat.android.core.data.repository.MessageRepository +import com.librechat.android.core.data.repository.PresetRepository +import com.librechat.android.core.data.repository.PromptRepository +import com.librechat.android.core.data.repository.SearchRepository +import com.librechat.android.core.data.repository.ShareRepository +import com.librechat.android.core.data.repository.SpeechRepository +import com.librechat.android.core.data.repository.TagRepository +import com.librechat.android.core.data.repository.UserRepository +import com.librechat.android.core.network.api.AgentsApi +import com.librechat.android.core.network.api.ApiKeysApi +import com.librechat.android.core.network.api.AuthApi +import com.librechat.android.core.network.api.BalanceApi +import com.librechat.android.core.network.api.BannerApi +import com.librechat.android.core.network.api.ChatApi +import com.librechat.android.core.network.api.ConfigApi +import com.librechat.android.core.network.api.ConversationsApi +import com.librechat.android.core.network.api.FilesApi +import com.librechat.android.core.network.api.FilesExtApi +import com.librechat.android.core.network.api.KeysApi +import com.librechat.android.core.network.api.McpApi +import com.librechat.android.core.network.api.MemoriesApi +import com.librechat.android.core.network.api.MessagesApi +import com.librechat.android.core.network.api.PresetsApi +import com.librechat.android.core.network.api.PromptsApi +import com.librechat.android.core.network.api.SearchApi +import com.librechat.android.core.network.api.ShareApi +import com.librechat.android.core.network.api.SpeechApi +import com.librechat.android.core.network.api.TagsApi +import com.librechat.android.core.network.api.UserApi +import com.librechat.android.core.network.client.SecureTokenStorage +import com.librechat.android.core.network.client.ServerUrlProvider +import com.librechat.android.core.network.client.TokenManager +import com.librechat.android.core.network.di.networkModule +import com.librechat.android.core.network.sse.SseClient +import com.librechat.android.feature.agents.di.agentsModule +import com.librechat.android.feature.auth.di.authModule +import com.librechat.android.feature.chat.di.chatModule +import com.librechat.android.feature.conversations.di.conversationsModule +import com.librechat.android.feature.files.di.filesModule +import com.librechat.android.feature.settings.di.settingsModule +import com.librechat.android.navigation.appModule +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import org.junit.Test +import org.koin.core.module.Module +import org.koin.test.verify.verify +import kotlin.reflect.KClass + +class KoinGraphVerificationTest { + + private val allModules: List = listOf( + commonModule, + networkModule, + dataModule, + appModule, + authModule, + chatModule, + conversationsModule, + settingsModule, + agentsModule, + filesModule, + ) + + /** + * Integration-level Koin graph verification. + * + * Koin's verify() operates per-module and cannot resolve definitions + * from other modules. This test whitelists all cross-module and + * framework types so every module is verified in a single test class. + * If a type is renamed or removed, this test will catch it. + */ + @OptIn(org.koin.core.annotation.KoinExperimentalAPI::class) + @Test + fun verifyFullKoinGraph() { + val extraTypes = mutableListOf>( + // Android framework + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + // core:common provides + CoroutineDispatcher::class, + CoroutineScope::class, + ConnectivityObserver::class, + // core:network provides + TokenManager::class, + SecureTokenStorage::class, + ServerUrlProvider::class, + SseClient::class, + AgentsApi::class, + ApiKeysApi::class, + AuthApi::class, + BalanceApi::class, + BannerApi::class, + ChatApi::class, + ConfigApi::class, + ConversationsApi::class, + FilesApi::class, + FilesExtApi::class, + KeysApi::class, + McpApi::class, + MemoriesApi::class, + MessagesApi::class, + PresetsApi::class, + PromptsApi::class, + SearchApi::class, + ShareApi::class, + SpeechApi::class, + TagsApi::class, + UserApi::class, + // core:data provides + ConfigCacheDataStore::class, + ServerDataStore::class, + SettingsDataStore::class, + ThemeDataStore::class, + AgentRepository::class, + ApiKeyRepository::class, + AuthRepository::class, + BalanceRepository::class, + BannerRepository::class, + ChatRepository::class, + ConfigRepository::class, + ConversationRepository::class, + DraftRepository::class, + FileRepository::class, + KeyRepository::class, + McpRepository::class, + MemoryRepository::class, + MessageRepository::class, + PresetRepository::class, + PromptRepository::class, + SearchRepository::class, + ShareRepository::class, + SpeechRepository::class, + TagRepository::class, + UserRepository::class, + // Wrappers/DSL types that verify can't resolve via constructor + kotlin.Lazy::class, + java.io.File::class, + ) + + // Types whose libraries aren't on the app test classpath (transitive + // implementation deps). Resolve via reflection at runtime. + val reflectionTypes = listOf( + "io.ktor.client.HttpClient", + "io.ktor.client.engine.HttpClientEngine", + "io.ktor.client.engine.HttpClientEngineConfig", + "kotlinx.serialization.json.Json", + "androidx.datastore.core.DataStore", + ) + reflectionTypes.forEach { className -> + extraTypes.add(Class.forName(className).kotlin) + } + + allModules.forEach { module -> + module.verify(extraTypes = extraTypes) + } + } +} diff --git a/build-logic/CLAUDE.md b/build-logic/CLAUDE.md index 33b13b8..45df9ec 100644 --- a/build-logic/CLAUDE.md +++ b/build-logic/CLAUDE.md @@ -9,8 +9,8 @@ Convention plugins that apply consistent Gradle configuration across all modules | `librechat.android.application` | `AndroidApplicationConventionPlugin` | AGP application plugin, compileSdk 35, minSdk 26, JDK 17 | | `librechat.android.library` | `AndroidLibraryConventionPlugin` | AGP library plugin, same SDK/JDK config | | `librechat.android.compose` | `AndroidComposeConventionPlugin` | Compose compiler, Compose BOM, Material 3 | -| `librechat.android.hilt` | `AndroidHiltConventionPlugin` | Hilt + KSP annotation processing | -| `librechat.android.feature` | `AndroidFeatureConventionPlugin` | Auto-applies: library + compose + hilt + serialization | +| `librechat.android.koin` | `AndroidKoinConventionPlugin` | Koin core + Android dependencies | +| `librechat.android.feature` | `AndroidFeatureConventionPlugin` | Auto-applies: library + compose + koin + serialization | | `librechat.android.room` | `AndroidRoomConventionPlugin` | Room + KSP, schema export config | | `librechat.kotlin.serialization` | `KotlinSerializationConventionPlugin` | Kotlinx Serialization plugin + JSON dependency | @@ -28,10 +28,10 @@ the `with` result instead of `Unit`, which causes a Gradle error. ## Version Catalog All dependency versions live in `gradle/libs.versions.toml`. Never hardcode versions in -build.gradle.kts files. Use `libs.` accessors (e.g., `libs.hilt.android`, `libs.ktor.client.core`). +build.gradle.kts files. Use `libs.` accessors (e.g., `libs.koin.android`, `libs.ktor.client.core`). ## Feature Module Convention Feature modules use `id("librechat.android.feature")` which auto-applies library, compose, -hilt, and serialization. They only need to add feature-specific dependencies. +koin, and serialization. They only need to add feature-specific dependencies. Feature modules depend on `:core:*` only, never on other feature modules. diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 16685bb..60c985b 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -24,10 +24,6 @@ gradlePlugin { id = "librechat.android.compose" implementationClass = "AndroidComposeConventionPlugin" } - register("androidHilt") { - id = "librechat.android.hilt" - implementationClass = "AndroidHiltConventionPlugin" - } register("androidFeature") { id = "librechat.android.feature" implementationClass = "AndroidFeatureConventionPlugin" @@ -36,6 +32,10 @@ gradlePlugin { id = "librechat.android.room" implementationClass = "AndroidRoomConventionPlugin" } + register("androidKoin") { + id = "librechat.android.koin" + implementationClass = "AndroidKoinConventionPlugin" + } register("kotlinSerialization") { id = "librechat.kotlin.serialization" implementationClass = "KotlinSerializationConventionPlugin" diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 1392ff1..3d72f8e 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -7,14 +7,16 @@ class AndroidFeatureConventionPlugin : Plugin { with(target) { pluginManager.apply("librechat.android.library") pluginManager.apply("librechat.android.compose") - pluginManager.apply("librechat.android.hilt") + pluginManager.apply("librechat.android.koin") dependencies { add("implementation", project(":core:ui")) add("implementation", project(":core:data")) add("implementation", project(":core:model")) add("implementation", project(":core:common")) - add("implementation", libs.findLibrary("hilt-navigation-compose").get()) + add("implementation", libs.findLibrary("koin-compose").get()) + add("implementation", libs.findLibrary("koin-compose-viewmodel").get()) + add("implementation", libs.findLibrary("koin-compose-viewmodel-navigation").get()) add("implementation", libs.findLibrary("navigation-compose").get()) add("implementation", libs.findBundle("lifecycle").get()) } diff --git a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidKoinConventionPlugin.kt similarity index 51% rename from build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt rename to build-logic/convention/src/main/kotlin/AndroidKoinConventionPlugin.kt index ecf2b6d..5a3da6c 100644 --- a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidKoinConventionPlugin.kt @@ -2,15 +2,14 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies -class AndroidHiltConventionPlugin : Plugin { +class AndroidKoinConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("com.google.dagger.hilt.android") - pluginManager.apply("com.google.devtools.ksp") - dependencies { - add("implementation", libs.findLibrary("hilt-android").get()) - add("ksp", libs.findLibrary("hilt-compiler").get()) + add("implementation", platform(libs.findLibrary("koin-bom").get())) + add("implementation", libs.findLibrary("koin-core").get()) + add("implementation", libs.findLibrary("koin-android").get()) + add("testImplementation", libs.findLibrary("koin-test").get()) } } } diff --git a/build.gradle.kts b/build.gradle.kts index fbf2d97..31b1312 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,5 @@ plugins { alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.ksp) apply false - alias(libs.plugins.hilt) apply false alias(libs.plugins.room) apply false } diff --git a/core/common/CLAUDE.md b/core/common/CLAUDE.md index 87f9aa5..a198a36 100644 --- a/core/common/CLAUDE.md +++ b/core/common/CLAUDE.md @@ -5,8 +5,7 @@ Pure Kotlin utilities shared by all modules. This is the lowest layer -- no othe ## What This Module Provides - **Result sealed class** (`result/Result.kt`): `Success`, `Error(exception, message)`, `Loading`. Used by repositories and ViewModels to propagate outcomes. -- **Dispatcher DI** (`di/DispatcherModule.kt`): `@Dispatcher(IO)`, `@Dispatcher(Default)`, `@Dispatcher(Main)` qualifiers for Hilt injection. Always inject dispatchers -- never hardcode `Dispatchers.IO`. -- **CoroutineScope DI** (`di/CoroutineScopeModule.kt`): `@ApplicationScope` for work that outlives ViewModels. +- **Dispatcher & Scope DI** (`di/CommonModule.kt`): Named Koin qualifiers (`named("io")`, `named("default")`, `named("main")`) for dispatchers and `named("applicationScope")` for coroutine scope. Always inject dispatchers -- never hardcode `Dispatchers.IO`. - **Extensions** (`extensions/`): `StringExt`, `DateExt`, `FlowExt` (includes `retryWithBackoff`, `throttleFirst`). - **ConnectivityObserver**: Wraps Android `ConnectivityManager.NetworkCallback` to detect network changes. Used by SSE reconnection logic. @@ -35,7 +34,7 @@ This lives here (not in `:core:network`) because repositories in `:core:data` ca ## Rules -- **Pure Kotlin preferred.** Minimal Android dependencies (only what ConnectivityObserver and Hilt require). +- **Pure Kotlin preferred.** Minimal Android dependencies (only what ConnectivityObserver and Koin require). - **No network or data dependencies.** This module must not depend on `:core:network`, `:core:data`, or `:core:model`. -- Dependencies: `coroutines-core`, `coroutines-android`, Hilt. -- Convention plugin: `librechat.android.library` + `librechat.android.hilt`. +- Dependencies: `coroutines-core`, `coroutines-android`, Koin. +- Convention plugin: `librechat.android.library` + `librechat.android.koin`. diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index af8c2eb..0c08f9b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("librechat.android.library") - id("librechat.android.hilt") + id("librechat.android.koin") } android { diff --git a/core/common/src/main/kotlin/com/librechat/android/core/common/di/CommonModule.kt b/core/common/src/main/kotlin/com/librechat/android/core/common/di/CommonModule.kt new file mode 100644 index 0000000..042c2cc --- /dev/null +++ b/core/common/src/main/kotlin/com/librechat/android/core/common/di/CommonModule.kt @@ -0,0 +1,26 @@ +package com.librechat.android.core.common.di + +import com.librechat.android.core.common.network.ConnectivityObserver +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val commonModule = module { + // Dispatchers + single(KoinQualifiers.IO) { Dispatchers.IO } + single(KoinQualifiers.Default) { Dispatchers.Default } + single(KoinQualifiers.Main) { Dispatchers.Main } + + // Note: This scope lives for the entire app process lifetime. + // Coroutines launched here persist across logout/login cycles. + // If session-scoped work is needed, use a dedicated session scope instead. + single(KoinQualifiers.ApplicationScope) { + CoroutineScope(SupervisorJob() + get(KoinQualifiers.Default)) + } + + // Connectivity + single { ConnectivityObserver(androidContext()) } +} diff --git a/core/common/src/main/kotlin/com/librechat/android/core/common/di/CoroutineScopeModule.kt b/core/common/src/main/kotlin/com/librechat/android/core/common/di/CoroutineScopeModule.kt deleted file mode 100644 index 7f04517..0000000 --- a/core/common/src/main/kotlin/com/librechat/android/core/common/di/CoroutineScopeModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.librechat.android.core.common.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import javax.inject.Qualifier -import javax.inject.Singleton - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class ApplicationScope - -@Module -@InstallIn(SingletonComponent::class) -object CoroutineScopeModule { - @Provides - @Singleton - @ApplicationScope - fun provideApplicationScope( - @Dispatcher(LibreChatDispatchers.Default) dispatcher: CoroutineDispatcher, - ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) -} diff --git a/core/common/src/main/kotlin/com/librechat/android/core/common/di/DispatcherModule.kt b/core/common/src/main/kotlin/com/librechat/android/core/common/di/DispatcherModule.kt deleted file mode 100644 index ed5301d..0000000 --- a/core/common/src/main/kotlin/com/librechat/android/core/common/di/DispatcherModule.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.librechat.android.core.common.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.RUNTIME) -annotation class Dispatcher(val dispatcher: LibreChatDispatchers) - -enum class LibreChatDispatchers { - IO, - Default, - Main, -} - -@Module -@InstallIn(SingletonComponent::class) -object DispatcherModule { - @Provides - @Dispatcher(LibreChatDispatchers.IO) - fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO - - @Provides - @Dispatcher(LibreChatDispatchers.Default) - fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default - - @Provides - @Dispatcher(LibreChatDispatchers.Main) - fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main -} diff --git a/core/common/src/main/kotlin/com/librechat/android/core/common/di/KoinQualifiers.kt b/core/common/src/main/kotlin/com/librechat/android/core/common/di/KoinQualifiers.kt new file mode 100644 index 0000000..adbedc9 --- /dev/null +++ b/core/common/src/main/kotlin/com/librechat/android/core/common/di/KoinQualifiers.kt @@ -0,0 +1,12 @@ +package com.librechat.android.core.common.di + +import org.koin.core.qualifier.named + +object KoinQualifiers { + val IO = named("io") + val Default = named("default") + val Main = named("main") + val ApplicationScope = named("applicationScope") + val Streaming = named("streaming") + val Refresh = named("refresh") +} diff --git a/core/common/src/main/kotlin/com/librechat/android/core/common/network/ConnectivityObserver.kt b/core/common/src/main/kotlin/com/librechat/android/core/common/network/ConnectivityObserver.kt index 73f89b6..89999f9 100644 --- a/core/common/src/main/kotlin/com/librechat/android/core/common/network/ConnectivityObserver.kt +++ b/core/common/src/main/kotlin/com/librechat/android/core/common/network/ConnectivityObserver.kt @@ -6,17 +6,13 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ConnectivityObserver @Inject constructor( - @ApplicationContext private val context: Context, +class ConnectivityObserver( + private val context: Context, ) { // Permission is declared in app module's AndroidManifest.xml @SuppressLint("MissingPermission") diff --git a/core/common/src/test/kotlin/com/librechat/android/core/common/di/CommonModuleVerificationTest.kt b/core/common/src/test/kotlin/com/librechat/android/core/common/di/CommonModuleVerificationTest.kt new file mode 100644 index 0000000..589da75 --- /dev/null +++ b/core/common/src/test/kotlin/com/librechat/android/core/common/di/CommonModuleVerificationTest.kt @@ -0,0 +1,16 @@ +package com.librechat.android.core.common.di + +import org.junit.Test +import org.koin.test.verify.verify + +class CommonModuleVerificationTest { + @Test + fun verifyCommonModule() { + commonModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + ), + ) + } +} diff --git a/core/data/CLAUDE.md b/core/data/CLAUDE.md index 386fee1..59d832d 100644 --- a/core/data/CLAUDE.md +++ b/core/data/CLAUDE.md @@ -24,7 +24,7 @@ interface ConversationRepository { } ``` -All impls are `@Inject constructor(api, dao, mapper, @Dispatcher(IO) dispatcher)`. +All impls take constructor parameters (api, dao, mapper, dispatcher) wired via Koin named qualifiers. ### Read-Through Cache @@ -46,16 +46,16 @@ Wrap `EncryptedSharedPreferences` in try/catch -- some OEM devices have broken K ### DataModule Bindings -`DataModule` is a Hilt `@Module` that `@Binds` repository interfaces to their implementations and `@Provides` the Room database, DAOs, and DataStore instances. +`DataModule.kt` defines a Koin `module { }` that binds repository interfaces to their implementations via `singleOf(::Impl) bind Interface::class` and provides the Room database, DAOs, and DataStore instances. ## Room TypeConverters -`Converters.kt` handles JSON serialization for complex Room fields: `List`, `MessageContentPart` lists, `Feedback`, `FileReference` lists, etc. Uses the same `Json` instance from the Hilt graph. +`Converters.kt` handles JSON serialization for complex Room fields: `List`, `MessageContentPart` lists, `Feedback`, `FileReference` lists, etc. Uses the same `Json` instance from the Koin graph. ## Rules -- Dependencies: `:core:network`, `:core:model`, `:core:common`, Room, DataStore, security-crypto, Ktor, kotlinx-serialization, Timber, Hilt. -- Convention plugins: `librechat.android.library` + `librechat.android.hilt` + `librechat.android.room` + `librechat.kotlin.serialization`. +- Dependencies: `:core:network`, `:core:model`, `:core:common`, Room, DataStore, security-crypto, Ktor, kotlinx-serialization, Timber, Koin. +- Convention plugins: `librechat.android.library` + `librechat.android.koin` + `librechat.android.room` + `librechat.kotlin.serialization`. - Repositories must use `safeApiCall` from `:core:common` for all network calls. - Entities are internal to this module -- feature modules work with domain models from `:core:model`. - All DAO read methods that the UI observes should return `Flow`, not suspend functions. @@ -63,7 +63,7 @@ Wrap `EncryptedSharedPreferences` in try/catch -- some OEM devices have broken K ### New Repositories (Round 2) - `MemoryRepository` / `MemoryRepositoryImpl` — wraps `MemoriesApi` for memory CRUD + preferences - `McpRepository` / `McpRepositoryImpl` — wraps `McpApi` for server CRUD, tools, connection status, reinitialize -- Both bound in `DataModule.kt` via `@Binds` +- Both bound in `DataModule.kt` via `singleOf` - Both use `safeApiCall` — no local caching (server is sole source of truth for memories and MCP) - **Gotcha**: Memory delete/update use `key` as identifier (not a separate ID field) - **Gotcha**: MCP server operations use `serverName` as identifier @@ -71,4 +71,4 @@ Wrap `EncryptedSharedPreferences` in try/catch -- some OEM devices have broken K ### New Repositories (Round 3) - `BannerRepository` / `BannerRepositoryImpl` — wraps `BannerApi` for fetching server banners - `AgentRepository.getAgentsPaginated()` — server-side paginated agent fetch, maps response to `PaginatedAgents` domain model -- Both bound in `DataModule.kt` via `@Binds`, both use `safeApiCall`, no local caching +- Both bound in `DataModule.kt` via `singleOf`, both use `safeApiCall`, no local caching diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 483ab51..aae6933 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("librechat.android.library") - id("librechat.android.hilt") + id("librechat.android.koin") id("librechat.android.room") id("librechat.kotlin.serialization") } diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ConfigCacheDataStore.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ConfigCacheDataStore.kt index a2aa7dd..6040fe1 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ConfigCacheDataStore.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ConfigCacheDataStore.kt @@ -12,11 +12,8 @@ import kotlinx.serialization.builtins.MapSerializer import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.Json import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ConfigCacheDataStore @Inject constructor( +class ConfigCacheDataStore( private val dataStore: DataStore, private val json: Json, ) { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ServerDataStore.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ServerDataStore.kt index 0d947db..698e886 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ServerDataStore.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ServerDataStore.kt @@ -11,11 +11,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ServerDataStore @Inject constructor( +class ServerDataStore( private val dataStore: DataStore, ) : ServerUrlProvider { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/SettingsDataStore.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/SettingsDataStore.kt index ae51502..2a56ed2 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/SettingsDataStore.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/SettingsDataStore.kt @@ -9,8 +9,6 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton enum class LatexRenderer { NATIVE, KATEX; @@ -46,8 +44,7 @@ enum class ChatFontSize { } } -@Singleton -class SettingsDataStore @Inject constructor( +class SettingsDataStore( private val dataStore: DataStore, ) { val latexRenderer: Flow = dataStore.data.map { prefs -> diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ThemeDataStore.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ThemeDataStore.kt index ffe011d..1339384 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ThemeDataStore.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/ThemeDataStore.kt @@ -6,15 +6,12 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton enum class ThemeMode { SYSTEM, LIGHT, DARK } -@Singleton -class ThemeDataStore @Inject constructor( +class ThemeDataStore( private val dataStore: DataStore, ) { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt index 0621a03..301d140 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/datastore/TokenDataStore.kt @@ -8,8 +8,6 @@ import com.librechat.android.core.model.response.RefreshResponse import com.librechat.android.core.network.client.CookieHelper import com.librechat.android.core.network.client.SecureTokenStorage import com.librechat.android.core.network.client.TokenManager -import com.librechat.android.core.network.di.RefreshClient -import dagger.hilt.android.qualifiers.ApplicationContext import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.ClientRequestException @@ -25,13 +23,10 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class TokenDataStore @Inject constructor( - @ApplicationContext context: Context, - @RefreshClient private val refreshClient: dagger.Lazy, +class TokenDataStore( + context: Context, + private val refreshClient: Lazy, ) : TokenManager, SecureTokenStorage { private val masterKey: MasterKey = MasterKey.Builder(context) @@ -81,7 +76,7 @@ class TokenDataStore @Inject constructor( // The LibreChat backend reads from req.cookies.refreshToken first, // falling back to req.body.refreshToken. Sending both ensures // compatibility across backend versions. - val httpResponse: HttpResponse = refreshClient.get() + val httpResponse: HttpResponse = refreshClient.value .post("/api/auth/refresh") { header("Cookie", "refreshToken=$storedRefreshToken") setBody(mapOf("refreshToken" to storedRefreshToken)) diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/di/DataModule.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/di/DataModule.kt index b275ce0..2934101 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/di/DataModule.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/di/DataModule.kt @@ -4,22 +4,24 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import androidx.room.Room +import com.librechat.android.core.common.di.KoinQualifiers +import com.librechat.android.core.data.datastore.ConfigCacheDataStore import com.librechat.android.core.data.datastore.ServerDataStore +import com.librechat.android.core.data.datastore.SettingsDataStore +import com.librechat.android.core.data.datastore.ThemeDataStore import com.librechat.android.core.data.datastore.TokenDataStore +import com.librechat.android.core.data.db.LibreChatDatabase import com.librechat.android.core.data.repository.AgentRepository import com.librechat.android.core.data.repository.AgentRepositoryImpl import com.librechat.android.core.data.repository.ApiKeyRepository import com.librechat.android.core.data.repository.ApiKeyRepositoryImpl import com.librechat.android.core.data.repository.AuthRepository +import com.librechat.android.core.data.repository.AuthRepositoryImpl import com.librechat.android.core.data.repository.BalanceRepository import com.librechat.android.core.data.repository.BalanceRepositoryImpl import com.librechat.android.core.data.repository.BannerRepository import com.librechat.android.core.data.repository.BannerRepositoryImpl -import com.librechat.android.core.data.repository.SearchRepository -import com.librechat.android.core.data.repository.SearchRepositoryImpl -import com.librechat.android.core.data.repository.TagRepository -import com.librechat.android.core.data.repository.TagRepositoryImpl -import com.librechat.android.core.data.repository.AuthRepositoryImpl import com.librechat.android.core.data.repository.ChatRepository import com.librechat.android.core.data.repository.ChatRepositoryImpl import com.librechat.android.core.data.repository.ConfigRepository @@ -42,132 +44,108 @@ import com.librechat.android.core.data.repository.PresetRepository import com.librechat.android.core.data.repository.PresetRepositoryImpl import com.librechat.android.core.data.repository.PromptRepository import com.librechat.android.core.data.repository.PromptRepositoryImpl +import com.librechat.android.core.data.repository.SearchRepository +import com.librechat.android.core.data.repository.SearchRepositoryImpl import com.librechat.android.core.data.repository.ShareRepository import com.librechat.android.core.data.repository.ShareRepositoryImpl import com.librechat.android.core.data.repository.SpeechRepository import com.librechat.android.core.data.repository.SpeechRepositoryImpl +import com.librechat.android.core.data.repository.TagRepository +import com.librechat.android.core.data.repository.TagRepositoryImpl import com.librechat.android.core.data.repository.UserRepository import com.librechat.android.core.data.repository.UserRepositoryImpl import com.librechat.android.core.network.client.SecureTokenStorage import com.librechat.android.core.network.client.ServerUrlProvider import com.librechat.android.core.network.client.TokenManager -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton +import io.ktor.client.HttpClient +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.binds +import org.koin.dsl.module private val Context.settingsDataStore: DataStore by preferencesDataStore( name = "librechat_settings", ) -@Module -@InstallIn(SingletonComponent::class) -abstract class DataModule { +val dataModule = module { - @Binds - @Singleton - abstract fun bindTokenManager(impl: TokenDataStore): TokenManager + // --- Database + DAOs --- - @Binds - @Singleton - abstract fun bindSecureTokenStorage(impl: TokenDataStore): SecureTokenStorage - - @Binds - @Singleton - abstract fun bindServerUrlProvider(impl: ServerDataStore): ServerUrlProvider - - @Binds - @Singleton - abstract fun bindAuthRepository(impl: AuthRepositoryImpl): AuthRepository - - @Binds - @Singleton - abstract fun bindBalanceRepository(impl: BalanceRepositoryImpl): BalanceRepository - - @Binds - @Singleton - abstract fun bindConfigRepository(impl: ConfigRepositoryImpl): ConfigRepository - - @Binds - @Singleton - abstract fun bindConversationRepository(impl: ConversationRepositoryImpl): ConversationRepository - - @Binds - @Singleton - abstract fun bindDraftRepository(impl: DraftRepositoryImpl): DraftRepository - - @Binds - @Singleton - abstract fun bindMessageRepository(impl: MessageRepositoryImpl): MessageRepository - - @Binds - @Singleton - abstract fun bindChatRepository(impl: ChatRepositoryImpl): ChatRepository - - @Binds - @Singleton - abstract fun bindAgentRepository(impl: AgentRepositoryImpl): AgentRepository - - @Binds - @Singleton - abstract fun bindTagRepository(impl: TagRepositoryImpl): TagRepository - - @Binds - @Singleton - abstract fun bindSearchRepository(impl: SearchRepositoryImpl): SearchRepository - - @Binds - @Singleton - abstract fun bindFileRepository(impl: FileRepositoryImpl): FileRepository - - @Binds - @Singleton - abstract fun bindKeyRepository(impl: KeyRepositoryImpl): KeyRepository - - @Binds - @Singleton - abstract fun bindApiKeyRepository(impl: ApiKeyRepositoryImpl): ApiKeyRepository - - @Binds - @Singleton - abstract fun bindMcpRepository(impl: McpRepositoryImpl): McpRepository - - @Binds - @Singleton - abstract fun bindMemoryRepository(impl: MemoryRepositoryImpl): MemoryRepository - - @Binds - @Singleton - abstract fun bindPresetRepository(impl: PresetRepositoryImpl): PresetRepository - - @Binds - @Singleton - abstract fun bindPromptRepository(impl: PromptRepositoryImpl): PromptRepository - - @Binds - @Singleton - abstract fun bindShareRepository(impl: ShareRepositoryImpl): ShareRepository - - @Binds - @Singleton - abstract fun bindSpeechRepository(impl: SpeechRepositoryImpl): SpeechRepository - - @Binds - @Singleton - abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository - - @Binds - @Singleton - abstract fun bindBannerRepository(impl: BannerRepositoryImpl): BannerRepository - - companion object { - @Provides - @Singleton - fun provideDataStore( - @ApplicationContext context: Context, - ): DataStore = context.settingsDataStore + single { + Room.databaseBuilder(androidContext(), LibreChatDatabase::class.java, "librechat.db").build() } + single { get().conversationDao() } + single { get().messageDao() } + single { get().fileDao() } + single { get().agentDao() } + single { get().presetDao() } + single { get().conversationTagDao() } + single { get().draftDao() } + + // --- DataStore --- + + single> { androidContext().settingsDataStore } + + // --- Datastores --- + + single { + TokenDataStore( + context = androidContext(), + refreshClient = lazy(LazyThreadSafetyMode.NONE) { get(KoinQualifiers.Refresh) }, + ) + } binds arrayOf(TokenManager::class, SecureTokenStorage::class) + + singleOf(::ServerDataStore) bind ServerUrlProvider::class + singleOf(::ThemeDataStore) + singleOf(::ConfigCacheDataStore) + singleOf(::SettingsDataStore) + + // --- Repositories (special wiring) --- + + single { + AuthRepositoryImpl( + authApi = get(), + userApi = get(), + tokenManager = get(), + context = androidContext(), + ) + } + + single { + ChatRepositoryImpl( + chatApi = get(), + sseClient = get(), + sseHttpClient = get(KoinQualifiers.Streaming), + connectivityObserver = get(), + ) + } + + single { + DraftRepositoryImpl( + draftDao = get(), + ioDispatcher = get(KoinQualifiers.IO), + ) + } + + // --- Repositories (simple auto-wiring) --- + + singleOf(::BalanceRepositoryImpl) bind BalanceRepository::class + singleOf(::ConfigRepositoryImpl) bind ConfigRepository::class + singleOf(::ConversationRepositoryImpl) bind ConversationRepository::class + singleOf(::MessageRepositoryImpl) bind MessageRepository::class + singleOf(::FileRepositoryImpl) bind FileRepository::class + singleOf(::AgentRepositoryImpl) bind AgentRepository::class + singleOf(::TagRepositoryImpl) bind TagRepository::class + singleOf(::SearchRepositoryImpl) bind SearchRepository::class + singleOf(::KeyRepositoryImpl) bind KeyRepository::class + singleOf(::ApiKeyRepositoryImpl) bind ApiKeyRepository::class + singleOf(::McpRepositoryImpl) bind McpRepository::class + singleOf(::MemoryRepositoryImpl) bind MemoryRepository::class + singleOf(::PresetRepositoryImpl) bind PresetRepository::class + singleOf(::PromptRepositoryImpl) bind PromptRepository::class + singleOf(::ShareRepositoryImpl) bind ShareRepository::class + singleOf(::SpeechRepositoryImpl) bind SpeechRepository::class + singleOf(::UserRepositoryImpl) bind UserRepository::class + singleOf(::BannerRepositoryImpl) bind BannerRepository::class } diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/di/DatabaseModule.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/di/DatabaseModule.kt deleted file mode 100644 index 0b70dfa..0000000 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/di/DatabaseModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.librechat.android.core.data.di - -import android.content.Context -import androidx.room.Room -import com.librechat.android.core.data.db.LibreChatDatabase -import com.librechat.android.core.data.db.dao.AgentDao -import com.librechat.android.core.data.db.dao.ConversationDao -import com.librechat.android.core.data.db.dao.ConversationTagDao -import com.librechat.android.core.data.db.dao.DraftDao -import com.librechat.android.core.data.db.dao.FileDao -import com.librechat.android.core.data.db.dao.MessageDao -import com.librechat.android.core.data.db.dao.PresetDao -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object DatabaseModule { - - @Provides - @Singleton - fun provideDatabase(@ApplicationContext context: Context): LibreChatDatabase = - Room.databaseBuilder( - context, - LibreChatDatabase::class.java, - "librechat.db", - ).build() - - @Provides fun provideConversationDao(db: LibreChatDatabase): ConversationDao = db.conversationDao() - @Provides fun provideMessageDao(db: LibreChatDatabase): MessageDao = db.messageDao() - @Provides fun provideFileDao(db: LibreChatDatabase): FileDao = db.fileDao() - @Provides fun provideAgentDao(db: LibreChatDatabase): AgentDao = db.agentDao() - @Provides fun providePresetDao(db: LibreChatDatabase): PresetDao = db.presetDao() - @Provides fun provideConversationTagDao(db: LibreChatDatabase): ConversationTagDao = db.conversationTagDao() - @Provides fun provideDraftDao(db: LibreChatDatabase): DraftDao = db.draftDao() -} diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt index 34f54ca..14867d6 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AgentRepositoryImpl.kt @@ -18,11 +18,8 @@ import com.librechat.android.core.model.request.RevertAgentRequest import com.librechat.android.core.model.request.ToolCallRequest import com.librechat.android.core.model.request.UpdateAgentRequest import com.librechat.android.core.network.api.AgentsApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class AgentRepositoryImpl @Inject constructor( +class AgentRepositoryImpl( private val agentsApi: AgentsApi, ) : AgentRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ApiKeyRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ApiKeyRepositoryImpl.kt index 4ab5e19..8da4225 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ApiKeyRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ApiKeyRepositoryImpl.kt @@ -5,11 +5,8 @@ import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.ApiKey import com.librechat.android.core.model.request.CreateApiKeyRequest import com.librechat.android.core.network.api.ApiKeysApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ApiKeyRepositoryImpl @Inject constructor( +class ApiKeyRepositoryImpl( private val apiKeysApi: ApiKeysApi, ) : ApiKeyRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AuthRepositoryImpl.kt index a213c84..67bae6f 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AuthRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/AuthRepositoryImpl.kt @@ -9,18 +9,14 @@ import com.librechat.android.core.model.response.TwoFactorSetupResponse import com.librechat.android.core.network.api.AuthApi import com.librechat.android.core.network.api.UserApi import com.librechat.android.core.network.client.TokenManager -import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import java.io.File -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class AuthRepositoryImpl @Inject constructor( +class AuthRepositoryImpl( private val authApi: AuthApi, private val userApi: UserApi, private val tokenManager: TokenManager, - @ApplicationContext private val context: Context, + private val context: Context, ) : AuthRepository { override suspend fun login(email: String, password: String): Result { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BalanceRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BalanceRepositoryImpl.kt index 5f9009e..393ce5e 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BalanceRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BalanceRepositoryImpl.kt @@ -4,11 +4,8 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.Balance import com.librechat.android.core.network.api.BalanceApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class BalanceRepositoryImpl @Inject constructor( +class BalanceRepositoryImpl( private val balanceApi: BalanceApi, ) : BalanceRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BannerRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BannerRepositoryImpl.kt index 1fd869b..e9e5312 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BannerRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/BannerRepositoryImpl.kt @@ -4,11 +4,8 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.Banner import com.librechat.android.core.network.api.BannerApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class BannerRepositoryImpl @Inject constructor( +class BannerRepositoryImpl( private val bannerApi: BannerApi, ) : BannerRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt index 9ef7009..5d104c3 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ChatRepositoryImpl.kt @@ -11,20 +11,16 @@ import com.librechat.android.core.model.request.ChatRequest import com.librechat.android.core.model.request.EphemeralAgent import com.librechat.android.core.model.response.ChatStatusResponse import com.librechat.android.core.network.api.ChatApi -import com.librechat.android.core.network.di.StreamingClient import com.librechat.android.core.network.sse.SseClient import io.ktor.client.HttpClient import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ChatRepositoryImpl @Inject constructor( +class ChatRepositoryImpl( private val chatApi: ChatApi, private val sseClient: SseClient, - @StreamingClient private val sseHttpClient: HttpClient, + private val sseHttpClient: HttpClient, private val connectivityObserver: ConnectivityObserver, ) : ChatRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt index 0a2c854..dee252e 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConfigRepositoryImpl.kt @@ -15,11 +15,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.SerializationException import timber.log.Timber import java.io.IOException -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ConfigRepositoryImpl @Inject constructor( +class ConfigRepositoryImpl( private val configApi: ConfigApi, private val configCache: ConfigCacheDataStore, ) : ConfigRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConversationRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConversationRepositoryImpl.kt index 73adadb..90832bb 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConversationRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ConversationRepositoryImpl.kt @@ -12,11 +12,8 @@ import com.librechat.android.core.network.api.ConversationsApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ConversationRepositoryImpl @Inject constructor( +class ConversationRepositoryImpl( private val conversationsApi: ConversationsApi, private val conversationDao: ConversationDao, ) : ConversationRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/DraftRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/DraftRepositoryImpl.kt index 51a7197..2119953 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/DraftRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/DraftRepositoryImpl.kt @@ -1,7 +1,5 @@ package com.librechat.android.core.data.repository -import com.librechat.android.core.common.di.Dispatcher -import com.librechat.android.core.common.di.LibreChatDispatchers import com.librechat.android.core.data.db.dao.DraftDao import com.librechat.android.core.data.db.entity.DraftEntity import kotlinx.coroutines.CoroutineDispatcher @@ -13,13 +11,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class DraftRepositoryImpl @Inject constructor( +class DraftRepositoryImpl( private val draftDao: DraftDao, - @Dispatcher(LibreChatDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, + private val ioDispatcher: CoroutineDispatcher, ) : DraftRepository { private val cache = ConcurrentHashMap() diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/FileRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/FileRepositoryImpl.kt index a187319..230d020 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/FileRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/FileRepositoryImpl.kt @@ -7,11 +7,8 @@ import com.librechat.android.core.model.request.DeleteFileEntry import com.librechat.android.core.model.request.DeleteFilesRequest import com.librechat.android.core.network.api.FilesApi import com.librechat.android.core.network.api.FilesExtApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class FileRepositoryImpl @Inject constructor( +class FileRepositoryImpl( private val filesApi: FilesApi, private val filesExtApi: FilesExtApi, ) : FileRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/KeyRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/KeyRepositoryImpl.kt index 57845a9..27f3415 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/KeyRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/KeyRepositoryImpl.kt @@ -5,11 +5,8 @@ import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.UserKey import com.librechat.android.core.model.request.UpdateKeyRequest import com.librechat.android.core.network.api.KeysApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class KeyRepositoryImpl @Inject constructor( +class KeyRepositoryImpl( private val keysApi: KeysApi, ) : KeyRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/McpRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/McpRepositoryImpl.kt index 4240e0f..2d5c6c5 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/McpRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/McpRepositoryImpl.kt @@ -10,11 +10,8 @@ import com.librechat.android.core.model.mcp.McpServerStatus import com.librechat.android.core.model.mcp.McpServerType import com.librechat.android.core.model.mcp.McpTool import com.librechat.android.core.network.api.McpApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class McpRepositoryImpl @Inject constructor( +class McpRepositoryImpl( private val mcpApi: McpApi, ) : McpRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MemoryRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MemoryRepositoryImpl.kt index 72f0d1a..3c66031 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MemoryRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MemoryRepositoryImpl.kt @@ -8,11 +8,8 @@ import com.librechat.android.core.model.request.CreateMemoryRequest import com.librechat.android.core.model.request.UpdateMemoryPreferencesRequest import com.librechat.android.core.model.request.UpdateMemoryRequest import com.librechat.android.core.network.api.MemoriesApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MemoryRepositoryImpl @Inject constructor( +class MemoryRepositoryImpl( private val memoriesApi: MemoriesApi, ) : MemoryRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MessageRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MessageRepositoryImpl.kt index 2394149..f6d3cfa 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MessageRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/MessageRepositoryImpl.kt @@ -13,11 +13,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class MessageRepositoryImpl @Inject constructor( +class MessageRepositoryImpl( private val messagesApi: MessagesApi, private val messageDao: MessageDao, ) : MessageRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PresetRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PresetRepositoryImpl.kt index 6556cf1..7468ad2 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PresetRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PresetRepositoryImpl.kt @@ -4,11 +4,8 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.Preset import com.librechat.android.core.network.api.PresetsApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class PresetRepositoryImpl @Inject constructor( +class PresetRepositoryImpl( private val presetsApi: PresetsApi, ) : PresetRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PromptRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PromptRepositoryImpl.kt index 930061f..3d4e8be 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PromptRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/PromptRepositoryImpl.kt @@ -10,11 +10,8 @@ import com.librechat.android.core.model.request.UpdatePromptGroupRequest import com.librechat.android.core.model.request.UpdatePromptTagRequest import com.librechat.android.core.model.response.PromptGroupListResponse import com.librechat.android.core.network.api.PromptsApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class PromptRepositoryImpl @Inject constructor( +class PromptRepositoryImpl( private val promptsApi: PromptsApi, ) : PromptRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SearchRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SearchRepositoryImpl.kt index c1ae542..09c380a 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SearchRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SearchRepositoryImpl.kt @@ -4,11 +4,8 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.Conversation import com.librechat.android.core.network.api.ConversationsApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class SearchRepositoryImpl @Inject constructor( +class SearchRepositoryImpl( private val conversationsApi: ConversationsApi, ) : SearchRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ShareRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ShareRepositoryImpl.kt index b4b1352..8489080 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ShareRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/ShareRepositoryImpl.kt @@ -7,11 +7,8 @@ import com.librechat.android.core.model.response.ShareLinkCheckResponse import com.librechat.android.core.model.response.SharedLinksResponse import com.librechat.android.core.network.api.ShareApi import com.librechat.android.core.network.client.ServerUrlProvider -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class ShareRepositoryImpl @Inject constructor( +class ShareRepositoryImpl( private val shareApi: ShareApi, private val serverUrlProvider: ServerUrlProvider, ) : ShareRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SpeechRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SpeechRepositoryImpl.kt index c54c423..b2e9178 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SpeechRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/SpeechRepositoryImpl.kt @@ -7,11 +7,8 @@ import com.librechat.android.core.model.speech.SpeechToTextResponse import com.librechat.android.core.model.speech.TextToSpeechRequest import com.librechat.android.core.model.speech.TtsVoice import com.librechat.android.core.network.api.SpeechApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class SpeechRepositoryImpl @Inject constructor( +class SpeechRepositoryImpl( private val speechApi: SpeechApi, ) : SpeechRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/TagRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/TagRepositoryImpl.kt index bab763d..691be12 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/TagRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/TagRepositoryImpl.kt @@ -4,11 +4,8 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.common.result.safeApiCall import com.librechat.android.core.model.ConversationTag import com.librechat.android.core.network.api.TagsApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class TagRepositoryImpl @Inject constructor( +class TagRepositoryImpl( private val tagsApi: TagsApi, ) : TagRepository { diff --git a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/UserRepositoryImpl.kt b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/UserRepositoryImpl.kt index b2c63df..3a1622b 100644 --- a/core/data/src/main/kotlin/com/librechat/android/core/data/repository/UserRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/librechat/android/core/data/repository/UserRepositoryImpl.kt @@ -8,11 +8,8 @@ import com.librechat.android.core.model.request.ResendVerificationRequest import com.librechat.android.core.model.request.VerifyEmailRequest import com.librechat.android.core.model.response.TermsResponse import com.librechat.android.core.network.api.UserApi -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class UserRepositoryImpl @Inject constructor( +class UserRepositoryImpl( private val userApi: UserApi, ) : UserRepository { diff --git a/core/data/src/test/kotlin/com/librechat/android/core/data/di/DataModuleVerificationTest.kt b/core/data/src/test/kotlin/com/librechat/android/core/data/di/DataModuleVerificationTest.kt new file mode 100644 index 0000000..e1168b7 --- /dev/null +++ b/core/data/src/test/kotlin/com/librechat/android/core/data/di/DataModuleVerificationTest.kt @@ -0,0 +1,45 @@ +package com.librechat.android.core.data.di + +import org.junit.Test +import org.koin.test.verify.verify + +class DataModuleVerificationTest { + @Test + fun verifyDataModule() { + dataModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + kotlin.Lazy::class, + io.ktor.client.HttpClient::class, + kotlinx.serialization.json.Json::class, + kotlinx.coroutines.CoroutineDispatcher::class, + com.librechat.android.core.common.network.ConnectivityObserver::class, + com.librechat.android.core.network.sse.SseClient::class, + com.librechat.android.core.network.api.AuthApi::class, + com.librechat.android.core.network.api.UserApi::class, + com.librechat.android.core.network.api.ChatApi::class, + com.librechat.android.core.network.api.ConversationsApi::class, + com.librechat.android.core.network.api.MessagesApi::class, + com.librechat.android.core.network.api.FilesApi::class, + com.librechat.android.core.network.api.FilesExtApi::class, + com.librechat.android.core.network.api.AgentsApi::class, + com.librechat.android.core.network.api.PresetsApi::class, + com.librechat.android.core.network.api.PromptsApi::class, + com.librechat.android.core.network.api.TagsApi::class, + com.librechat.android.core.network.api.ShareApi::class, + com.librechat.android.core.network.api.ConfigApi::class, + com.librechat.android.core.network.api.BalanceApi::class, + com.librechat.android.core.network.api.SearchApi::class, + com.librechat.android.core.network.api.KeysApi::class, + com.librechat.android.core.network.api.ApiKeysApi::class, + com.librechat.android.core.network.api.McpApi::class, + com.librechat.android.core.network.api.MemoriesApi::class, + com.librechat.android.core.network.api.SpeechApi::class, + com.librechat.android.core.network.api.BannerApi::class, + androidx.datastore.core.DataStore::class, + ), + ) + } +} diff --git a/core/network/CLAUDE.md b/core/network/CLAUDE.md index d83ed2f..f189dca 100644 --- a/core/network/CLAUDE.md +++ b/core/network/CLAUDE.md @@ -4,11 +4,11 @@ Ktor HttpClient, API service classes, SSE streaming client, auth interceptor. Al ## What This Module Provides -- **Ktor HttpClient factory** (`di/NetworkModule.kt`): Provides singleton `HttpClient(OkHttp)` with ContentNegotiation, Logging, HttpTimeout, HttpRequestRetry, and AuthInterceptorPlugin. +- **Ktor HttpClient factory** (`di/NetworkModule.kt`): Provides singleton `HttpClient(OkHttp)` with ContentNegotiation, Logging, HttpTimeout, HttpRequestRetry, and AuthInterceptorPlugin via Koin module. - **AuthInterceptorPlugin** (`client/AuthInterceptor.kt`): Custom Ktor plugin that injects `Authorization: Bearer` on outgoing requests and retries on 401 after refreshing tokens. Skips auth endpoints (`auth/login`, `auth/register`, `auth/refresh`, etc.). - **TokenManager interface** (`client/TokenManager.kt`): `getAccessToken()`, `setTokens()`, `refreshAccessToken()` (Mutex-guarded), `clearTokens()`, `sessionExpiredFlow`. Implemented in `:core:data`. - **ServerUrlProvider interface** (`client/ServerUrlProvider.kt`): Resolves the user-configured base URL. Implemented in `:core:data`. -- **API services** (`api/`): One class per domain -- `AuthApi`, `ConversationsApi`, `MessagesApi`, `ChatStreamApi`, `FilesApi`, `AgentsApi`, `PresetsApi`, `PromptsApi`, `TagsApi`, `ShareApi`, `ConfigApi`, `EndpointsApi`, `BalanceApi`, `UserApi`, `SearchApi`. Each is `@Inject constructor(client: HttpClient)`. +- **API services** (`api/`): One class per domain -- `AuthApi`, `ConversationsApi`, `MessagesApi`, `ChatStreamApi`, `FilesApi`, `AgentsApi`, `PresetsApi`, `PromptsApi`, `TagsApi`, `ShareApi`, `ConfigApi`, `EndpointsApi`, `BalanceApi`, `UserApi`, `SearchApi`. Each takes `HttpClient` as a constructor parameter, wired via Koin. - **SSE client** (`sse/`): `SseClient`, `SseEvent`, `SseEventParser`, `SseConnectionManager`. - **DTO mappers** (`mapper/`): Convert network DTOs to domain models from `:core:model`. @@ -40,7 +40,7 @@ On reconnection, append `?resume=true` to get a `sync` event with `runSteps[]` + ## Rules -- Dependencies: `:core:model`, `:core:common`, Ktor bundles, kotlinx-serialization, Timber, Hilt. -- Convention plugins: `librechat.android.library` + `librechat.android.hilt` + `librechat.kotlin.serialization`. +- Dependencies: `:core:model`, `:core:common`, Ktor bundles, kotlinx-serialization, Timber, Koin. +- Convention plugins: `librechat.android.library` + `librechat.android.koin` + `librechat.kotlin.serialization`. - API services must not contain business logic -- they are thin HTTP wrappers. - All `arg`-wrapped endpoints must match the backend pattern: `setBody(mapOf("arg" to mapOf(...)))`. diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index b579ca3..55f5a0c 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("librechat.android.library") - id("librechat.android.hilt") + id("librechat.android.koin") id("librechat.kotlin.serialization") } diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt index 507b5c1..0c49263 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AgentsApi.kt @@ -29,9 +29,8 @@ import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.path import timber.log.Timber -import javax.inject.Inject -class AgentsApi @Inject constructor( +class AgentsApi constructor( private val client: HttpClient, ) { diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ApiKeysApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ApiKeysApi.kt index 2019397..38ff6f3 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ApiKeysApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ApiKeysApi.kt @@ -11,9 +11,8 @@ import io.ktor.client.request.setBody import io.ktor.client.statement.bodyAsText import io.ktor.http.path import kotlinx.serialization.json.Json -import javax.inject.Inject -class ApiKeysApi @Inject constructor( +class ApiKeysApi constructor( private val client: HttpClient, ) { private val lenientJson = Json { diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt index a794036..1fdd7cb 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/AuthApi.kt @@ -21,7 +21,6 @@ import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.path import kotlinx.serialization.Serializable -import javax.inject.Inject data class LoginResult( val response: LoginResponse, @@ -34,7 +33,7 @@ data class RefreshResult( val newRefreshToken: String?, ) -class AuthApi @Inject constructor( +class AuthApi constructor( private val client: HttpClient, ) { suspend fun login(email: String, password: String): LoginResult { diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/BalanceApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/BalanceApi.kt index cc620f8..baf66fc 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/BalanceApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/BalanceApi.kt @@ -5,9 +5,8 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.http.path -import javax.inject.Inject -class BalanceApi @Inject constructor( +class BalanceApi constructor( private val client: HttpClient, ) { suspend fun getBalance(): Balance = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/BannerApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/BannerApi.kt index b18cc28..fa4915e 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/BannerApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/BannerApi.kt @@ -5,9 +5,8 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.http.path -import javax.inject.Inject -class BannerApi @Inject constructor( +class BannerApi constructor( private val client: HttpClient, ) { suspend fun getBanners(): List = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ChatApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ChatApi.kt index 6d078ed..060bf62 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ChatApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ChatApi.kt @@ -12,9 +12,8 @@ import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.path -import javax.inject.Inject -class ChatApi @Inject constructor( +class ChatApi constructor( private val client: HttpClient, ) { suspend fun startChat(endpoint: String, request: ChatRequest): ChatStartResponse = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConfigApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConfigApi.kt index 710987b..107d50f 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConfigApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConfigApi.kt @@ -9,9 +9,8 @@ import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText import io.ktor.http.path import kotlinx.serialization.json.Json -import javax.inject.Inject -class ConfigApi @Inject constructor( +class ConfigApi constructor( private val client: HttpClient, ) { suspend fun getStartupConfig(): StartupConfig = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt index 4143415..1cb16bf 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ConversationsApi.kt @@ -24,9 +24,8 @@ import io.ktor.client.request.setBody import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.path -import javax.inject.Inject -class ConversationsApi @Inject constructor( +class ConversationsApi constructor( private val client: HttpClient, ) { suspend fun getConversations( diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt index d89cc87..927e493 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesApi.kt @@ -15,9 +15,8 @@ import io.ktor.http.HttpHeaders import io.ktor.http.path import timber.log.Timber import java.util.UUID -import javax.inject.Inject -class FilesApi @Inject constructor( +class FilesApi constructor( private val client: HttpClient, ) { suspend fun getFiles(): List = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesExtApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesExtApi.kt index 9bf98a3..3b97d91 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesExtApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/FilesExtApi.kt @@ -5,9 +5,8 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.http.path -import javax.inject.Inject -class FilesExtApi @Inject constructor( +class FilesExtApi constructor( private val client: HttpClient, ) { suspend fun getAgentFiles(agentId: String): List = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/KeysApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/KeysApi.kt index e0e2eb5..0c58736 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/KeysApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/KeysApi.kt @@ -11,9 +11,8 @@ import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.http.encodeURLPathPart import io.ktor.http.path -import javax.inject.Inject -class KeysApi @Inject constructor( +class KeysApi constructor( private val client: HttpClient, ) { suspend fun getKeyExpiry(): List = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/McpApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/McpApi.kt index ac19b76..fcbe6af 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/McpApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/McpApi.kt @@ -26,9 +26,8 @@ import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import javax.inject.Inject -class McpApi @Inject constructor( +class McpApi constructor( private val client: HttpClient, private val json: Json, ) { diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/MemoriesApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/MemoriesApi.kt index 0ffb99d..d1dc08e 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/MemoriesApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/MemoriesApi.kt @@ -14,9 +14,8 @@ import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.encodeURLPathPart import io.ktor.http.path -import javax.inject.Inject -class MemoriesApi @Inject constructor( +class MemoriesApi constructor( private val client: HttpClient, ) { diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/MessagesApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/MessagesApi.kt index cbc90be..8524e21 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/MessagesApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/MessagesApi.kt @@ -16,9 +16,8 @@ import io.ktor.client.request.post import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.http.path -import javax.inject.Inject -class MessagesApi @Inject constructor( +class MessagesApi constructor( private val client: HttpClient, ) { suspend fun getMessages(conversationId: String): List = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/PresetsApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/PresetsApi.kt index eb04193..7b1600d 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/PresetsApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/PresetsApi.kt @@ -8,9 +8,8 @@ import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.path -import javax.inject.Inject -class PresetsApi @Inject constructor( +class PresetsApi constructor( private val client: HttpClient, ) { suspend fun getPresets(): List = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/PromptsApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/PromptsApi.kt index 0932591..0417c12 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/PromptsApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/PromptsApi.kt @@ -17,9 +17,8 @@ import io.ktor.client.request.patch import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.path -import javax.inject.Inject -class PromptsApi @Inject constructor( +class PromptsApi constructor( private val client: HttpClient, ) { suspend fun getPromptGroups( diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/SearchApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/SearchApi.kt index 61b11d7..56397f4 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/SearchApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/SearchApi.kt @@ -4,9 +4,8 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.http.path -import javax.inject.Inject -class SearchApi @Inject constructor( +class SearchApi constructor( private val client: HttpClient, ) { suspend fun checkSearchEnabled(): Boolean = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ShareApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ShareApi.kt index d50e0e4..e731b16 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/ShareApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/ShareApi.kt @@ -13,9 +13,8 @@ import io.ktor.client.request.patch import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.path -import javax.inject.Inject -class ShareApi @Inject constructor( +class ShareApi constructor( private val client: HttpClient, ) { suspend fun getSharedLinks(cursor: String? = null): SharedLinksResponse = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/SpeechApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/SpeechApi.kt index 5d4386d..a33f030 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/SpeechApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/SpeechApi.kt @@ -16,9 +16,8 @@ import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.contentType import io.ktor.http.path -import javax.inject.Inject -class SpeechApi @Inject constructor( +class SpeechApi constructor( private val client: HttpClient, ) { suspend fun speechToText(audioData: ByteArray, mimeType: String): SpeechToTextResponse { diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/TagsApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/TagsApi.kt index 6437b99..2315eba 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/TagsApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/TagsApi.kt @@ -12,9 +12,8 @@ import io.ktor.client.request.post import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.http.path -import javax.inject.Inject -class TagsApi @Inject constructor( +class TagsApi constructor( private val client: HttpClient, ) { suspend fun getTags(): List = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/api/UserApi.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/api/UserApi.kt index 5c930a8..9d2d597 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/api/UserApi.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/api/UserApi.kt @@ -18,7 +18,6 @@ import io.ktor.http.Headers import io.ktor.http.HttpHeaders import io.ktor.http.path import kotlinx.serialization.Serializable -import javax.inject.Inject @Serializable data class UserUpdateRequest( @@ -36,7 +35,7 @@ data class UpdatePluginsRequest( val plugins: List, ) -class UserApi @Inject constructor( +class UserApi constructor( private val client: HttpClient, ) { suspend fun getUser(): User = diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/di/NetworkModule.kt index 4c75c1d..5c8b8db 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/di/NetworkModule.kt @@ -1,15 +1,32 @@ package com.librechat.android.core.network.di -import android.content.Context +import com.librechat.android.core.common.di.KoinQualifiers +import com.librechat.android.core.network.api.AgentsApi +import com.librechat.android.core.network.api.ApiKeysApi +import com.librechat.android.core.network.api.AuthApi +import com.librechat.android.core.network.api.BalanceApi +import com.librechat.android.core.network.api.BannerApi +import com.librechat.android.core.network.api.ChatApi +import com.librechat.android.core.network.api.ConfigApi +import com.librechat.android.core.network.api.ConversationsApi +import com.librechat.android.core.network.api.FilesApi +import com.librechat.android.core.network.api.FilesExtApi +import com.librechat.android.core.network.api.KeysApi +import com.librechat.android.core.network.api.McpApi +import com.librechat.android.core.network.api.MemoriesApi +import com.librechat.android.core.network.api.MessagesApi +import com.librechat.android.core.network.api.PresetsApi +import com.librechat.android.core.network.api.PromptsApi +import com.librechat.android.core.network.api.SearchApi +import com.librechat.android.core.network.api.ShareApi +import com.librechat.android.core.network.api.SpeechApi +import com.librechat.android.core.network.api.TagsApi +import com.librechat.android.core.network.api.UserApi +import com.librechat.android.core.network.client.AuthInterceptorPlugin import com.librechat.android.core.network.client.LibreChatHttpClient import com.librechat.android.core.network.client.ServerUrlProvider import com.librechat.android.core.network.client.TokenManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import com.librechat.android.core.network.client.AuthInterceptorPlugin +import com.librechat.android.core.network.sse.SseClient import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout @@ -21,87 +38,98 @@ import io.ktor.http.contentType import io.ktor.http.takeFrom import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import javax.inject.Singleton +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module -@Module -@InstallIn(SingletonComponent::class) -object NetworkModule { +val networkModule = module { - @Provides - @Singleton - fun provideJson(): Json = Json { - ignoreUnknownKeys = true - isLenient = true - encodeDefaults = false - explicitNulls = false - coerceInputValues = true + single { + Json { + ignoreUnknownKeys = true + isLenient = true + encodeDefaults = false + explicitNulls = false + coerceInputValues = true + } } - @Provides - @Singleton - fun provideHttpClient( - @ApplicationContext context: Context, - json: Json, - tokenManager: TokenManager, - serverUrlProvider: ServerUrlProvider, - ): HttpClient = LibreChatHttpClient.create( - json = json, - tokenManager = tokenManager, - serverUrlProvider = serverUrlProvider, - cacheDir = context.cacheDir, - ) + single { + LibreChatHttpClient.create( + json = get(), + tokenManager = get(), + serverUrlProvider = get(), + cacheDir = androidContext().cacheDir, + ) + } - /** - * Minimal HttpClient for SSE streaming. No ContentNegotiation (avoids - * response body buffering), infinite request/socket timeouts (streams - * can run for minutes), and no response validator (SSE errors are - * handled by SseClient's retry logic). - */ - @Provides - @Singleton - @StreamingClient - fun provideSseClient( - tokenManager: TokenManager, - serverUrlProvider: ServerUrlProvider, - ): HttpClient = HttpClient(OkHttp) { - install(AuthInterceptorPlugin) { - this.tokenManager = tokenManager - } - install(HttpTimeout) { - connectTimeoutMillis = 10_000 - requestTimeoutMillis = Long.MAX_VALUE - socketTimeoutMillis = Long.MAX_VALUE - } - defaultRequest { - val baseUrl = serverUrlProvider.getBaseUrl() - if (baseUrl.isNotEmpty()) { - url.takeFrom(baseUrl) + single(KoinQualifiers.Streaming) { + val tokenManager = get() + val serverUrlProvider = get() + HttpClient(OkHttp) { + install(AuthInterceptorPlugin) { + this.tokenManager = tokenManager + } + install(HttpTimeout) { + connectTimeoutMillis = 10_000 + requestTimeoutMillis = Long.MAX_VALUE + socketTimeoutMillis = Long.MAX_VALUE + } + defaultRequest { + val baseUrl = serverUrlProvider.getBaseUrl() + if (baseUrl.isNotEmpty()) { + url.takeFrom(baseUrl) + } + headers.append( + HttpHeaders.UserAgent, + "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", + ) } - headers.append( - HttpHeaders.UserAgent, - "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36", - ) } } - @Provides - @Singleton - @RefreshClient - fun provideRefreshClient( - json: Json, - serverUrlProvider: ServerUrlProvider, - ): HttpClient = HttpClient(OkHttp) { - install(ContentNegotiation) { json(json) } - install(HttpTimeout) { - requestTimeoutMillis = 15_000 - connectTimeoutMillis = 10_000 - } - defaultRequest { - val baseUrl = serverUrlProvider.getBaseUrl() - if (baseUrl.isNotEmpty()) { - url.takeFrom(baseUrl) + single(KoinQualifiers.Refresh) { + val json = get() + val serverUrlProvider = get() + HttpClient(OkHttp) { + install(ContentNegotiation) { json(json) } + install(HttpTimeout) { + requestTimeoutMillis = 15_000 + connectTimeoutMillis = 10_000 + } + defaultRequest { + val baseUrl = serverUrlProvider.getBaseUrl() + if (baseUrl.isNotEmpty()) { + url.takeFrom(baseUrl) + } + contentType(ContentType.Application.Json) } - contentType(ContentType.Application.Json) } } + + // SSE + singleOf(::SseClient) + + // API services + singleOf(::AgentsApi) + singleOf(::ApiKeysApi) + singleOf(::AuthApi) + singleOf(::BalanceApi) + singleOf(::BannerApi) + singleOf(::ChatApi) + singleOf(::ConfigApi) + singleOf(::ConversationsApi) + singleOf(::FilesApi) + singleOf(::FilesExtApi) + singleOf(::KeysApi) + singleOf(::McpApi) + singleOf(::MemoriesApi) + singleOf(::MessagesApi) + singleOf(::PresetsApi) + singleOf(::PromptsApi) + singleOf(::SearchApi) + singleOf(::ShareApi) + singleOf(::SpeechApi) + singleOf(::TagsApi) + singleOf(::UserApi) } diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/di/RefreshClient.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/di/RefreshClient.kt deleted file mode 100644 index e1d39cc..0000000 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/di/RefreshClient.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.librechat.android.core.network.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class RefreshClient diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/di/StreamingClient.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/di/StreamingClient.kt deleted file mode 100644 index f8e26be..0000000 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/di/StreamingClient.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.librechat.android.core.network.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class StreamingClient diff --git a/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseClient.kt b/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseClient.kt index 14d4d66..1a069c7 100644 --- a/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseClient.kt +++ b/core/network/src/main/kotlin/com/librechat/android/core/network/sse/SseClient.kt @@ -16,10 +16,9 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.serialization.json.Json import timber.log.Timber -import javax.inject.Inject import kotlin.math.min -class SseClient @Inject constructor( +class SseClient( private val json: Json, ) { private val mapper = SseEventMapper(json) diff --git a/core/network/src/test/kotlin/com/librechat/android/core/network/di/NetworkModuleVerificationTest.kt b/core/network/src/test/kotlin/com/librechat/android/core/network/di/NetworkModuleVerificationTest.kt new file mode 100644 index 0000000..8b6d19b --- /dev/null +++ b/core/network/src/test/kotlin/com/librechat/android/core/network/di/NetworkModuleVerificationTest.kt @@ -0,0 +1,19 @@ +package com.librechat.android.core.network.di + +import org.junit.Test +import org.koin.test.verify.verify + +class NetworkModuleVerificationTest { + @Test + fun verifyNetworkModule() { + networkModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + io.ktor.client.engine.HttpClientEngine::class, + com.librechat.android.core.network.client.TokenManager::class, + com.librechat.android.core.network.client.ServerUrlProvider::class, + ), + ) + } +} diff --git a/core/ui/CLAUDE.md b/core/ui/CLAUDE.md index 1cd3ded..4a297f5 100644 --- a/core/ui/CLAUDE.md +++ b/core/ui/CLAUDE.md @@ -43,7 +43,7 @@ Material 3 theme and shared Compose components used across all feature modules. - All components must be stateless or hoist state to the caller. - Dependencies: `:core:model`, `:core:common`, Coil for image loading, Compose libraries. - Convention plugins: `librechat.android.library` + `librechat.android.compose`. -- No Hilt in this module (no `@Inject`, no `@Module`). +- No DI in this module (no Koin modules, no injected classes). - Use `@Preview` annotations on all components for Android Studio preview support. - During SSE streaming, buffer markdown re-renders to ~100ms intervals to avoid frame drops. diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/di/AgentsModule.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/di/AgentsModule.kt new file mode 100644 index 0000000..021b345 --- /dev/null +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/di/AgentsModule.kt @@ -0,0 +1,23 @@ +package com.librechat.android.feature.agents.di + +import com.librechat.android.feature.agents.viewmodel.AgentDetailViewModel +import com.librechat.android.feature.agents.viewmodel.AgentEditorViewModel +import com.librechat.android.feature.agents.viewmodel.AgentMarketplaceViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val agentsModule = module { + viewModelOf(::AgentMarketplaceViewModel) + viewModelOf(::AgentDetailViewModel) + viewModel { + AgentEditorViewModel( + savedStateHandle = get(), + agentRepository = get(), + configRepository = get(), + mcpRepository = get(), + appContext = androidContext(), + ) + } +} diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt index 0b78f03..2e7892b 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentDetailScreen.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.R as CoreUiR import com.librechat.android.core.ui.components.AvatarImage @@ -65,7 +65,7 @@ fun AgentDetailScreen( onEdit: (String) -> Unit, onDuplicated: (String) -> Unit, modifier: Modifier = Modifier, - viewModel: AgentDetailViewModel = hiltViewModel(), + viewModel: AgentDetailViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt index 5fd6ebe..9618b56 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentEditorScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator @@ -79,7 +79,7 @@ fun AgentEditorScreen( modifier: Modifier = Modifier, onDeleted: () -> Unit = onBack, onDuplicated: (String) -> Unit = onSaved, - viewModel: AgentEditorViewModel = hiltViewModel(), + viewModel: AgentEditorViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() var showToolDialog by rememberSaveable { mutableStateOf(false) } diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt index 76c08b9..b174db4 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/screen/AgentMarketplaceScreen.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.R as CoreUiR import com.librechat.android.core.ui.components.AvatarImage @@ -62,7 +62,7 @@ fun AgentMarketplaceScreen( onAgentClick: (String) -> Unit, onCreateAgent: () -> Unit, modifier: Modifier = Modifier, - viewModel: AgentMarketplaceViewModel = hiltViewModel(), + viewModel: AgentMarketplaceViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val gridState = rememberLazyGridState() diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt index c1cf5d6..3daf0d0 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentDetailViewModel.kt @@ -9,7 +9,6 @@ import com.librechat.android.core.data.datastore.ServerDataStore import com.librechat.android.core.data.repository.AgentRepository import com.librechat.android.core.model.Agent import com.librechat.android.feature.agents.AgentDetailDisplayData -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -17,7 +16,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class AgentDetailUiState( @@ -34,8 +32,7 @@ sealed interface AgentDetailEvent { data class Duplicated(val agentId: String) : AgentDetailEvent } -@HiltViewModel -class AgentDetailViewModel @Inject constructor( +class AgentDetailViewModel( savedStateHandle: SavedStateHandle, private val agentRepository: AgentRepository, private val serverDataStore: ServerDataStore, diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt index 9cad869..f9b7f62 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentEditorViewModel.kt @@ -34,8 +34,6 @@ import com.librechat.android.feature.agents.components.AgentVersion import com.librechat.android.feature.agents.components.AgentVisibility import com.librechat.android.feature.agents.components.ModelOption import com.librechat.android.feature.agents.components.SupportContactState -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -50,7 +48,6 @@ import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import timber.log.Timber -import javax.inject.Inject @Immutable data class AgentEditorUiState( @@ -106,13 +103,12 @@ sealed interface AgentEditorEvent { data object DeleteSuccess : AgentEditorEvent } -@HiltViewModel -class AgentEditorViewModel @Inject constructor( +class AgentEditorViewModel( savedStateHandle: SavedStateHandle, private val agentRepository: AgentRepository, private val configRepository: ConfigRepository, private val mcpRepository: McpRepository, - @ApplicationContext private val appContext: Context, + private val appContext: Context, ) : ViewModel() { private val editAgentId: String? = savedStateHandle["agentId"] diff --git a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt index 347904e..35f20c3 100644 --- a/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt +++ b/feature/agents/src/main/kotlin/com/librechat/android/feature/agents/viewmodel/AgentMarketplaceViewModel.kt @@ -8,14 +8,12 @@ import com.librechat.android.core.data.datastore.ServerDataStore import com.librechat.android.core.data.repository.AgentRepository import com.librechat.android.core.model.Agent import com.librechat.android.feature.agents.AgentCardDisplayData -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class AgentMarketplaceUiState( @@ -32,8 +30,7 @@ data class AgentMarketplaceUiState( val currentPage: Int = 1, ) -@HiltViewModel -class AgentMarketplaceViewModel @Inject constructor( +class AgentMarketplaceViewModel( private val agentRepository: AgentRepository, private val serverDataStore: ServerDataStore, ) : ViewModel() { diff --git a/feature/agents/src/test/kotlin/com/librechat/android/feature/agents/di/AgentsModuleVerificationTest.kt b/feature/agents/src/test/kotlin/com/librechat/android/feature/agents/di/AgentsModuleVerificationTest.kt new file mode 100644 index 0000000..74f2ecc --- /dev/null +++ b/feature/agents/src/test/kotlin/com/librechat/android/feature/agents/di/AgentsModuleVerificationTest.kt @@ -0,0 +1,21 @@ +package com.librechat.android.feature.agents.di + +import org.junit.Test +import org.koin.test.verify.verify + +class AgentsModuleVerificationTest { + @Test + fun verifyAgentsModule() { + agentsModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + com.librechat.android.core.data.repository.AgentRepository::class, + com.librechat.android.core.data.repository.ConfigRepository::class, + com.librechat.android.core.data.repository.McpRepository::class, + com.librechat.android.core.data.datastore.ServerDataStore::class, + ), + ) + } +} diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/di/AuthModule.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/di/AuthModule.kt new file mode 100644 index 0000000..535049c --- /dev/null +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/di/AuthModule.kt @@ -0,0 +1,27 @@ +package com.librechat.android.feature.auth.di + +import com.librechat.android.feature.auth.oauth.OAuthManager +import com.librechat.android.feature.auth.viewmodel.ForgotPasswordViewModel +import com.librechat.android.feature.auth.viewmodel.LoginViewModel +import com.librechat.android.feature.auth.viewmodel.RegisterViewModel +import com.librechat.android.feature.auth.viewmodel.ResetPasswordViewModel +import com.librechat.android.feature.auth.viewmodel.ServerUrlViewModel +import com.librechat.android.feature.auth.viewmodel.TermsViewModel +import com.librechat.android.feature.auth.viewmodel.TwoFactorViewModel +import com.librechat.android.feature.auth.viewmodel.VerifyEmailViewModel +import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val authModule = module { + singleOf(::OAuthManager) + + viewModelOf(::ServerUrlViewModel) + viewModelOf(::LoginViewModel) + viewModelOf(::RegisterViewModel) + viewModelOf(::VerifyEmailViewModel) + viewModelOf(::ForgotPasswordViewModel) + viewModelOf(::ResetPasswordViewModel) + viewModelOf(::TwoFactorViewModel) + viewModelOf(::TermsViewModel) +} diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/oauth/OAuthManager.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/oauth/OAuthManager.kt index ce53d5d..37e3f3c 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/oauth/OAuthManager.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/oauth/OAuthManager.kt @@ -5,9 +5,7 @@ import android.net.Uri import android.webkit.CookieManager import androidx.browser.customtabs.CustomTabsIntent import com.librechat.android.core.data.datastore.ServerDataStore -import javax.inject.Inject - -class OAuthManager @Inject constructor( +class OAuthManager( private val serverDataStore: ServerDataStore, ) { /** diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt index c05753a..f64f461 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ForgotPasswordScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.auth.viewmodel.ForgotPasswordViewModel import com.librechat.android.feature.auth.R @@ -37,7 +37,7 @@ import androidx.compose.ui.res.stringResource fun ForgotPasswordScreen( onBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: ForgotPasswordViewModel = hiltViewModel(), + viewModel: ForgotPasswordViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt index bfb1d3c..167813e 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/LoginScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -41,7 +41,7 @@ fun LoginScreen( onNavigateToForgotPassword: () -> Unit = {}, onNavigateToTwoFactor: (String) -> Unit = {}, modifier: Modifier = Modifier, - viewModel: LoginViewModel = hiltViewModel(), + viewModel: LoginViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt index 90757d8..84f9ce6 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/RegisterScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.auth.viewmodel.RegisterViewModel import com.librechat.android.feature.auth.R @@ -35,7 +35,7 @@ fun RegisterScreen( onRegistered: () -> Unit, onNavigateToLogin: () -> Unit, modifier: Modifier = Modifier, - viewModel: RegisterViewModel = hiltViewModel(), + viewModel: RegisterViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt index 71af1e3..c35b9ec 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ResetPasswordScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.auth.viewmodel.ResetPasswordViewModel import com.librechat.android.feature.auth.R @@ -39,7 +39,7 @@ fun ResetPasswordScreen( onResetComplete: () -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: ResetPasswordViewModel = hiltViewModel(), + viewModel: ResetPasswordViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt index 9641836..2a58800 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/ServerUrlScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.auth.viewmodel.ServerUrlViewModel import com.librechat.android.feature.auth.R @@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource fun ServerUrlScreen( onServerValidated: () -> Unit, modifier: Modifier = Modifier, - viewModel: ServerUrlViewModel = hiltViewModel(), + viewModel: ServerUrlViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt index 0e39b64..038eef8 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TermsScreen.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.auth.viewmodel.TermsViewModel import com.librechat.android.feature.auth.R @@ -43,7 +43,7 @@ fun TermsScreen( onAccepted: () -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: TermsViewModel = hiltViewModel(), + viewModel: TermsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt index 606257a..7f09861 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/TwoFactorScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.auth.viewmodel.TwoFactorViewModel import com.librechat.android.feature.auth.R @@ -48,7 +48,7 @@ fun TwoFactorScreen( onVerified: () -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: TwoFactorViewModel = hiltViewModel(), + viewModel: TwoFactorViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt index cd96fc0..c2779d1 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/screen/VerifyEmailScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.auth.viewmodel.VerifyEmailViewModel import com.librechat.android.feature.auth.R @@ -41,7 +41,7 @@ fun VerifyEmailScreen( onVerified: () -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: VerifyEmailViewModel = hiltViewModel(), + viewModel: VerifyEmailViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt index cd11fef..001be4a 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ForgotPasswordViewModel.kt @@ -5,12 +5,10 @@ import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.AuthRepository -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class ForgotPasswordUiState( @@ -20,8 +18,7 @@ data class ForgotPasswordUiState( val isSent: Boolean = false, ) -@HiltViewModel -class ForgotPasswordViewModel @Inject constructor( +class ForgotPasswordViewModel( private val authRepository: AuthRepository, ) : ViewModel() { diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/LoginViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/LoginViewModel.kt index dab6243..2952ab5 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/LoginViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/LoginViewModel.kt @@ -10,12 +10,10 @@ import com.librechat.android.core.data.repository.AuthRepository import com.librechat.android.core.data.repository.ConfigRepository import com.librechat.android.core.model.LoginOutcome import com.librechat.android.feature.auth.oauth.OAuthManager -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class LoginUiState( @@ -30,8 +28,7 @@ data class LoginUiState( val socialLogins: List = emptyList(), ) -@HiltViewModel -class LoginViewModel @Inject constructor( +class LoginViewModel( private val authRepository: AuthRepository, private val configRepository: ConfigRepository, private val oAuthManager: OAuthManager, diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt index b9ab81e..fdf1dde 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/RegisterViewModel.kt @@ -6,12 +6,10 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.AuthRepository import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.ConfigRepository -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class RegisterUiState( @@ -26,8 +24,7 @@ data class RegisterUiState( val minPasswordLength: Int = 8, ) -@HiltViewModel -class RegisterViewModel @Inject constructor( +class RegisterViewModel( private val authRepository: AuthRepository, private val configRepository: ConfigRepository, ) : ViewModel() { diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt index aadf617..603ca6b 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ResetPasswordViewModel.kt @@ -6,12 +6,10 @@ import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.AuthRepository -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class ResetPasswordUiState( @@ -22,8 +20,7 @@ data class ResetPasswordUiState( val isReset: Boolean = false, ) -@HiltViewModel -class ResetPasswordViewModel @Inject constructor( +class ResetPasswordViewModel( savedStateHandle: SavedStateHandle, private val authRepository: AuthRepository, ) : ViewModel() { diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt index bca93fe..2b7f785 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/ServerUrlViewModel.kt @@ -6,12 +6,10 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.datastore.ServerDataStore import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.ConfigRepository -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class ServerUrlUiState( @@ -23,8 +21,7 @@ data class ServerUrlUiState( val showHttpWarning: Boolean = false, ) -@HiltViewModel -class ServerUrlViewModel @Inject constructor( +class ServerUrlViewModel( private val serverDataStore: ServerDataStore, private val configRepository: ConfigRepository, ) : ViewModel() { diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt index 0debd23..11a4327 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TermsViewModel.kt @@ -5,12 +5,10 @@ import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.UserRepository -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class TermsUiState( @@ -21,8 +19,7 @@ data class TermsUiState( ) /** Loads and accepts server terms of service; use consumeAccepted() to reset the navigation trigger. */ -@HiltViewModel -class TermsViewModel @Inject constructor( +class TermsViewModel( private val userRepository: UserRepository, ) : ViewModel() { diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt index 173bdd8..37596bb 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/TwoFactorViewModel.kt @@ -6,12 +6,10 @@ import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.AuthRepository -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class TwoFactorUiState( @@ -23,8 +21,7 @@ data class TwoFactorUiState( val isVerified: Boolean = false, ) -@HiltViewModel -class TwoFactorViewModel @Inject constructor( +class TwoFactorViewModel( savedStateHandle: SavedStateHandle, private val authRepository: AuthRepository, ) : ViewModel() { diff --git a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt index 68e281d..5fec2b3 100644 --- a/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt +++ b/feature/auth/src/main/kotlin/com/librechat/android/feature/auth/viewmodel/VerifyEmailViewModel.kt @@ -5,14 +5,12 @@ import androidx.lifecycle.viewModelScope import com.librechat.android.core.common.result.Result import androidx.compose.runtime.Immutable import com.librechat.android.core.data.repository.UserRepository -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class VerifyEmailUiState( @@ -24,8 +22,7 @@ data class VerifyEmailUiState( val resendSuccess: Boolean = false, ) -@HiltViewModel -class VerifyEmailViewModel @Inject constructor( +class VerifyEmailViewModel( private val userRepository: UserRepository, ) : ViewModel() { diff --git a/feature/auth/src/test/kotlin/com/librechat/android/feature/auth/di/AuthModuleVerificationTest.kt b/feature/auth/src/test/kotlin/com/librechat/android/feature/auth/di/AuthModuleVerificationTest.kt new file mode 100644 index 0000000..6316e58 --- /dev/null +++ b/feature/auth/src/test/kotlin/com/librechat/android/feature/auth/di/AuthModuleVerificationTest.kt @@ -0,0 +1,22 @@ +package com.librechat.android.feature.auth.di + +import org.junit.Test +import org.koin.test.verify.verify + +class AuthModuleVerificationTest { + @Test + fun verifyAuthModule() { + authModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + com.librechat.android.core.data.datastore.ServerDataStore::class, + com.librechat.android.core.data.repository.AuthRepository::class, + com.librechat.android.core.data.repository.ConfigRepository::class, + com.librechat.android.core.data.repository.UserRepository::class, + com.librechat.android.core.network.client.SecureTokenStorage::class, + ), + ) + } +} diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/di/ChatModule.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/di/ChatModule.kt new file mode 100644 index 0000000..b29a07f --- /dev/null +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/di/ChatModule.kt @@ -0,0 +1,36 @@ +package com.librechat.android.feature.chat.di + +import com.librechat.android.feature.chat.prompts.PromptEditorViewModel +import com.librechat.android.feature.chat.prompts.PromptsViewModel +import com.librechat.android.feature.chat.viewmodel.ChatViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val chatModule = module { + viewModel { + ChatViewModel( + savedStateHandle = get(), + appContext = androidContext(), + agentRepository = get(), + chatRepository = get(), + messageRepository = get(), + configRepository = get(), + conversationRepository = get(), + draftRepository = get(), + fileRepository = get(), + presetRepository = get(), + promptRepository = get(), + shareRepository = get(), + speechRepository = get(), + mcpRepository = get(), + userRepository = get(), + connectivityObserver = get(), + serverDataStore = get(), + settingsDataStore = get(), + ) + } + viewModelOf(::PromptsViewModel) + viewModelOf(::PromptEditorViewModel) +} diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt index 2733c70..0787b74 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorScreen.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.components.LoadingIndicator import androidx.compose.ui.res.stringResource @@ -43,7 +43,7 @@ import com.librechat.android.feature.chat.R fun PromptEditorScreen( onBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: PromptEditorViewModel = hiltViewModel(), + viewModel: PromptEditorViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt index 41182bd..32ee3c3 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptEditorViewModel.kt @@ -14,12 +14,10 @@ import com.librechat.android.core.model.request.CreatePromptRequest import com.librechat.android.core.model.request.UpdatePromptGroupRequest import com.librechat.android.core.model.request.UpdatePromptTagRequest import androidx.compose.runtime.Immutable -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class PromptEditorUiState( @@ -40,8 +38,7 @@ data class PromptEditorUiState( val isNewPrompt: Boolean get() = groupId == null } -@HiltViewModel -class PromptEditorViewModel @Inject constructor( +class PromptEditorViewModel( private val promptRepository: PromptRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt index 3e48b94..f3cac2d 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsLibraryScreen.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import com.librechat.android.core.ui.components.EmptyState import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LibreChatTopBar @@ -63,7 +63,7 @@ fun PromptsLibraryScreen( onUseInChat: (String) -> Unit, onNavigateToEditor: (groupId: String?) -> Unit, modifier: Modifier = Modifier, - viewModel: PromptsViewModel = hiltViewModel(), + viewModel: PromptsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt index 3e1eba9..bfbb8ee 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/prompts/PromptsViewModel.kt @@ -14,12 +14,10 @@ import com.librechat.android.core.model.request.UpdatePromptGroupRequest import com.librechat.android.core.model.request.UpdatePromptTagRequest import com.librechat.android.feature.chat.prompts.components.PromptSortOrder import com.librechat.android.feature.chat.prompts.components.extractVariables -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class PromptsUiState( @@ -45,8 +43,7 @@ data class PromptsUiState( val variableNames: List = emptyList(), ) -@HiltViewModel -class PromptsViewModel @Inject constructor( +class PromptsViewModel( private val promptRepository: PromptRepository, ) : ViewModel() { diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt index 0cfc8dd..819e073 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/screen/ChatScreen.kt @@ -71,7 +71,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect import com.librechat.android.core.common.EndpointConstants @@ -104,7 +104,7 @@ import com.librechat.android.feature.chat.R @Composable fun ChatScreen( modifier: Modifier = Modifier, - viewModel: ChatViewModel = hiltViewModel(), + viewModel: ChatViewModel = koinViewModel(), onConversationStarted: ((String) -> Unit)? = null, onNavigateToConversation: ((String) -> Unit)? = null, onOpenDrawer: (() -> Unit)? = null, diff --git a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt index 27e6563..d3f7429 100644 --- a/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt +++ b/feature/chat/src/main/kotlin/com/librechat/android/feature/chat/viewmodel/ChatViewModel.kt @@ -47,8 +47,6 @@ import com.librechat.android.feature.chat.viewmodel.delegate.PresetPromptDelegat import com.librechat.android.feature.chat.viewmodel.delegate.TextToSpeechDelegate import com.librechat.android.feature.chat.viewmodel.delegate.VoiceInputDelegate import com.librechat.android.feature.chat.viewmodel.delegate.toSerialName -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -65,12 +63,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.UUID -import javax.inject.Inject - -@HiltViewModel -class ChatViewModel @Inject constructor( +class ChatViewModel( savedStateHandle: SavedStateHandle, - @ApplicationContext private val appContext: Context, + private val appContext: Context, private val agentRepository: AgentRepository, private val chatRepository: ChatRepository, private val messageRepository: MessageRepository, diff --git a/feature/chat/src/test/kotlin/com/librechat/android/feature/chat/di/ChatModuleVerificationTest.kt b/feature/chat/src/test/kotlin/com/librechat/android/feature/chat/di/ChatModuleVerificationTest.kt new file mode 100644 index 0000000..cb3e910 --- /dev/null +++ b/feature/chat/src/test/kotlin/com/librechat/android/feature/chat/di/ChatModuleVerificationTest.kt @@ -0,0 +1,33 @@ +package com.librechat.android.feature.chat.di + +import org.junit.Test +import org.koin.test.verify.verify + +class ChatModuleVerificationTest { + @Test + fun verifyChatModule() { + chatModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + com.librechat.android.core.data.repository.AgentRepository::class, + com.librechat.android.core.data.repository.ChatRepository::class, + com.librechat.android.core.data.repository.MessageRepository::class, + com.librechat.android.core.data.repository.ConfigRepository::class, + com.librechat.android.core.data.repository.ConversationRepository::class, + com.librechat.android.core.data.repository.DraftRepository::class, + com.librechat.android.core.data.repository.FileRepository::class, + com.librechat.android.core.data.repository.PresetRepository::class, + com.librechat.android.core.data.repository.PromptRepository::class, + com.librechat.android.core.data.repository.ShareRepository::class, + com.librechat.android.core.data.repository.SpeechRepository::class, + com.librechat.android.core.data.repository.McpRepository::class, + com.librechat.android.core.data.repository.UserRepository::class, + com.librechat.android.core.common.network.ConnectivityObserver::class, + com.librechat.android.core.data.datastore.ServerDataStore::class, + com.librechat.android.core.data.datastore.SettingsDataStore::class, + ), + ) + } +} diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/di/ConversationsModule.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/di/ConversationsModule.kt new file mode 100644 index 0000000..97d4c68 --- /dev/null +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/di/ConversationsModule.kt @@ -0,0 +1,17 @@ +package com.librechat.android.feature.conversations.di + +import com.librechat.android.feature.conversations.export.ConversationExporter +import com.librechat.android.feature.conversations.export.ConversationImporter +import com.librechat.android.feature.conversations.viewmodel.ArchivedConversationsViewModel +import com.librechat.android.feature.conversations.viewmodel.ConversationListViewModel +import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val conversationsModule = module { + singleOf(::ConversationExporter) + singleOf(::ConversationImporter) + + viewModelOf(::ConversationListViewModel) + viewModelOf(::ArchivedConversationsViewModel) +} diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationExporter.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationExporter.kt index ea4149a..5039bf6 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationExporter.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationExporter.kt @@ -9,9 +9,7 @@ import kotlinx.serialization.json.Json import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -import javax.inject.Inject - -class ConversationExporter @Inject constructor( +class ConversationExporter( private val conversationRepository: ConversationRepository, private val messageRepository: MessageRepository, ) { diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationImporter.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationImporter.kt index 6e1aa3b..9600acc 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationImporter.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/export/ConversationImporter.kt @@ -3,9 +3,8 @@ package com.librechat.android.feature.conversations.export import com.librechat.android.core.common.result.Result import com.librechat.android.core.model.ConversationExport import kotlinx.serialization.json.Json -import javax.inject.Inject -class ConversationImporter @Inject constructor() { +class ConversationImporter { private val json = Json { ignoreUnknownKeys = true isLenient = true diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt index 6222cc8..96b40d9 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ArchivedConversationsScreen.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.conversations.ArchivedConversationDisplayData import com.librechat.android.core.ui.components.EmptyState @@ -53,7 +53,7 @@ import androidx.compose.ui.res.stringResource fun ArchivedConversationsScreen( onNavigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: ArchivedConversationsViewModel = hiltViewModel(), + viewModel: ArchivedConversationsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt index f6cb92f..df3c5e9 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/screen/ConversationListScreen.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.model.Conversation import com.librechat.android.core.ui.components.EmptyState @@ -69,7 +69,7 @@ fun ConversationListScreen( modifier: Modifier = Modifier, onNewChatClick: () -> Unit = {}, onNavigateToArchived: () -> Unit = {}, - viewModel: ConversationListViewModel = hiltViewModel(), + viewModel: ConversationListViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt index d641e0d..d951ef6 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ArchivedConversationsViewModel.kt @@ -8,7 +8,6 @@ import com.librechat.android.core.data.repository.ConversationRepository import com.librechat.android.core.model.Conversation import com.librechat.android.feature.conversations.ArchivedConversationDisplayData import com.librechat.android.feature.conversations.toArchivedDisplayData -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -16,7 +15,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class ArchivedConversationsUiState( @@ -30,8 +28,7 @@ sealed interface ArchivedConversationsEvent { data class ShowError(val message: String) : ArchivedConversationsEvent } -@HiltViewModel -class ArchivedConversationsViewModel @Inject constructor( +class ArchivedConversationsViewModel( private val conversationRepository: ConversationRepository, ) : ViewModel() { diff --git a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt index 57aa95e..eb0b272 100644 --- a/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt +++ b/feature/conversations/src/main/kotlin/com/librechat/android/feature/conversations/viewmodel/ConversationListViewModel.kt @@ -19,7 +19,6 @@ import com.librechat.android.feature.conversations.components.toDisplayData import com.librechat.android.feature.conversations.export.ConversationExporter import com.librechat.android.feature.conversations.export.ConversationImporter import com.librechat.android.feature.conversations.export.ExportFormat -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -29,7 +28,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class ConversationListUiState( @@ -54,8 +52,7 @@ sealed interface ConversationListEvent { data class ImportSuccess(val title: String) : ConversationListEvent } -@HiltViewModel -class ConversationListViewModel @Inject constructor( +class ConversationListViewModel( private val conversationRepository: ConversationRepository, private val tagRepository: TagRepository, private val searchRepository: SearchRepository, diff --git a/feature/conversations/src/test/kotlin/com/librechat/android/feature/conversations/di/ConversationsModuleVerificationTest.kt b/feature/conversations/src/test/kotlin/com/librechat/android/feature/conversations/di/ConversationsModuleVerificationTest.kt new file mode 100644 index 0000000..66e354b --- /dev/null +++ b/feature/conversations/src/test/kotlin/com/librechat/android/feature/conversations/di/ConversationsModuleVerificationTest.kt @@ -0,0 +1,25 @@ +package com.librechat.android.feature.conversations.di + +import org.junit.Test +import org.koin.test.verify.verify + +class ConversationsModuleVerificationTest { + @Test + fun verifyConversationsModule() { + conversationsModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + com.librechat.android.core.data.repository.ConversationRepository::class, + com.librechat.android.core.data.repository.MessageRepository::class, + com.librechat.android.core.data.repository.TagRepository::class, + com.librechat.android.core.data.repository.ShareRepository::class, + com.librechat.android.core.data.repository.SearchRepository::class, + com.librechat.android.core.data.repository.ConfigRepository::class, + com.librechat.android.core.data.datastore.ServerDataStore::class, + com.librechat.android.core.data.datastore.SettingsDataStore::class, + ), + ) + } +} diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt new file mode 100644 index 0000000..33d71e1 --- /dev/null +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/di/FilesModule.kt @@ -0,0 +1,16 @@ +package com.librechat.android.feature.files.di + +import com.librechat.android.feature.files.viewmodel.FilesViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val filesModule = module { + viewModel { + FilesViewModel( + fileRepository = get(), + context = androidContext(), + serverDataStore = get(), + ) + } +} diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt index af747c2..4d7f9cb 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/screen/FilesScreen.kt @@ -73,7 +73,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.librechat.android.core.ui.components.EmptyState @@ -90,7 +90,7 @@ import androidx.compose.ui.res.stringResource @Composable fun FilesScreen( modifier: Modifier = Modifier, - viewModel: FilesViewModel = hiltViewModel(), + viewModel: FilesViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt b/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt index b32b62f..f5f86c7 100644 --- a/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt +++ b/feature/files/src/main/kotlin/com/librechat/android/feature/files/viewmodel/FilesViewModel.kt @@ -14,8 +14,6 @@ import com.librechat.android.core.model.FileObject import com.librechat.android.core.model.request.DeleteFileEntry import com.librechat.android.feature.files.FileDisplayData import com.librechat.android.feature.files.FilePreviewDisplayData -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -24,7 +22,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID -import javax.inject.Inject enum class FileTypeFilter(val label: String) { ALL("All"), @@ -70,10 +67,9 @@ data class FilesUiState( val viewMode: FileViewMode = FileViewMode.LIST, ) -@HiltViewModel -class FilesViewModel @Inject constructor( +class FilesViewModel( private val fileRepository: FileRepository, - @ApplicationContext private val context: Context, + private val context: Context, private val serverDataStore: ServerDataStore, ) : ViewModel() { diff --git a/feature/files/src/test/kotlin/com/librechat/android/feature/files/di/FilesModuleVerificationTest.kt b/feature/files/src/test/kotlin/com/librechat/android/feature/files/di/FilesModuleVerificationTest.kt new file mode 100644 index 0000000..f05202c --- /dev/null +++ b/feature/files/src/test/kotlin/com/librechat/android/feature/files/di/FilesModuleVerificationTest.kt @@ -0,0 +1,19 @@ +package com.librechat.android.feature.files.di + +import org.junit.Test +import org.koin.test.verify.verify + +class FilesModuleVerificationTest { + @Test + fun verifyFilesModule() { + filesModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + com.librechat.android.core.data.repository.FileRepository::class, + com.librechat.android.core.data.datastore.ServerDataStore::class, + ), + ) + } +} diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt new file mode 100644 index 0000000..a01c732 --- /dev/null +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/di/SettingsModule.kt @@ -0,0 +1,35 @@ +package com.librechat.android.feature.settings.di + +import com.librechat.android.feature.settings.viewmodel.ApiKeysViewModel +import com.librechat.android.feature.settings.viewmodel.McpViewModel +import com.librechat.android.feature.settings.viewmodel.MemoriesViewModel +import com.librechat.android.feature.settings.viewmodel.PresetManagerViewModel +import com.librechat.android.feature.settings.viewmodel.SettingsViewModel +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val settingsModule = module { + viewModel { + SettingsViewModel( + context = androidContext(), + userRepository = get(), + authRepository = get(), + conversationRepository = get(), + themeDataStore = get(), + serverDataStore = get(), + settingsDataStore = get(), + mcpRepository = get(), + memoryRepository = get(), + speechRepository = get(), + balanceRepository = get(), + shareRepository = get(), + keyRepository = get(), + ) + } + viewModelOf(::ApiKeysViewModel) + viewModelOf(::MemoriesViewModel) + viewModelOf(::McpViewModel) + viewModelOf(::PresetManagerViewModel) +} diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt index 1129a54..e241672 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/navigation/SettingsNavigation.kt @@ -1,6 +1,6 @@ package com.librechat.android.feature.settings.navigation -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -84,7 +84,7 @@ fun NavGraphBuilder.settingsGraph( } } composable(SHARED_LINKS_ROUTE) { - val viewModel: SettingsViewModel = hiltViewModel() + val viewModel: SettingsViewModel = koinViewModel() val uiState = viewModel.uiState.collectAsStateWithLifecycle() // Load shared links when this screen is first shown diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt index 5d74098..81b9775 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/AccountSettingsScreen.kt @@ -53,7 +53,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.librechat.android.feature.settings.R @@ -71,7 +71,7 @@ fun AccountSettingsScreen( onNavigateBack: () -> Unit, onNavigateToApiKeys: () -> Unit, modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val snackbarHostState = remember { SnackbarHostState() } @@ -114,7 +114,7 @@ fun AccountSettingsContent( onNavigateToApiKeys: () -> Unit, modifier: Modifier = Modifier, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt index ef90d49..f0faca3 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ApiKeysScreen.kt @@ -45,7 +45,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.core.model.ApiKey @@ -57,7 +57,7 @@ import com.librechat.android.feature.settings.viewmodel.ApiKeysViewModel fun ApiKeysScreen( onNavigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: ApiKeysViewModel = hiltViewModel(), + viewModel: ApiKeysViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt index 0c54819..fb8a1c5 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/ChatSettingsScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.SettingsViewModel @@ -47,7 +47,7 @@ fun ChatSettingsScreen( onNavigateBack: () -> Unit, onNavigateToPresets: () -> Unit, modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { Scaffold( modifier = modifier, @@ -83,7 +83,7 @@ fun ChatSettingsScreen( fun ChatSettingsContent( onNavigateToPresets: () -> Unit, modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt index 1cf800e..e397f03 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/DataSettingsScreen.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.SettingsViewModel @@ -52,7 +52,7 @@ fun DataSettingsScreen( onNavigateToArchived: () -> Unit, onNavigateToSharedLinks: () -> Unit, modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val snackbarHostState = remember { SnackbarHostState() } @@ -95,7 +95,7 @@ fun DataSettingsContent( onNavigateToSharedLinks: () -> Unit, modifier: Modifier = Modifier, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt index ae6781d..958e65e 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/GeneralSettingsScreen.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.core.data.datastore.ThemeMode @@ -50,7 +50,7 @@ import com.librechat.android.feature.settings.viewmodel.SettingsViewModel fun GeneralSettingsScreen( onNavigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { Scaffold( modifier = modifier, @@ -84,7 +84,7 @@ fun GeneralSettingsScreen( @Composable fun GeneralSettingsContent( modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt index b7fecf1..f8980c5 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/McpServersScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.core.model.mcp.McpServer @@ -64,7 +64,7 @@ import com.librechat.android.feature.settings.viewmodel.McpViewModel fun McpServersScreen( onNavigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: McpViewModel = hiltViewModel(), + viewModel: McpViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt index 62b05dd..74499a1 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/MemoriesScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.core.model.Memory @@ -63,7 +63,7 @@ import com.librechat.android.feature.settings.viewmodel.MemoriesViewModel fun MemoriesScreen( onNavigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: MemoriesViewModel = hiltViewModel(), + viewModel: MemoriesViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt index 7ae6cf5..55883db 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/PresetManagerScreen.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.feature.settings.R import com.librechat.android.core.ui.components.EmptyState @@ -55,7 +55,7 @@ import com.librechat.android.feature.settings.viewmodel.PresetManagerViewModel fun PresetManagerScreen( onNavigateBack: () -> Unit, modifier: Modifier = Modifier, - viewModel: PresetManagerViewModel = hiltViewModel(), + viewModel: PresetManagerViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt index 6453d12..4e8aede 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/SettingsScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.librechat.android.core.ui.components.ErrorBanner import com.librechat.android.core.ui.components.LoadingIndicator @@ -64,7 +64,7 @@ fun SettingsScreen( onNavigateToSharedLinks: () -> Unit = {}, onNavigateToApiKeys: () -> Unit = {}, modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt index d06c446..0b5b66f 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/screen/TabbedSettingsScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel +import org.koin.compose.viewmodel.koinViewModel import com.librechat.android.feature.settings.R import com.librechat.android.feature.settings.viewmodel.SettingsViewModel import kotlinx.coroutines.launch @@ -45,7 +45,7 @@ fun TabbedSettingsScreen( onNavigateToPresets: () -> Unit, onNavigateToApiKeys: () -> Unit, modifier: Modifier = Modifier, - viewModel: SettingsViewModel = hiltViewModel(), + viewModel: SettingsViewModel = koinViewModel(), ) { val pagerState = rememberPagerState(pageCount = { SettingsTabs.size }) val scope = rememberCoroutineScope() diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/ApiKeysViewModel.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/ApiKeysViewModel.kt index 933f34e..7d744ab 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/ApiKeysViewModel.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/ApiKeysViewModel.kt @@ -7,12 +7,10 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.ApiKeyRepository import com.librechat.android.core.model.ApiKey import com.librechat.android.core.model.request.CreateApiKeyRequest -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class ApiKeysUiState( @@ -32,8 +30,7 @@ data class ApiKeysUiState( * After creation, the key value is shown once via [ApiKeysUiState.createdKey] * and cannot be retrieved again from the server. */ -@HiltViewModel -class ApiKeysViewModel @Inject constructor( +class ApiKeysViewModel( private val apiKeyRepository: ApiKeyRepository, ) : ViewModel() { diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt index f388a7c..1b9fed1 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/McpViewModel.kt @@ -11,13 +11,11 @@ import com.librechat.android.core.model.mcp.McpServer import com.librechat.android.core.model.mcp.McpServerStatus import com.librechat.android.core.model.mcp.McpServerType import com.librechat.android.core.model.mcp.McpTool -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import android.util.Log -import javax.inject.Inject @Immutable data class McpUiState( @@ -35,8 +33,7 @@ data class McpUiState( val successMessage: String? = null, ) -@HiltViewModel -class McpViewModel @Inject constructor( +class McpViewModel( private val mcpRepository: McpRepository, ) : ViewModel() { diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/MemoriesViewModel.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/MemoriesViewModel.kt index b09bece..c937e80 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/MemoriesViewModel.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/MemoriesViewModel.kt @@ -9,12 +9,10 @@ import com.librechat.android.core.model.Memory import com.librechat.android.core.model.request.CreateMemoryRequest import com.librechat.android.core.model.request.UpdateMemoryPreferencesRequest import com.librechat.android.core.model.request.UpdateMemoryRequest -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class MemoriesUiState( @@ -27,8 +25,7 @@ data class MemoriesUiState( val editingMemory: Memory? = null, ) -@HiltViewModel -class MemoriesViewModel @Inject constructor( +class MemoriesViewModel( private val memoryRepository: MemoryRepository, ) : ViewModel() { diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/PresetManagerViewModel.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/PresetManagerViewModel.kt index 0992f0a..fbf5c7e 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/PresetManagerViewModel.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/PresetManagerViewModel.kt @@ -7,12 +7,10 @@ import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.repository.PresetRepository import com.librechat.android.core.model.Preset import com.librechat.android.feature.settings.PresetManagerDisplayData -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import javax.inject.Inject @Immutable data class PresetManagerUiState( @@ -22,8 +20,7 @@ data class PresetManagerUiState( val error: String? = null, ) -@HiltViewModel -class PresetManagerViewModel @Inject constructor( +class PresetManagerViewModel( private val presetRepository: PresetRepository, ) : ViewModel() { diff --git a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt index bb7b280..2c443df 100644 --- a/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/com/librechat/android/feature/settings/viewmodel/SettingsViewModel.kt @@ -6,7 +6,6 @@ import timber.log.Timber import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dagger.hilt.android.qualifiers.ApplicationContext import com.librechat.android.core.common.ChatLayoutConstants import com.librechat.android.core.common.result.Result import com.librechat.android.core.data.datastore.ChatFontSize @@ -45,7 +44,6 @@ import com.librechat.android.feature.settings.viewmodel.delegate.MemoryManagemen import com.librechat.android.feature.settings.viewmodel.delegate.SpeechSettingsDelegate import com.librechat.android.feature.settings.viewmodel.delegate.TwoFactorSecurityDelegate import com.librechat.android.feature.settings.viewmodel.delegate.isHttpStatus -import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -53,7 +51,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import javax.inject.Inject data class SettingsCommand( val name: String, @@ -188,9 +185,8 @@ private data class DataStorePreferences( val selectedVoiceId: String, ) -@HiltViewModel -class SettingsViewModel @Inject constructor( - @ApplicationContext private val context: Context, +class SettingsViewModel( + private val context: Context, private val userRepository: UserRepository, private val authRepository: AuthRepository, conversationRepository: ConversationRepository, diff --git a/feature/settings/src/test/kotlin/com/librechat/android/feature/settings/di/SettingsModuleVerificationTest.kt b/feature/settings/src/test/kotlin/com/librechat/android/feature/settings/di/SettingsModuleVerificationTest.kt new file mode 100644 index 0000000..28a4dca --- /dev/null +++ b/feature/settings/src/test/kotlin/com/librechat/android/feature/settings/di/SettingsModuleVerificationTest.kt @@ -0,0 +1,31 @@ +package com.librechat.android.feature.settings.di + +import org.junit.Test +import org.koin.test.verify.verify + +class SettingsModuleVerificationTest { + @Test + fun verifySettingsModule() { + settingsModule.verify( + extraTypes = listOf( + android.content.Context::class, + android.app.Application::class, + androidx.lifecycle.SavedStateHandle::class, + com.librechat.android.core.data.repository.UserRepository::class, + com.librechat.android.core.data.repository.AuthRepository::class, + com.librechat.android.core.data.repository.ConversationRepository::class, + com.librechat.android.core.data.repository.McpRepository::class, + com.librechat.android.core.data.repository.MemoryRepository::class, + com.librechat.android.core.data.repository.SpeechRepository::class, + com.librechat.android.core.data.repository.BalanceRepository::class, + com.librechat.android.core.data.repository.ShareRepository::class, + com.librechat.android.core.data.repository.KeyRepository::class, + com.librechat.android.core.data.repository.ApiKeyRepository::class, + com.librechat.android.core.data.repository.PresetRepository::class, + com.librechat.android.core.data.datastore.ThemeDataStore::class, + com.librechat.android.core.data.datastore.ServerDataStore::class, + com.librechat.android.core.data.datastore.SettingsDataStore::class, + ), + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e39fae..65361ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,8 +10,7 @@ navigation-compose = "2.8.5" lifecycle = "2.8.7" # DI -hilt = "2.53.1" -hilt-navigation-compose = "1.2.0" +koin = "4.1.0" # Network ktor = "3.0.3" @@ -83,10 +82,14 @@ navigation-compose = { group = "androidx.navigation", name = "navigation-compose lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } -# Hilt -hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } -hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } -hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" } +# Koin +koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin" } +koin-core = { group = "io.insert-koin", name = "koin-core" } +koin-android = { group = "io.insert-koin", name = "koin-android" } +koin-compose = { group = "io.insert-koin", name = "koin-compose" } +koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel" } +koin-compose-viewmodel-navigation = { group = "io.insert-koin", name = "koin-compose-viewmodel-navigation" } +koin-test = { group = "io.insert-koin", name = "koin-test" } # Ktor ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } @@ -198,7 +201,6 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } room = { id = "androidx.room", version.ref = "room" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }