30、实战项目(下):搜索功能、用户登录、主题切换、性能优化、总结与展望
好,咱们接着上一章继续聊。上一章我们把项目架子搭起来了,数据层也铺好了。这一章,咱们要把剩下的几个核心功能补全,再聊聊性能优化,最后做个总结。
说实话,一个项目做到这个份上,才算真正有了“灵魂”。搜索、登录、主题切换,这些都是用户天天打交道的功能。做得顺手,用户就舒服;做得卡顿,用户扭头就走。
一、搜索功能:别小看这个输入框
搜索功能看起来简单,不就是个输入框加个列表过滤吗?但我在项目中踩过不少坑。比如用户输入太快,频繁触发网络请求,直接把后端打挂了。再比如中文输入法下的拼音匹配问题。
咱们用 Compose 实现一个带防抖的本地搜索。
@Composable
fun SearchScreen(viewModel: NoteViewModel) {
var query by remember { mutableStateOf("") }
val debouncedQuery = remember { mutableStateOf("") }
// 防抖:用户停止输入 300ms 后再搜索
LaunchedEffect(query) {
delay(300)
debouncedQuery.value = query
}
val searchResults by viewModel.searchNotes(debouncedQuery.value).collectAsState(initial = emptyList())
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = query,
onValueChange = { query = it },
placeholder = { Text("搜索笔记...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
LazyColumn {
items(searchResults) { note ->
NoteItem(note = note)
}
}
}
}
LaunchedEffect 就够了;如果是网络搜索,建议用 Flow.debounce() 配合 flatMapLatest。
二、用户登录:Token 管理与状态持久化
登录功能几乎是每个 App 的标配。但很多新手容易忽略一个问题:登录状态怎么保持? 总不能每次打开 App 都让用户重新登录吧?
我的做法是:用 DataStore 存 Token,用拦截器自动附加 Token,用 sealed class 管理登录状态。
// 登录状态
sealed class AuthState {
object Loading : AuthState()
object Unauthenticated : AuthState()
data class Authenticated(val token: String) : AuthState()
}
// 登录 ViewModel
class AuthViewModel(private val dataStore: DataStore<Preferences>) : ViewModel() {
private val _authState = MutableStateFlow<AuthState>(AuthState.Loading)
val authState: StateFlow<AuthState> = _authState.asStateFlow()
init {
// 启动时检查本地 Token
viewModelScope.launch {
val token = dataStore.data.first()[TOKEN_KEY] ?: ""
_authState.value = if (token.isBlank()) {
AuthState.Unauthenticated
} else {
AuthState.Authenticated(token)
}
}
}
fun login(username: String, password: String) {
viewModelScope.launch {
// 模拟网络请求
delay(1000)
val token = "mock_token_${System.currentTimeMillis()}"
dataStore.edit { it[TOKEN_KEY] = token }
_authState.value = AuthState.Authenticated(token)
}
}
fun logout() {
viewModelScope.launch {
dataStore.edit { it.remove(TOKEN_KEY) }
_authState.value = AuthState.Unauthenticated
}
}
companion object {
private val TOKEN_KEY = stringPreferencesKey("auth_token")
}
}
三、主题切换:白天黑夜,一键搞定
主题切换现在几乎是 App 的标配了。Compose 里做这个特别方便,核心就是 MaterialTheme 配合 CompositionLocal。
我习惯把主题定义成 sealed class,然后通过一个全局的 StateHolder 来控制。
// 主题枚举
enum class ThemeMode { LIGHT, DARK, SYSTEM }
// 主题管理
class ThemeManager(private val dataStore: DataStore<Preferences>) {
val themeMode: Flow<ThemeMode> = dataStore.data.map { prefs ->
val name = prefs[THEME_KEY] ?: ThemeMode.SYSTEM.name
ThemeMode.valueOf(name)
}
suspend fun setThemeMode(mode: ThemeMode) {
dataStore.edit { it[THEME_KEY] = mode.name }
}
companion object {
private val THEME_KEY = stringPreferencesKey("theme_mode")
}
}
// 在 Compose 中使用
@Composable
fun AppTheme(
themeManager: ThemeManager,
content: @Composable () -> Unit
) {
val themeMode by themeManager.themeMode.collectAsState(initial = ThemeMode.SYSTEM)
val isDarkTheme = when (themeMode) {
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
ThemeMode.SYSTEM -> isSystemInDarkTheme()
}
MaterialTheme(
colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme(),
content = content
)
}
四、性能优化:别让 App 卡成 PPT
性能优化是个大话题,但咱们这个项目里,有几个点特别值得注意。
4.1 列表性能:LazyColumn 的正确用法
很多新手用 LazyColumn 时,喜欢在 item 里写复杂计算。其实 key 参数特别重要,它能让 Compose 知道哪些 item 需要重组。
LazyColumn {
items(
items = notes,
key = { it.id } // 给每个 item 一个稳定的 key
) { note ->
NoteItem(note = note)
}
}
4.2 重组优化:remember 和 derivedStateOf
Compose 的重组机制很智能,但如果你不小心,它也会做很多无用功。比如下面这个例子:
// ❌ 错误写法:每次重组都重新计算
val filteredList = notes.filter { it.title.contains(query) }
// ✅ 正确写法:用 derivedStateOf 缓存计算结果
val filteredList by remember(notes, query) {
derivedStateOf { notes.filter { it.title.contains(query) } }
}
4.3 图片加载:用 Coil 别用 Glide
在 Compose 里,我强烈推荐用 Coil。它原生支持 Compose,而且内存管理做得很好。
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
contentDescription = null,
modifier = Modifier.size(64.dp)
)
MemoryCache。Coil 默认就有缓存,但别忘了在 Application 里配置 ImageLoader。
五、总结与展望:这条路才刚刚开始
好了,咱们这个实战项目到这里就结束了。从项目搭建、数据层、UI 层,到搜索、登录、主题切换、性能优化,咱们走了一遍完整的开发流程。
说实话,30 章的内容,不可能把每个细节都讲透。但我希望你能掌握一个核心思路:用 Compose 做 Android 开发,核心是状态管理和重组机制。只要把状态管好了,UI 自然就流畅了。
最后,我想说几句心里话。
做技术这件事,没有捷径。我见过太多人,看了一堆教程,但自己不动手写,结果面试时一问三不知。我的建议是:把咱们这个项目从头到尾自己敲一遍,遇到问题就去查文档、看源码。等你把这个项目跑通了,Compose 的基本功就算扎实了。
未来 Compose 会越来越普及。Google 已经在推 Compose 作为 Android 开发的首选方案。你现在学的这些,两三年后依然有用。
嗯,就聊这么多吧。希望你能从这门课里真正学到东西。咱们江湖再见。
- 搜索功能:防抖 + LaunchedEffect
- 用户登录:Token 管理 + DataStore 持久化
- 主题切换:CompositionLocal + 枚举模式
- 性能优化:key、derivedStateOf、Coil
- 核心思想:状态驱动 UI,重组最小化
公众号:蓝海资料掘金营,微信deep3321