View→Compose, RxJava→Flow 等技術遷移指南
當需要把舊技術棧漸進改為現代等價物時載入。本 skill 是 Dagger→Hilt 與 kapt→KSP 的唯一權威來源(DI mastery 與 platform_modernization 都指向這裡,避免講法不一致)。遷移前必先有測試安全網(→ @testing_legacy_strategies)。
@dependency_injection_mastery@kotlin_multiplatform@platform_modernization_2026@testing_legacy_strategies@testing_legacy_strategies 加 characterization tests + visual regression。| 從 | 到 | 推薦時機 | 注意 |
|---|---|---|---|
| View XML | Compose | 全部 | 用 ComposeView 漸進嵌入 |
| Fragment | Composable + Navigation 3 | 大多場景 | 複雜 lifecycle 維持 Fragment 殼 |
| RxJava 2/3 | Coroutines + Flow | 全部 | 注意 backpressure / cancellation |
| LiveData | StateFlow / SharedFlow | 全部 | UI 端用 collectAsStateWithLifecycle |
| Dagger 2 | Hilt 2.52+ | 全部 | API 介面層先共存,逐 module 遷移 |
| kapt | KSP 2.x | 全部 | DataBinding 仍綁 kapt → 棄用 |
| Accompanist Pager | androidx.compose.foundation.pager |
立即 | 已主線化 |
| Accompanist SystemUiController | enableEdgeToEdge() + WindowInsets |
立即 | 配 @platform_modernization_2026 |
| Accompanist Permissions | androidx.activity.compose.rememberLauncher... 或 Accompanist 仍維護版 |
視情況 | |
| WorkManager RxWorker | CoroutineWorker | 全部 |
遷移範圍: <例:Fragment X 換 Composable>
測試安全網: characterization 5 cases + screenshot 3 states
回歸指標: cold start ms, jank %, crash-free
PR 拆分: 1 file ≈ 1 PR
驗收: Quick Checklist
class LegacyActivity : AppCompatActivity() {
override fun onCreate(s: Bundle?) {
super.onCreate(s)
setContentView(R.layout.activity_legacy)
findViewById<ComposeView>(R.id.compose_view).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(lifecycle))
setContent { AppTheme { NewFeatureCard(viewModel = ...) } }
}
}
}
setViewCompositionStrategy 必設,否則 Composition 不會在 Activity 銷毀時清理。
@Composable
fun MapView(modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { ctx -> MapView(ctx).apply { onCreate(null) } },
update = { mv -> mv.getMapAsync { /* config */ } },
onRelease = { mv -> mv.onDestroy() },
)
}
onRelease 必填,否則 leak。
| 步驟 | 動作 |
|---|---|
| 1 | Fragment 內 ViewModel 改用 viewModel() 並暴露 StateFlow |
| 2 | 抽出 UI 為 @Composable 接 state + callbacks |
| 3 | Fragment 改用 ComposeView 容器只渲染上述 Composable |
| 4 | 確認所有 Fragment lifecycle 行為(onResume 計時、onPause 暫停動畫)已搬到 Composable 的 LaunchedEffect / DisposableEffect |
| 5 | 上層 NavGraph 改 Compose Navigation,把 Fragment 殼移除 |
| 6 | 驗證 back stack、Activity result、結果回傳 |
Activity Result API:
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
viewModel.onMediaPicked(uri)
}
替代 Fragment.startActivityForResult。
| RxJava | Coroutines / Flow | 備註 |
|---|---|---|
Observable<T> |
Flow<T> |
cold |
Single<T> |
suspend fun (): T |
單值 |
Maybe<T> |
suspend fun (): T? |
可空 |
Completable |
suspend fun (): Unit |
無回傳 |
Flowable<T> + Backpressure |
Flow<T> + buffer / conflate / collectLatest |
見下 |
flatMap |
flatMapMerge |
並行 |
concatMap |
flatMapConcat |
依序 |
switchMap |
flatMapLatest |
取消舊 |
combineLatest |
combine |
|
zip |
zip |
|
merge |
merge |
|
debounce |
debounce |
|
throttleFirst |
sample 或自寫 |
|
subscribeOn(io) |
flowOn(IO) |
影響上游 |
observeOn(main) |
.flowOn(Main) 之後或 collect 在 main scope |
位置不同 |
retry(3) |
retry(3) { it is IOException } |
|
timeout(5s) |
withTimeout(5_000) 或 timeout(5.seconds)(Flow 1.7+) |
|
onErrorReturn |
catch { emit(default) } |
|
Disposable.dispose() |
Job.cancel() / scope 結束自清 |
// RxJava: BackpressureStrategy.LATEST
upstream.toFlowable(BackpressureStrategy.LATEST)
.observeOn(...).subscribe { ... }
// Flow 等價:collectLatest 或 conflate
upstream.conflate().collect { ... } // 跳過中間值,只處理最新
upstream.buffer(64).collect { ... } // 有限 buffer
upstream.collectLatest { value -> // 上游新值來,取消當前 collect
longRunning(value)
}
// Before(RxJava)
RxTextView.textChanges(searchEt)
.debounce(300, TimeUnit.MILLISECONDS)
.switchMap { q -> api.search(q.toString()).toObservable() }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { results -> updateUi(results) }
// After(Flow)
queryFlow
.debounce(300.milliseconds)
.flatMapLatest { q -> flow { emit(api.search(q)) } }
.flowOn(Dispatchers.IO)
.collect { results -> updateUi(results) }
// Before
val scheduler = TestScheduler()
upstream.subscribeOn(scheduler).test()
scheduler.advanceTimeBy(300, TimeUnit.MILLISECONDS)
// After
@OptIn(ExperimentalCoroutinesApi::class)
@Test fun search() = runTest {
val emissions = mutableListOf<List<Result>>()
val job = launch { searchFlow.toList(emissions) }
advanceTimeBy(300)
job.cancel()
assertEquals(expected, emissions.last())
}
runTest 自動使用 TestDispatcher,advanceTimeBy / runCurrent 控時。
// Step 1: ViewModel 內部加 StateFlow,舊 LiveData 改成投影
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state.asStateFlow()
@Deprecated("use state", ReplaceWith("state"))
val stateLive: LiveData<UiState> = state.asLiveData(viewModelScope.coroutineContext)
}
// Step 2: 新 UI(Compose)直接 collect
@Composable
fun Screen(vm: MyViewModel = hiltViewModel()) {
val s by vm.state.collectAsStateWithLifecycle()
}
// Step 3: 舊 Fragment 觀察 LiveData → 改觀察 StateFlow
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { vm.state.collect { render(it) } }
}
// Step 4: 移除 LiveData 投影
坑:MediatorLiveData / Transformations.switchMap 改 combine / flatMapLatest;LiveData.observeForever 在 Flow 沒有等價物,請改成 explicit MutableSharedFlow(replay = 1)。
// 1. App class 加 @HiltAndroidApp,保留舊 Dagger Component 為 lazy
@HiltAndroidApp
class MyApp : Application() {
val legacy: LegacyComponent by lazy { DaggerLegacyComponent.create() }
}
// 2. 新模組純 Hilt
@Module @InstallIn(SingletonComponent::class)
object NewModule {
@Provides @Singleton fun provideX(): X = X()
}
// 3. 橋接:用 @Provides 把 Dagger 提供的服務曝給 Hilt
@Module @InstallIn(SingletonComponent::class)
object LegacyBridgeModule {
@Provides fun provideLegacyService(@ApplicationContext ctx: Context): LegacyService =
(ctx.applicationContext as MyApp).legacy.legacyService()
}
| 階段 | 動作 |
|---|---|
| 1 | App 加 @HiltAndroidApp、Activity 加 @AndroidEntryPoint |
| 2 | 全部 build 用 KSP(@platform_modernization_2026 已說明 kapt→KSP) |
| 3 | 共用基礎服務(Network、DB、Analytics)先以 LegacyBridge 暴給 Hilt |
| 4 | 一次處理一個 feature module:把該 module 的 Dagger Subcomponent 改 Hilt Module |
| 5 | 所有 feature 遷完後,移除 LegacyBridge 與 Dagger Component |
@AssistedInject 與 Dagger 並存:必須兩端都用 Hilt 版的 AssistedInject。@Subcomponent 直接搬:Hilt 用 @DefineComponent + @DefineComponent.Builder,不是 1:1 對應。@PerActivity 想保留,定義 Custom Component(見 @dependency_injection_mastery)。完整步驟在 @platform_modernization_2026 的 §「kapt → KSP2 遷移」。本 skill 補充:
glide-ksp 或改用 Coil 3(KMP-ready)。| Accompanist | 官方替代 |
|---|---|
accompanist-pager / pager-indicators |
androidx.compose.foundation.pager.HorizontalPager / VerticalPager |
accompanist-systemuicontroller |
enableEdgeToEdge() + WindowCompat.getInsetsController |
accompanist-flowlayout |
androidx.compose.foundation.layout.FlowRow / FlowColumn |
accompanist-swiperefresh |
material3.pulltorefresh.PullToRefreshBox |
accompanist-placeholder |
自寫或 Modifier.placeholder()(社群) |
accompanist-navigation-animation |
Navigation Compose 內建 transitions |
accompanist-permissions |
仍維護,可繼續用;或 rememberLauncherForActivityResult |
accompanist-insets |
WindowInsets 與 windowInsetsPadding |
// 舊 RxWorker
class MyRxWorker(ctx: Context, params: WorkerParameters) : RxWorker(ctx, params) {
override fun createWork(): Single<Result> = api.sync()
.map { Result.success() }
.onErrorReturn { Result.retry() }
}
// 新 CoroutineWorker
@HiltWorker
class MyWorker @AssistedInject constructor(
@Assisted ctx: Context, @Assisted params: WorkerParameters,
private val repo: Repo,
) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result = runCatching { repo.sync() }
.map { Result.success() }
.getOrElse { Result.retry() }
}
ListenableWorker callback 風格也可改用 await() extension。
@testing_legacy_strategies:遷移前的安全網。@dependency_injection_mastery:Hilt 已就位後的進階用法。@platform_modernization_2026:Kotlin/Gradle/AGP/KSP 主版本升級。@kotlin_multiplatform:把 Android-only 模組改 commonMain 的後續。@deep_performance_tuning:遷移後的回歸量測(cold start、jank)。ComposeView 設 setViewCompositionStrategyAndroidView 含 onRelease 釋放資源collectAsStateWithLifecycle 觀察