405 lines
18 KiB
Markdown
405 lines
18 KiB
Markdown
|
|
# Network Runtime Ports Refactor Implementation Plan
|
||
|
|
|
||
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||
|
|
|
||
|
|
**Goal:** Move network client lifecycle and warm-up policy out of `ApiService` / `AuthService` / `WebSocketClient`, then migrate feature code to narrow ports so business code no longer depends on Ktor or WebSocket implementation details.
|
||
|
|
|
||
|
|
**Architecture:** Introduce a composition-root-owned app container with separate REST and realtime provider layers. The REST side must support at least two client profiles (`auth` and `api`) because `AuthService` and `ApiService` have different plugin stacks today. `ApiService`, `AuthService`, and `WebSocketClient` become adapters over those providers; feature code depends on capability-level ports and explicit readiness methods instead of concrete transport classes.
|
||
|
|
|
||
|
|
**Tech Stack:** Android `Application`, `ViewModelFactory`, Ktor HTTP/WebSocket clients, Kotlin coroutines and `StateFlow`, JUnit4, `mockito-inline`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 1: Introduce the Composition Root and Client Providers
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/app/LifeEchoApp.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RestClientProvider.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RealtimeTransportProvider.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/network/runtime/NetworkReadiness.kt`
|
||
|
|
- Modify: `app-android/app/src/main/AndroidManifest.xml`
|
||
|
|
- Test: `app-android/app/src/test/java/com/huaga/life_echo/network/runtime/RestClientProviderTest.kt`
|
||
|
|
- Test: `app-android/app/src/test/java/com/huaga/life_echo/network/runtime/RealtimeTransportProviderTest.kt`
|
||
|
|
|
||
|
|
**Step 1: Write the failing tests**
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@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)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@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)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2: Run tests to verify they fail**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.network.runtime.RestClientProviderTest --tests com.huaga.life_echo.network.runtime.RealtimeTransportProviderTest`
|
||
|
|
|
||
|
|
Expected: FAIL because the runtime/provider classes do not exist yet.
|
||
|
|
|
||
|
|
**Step 3: Write the minimal implementation**
|
||
|
|
|
||
|
|
Create provider APIs with explicit ownership:
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
enum class RestClientProfile { AUTH, API }
|
||
|
|
|
||
|
|
interface ClosableClientProvider<T> {
|
||
|
|
val readiness: StateFlow<NetworkReadiness>
|
||
|
|
fun warmUp()
|
||
|
|
fun getClient(): T
|
||
|
|
fun close()
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`RestClientProvider` should internally manage one client instance per `RestClientProfile`, not a single global client. `AppContainer` should own these providers and expose them to the rest of the app. `LifeEchoApp` should create one `AppContainer` and make it reachable from `ViewModelFactory`.
|
||
|
|
|
||
|
|
**Step 4: Run tests to verify they pass**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.network.runtime.RestClientProviderTest --tests com.huaga.life_echo.network.runtime.RealtimeTransportProviderTest`
|
||
|
|
|
||
|
|
Expected: PASS
|
||
|
|
|
||
|
|
**Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add app-android/app/src/main/java/com/huaga/life_echo/app/LifeEchoApp.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RestClientProvider.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RealtimeTransportProvider.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/network/runtime/NetworkReadiness.kt \
|
||
|
|
app-android/app/src/main/AndroidManifest.xml \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/network/runtime/RestClientProviderTest.kt \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/network/runtime/RealtimeTransportProviderTest.kt
|
||
|
|
git commit -m "refactor: add network client providers"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Task 2: Turn `AuthService`, `ApiService`, and `WebSocketClient` Into Adapters
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/network/ApiService.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt`
|
||
|
|
- Create: `app-android/app/src/test/java/com/huaga/life_echo/network/AuthServiceProviderTest.kt`
|
||
|
|
- Create: `app-android/app/src/test/java/com/huaga/life_echo/network/ApiServiceProviderTest.kt`
|
||
|
|
|
||
|
|
**Step 1: Write the failing tests**
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@Test
|
||
|
|
fun api_service_uses_api_profile_from_provider() {
|
||
|
|
val provider = FakeRestClientProvider()
|
||
|
|
ApiService(provider = provider, tokenManager = fakeTokenManager, authService = fakeAuthService)
|
||
|
|
|
||
|
|
provider.assertNoClientRequested()
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@Test
|
||
|
|
fun auth_service_uses_auth_profile_from_provider_when_called() = runTest {
|
||
|
|
val provider = FakeRestClientProvider()
|
||
|
|
val service = AuthService(provider = provider)
|
||
|
|
|
||
|
|
service.login("13800000000", "pw", true)
|
||
|
|
|
||
|
|
assertEquals(listOf(RestClientProfile.AUTH), provider.requestedProfiles)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2: Run tests to verify they fail**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.network.AuthServiceProviderTest --tests com.huaga.life_echo.network.ApiServiceProviderTest`
|
||
|
|
|
||
|
|
Expected: FAIL because the services still own raw `HttpClient` instances.
|
||
|
|
|
||
|
|
**Step 3: Write the minimal implementation**
|
||
|
|
|
||
|
|
Refactor constructors so the adapters receive providers instead of owning lifecycle:
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
class AuthService(
|
||
|
|
private val provider: RestClientProvider,
|
||
|
|
) { /* use provider.getClient(RestClientProfile.AUTH) */ }
|
||
|
|
|
||
|
|
class ApiService(
|
||
|
|
private val provider: RestClientProvider,
|
||
|
|
private val tokenManager: TokenManager,
|
||
|
|
private val authService: AuthService,
|
||
|
|
) { /* use provider.getClient(RestClientProfile.API) */ }
|
||
|
|
```
|
||
|
|
|
||
|
|
`WebSocketClient` should take `RealtimeTransportProvider` and should no longer own a `HttpClient(OkHttp)` field directly.
|
||
|
|
|
||
|
|
**Step 4: Run tests to verify they pass**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.network.AuthServiceProviderTest --tests com.huaga.life_echo.network.ApiServiceProviderTest`
|
||
|
|
|
||
|
|
Expected: PASS
|
||
|
|
|
||
|
|
**Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add app-android/app/src/main/java/com/huaga/life_echo/network/AuthService.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/network/ApiService.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/network/WebSocketClient.kt \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/network/AuthServiceProviderTest.kt \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/network/ApiServiceProviderTest.kt
|
||
|
|
git commit -m "refactor: move network adapters onto providers"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Task 3: Add Capability-Level Ports for the Conversation Flow
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/ports/ConversationApiPort.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/ports/ConversationRealtimePort.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/adapters/ConversationApiAdapter.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/adapters/ConversationRealtimeAdapter.kt`
|
||
|
|
- Create: `app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/adapters/ConversationRealtimeAdapterTest.kt`
|
||
|
|
|
||
|
|
**Step 1: Write the failing tests**
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@Test
|
||
|
|
fun connect_reports_realtime_connected_only_after_session_connects() = runTest {
|
||
|
|
val adapter = ConversationRealtimeAdapter(fakeSocketClient)
|
||
|
|
|
||
|
|
adapter.connect("conversation-1", "token")
|
||
|
|
|
||
|
|
assertEquals(RealtimeConversationPort.State.Connected("conversation-1"), adapter.state.value)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2: Run tests to verify they fail**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.adapters.ConversationRealtimeAdapterTest`
|
||
|
|
|
||
|
|
Expected: FAIL because the port and adapter do not exist.
|
||
|
|
|
||
|
|
**Step 3: Write the minimal implementation**
|
||
|
|
|
||
|
|
Define ports in business terms, not transport terms:
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
interface ConversationApiPort {
|
||
|
|
suspend fun createConversation(): Result<CreateConversationResponse>
|
||
|
|
suspend fun getMessages(conversationId: String): Result<List<MessageDto>>
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ConversationRealtimePort {
|
||
|
|
val state: StateFlow<State>
|
||
|
|
suspend fun prepare()
|
||
|
|
suspend fun connect(conversationId: String, token: String?)
|
||
|
|
suspend fun sendText(conversationId: String, text: String)
|
||
|
|
suspend fun sendAudioSegment(request: AudioSegmentRequest)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The adapters should delegate to `ApiService` and `WebSocketClient` without leaking Ktor or `DefaultWebSocketSession` to callers.
|
||
|
|
|
||
|
|
**Step 4: Run tests to verify they pass**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.feature.conversation.adapters.ConversationRealtimeAdapterTest`
|
||
|
|
|
||
|
|
Expected: PASS
|
||
|
|
|
||
|
|
**Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/ports/ConversationApiPort.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/ports/ConversationRealtimePort.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/adapters/ConversationApiAdapter.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/conversation/adapters/ConversationRealtimeAdapter.kt \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/feature/conversation/adapters/ConversationRealtimeAdapterTest.kt
|
||
|
|
git commit -m "refactor: add conversation network ports"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Task 4: Migrate `CreateMemoryViewModel` and `ViewModelFactory` to Ports and Explicit Warm-Up
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt`
|
||
|
|
- Modify: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt`
|
||
|
|
- Create: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt`
|
||
|
|
|
||
|
|
**Step 1: Write the failing tests**
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@Test
|
||
|
|
fun initialize_conversation_prepares_realtime_before_connect() = runTest {
|
||
|
|
val realtime = FakeConversationRealtimePort()
|
||
|
|
val viewModel = newViewModel(realtime = realtime)
|
||
|
|
|
||
|
|
viewModel.initializeConversation("conversation-1")
|
||
|
|
advanceUntilIdle()
|
||
|
|
|
||
|
|
assertEquals(listOf("prepare", "connect:conversation-1"), realtime.calls)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2: Run tests to verify they fail**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelWarmupTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelRecordingCoordinatorTest`
|
||
|
|
|
||
|
|
Expected: FAIL because `CreateMemoryViewModel` still constructs `WebSocketClient` and `ApiService` directly.
|
||
|
|
|
||
|
|
**Step 3: Write the minimal implementation**
|
||
|
|
|
||
|
|
`CreateMemoryViewModel` should take ports instead of concrete network classes. `ViewModelFactory` should fetch the ports from `AppContainer` and decide warm-up policy there or in the screen entry path. The warm-up rule for this feature should be:
|
||
|
|
|
||
|
|
- `initializeConversation()` or page entry triggers realtime `prepare()`
|
||
|
|
- conversation creation / connect still happens explicitly
|
||
|
|
- recording code remains independent of network transport implementation
|
||
|
|
|
||
|
|
**Step 4: Run tests to verify they pass**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelWarmupTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelRecordingCoordinatorTest`
|
||
|
|
|
||
|
|
Expected: PASS
|
||
|
|
|
||
|
|
**Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelWarmupTest.kt \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModelRecordingCoordinatorTest.kt
|
||
|
|
git commit -m "refactor: inject conversation ports into create memory flow"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Task 5: Extract Feature-Specific REST Ports for Memoir, Profile, Payment, and Repository Callers
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/memoir/ports/MemoirApiPort.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/profile/ports/ProfileApiPort.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/payment/ports/PaymentApiPort.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/memoir/adapters/MemoirApiAdapter.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/profile/adapters/ProfileApiAdapter.kt`
|
||
|
|
- Create: `app-android/app/src/main/java/com/huaga/life_echo/feature/payment/adapters/PaymentApiAdapter.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/MyMemoirViewModel.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/data/repository/ProfileRepository.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/data/repository/PaymentRepository.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/data/repository/ConversationRepository.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/data/repository/MessageRepository.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt`
|
||
|
|
- Test: `app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/MyMemoirViewModelTest.kt`
|
||
|
|
|
||
|
|
**Step 1: Write the failing tests**
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@Test
|
||
|
|
fun refresh_chapters_uses_memoir_port_instead_of_raw_api_service() = runTest {
|
||
|
|
val memoirApi = FakeMemoirApiPort(chaptersResult = Result.success(listOf()))
|
||
|
|
val viewModel = MyMemoirViewModel(chapterRepository = fakeChapterRepository, memoirApi = memoirApi)
|
||
|
|
|
||
|
|
viewModel.refreshChapters()
|
||
|
|
advanceUntilIdle()
|
||
|
|
|
||
|
|
assertEquals(1, memoirApi.getChaptersCalls)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2: Run tests to verify they fail**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.MyMemoirViewModelTest`
|
||
|
|
|
||
|
|
Expected: FAIL because the viewmodel and repositories still depend on `ApiService` directly.
|
||
|
|
|
||
|
|
**Step 3: Write the minimal implementation**
|
||
|
|
|
||
|
|
Replace direct `ApiService` dependencies with feature ports one feature at a time. The rule is:
|
||
|
|
|
||
|
|
- ViewModels depend on ports or repositories
|
||
|
|
- Repositories depend on ports
|
||
|
|
- Only adapters depend on `ApiService`
|
||
|
|
|
||
|
|
Do not attempt to create one giant `EverythingApiPort`. Keep ports small and feature-shaped.
|
||
|
|
|
||
|
|
**Step 4: Run tests to verify they pass**
|
||
|
|
|
||
|
|
Run: `./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.MyMemoirViewModelTest`
|
||
|
|
|
||
|
|
Expected: PASS
|
||
|
|
|
||
|
|
**Step 5: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add app-android/app/src/main/java/com/huaga/life_echo/feature/memoir/ports/MemoirApiPort.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/profile/ports/ProfileApiPort.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/payment/ports/PaymentApiPort.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/memoir/adapters/MemoirApiAdapter.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/profile/adapters/ProfileApiAdapter.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/feature/payment/adapters/PaymentApiAdapter.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/MyMemoirViewModel.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/data/repository/ProfileRepository.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/data/repository/PaymentRepository.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/data/repository/ConversationRepository.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/data/repository/MessageRepository.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/ViewModelFactory.kt \
|
||
|
|
app-android/app/src/test/java/com/huaga/life_echo/ui/viewmodel/MyMemoirViewModelTest.kt
|
||
|
|
git commit -m "refactor: migrate feature code onto network ports"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Task 6: Final Verification and Cleanup
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RestClientProvider.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RealtimeTransportProvider.kt`
|
||
|
|
- Modify: `app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt`
|
||
|
|
- Modify: `docs/plans/2026-03-12-network-runtime-ports-refactor.md`
|
||
|
|
|
||
|
|
**Step 1: Write the failing verification test if any behavior regressed**
|
||
|
|
|
||
|
|
Use the most fragile edge first:
|
||
|
|
|
||
|
|
```kotlin
|
||
|
|
@Test
|
||
|
|
fun create_memory_recording_limit_race_still_passes_after_network_refactor() = runTest {
|
||
|
|
// reuse the existing regression path and keep it green during cleanup
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Step 2: Run focused and full verification**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
./gradlew :app:testDebugUnitTest --tests com.huaga.life_echo.ui.viewmodel.CreateMemoryViewModelRecordingCoordinatorTest
|
||
|
|
./gradlew :app:testDebugUnitTest
|
||
|
|
git diff --check
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: all PASS, no whitespace errors
|
||
|
|
|
||
|
|
**Step 3: Remove temporary compatibility shims**
|
||
|
|
|
||
|
|
If `ApiService` or `WebSocketClient` still expose old constructors only for migration, remove them once every caller is port-based. Keep `AppContainer.close()` and provider `close()` wired for future lifecycle cleanup.
|
||
|
|
|
||
|
|
**Step 4: Commit**
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git add app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RestClientProvider.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/network/runtime/RealtimeTransportProvider.kt \
|
||
|
|
app-android/app/src/main/java/com/huaga/life_echo/app/AppContainer.kt \
|
||
|
|
docs/plans/2026-03-12-network-runtime-ports-refactor.md
|
||
|
|
git commit -m "refactor: finalize network runtime composition"
|
||
|
|
```
|
||
|
|
|