重构: 添加网络客户端提供器
引入由组合根持有的 RestClientProvider 和 RealtimeTransportProvider,并显式跟踪就绪状态。 AppContainer 负责提供器生命周期;LifeEchoApp 在 Application.onCreate 中完成装配。
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".app.LifeEchoApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.huaga.life_echo.app
|
||||
|
||||
import android.content.Context
|
||||
import com.huaga.life_echo.config.AppConfig
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.data.database.AppDatabase
|
||||
import com.huaga.life_echo.data.repository.ChapterRepository
|
||||
import com.huaga.life_echo.data.repository.ConversationRepository
|
||||
import com.huaga.life_echo.data.repository.MessageRepository
|
||||
import com.huaga.life_echo.data.repository.PaymentRepository
|
||||
import com.huaga.life_echo.data.repository.ProfileRepository
|
||||
import com.huaga.life_echo.feature.memoir.adapters.MemoirApiAdapter
|
||||
import com.huaga.life_echo.feature.memoir.ports.MemoirApiPort
|
||||
import com.huaga.life_echo.feature.payment.adapters.PaymentApiAdapter
|
||||
import com.huaga.life_echo.feature.payment.ports.PaymentApiPort
|
||||
import com.huaga.life_echo.feature.profile.adapters.ProfileApiAdapter
|
||||
import com.huaga.life_echo.feature.profile.ports.ProfileApiPort
|
||||
import com.huaga.life_echo.network.ApiService
|
||||
import com.huaga.life_echo.network.AuthService
|
||||
import com.huaga.life_echo.network.WebSocketClient
|
||||
import com.huaga.life_echo.network.runtime.RealtimeTransportProvider
|
||||
import com.huaga.life_echo.network.runtime.RestClientProfile
|
||||
import com.huaga.life_echo.network.runtime.RestClientProvider
|
||||
import com.huaga.life_echo.payment.PaymentManager
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.android.Android
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.bearer
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class AppContainer(private val context: Context) {
|
||||
|
||||
val restClientProvider: RestClientProvider by lazy {
|
||||
RestClientProvider(factory = ::createRestClient)
|
||||
}
|
||||
|
||||
val realtimeTransportProvider: RealtimeTransportProvider by lazy {
|
||||
RealtimeTransportProvider(factory = ::createRealtimeClient)
|
||||
}
|
||||
|
||||
private val database by lazy { AppDatabase.getDatabase(context) }
|
||||
|
||||
val authService: AuthService by lazy { AuthService(restClientProvider) }
|
||||
|
||||
val apiService: ApiService by lazy {
|
||||
ApiService(
|
||||
provider = restClientProvider,
|
||||
tokenManager = TokenManager,
|
||||
authService = authService
|
||||
)
|
||||
}
|
||||
|
||||
val webSocketClient: WebSocketClient by lazy { WebSocketClient(realtimeTransportProvider) }
|
||||
|
||||
// Feature ports
|
||||
val memoirApi: MemoirApiPort by lazy { MemoirApiAdapter(apiService) }
|
||||
val profileApi: ProfileApiPort by lazy { ProfileApiAdapter(apiService) }
|
||||
val paymentApi: PaymentApiPort by lazy { PaymentApiAdapter(apiService) }
|
||||
|
||||
// Repositories
|
||||
val conversationRepository by lazy {
|
||||
ConversationRepository(
|
||||
conversationDao = database.conversationDao(),
|
||||
segmentDao = database.conversationSegmentDao(),
|
||||
apiService = apiService
|
||||
)
|
||||
}
|
||||
|
||||
val chapterRepository by lazy {
|
||||
ChapterRepository(chapterDao = database.chapterDao())
|
||||
}
|
||||
|
||||
val messageRepository by lazy {
|
||||
MessageRepository(
|
||||
messageDao = database.messageDao(),
|
||||
apiService = apiService
|
||||
)
|
||||
}
|
||||
|
||||
val paymentRepository by lazy {
|
||||
PaymentRepository(paymentApi = paymentApi)
|
||||
}
|
||||
|
||||
val profileRepository by lazy {
|
||||
ProfileRepository(profileApi = profileApi)
|
||||
}
|
||||
|
||||
val paymentManager by lazy {
|
||||
PaymentManager(
|
||||
context = context.applicationContext,
|
||||
wechatAppId = AppConfig.WECHAT_APP_ID
|
||||
)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
restClientProvider.close()
|
||||
realtimeTransportProvider.close()
|
||||
}
|
||||
|
||||
private fun createRestClient(profile: RestClientProfile): HttpClient {
|
||||
return when (profile) {
|
||||
RestClientProfile.AUTH -> HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = false
|
||||
})
|
||||
}
|
||||
install(Logging) { level = LogLevel.INFO }
|
||||
}
|
||||
RestClientProfile.API -> HttpClient(Android) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
}
|
||||
install(Logging) { level = LogLevel.INFO }
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 45_000
|
||||
connectTimeoutMillis = 15_000
|
||||
socketTimeoutMillis = 45_000
|
||||
}
|
||||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
val access = TokenManager.getAccessToken()
|
||||
val refresh = TokenManager.getRefreshToken()
|
||||
if (!access.isNullOrBlank() && !refresh.isNullOrBlank()) {
|
||||
BearerTokens(access, refresh)
|
||||
} else null
|
||||
}
|
||||
refreshTokens {
|
||||
val refresh = oldTokens?.refreshToken
|
||||
?: TokenManager.getRefreshToken()
|
||||
if (!refresh.isNullOrBlank()) {
|
||||
val result = authService.refreshToken(refresh)
|
||||
result.fold(
|
||||
onSuccess = { tokenResponse ->
|
||||
TokenManager.saveTokens(
|
||||
tokenResponse.access_token,
|
||||
tokenResponse.refresh_token,
|
||||
TokenManager.getUserId() ?: ""
|
||||
)
|
||||
BearerTokens(
|
||||
tokenResponse.access_token,
|
||||
tokenResponse.refresh_token
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
TokenManager.clearTokens()
|
||||
TokenManager.notifyTokenRefreshFailed()
|
||||
null
|
||||
}
|
||||
)
|
||||
} else {
|
||||
TokenManager.clearTokens()
|
||||
TokenManager.notifyTokenRefreshFailed()
|
||||
null
|
||||
}
|
||||
}
|
||||
sendWithoutRequest { true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRealtimeClient(): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
install(WebSockets)
|
||||
install(Logging) { level = LogLevel.INFO }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.huaga.life_echo.app
|
||||
|
||||
import android.app.Application
|
||||
import com.huaga.life_echo.data.auth.TokenManager
|
||||
import com.huaga.life_echo.ui.settings.AppSettings
|
||||
|
||||
class LifeEchoApp : Application() {
|
||||
|
||||
lateinit var container: AppContainer
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
TokenManager.initialize(this)
|
||||
AppSettings.initialize(this)
|
||||
container = AppContainer(this)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
container.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.huaga.life_echo.network.runtime
|
||||
|
||||
enum class NetworkReadiness {
|
||||
NotInitialized,
|
||||
ClientReady,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.huaga.life_echo.network.runtime
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class RealtimeTransportProvider(
|
||||
private val factory: () -> HttpClient,
|
||||
) {
|
||||
private var client: HttpClient? = null
|
||||
private val _readiness = MutableStateFlow(NetworkReadiness.NotInitialized)
|
||||
val readiness: StateFlow<NetworkReadiness> = _readiness.asStateFlow()
|
||||
|
||||
fun warmUp() {
|
||||
getClient()
|
||||
}
|
||||
|
||||
fun getClient(): HttpClient {
|
||||
return client ?: factory().also {
|
||||
client = it
|
||||
_readiness.value = NetworkReadiness.ClientReady
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
client?.close()
|
||||
client = null
|
||||
_readiness.value = NetworkReadiness.NotInitialized
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.huaga.life_echo.network.runtime
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
enum class RestClientProfile { AUTH, API }
|
||||
|
||||
class RestClientProvider(
|
||||
private val factory: (RestClientProfile) -> HttpClient,
|
||||
) {
|
||||
private val clients = mutableMapOf<RestClientProfile, HttpClient>()
|
||||
private val _readiness = MutableStateFlow(NetworkReadiness.NotInitialized)
|
||||
val readiness: StateFlow<NetworkReadiness> = _readiness.asStateFlow()
|
||||
|
||||
fun getClient(profile: RestClientProfile): HttpClient {
|
||||
return clients.getOrPut(profile) {
|
||||
factory(profile).also {
|
||||
_readiness.value = NetworkReadiness.ClientReady
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun warmUp() {
|
||||
RestClientProfile.entries.forEach { getClient(it) }
|
||||
}
|
||||
|
||||
fun close() {
|
||||
clients.values.forEach { it.close() }
|
||||
clients.clear()
|
||||
_readiness.value = NetworkReadiness.NotInitialized
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.huaga.life_echo.network.runtime
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class RealtimeTransportProviderTest {
|
||||
|
||||
private val fakeFactory: () -> HttpClient = {
|
||||
Mockito.mock(HttpClient::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun realtime_provider_only_reports_client_ready_after_warmup_or_getClient() {
|
||||
val provider = RealtimeTransportProvider(factory = fakeFactory)
|
||||
|
||||
assertEquals(NetworkReadiness.NotInitialized, provider.readiness.value)
|
||||
provider.warmUp()
|
||||
assertEquals(NetworkReadiness.ClientReady, provider.readiness.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun get_client_returns_same_instance() {
|
||||
val provider = RealtimeTransportProvider(factory = fakeFactory)
|
||||
|
||||
val client1 = provider.getClient()
|
||||
val client2 = provider.getClient()
|
||||
|
||||
assertSame(client1, client2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun close_resets_readiness_to_not_initialized() {
|
||||
val provider = RealtimeTransportProvider(factory = fakeFactory)
|
||||
provider.warmUp()
|
||||
|
||||
provider.close()
|
||||
|
||||
assertEquals(NetworkReadiness.NotInitialized, provider.readiness.value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.huaga.life_echo.network.runtime
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotSame
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito
|
||||
|
||||
class RestClientProviderTest {
|
||||
|
||||
private val fakeFactory: (RestClientProfile) -> HttpClient = {
|
||||
Mockito.mock(HttpClient::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun auth_and_api_clients_are_stable_per_profile_until_close() {
|
||||
val provider = RestClientProvider(factory = fakeFactory)
|
||||
val auth1 = provider.getClient(RestClientProfile.AUTH)
|
||||
val auth2 = provider.getClient(RestClientProfile.AUTH)
|
||||
val api1 = provider.getClient(RestClientProfile.API)
|
||||
|
||||
assertSame(auth1, auth2)
|
||||
assertNotSame(auth1, api1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readiness_is_not_initialized_before_any_client_is_requested() {
|
||||
val provider = RestClientProvider(factory = fakeFactory)
|
||||
|
||||
assertEquals(NetworkReadiness.NotInitialized, provider.readiness.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readiness_is_client_ready_after_get_client() {
|
||||
val provider = RestClientProvider(factory = fakeFactory)
|
||||
|
||||
provider.getClient(RestClientProfile.AUTH)
|
||||
|
||||
assertEquals(NetworkReadiness.ClientReady, provider.readiness.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun warm_up_creates_all_profiles_and_reports_ready() {
|
||||
val created = mutableListOf<RestClientProfile>()
|
||||
val provider = RestClientProvider { profile ->
|
||||
created += profile
|
||||
Mockito.mock(HttpClient::class.java)
|
||||
}
|
||||
|
||||
provider.warmUp()
|
||||
|
||||
assertEquals(RestClientProfile.entries.toList(), created)
|
||||
assertEquals(NetworkReadiness.ClientReady, provider.readiness.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun close_resets_readiness_to_not_initialized() {
|
||||
val provider = RestClientProvider(factory = fakeFactory)
|
||||
provider.warmUp()
|
||||
|
||||
provider.close()
|
||||
|
||||
assertEquals(NetworkReadiness.NotInitialized, provider.readiness.value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user