| name | compose-migration |
| description | 将Android View系统界面重构为Jetpack Compose框架。当用户要求将某个界面改为Compose、重构界面、或提到Compose迁移时调用此技能。 |
View系统重构为Jetpack Compose指南
本技能提供将传统View系统(XML布局 + RecyclerView + Adapter)重构为Jetpack Compose的完整流程。
适用场景
- 用户说"把XX界面改成Compose"
- 用户说"重构XX界面"
- 用户说"用Compose重写XX"
- 需要将XML布局转换为Kotlin代码
重构前检查清单
1. 分析目标界面
需要收集的信息:
├── Activity/Fragment文件路径
├── XML布局文件路径
├── Adapter文件路径(如果有列表)
├── ViewModel文件路径
├── 是否有菜单
├── 是否有对话框
└── 是否有搜索功能
2. 检查项目Compose配置
grep -n "compose" build.gradle
grep -n "compose" libs.versions.toml
find . -name "*Theme*.kt" | head -5
重构步骤
Step 1: 创建Screen组件文件
文件命名规范:XxxScreen.kt
package io.legado.app.ui.xxx
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun XxxScreen(
viewModel: XxxViewModel = viewModel(),
onBackClick: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("标题") },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(Icons.Default.ArrowBack, "返回")
}
},
actions = {
}
)
}
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
}
}
}
Step 2: 修改Activity使用setContent
class XxxActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
initTheme()
super.onCreate(savedInstanceState)
setContent {
XxxContent(
onBackClick = { finish() }
)
}
}
}
Step 3: 状态管理改造
ViewModel改造:
class XxxViewModel : BaseViewModel() {
private val _uiState = MutableStateFlow<XxxUIState>(XxxUIState.Loading)
val uiState: StateFlow<XxxUIState> = _uiState.asStateFlow()
private val _isEnabled = MutableStateFlow(AppConfig.isEnabled)
val isEnabled: StateFlow<Boolean> = _isEnabled.asStateFlow()
fun setEnabled(enabled: Boolean) {
AppConfig.isEnabled = enabled
_isEnabled.value = enabled
}
}
UIState定义:
sealed class XxxUIState {
object Loading : XxxUIState()
data class Success(val data: List<Item>) : XxxUIState()
data class Error(val message: String) : XxxUIState()
object Empty : XxxUIState()
}
Step 4: 列表迁移
RecyclerView → LazyColumn:
recyclerView.adapter = MyAdapter(items)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(items, key = { it.id }) { item ->
ItemCard(item = item)
}
}
列表项组件:
@Composable
private fun ItemCard(item: Item) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp),
shape = RoundedCornerShape(12.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(text = item.title, style = MaterialTheme.typography.titleMedium)
Text(text = item.subtitle, style = MaterialTheme.typography.bodySmall)
}
}
}
Step 5: 菜单迁移
onCreateOptionsMenu → DropdownMenu:
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.xxx, menu)
return true
}
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, "更多")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { Text("选项1") },
onClick = { }
)
}
Step 6: 对话框迁移
AlertDialog.Builder → AlertDialog组件:
AlertDialog.Builder(this)
.setTitle("标题")
.setMessage("内容")
.setPositiveButton("确定") { _, _ -> }
.show()
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("标题") },
text = { Text("内容") },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text("确定")
}
}
)
}
Step 7: 搜索功能迁移
SearchView → OutlinedTextField:
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.search(newText)
return true
}
})
var searchQuery by remember { mutableStateOf("") }
OutlinedTextField(
value = searchQuery,
onValueChange = {
searchQuery = it
viewModel.search(it.ifBlank { null })
},
placeholder = { Text("搜索...") },
leadingIcon = { Icon(Icons.Default.Search, null) }
)
Step 8: 删除旧文件
重构完成后删除:
需要删除的文件:
├── res/layout/activity_xxx.xml # Activity布局
├── res/layout/item_xxx.xml # 列表项布局
├── res/menu/xxx.xml # 菜单文件(可选保留)
└── XxxAdapter.kt # RecyclerView适配器
重构后验证清单
| 检查项 | 命令/方法 |
|---|
| Activity使用setContent | grep "setContent" XxxActivity.kt |
| 无XML布局文件 | find . -name "activity_xxx.xml" |
| 无Adapter文件 | find . -name "*Adapter*.kt" |
| 组件有@Composable | grep "@Composable" XxxScreen.kt |
| 列表用LazyColumn | grep "LazyColumn" XxxScreen.kt |
| 状态用collectAsState | grep "collectAsState" XxxScreen.kt |
| 编译通过 | ./gradlew compileDebugKotlin |
常见问题处理
1. suspend函数调用
onClick = { viewModel.clearAll() }
val coroutineScope = rememberCoroutineScope()
onClick = {
coroutineScope.launch {
viewModel.clearAll()
}
}
2. 状态不更新
val isEnabled = viewModel.isEnabled()
val isEnabled by viewModel.isEnabled.collectAsState()
3. Context获取
val context = LocalContext.current
val application = context.applicationContext as Application
4. 资源获取
stringResource(R.string.xxx)
colorResource(R.color.xxx)
dimensionResource(R.dimen.xxx)
代码模板
完整的Screen模板
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun XxxScreen(
viewModel: XxxViewModel = viewModel(),
onBackClick: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
val items by viewModel.items.collectAsState()
var showSearch by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
var showMenu by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(searchQuery) {
viewModel.search(searchQuery.ifBlank { null })
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("标题") },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(Icons.Default.ArrowBack, "返回")
}
},
actions = {
IconButton(onClick = { showSearch = !showSearch }) {
Icon(Icons.Default.Search, "搜索")
}
Box {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, "更多")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { Text("选项") },
onClick = { showMenu = false }
)
}
}
}
)
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
AnimatedVisibility(visible = showSearch) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth().padding(16.dp),
placeholder = { Text("搜索...") }
)
}
when (val state = uiState) {
is XxxUIState.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is XxxUIState.Success -> {
LazyColumn(Modifier.fillMaxSize()) {
items(state.data, key = { it.id }) { item ->
ItemCard(item)
}
}
}
is XxxUIState.Empty -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("暂无数据")
}
}
is XxxUIState.Error -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(state.message, color = MaterialTheme.colorScheme.error)
}
}
}
}
}
}
输出要求
重构完成后,提供以下信息:
-
文件变更清单
-
代码对比
-
验证结果