Migrate DI from Hilt to Koin 4.1
This commit is contained in:
parent
d30988f8b4
commit
e4effcf40f
149 changed files with 1065 additions and 716 deletions
10
CLAUDE.md
10
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/<Feature>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": { ... } }`
|
||||
|
|
|
|||
|
|
@ -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<Set<String>>` and `toggleFavorite()`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
|
|
@ -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 { <init>(...); }
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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<android.net.Uri?>(null)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Module> = 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<KClass<*>>(
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -7,14 +7,16 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
|
|||
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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ import org.gradle.api.Plugin
|
|||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
class AndroidHiltConventionPlugin : Plugin<Project> {
|
||||
class AndroidKoinConventionPlugin : Plugin<Project> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T>`, `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`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
plugins {
|
||||
id("librechat.android.library")
|
||||
id("librechat.android.hilt")
|
||||
id("librechat.android.koin")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -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<CoroutineDispatcher>(KoinQualifiers.IO) { Dispatchers.IO }
|
||||
single<CoroutineDispatcher>(KoinQualifiers.Default) { Dispatchers.Default }
|
||||
single<CoroutineDispatcher>(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<CoroutineScope>(KoinQualifiers.ApplicationScope) {
|
||||
CoroutineScope(SupervisorJob() + get<CoroutineDispatcher>(KoinQualifiers.Default))
|
||||
}
|
||||
|
||||
// Connectivity
|
||||
single { ConnectivityObserver(androidContext()) }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>`, `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<String>`, `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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Preferences>,
|
||||
private val json: Json,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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<Preferences>,
|
||||
) : ServerUrlProvider {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Preferences>,
|
||||
) {
|
||||
val latexRenderer: Flow<LatexRenderer> = dataStore.data.map { prefs ->
|
||||
|
|
|
|||
|
|
@ -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<Preferences>,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HttpClient>,
|
||||
class TokenDataStore(
|
||||
context: Context,
|
||||
private val refreshClient: Lazy<HttpClient>,
|
||||
) : 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))
|
||||
|
|
|
|||
|
|
@ -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<Preferences> 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<Preferences> = context.settingsDataStore
|
||||
single {
|
||||
Room.databaseBuilder(androidContext(), LibreChatDatabase::class.java, "librechat.db").build()
|
||||
}
|
||||
single { get<LibreChatDatabase>().conversationDao() }
|
||||
single { get<LibreChatDatabase>().messageDao() }
|
||||
single { get<LibreChatDatabase>().fileDao() }
|
||||
single { get<LibreChatDatabase>().agentDao() }
|
||||
single { get<LibreChatDatabase>().presetDao() }
|
||||
single { get<LibreChatDatabase>().conversationTagDao() }
|
||||
single { get<LibreChatDatabase>().draftDao() }
|
||||
|
||||
// --- DataStore ---
|
||||
|
||||
single<DataStore<Preferences>> { androidContext().settingsDataStore }
|
||||
|
||||
// --- Datastores ---
|
||||
|
||||
single {
|
||||
TokenDataStore(
|
||||
context = androidContext(),
|
||||
refreshClient = lazy(LazyThreadSafetyMode.NONE) { get<HttpClient>(KoinQualifiers.Refresh) },
|
||||
)
|
||||
} binds arrayOf(TokenManager::class, SecureTokenStorage::class)
|
||||
|
||||
singleOf(::ServerDataStore) bind ServerUrlProvider::class
|
||||
singleOf(::ThemeDataStore)
|
||||
singleOf(::ConfigCacheDataStore)
|
||||
singleOf(::SettingsDataStore)
|
||||
|
||||
// --- Repositories (special wiring) ---
|
||||
|
||||
single<AuthRepository> {
|
||||
AuthRepositoryImpl(
|
||||
authApi = get(),
|
||||
userApi = get(),
|
||||
tokenManager = get(),
|
||||
context = androidContext(),
|
||||
)
|
||||
}
|
||||
|
||||
single<ChatRepository> {
|
||||
ChatRepositoryImpl(
|
||||
chatApi = get(),
|
||||
sseClient = get(),
|
||||
sseHttpClient = get(KoinQualifiers.Streaming),
|
||||
connectivityObserver = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single<DraftRepository> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<LoginOutcome> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<String, String>()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(...)))`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
plugins {
|
||||
id("librechat.android.library")
|
||||
id("librechat.android.hilt")
|
||||
id("librechat.android.koin")
|
||||
id("librechat.kotlin.serialization")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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<Banner> =
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<FileObject> =
|
||||
|
|
|
|||
|
|
@ -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<FileObject> =
|
||||
|
|
|
|||
|
|
@ -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<UserKey> =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Message> =
|
||||
|
|
|
|||
|
|
@ -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<Preset> =
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<ConversationTag> =
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
)
|
||||
|
||||
class UserApi @Inject constructor(
|
||||
class UserApi constructor(
|
||||
private val client: HttpClient,
|
||||
) {
|
||||
suspend fun getUser(): User =
|
||||
|
|
|
|||
|
|
@ -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<TokenManager>()
|
||||
val serverUrlProvider = get<ServerUrlProvider>()
|
||||
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<Json>()
|
||||
val serverUrlProvider = get<ServerUrlProvider>()
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package com.librechat.android.core.network.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class RefreshClient
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.librechat.android.core.network.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class StreamingClient
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
) {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue