🤖 Android 模块配置
7.1 composeApp/build.gradle.kts
composeApp/build.gradle.kts
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
androidTarget()
sourceSets {
androidMain.dependencies {
// Compose BOM(统一版本)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.material3)
implementation(libs.compose.ui.tooling)
implementation(libs.activity.compose)
implementation(libs.navigation.compose)
implementation(libs.lifecycle.viewmodel.compose)
// Koin
implementation(libs.koin.android)
implementation(libs.koin.compose)
// Coil3
implementation(libs.coil.compose)
implementation(libs.coil.network.ktor)
}
}
}
android {
namespace = "com.example.newsapp"
compileSdk = 35
defaultConfig {
applicationId = "com.example.newsapp"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildFeatures { compose = true }
}
💉 Koin 依赖注入
7.2 Application 初始化
androidMain — MyApplication.kt
@HiltAndroidApp // 如使用 Koin,不需要这个注解
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger(Level.DEBUG)
androidContext(this@MyApplication)
modules(
sharedModule, // commonMain 中定义
databaseModule, // commonMain 中定义(expect/actual 驱动)
androidModule // Android 特有模块
)
}
}
}
// Android 特有模块(如需要 Context 的依赖)
val androidModule = module {
single<TokenStorage> { AndroidTokenStorage(androidContext()) }
}
7.3 AndroidManifest.xml 注册 Application
AndroidManifest.xml
<application
android:name=".MyApplication"
android:label="@string/app_name"
android:theme="@style/Theme.App"
... />
🔗 消费共享 ViewModel
7.4 MainActivity + collectAsState
androidMain — MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AppTheme {
AppNavigation()
}
}
}
}
HomeScreen.kt — 消费共享 ViewModel
@Composable
fun HomeScreen(
// koinViewModel() 从 Koin 容器获取 ViewModel
viewModel: HomeViewModel = koinViewModel(),
onArticleClick: (String) -> Unit = {}
) {
// collectAsStateWithLifecycle:仅在 Resumed 生命周期时收集,节省资源
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(title = { Text("每日资讯") })
}
) { paddingValues ->
Box(Modifier.padding(paddingValues)) {
when {
uiState.isLoading -> LoadingView()
uiState.error != null -> ErrorView(
message = uiState.error!!,
onRetry = viewModel::refresh
)
else -> ArticleList(
articles = uiState.articles,
isRefreshing = uiState.isRefreshing,
onRefresh = viewModel::refresh,
onArticleClick = onArticleClick,
onBookmarkClick = viewModel::toggleBookmark
)
}
}
}
}
📋 LazyColumn 文章列表
ArticleList.kt
@Composable
fun ArticleList(
articles: List<Article>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onArticleClick: (String) -> Unit,
onBookmarkClick: (String) -> Unit
) {
val pullRefreshState = rememberPullToRefreshState()
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = onRefresh,
state = pullRefreshState
) {
if (articles.isEmpty()) {
EmptyState()
} else {
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = articles,
key = { it.id } // 唯一 key 提升滚动性能
) { article ->
ArticleCard(
article = article,
onClick = { onArticleClick(article.id) },
onBookmarkClick = { onBookmarkClick(article.id) }
)
}
}
}
}
}
@Composable
fun ArticleCard(
article: Article,
onClick: () -> Unit,
onBookmarkClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// 封面图片(Coil3 异步加载)
article.imageUrl?.let { url ->
AsyncImage(
model = url,
contentDescription = null,
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
}
Column(Modifier.weight(1f)) {
Text(
text = article.title,
style = MaterialTheme.typography.titleSmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(Modifier.height(4.dp))
Text(
text = article.publishedAt.toDisplayString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
IconButton(onClick = onBookmarkClick) {
Icon(
imageVector = if (article.isBookmarked)
Icons.Filled.Bookmark else Icons.Outlined.BookmarkBorder,
contentDescription = "收藏",
tint = if (article.isBookmarked)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
🖼️ Coil3 图片加载
7.5 Coil3 配置
androidMain — Coil 单例配置
class MyApplication : Application(), SingletonImageLoader.Factory {
override fun newImageLoader(context: PlatformContext): ImageLoader {
return ImageLoader.Builder(context)
.crossfade(true)
.components {
// 使用 Ktor 作为网络引擎(与 Ktor Client 共享连接池)
add(KtorNetworkFetcherFactory())
}
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build()
}
}
Composable — AsyncImage 使用
import coil3.compose.*
@Composable
fun ArticleCoverImage(url: String?) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_placeholder),
error = painterResource(R.drawable.ic_error),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
contentScale = ContentScale.Crop
)
}
🎨 Material 3 主题
androidMain — AppTheme.kt
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF7F52FF),
onPrimary = Color.White,
primaryContainer = Color(0xFF4527A0),
surface = Color(0xFF13101E),
background = Color(0xFF0D0B14)
)
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF7F52FF),
onPrimary = Color.White,
primaryContainer = Color(0xFFEDE7F6),
surface = Color.White,
background = Color(0xFFF5F5F5)
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(),
content = content
)
}
📱 完整 Android 端实战示例
SearchBar + 列表整合
@Composable
fun HomeScreen(viewModel: HomeViewModel = koinViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
Column {
TopAppBar(
title = { Text("📰 每日资讯") },
actions = {
IconButton(onClick = viewModel::refresh) {
Icon(Icons.Filled.Refresh, "刷新")
}
}
)
// 搜索框
SearchBar(
query = uiState.searchQuery,
onQueryChange = viewModel::onSearchQueryChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
) { padding ->
AnimatedContent(
targetState = uiState.isLoading,
modifier = Modifier.padding(padding)
) { isLoading ->
if (isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
ArticleList(
articles = uiState.articles,
isRefreshing = uiState.isRefreshing,
onRefresh = viewModel::refresh,
onArticleClick = {},
onBookmarkClick = viewModel::toggleBookmark
)
}
}
}
// 错误提示 Snackbar
uiState.error?.let { error ->
LaunchedEffect(error) {
snackbarHostState.showSnackbar(error)
viewModel.dismissError()
}
}
}
🎯 实践任务
- 配置 composeApp 模块,添加 Compose BOM 和 Koin 依赖
- 在 Application 中初始化 Koin,注册共享模块和 Android 模块
- 实现 HomeScreen,用
koinViewModel()获取共享 ViewModel - 实现 ArticleCard,包含标题、封面图(Coil3)和收藏按钮