3. Kotlin函数与面向对象:函数声明、默认参数、命名参数、高阶函数、Lambda表达式、类、对象、继承、接口、数据类、密封类
Kotlin 的函数与面向对象体系,是 Jetpack Compose 声明式 UI 的语法基石。Compose 中几乎每一个界面元素(如 Text、Button)都是通过函数调用来构建的,而状态管理、主题定制、业务逻辑封装则高度依赖类、接口、数据类与密封类的配合。本章将系统梳理 Kotlin 函数从基础声明到高阶抽象的演进路径,并深入剖析面向对象的核心机制,帮助你在 Compose 开发中写出更简洁、更安全、更易维护的代码。
3.1 函数声明:从基础到表达式体
核心原理
Kotlin 函数使用 fun 关键字声明,基本语法为 fun 函数名(参数列表): 返回类型 { 函数体 }。与 Java 不同,Kotlin 支持表达式体函数——当函数体仅由单个表达式构成时,可以省略花括号和 return,直接使用 = 连接表达式。这一特性在 Compose 中极为常见,例如声明一个简单的 UI 组件:
// 传统块体函数
fun calculateArea(width: Int, height: Int): Int {
return width * height
}
// 表达式体函数(类型可推断)
fun calculateArea(width: Int, height: Int) = width * height
// Compose 中的实际应用
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(text = "Hello, $name!", modifier = modifier)
}
判断方法与操作流程
选择块体还是表达式体,核心判断标准是函数体的复杂度。如果函数逻辑超过一个表达式(包含条件分支、循环、多步操作),必须使用块体;如果函数只是简单计算或返回一个值,优先使用表达式体。在 Compose 中,@Composable 函数通常使用块体,因为内部包含多个 UI 元素的组合调用。
案例拆解:Compose 中的函数设计
假设我们要构建一个用户信息展示组件,需要根据用户等级显示不同颜色。初始版本使用块体函数,代码冗长;重构后使用表达式体配合 when 表达式,代码更加简洁:
// 重构前
@Composable
fun UserBadge(level: Int) {
val color = when (level) {
1 -> Color.Gray
2 -> Color.Blue
3 -> Color.Gold
else -> Color.Black
}
Box(modifier = Modifier.background(color).size(40.dp))
}
// 重构后:提取表达式体函数
fun badgeColor(level: Int): Color = when (level) {
1 -> Color.Gray
2 -> Color.Blue
3 -> Color.Gold
else -> Color.Black
}
@Composable
fun UserBadge(level: Int) {
Box(modifier = Modifier.background(badgeColor(level)).size(40.dp))
}
常见误区
- 过度使用表达式体:当表达式体函数超过一行时,可读性反而下降,建议保持简洁。
- 忽略 Unit 返回类型:无返回值的函数应显式声明
: Unit或省略,但不要写成= Unit。 - Composable 函数命名:必须以大写字母开头,这是 Compose 编译器的强制要求。
3.2 默认参数与命名参数:提升 API 灵活性
核心原理
Kotlin 允许在函数声明时为参数指定默认值,调用时可以省略这些参数。命名参数则允许在调用时通过参数名指定值,不受声明顺序限制。两者结合,可以大幅减少函数重载的数量,让 API 设计更加清晰。在 Compose 中,几乎所有组件都使用了默认参数,例如 Modifier、color、fontSize 等都有默认值。
// 带默认参数的函数
fun createButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
backgroundColor: Color = Color.Blue
) { /* ... */ }
// 调用方式
createButton("点击", onClick = { /* 处理点击 */ })
createButton("提交", onClick = { /* 提交 */ }, enabled = false, backgroundColor = Color.Red)
操作流程与判断方法
设计默认参数时,应遵循以下原则:
- 将最常用的参数放在前面,带默认值的参数放在后面。
- 默认值应选择“安全”的初始值,避免空指针或异常。
- 在 Compose 中,
modifier参数通常放在第一个或第二个位置,并默认Modifier。 - 命名参数在调用时能显著提升代码可读性,尤其是参数较多时。
案例拆解:Compose 组件参数设计
以自定义一个 NetworkImage 组件为例,展示默认参数与命名参数的实际应用:
@Composable
fun NetworkImage(
url: String,
modifier: Modifier = Modifier,
placeholder: Painter? = null,
error: Painter? = null,
contentScale: ContentScale = ContentScale.Fit,
crossfade: Boolean = true
) {
// 实际实现使用 Coil 或 Glide 加载图片
}
// 调用时使用命名参数,清晰表达意图
NetworkImage(
url = "https://example.com/image.jpg",
modifier = Modifier.size(100.dp).clip(CircleShape),
contentScale = ContentScale.Crop,
crossfade = false
)
Painter?),应确保默认值不会导致不必要的对象创建。对于 Modifier,默认值 Modifier 是单例,不会产生额外开销。但自定义默认对象时需谨慎,避免每次重组都创建新实例。
常见误区
- 默认参数顺序错误:带默认值的参数不能位于无默认值参数之前(除非使用命名参数调用)。
- 重载冲突:在 Java 互操作时,默认参数不会自动生成重载方法,需使用
@JvmOverloads注解。 - 命名参数与位置参数混用:一旦使用命名参数,后续参数也必须使用命名参数(Kotlin 1.4+ 允许位置参数在命名参数之后,但建议保持一致性)。
3.3 高阶函数与 Lambda 表达式:函数式编程的核心
核心原理
高阶函数是指以函数作为参数或返回值的函数。Lambda 表达式则是匿名函数的简洁写法。Kotlin 的函数类型语法为 (参数类型) -> 返回类型。在 Compose 中,高阶函数无处不在:onClick 回调、LaunchedEffect 中的挂起函数、remember 的计算逻辑等。
// 高阶函数:接收函数参数
fun List<T>.customFilter(predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (predicate(item)) result.add(item)
}
return result
}
// Lambda 表达式调用
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evenNumbers = numbers.customFilter { it % 2 == 0 }
// Compose 中的高阶函数应用
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { // Lambda 作为参数
Text("点击次数: $count")
}
}
操作流程与判断方法
编写高阶函数时,需要明确以下几点:
- 确定函数类型的参数签名,包括参数个数和返回类型。
- 考虑是否使用
inline修饰符——当函数参数是 Lambda 且被频繁调用时,内联可以消除 Lambda 对象创建的开销。 - 在 Compose 中,
@Composable函数不能直接作为普通高阶函数的参数,需要使用@Composable标记的 Lambda。
案例拆解:Compose 中的事件回调设计
假设我们需要设计一个可复用的列表组件,支持自定义点击行为。通过高阶函数,我们可以将事件处理逻辑完全交给调用方:
@Composable
fun <T> ItemList(
items: List<T>,
modifier: Modifier = Modifier,
onItemClick: (T) -> Unit = {},
itemContent: @Composable (T) -> Unit
) {
LazyColumn(modifier = modifier) {
items(items) { item ->
Box(
modifier = Modifier
.fillMaxWidth()
.clickable { onItemClick(item) }
) {
itemContent(item)
}
}
}
}
// 使用示例
ItemList(
items = userList,
onItemClick = { user -> navigateToProfile(user.id) },
itemContent = { user -> UserRow(user) }
)
remember 缓存。对于高频重组场景,使用 rememberUpdatedState 或 rememberCoroutineScope 来优化回调的稳定性。
常见误区
- 滥用高阶函数:过度嵌套 Lambda 会降低代码可读性,建议适当提取具名函数。
- 忽略内联优化:对于性能敏感的高阶函数,不使用
inline可能导致额外的对象分配。 - Composable Lambda 误用:普通 Lambda 不能调用 Composable 函数,必须使用
@Composable注解的 Lambda 类型。
3.4 类、对象与继承:构建 UI 组件的骨架
核心原理
Kotlin 的类声明使用 class 关键字,支持主构造函数和次构造函数。对象(object)用于创建单例。继承使用 : 符号,默认情况下类和方法都是 final 的,需要显式使用 open 关键字才能被继承和重写。在 Compose 中,类主要用于 ViewModel、状态容器和业务逻辑封装。
// 类与继承
open class BaseViewModel {
open fun loadData() { /* 默认实现 */ }
}
class UserViewModel : BaseViewModel() {
override fun loadData() {
// 具体实现
}
}
// 单例对象
object AppConfig {
val apiBaseUrl = "https://api.example.com"
fun getTimeout(): Long = 30_000
}
// 伴生对象(类似静态成员)
class NetworkClient {
companion object {
const val TAG = "NetworkClient"
fun create(): NetworkClient = NetworkClient()
}
}
判断方法与操作流程
在 Compose 项目中,选择类还是函数取决于职责:
- UI 组件:优先使用 Composable 函数,而非自定义 View 类。
- 业务逻辑:使用 ViewModel 类,配合
StateFlow或mutableStateOf管理状态。 - 工具类:使用单例对象(
object)或顶层函数。 - 数据模型:使用数据类(见 3.6 节)。
案例拆解:ViewModel 与 Compose 的协作
以下是一个典型的 ViewModel 类设计,展示类、继承和对象在 Compose 项目中的实际应用:
// 基类:封装通用状态处理
open class BaseViewModel : ViewModel() {
protected val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> = _isLoading
protected fun showLoading() { _isLoading.value = true }
protected fun hideLoading() { _isLoading.value = false }
}
// 具体业务 ViewModel
class ProductViewModel : BaseViewModel() {
private val _products = mutableStateOf<List<Product>>(emptyList())
val products: State<List<Product>> = _products
fun fetchProducts() {
showLoading()
viewModelScope.launch {
try {
val result = repository.getProducts()
_products.value = result
} finally {
hideLoading()
}
}
}
}
// 在 Composable 中使用
@Composable
fun ProductScreen(viewModel: ProductViewModel = viewModel()) {
val products by viewModel.products
val isLoading by viewModel.isLoading
if (isLoading) {
CircularProgressIndicator()
} else {
ProductList(products)
}
}
mutableStateOf 可能导致状态被外部修改。建议使用 State 只读接口暴露状态,修改操作封装在 ViewModel 内部方法中。
常见误区
- 过度使用继承:Compose 推荐组合优于继承,尽量使用函数参数和 Lambda 实现代码复用。
- 单例滥用:将可变状态放入单例对象会导致全局状态污染,应使用依赖注入或 ViewModel。
- 忽略 open 关键字:在 Kotlin 中,类和方法默认 final,需要继承时必须显式声明
open。
3.5 接口与抽象类:定义行为契约
核心原理
接口(interface)定义了一组行为契约,Kotlin 接口支持默认方法实现和属性声明。抽象类(abstract class)可以包含抽象成员和具体实现,适合有共享状态的场景。在 Compose 中,接口常用于定义仓库(Repository)层、状态管理策略等。
// 接口定义
interface ProductRepository {
val cacheSize: Int // 接口属性
suspend fun getProducts(): List<Product>
suspend fun getProductById(id: String): Product?
// 默认方法实现
fun logAccess() {
println("Repository accessed at ${System.currentTimeMillis()}")
}
}
// 实现接口
class NetworkProductRepository(
private val api: ProductApi
) : ProductRepository {
override val cacheSize: Int = 100
override suspend fun getProducts(): List<Product> {
return api.fetchProducts()
}
override suspend fun getProductById(id: String): Product? {
return api.fetchProduct(id)
}
}
// 抽象类示例
abstract class BaseRepository {
abstract val tag: String
protected fun logError(e: Exception) {
Log.e(tag, "Error: ${e.message}")
}
}
判断方法与操作流程
选择接口还是抽象类,可以参考以下标准:
| 维度 | 接口 | 抽象类 |
|---|---|---|
| 多继承 | 支持(一个类可实现多个接口) | 不支持(单继承) |
| 状态存储 | 不能存储实例状态(属性需实现) | 可以存储状态 |
| 构造器 | 无构造器 | 有构造器 |
| 访问控制 | 成员默认 public | 支持 protected 等 |
| 适用场景 | 行为契约、多态 | 共享代码、模板方法 |
案例拆解:依赖注入中的接口应用
在 Compose 项目中,接口常用于解耦数据层与 UI 层。以下是一个完整的依赖注入示例:
// 定义接口
interface AuthRepository {
suspend fun login(username: String, password: String): Result<User>
suspend fun logout()
}
// 实现类
class FirebaseAuthRepository : AuthRepository {
override suspend fun login(username: String, password: String): Result<User> {
return try {
val user = firebaseAuth.signInWithEmailAndPassword(username, password)
Result.success(user.toDomain())
} catch (e: Exception) {
Result.failure(e)
}
}
override suspend fun logout() {
firebaseAuth.signOut()
}
}
// 在 ViewModel 中注入接口
class LoginViewModel(
private val authRepository: AuthRepository
) : ViewModel() {
fun login(username: String, password: String) {
viewModelScope.launch {
authRepository.login(username, password)
.onSuccess { user -> /* 更新 UI 状态 */ }
.onFailure { error -> /* 显示错误 */ }
}
}
}
常见误区
- 接口设计过于庞大:遵循接口隔离原则,将大接口拆分为多个小接口。
- 抽象类滥用:如果不需要共享状态,优先使用接口。
- 忽略默认方法:Kotlin 接口的默认方法可以减少实现类的重复代码。
3.6 数据类与密封类:状态管理的利器
核心原理
数据类(data class)自动生成 equals()、hashCode()、toString()、copy() 和 componentN() 方法,非常适合承载不可变数据。密封类(sealed class)用于表示受限的类层次结构,所有子类必须嵌套在密封类内部或同一文件中。在 Compose 中,密封类常用于管理 UI 状态(加载中、成功、失败),数据类用于承载界面数据。
// 数据类
data class User(
val id: String,
val name: String,
val email: String,
val avatarUrl: String? = null
)
// 使用 copy 方法
val updatedUser = user.copy(name = "新名字")
// 密封类管理 UI 状态
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String, val code: Int? = null) : UiState<Nothing>()
}
// 在 ViewModel 中使用
class ProfileViewModel : ViewModel() {
private val _uiState = mutableStateOf<UiState<User>>(UiState.Loading)
val uiState: State<UiState<User>> = _uiState
fun loadProfile() {
_uiState.value = UiState.Loading
viewModelScope.launch {
_uiState.value = try {
UiState.Success(repository.getProfile())
} catch (e: Exception) {
UiState.Error(e.message ?: "未知错误")
}
}
}
}
操作流程与判断方法
使用数据类和密封类的关键决策点:
- 数据类:当类主要承载数据且需要比较、复制时使用。注意数据类不能是抽象类、开放类、密封类或内部类。
- 密封类:当状态或操作有固定且有限的类型集合时使用。在
when表达式中,密封类可以穷举所有分支,无需else。 - 密封接口:Kotlin 1.5+ 支持
sealed interface,适合更灵活的状态设计。
案例拆解:完整的 UI 状态管理
以下是一个结合数据类和密封类的完整案例,展示如何在 Compose 中优雅地处理异步状态:
// 定义数据模型
data class Article(
val id: Long,
val title: String,
val content: String,
val author: String,
val publishedAt: Long
)
// 定义 UI 状态
sealed class ArticleUiState {
object Loading : ArticleUiState()
data class Success(val articles: List<Article>) : ArticleUiState()
data class Error(val message: String, val retryAction: (() -> Unit)? = null) : ArticleUiState()
}
// ViewModel
class ArticleViewModel : ViewModel() {
private val _uiState = mutableStateOf<ArticleUiState>(ArticleUiState.Loading)
val uiState: State<ArticleUiState> = _uiState
fun loadArticles() {
_uiState.value = ArticleUiState.Loading
viewModelScope.launch {
_uiState.value = try {
val articles = repository.getArticles()
ArticleUiState.Success(articles)
} catch (e: IOException) {
ArticleUiState.Error("网络连接失败", retryAction = { loadArticles() })
} catch (e: Exception) {
ArticleUiState.Error("加载失败: ${e.message}")
}
}
}
}
// Composable 中的 when 穷举
@Composable
fun ArticleScreen(viewModel: ArticleViewModel = viewModel()) {
val uiState by viewModel.uiState
when (val state = uiState) {
is ArticleUiState.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is ArticleUiState.Success -> {
LazyColumn {
items(state.articles) { article ->
ArticleCard(article)
}
}
}
is ArticleUiState.Error -> {
ErrorView(
message = state.message,
onRetry = { state.retryAction?.invoke() }
)
}
}
}
when 穷举是 Compose 状态管理的黄金组合。它强制开发者处理所有可能的状态分支,避免遗漏。同时,数据类的 copy() 方法可以方便地更新部分字段,保持不可变性。
常见误区
- 数据类属性过多:超过 5-6 个属性时,考虑拆分或使用嵌套数据类。
- 密封类子类分散:Kotlin 1.5 之前,子类必须嵌套在密封类内部;1.5+ 允许同文件不同类,但建议保持嵌套以提升可读性。
- 在数据类中使用可变属性:数据类应保持不可变性(
val),避免使用var,否则copy()和equals()的行为可能不符合预期。 - 忽略解构声明:数据类的
componentN()方法支持解构,如val (id, name) = user,但过度使用会降低代码可读性。
3.7 综合实战:构建一个完整的 Compose 功能模块
实战场景
我们将综合运用本章所有知识点,构建一个简单的“待办事项”功能模块。该模块包含:数据模型(数据类)、状态管理(密封类)、业务逻辑(ViewModel + 接口)、UI 组件(Composable 函数 + 高阶函数)。
实现步骤
// 1. 数据模型
data class TodoItem(
val id: Long,
val title: String,
val isCompleted: Boolean = false,
val createdAt: Long = System.currentTimeMillis()
)
// 2. 仓库接口
interface TodoRepository {
suspend fun getTodos(): List<TodoItem>
suspend fun addTodo(title: String): TodoItem
suspend fun toggleTodo(id: Long)
}
// 3. 实现类
class LocalTodoRepository : TodoRepository {
override suspend fun getTodos(): List<TodoItem> = todoDao.getAll()
override suspend fun addTodo(title: String): TodoItem = todoDao.insert(TodoItem(title = title))
override suspend fun toggleTodo(id: Long) = todoDao.toggle(id)
}
// 4. UI 状态
sealed class TodoUiState {
object Loading : TodoUiState()
data class Success(val todos: List<TodoItem>) : TodoUiState()
data class Error(val message: String) : TodoUiState()
}
// 5. ViewModel
class TodoViewModel(
private val repository: TodoRepository
) : ViewModel() {
private val _uiState = mutableStateOf<TodoUiState>(TodoUiState.Loading)
val uiState: State<TodoUiState> = _uiState
fun loadTodos() {
_uiState.value = TodoUiState.Loading
viewModelScope.la