安卓面试经验 - 现代Android开发技术篇
本文最后更新于:2025年3月10日 下午
第六部分:现代Android开发技术篇
33、Jetpack Compose
什么是Jetpack Compose?它与传统View系统有什么区别? ⭐⭐⭐⭐⭐
Jetpack Compose是Android的现代声明式UI工具包,基于Kotlin构建。
与传统View系统的主要区别:
- 声明式vs命令式:Compose使用声明式编程模型,开发者描述UI应该是什么样子,而不是如何创建/更新UI
- 无XML:Compose完全使用Kotlin代码定义UI,无需XML布局文件
- 更少的模板代码:不需要findViewById、ViewHolder等模板代码
- 状态驱动:UI自动响应状态变化,不需要手动更新视图
- 一致性动画:内置动画系统,更容易实现复杂动画
- 无需Fragment:可以直接在Composable函数间导航,简化架构
- 更好的性能:智能重组系统只更新需要变化的部分
Compose中的重组(Recomposition)是什么?什么时候会触发重组? ⭐⭐⭐⭐
重组是Compose更新UI的过程,当组件依赖的数据发生变化时,Compose会重新执行相关的Composable函数来更新UI。
触发重组的情况:
- 状态变化:使用mutableStateOf、remember等创建的状态对象值改变时
- 参数变化:传递给Composable函数的参数发生变化时
- 组合参数变化:CompositionLocal值在当前作用域变化时
- 配置变化:如设备旋转、暗黑模式切换等
- 手动调用:通过recomposer手动触发重组
重组是智能的,只会重组受影响的部分,而不是整个UI树。
如何在Compose中管理状态?State、MutableState、remember和rememberSaveable的区别? ⭐⭐⭐⭐⭐
State与MutableState:
State<T>
:只读状态接口,提供value属性读取值MutableState<T>
:可变状态接口,继承State,额外提供修改value的能力- 当State/MutableState值变化时自动触发重组
remember:
- 在组件初始创建时计算一个值并在重组过程中保留该值
- 基本用法:
val count = remember { mutableStateOf(0) }
- 仅在组件在组合中存在期间保留值,组件被移除后值会丢失
- 配置变化(如旋转屏幕)时值也会丢失
rememberSaveable:
- 类似remember,但在配置变化(如屏幕旋转)和进程重建时保留状态
- 基本用法:
val count = rememberSaveable { mutableStateOf(0) }
- 可通过Saver、Parcelize、mapSaver等方式存储复杂对象
选择指南:
- 临时UI状态(如动画状态):使用remember
- 需要持久化的用户数据:使用rememberSaveable
- 需要在多个组件间共享的状态:使用ViewModel结合collect
Compose中的副作用(Side-Effects)有哪些?各自应用场景是什么? ⭐⭐⭐⭐
Compose中的主要副作用API:
LaunchedEffect:
- 启动协程作用域,当键发生变化时重新启动
- 适用于:异步操作、一次性事件、动画
- 例:
LaunchedEffect(key) { /* 协程代码 */ }
DisposableEffect:
- 需要清理资源的副作用,如注册/注销监听器
- 在离开组合或键变化时调用onDispose清理
- 例:
DisposableEffect(key) { onDispose { /* 清理 */ } }
SideEffect:
- 每次重组成功后运行,用于与非Compose代码同步状态
- 例:
SideEffect { /* 同步到非Compose系统 */ }
produceState:
- 将非Compose状态转换为Compose状态
- 返回可在组合中读取的State对象
- 例:
val state = produceState(initial) { /* 更新value */ }
derivedStateOf:
- 基于其他状态计算派生状态,避免不必要的重组
- 例:
val filtered = remember { derivedStateOf { list.filter(predicate) } }
rememberCoroutineScope:
- 创建绑定到组合点的协程作用域,可在事件处理程序中使用
- 例:
val scope = rememberCoroutineScope()
rememberUpdatedState:
- 在长寿命效应中引用最新值
- 例:
val latest = rememberUpdatedState(value)
snapshotFlow:
- 将Compose状态转换为Flow
- 例:
snapshotFlow { state.value }
如何在Compose UI中集成传统View?反过来呢? ⭐⭐⭐
在Compose中集成传统View:
- 使用
AndroidView
组件包装传统ViewAndroidView( factory = { context -> TextView(context).apply { text = "这是一个传统View" } }, update = { textView -> textView.text = "更新的文本" } )
- 可以通过update参数响应Compose状态变化
- 复杂View如RecyclerView也可以包装集成
在传统View中集成Compose:
- 使用
ComposeView
类// 在Activity/Fragment中 val composeView = ComposeView(context) composeView.setContent { MaterialTheme { MyComposable() } } layout.addView(composeView)
- 在XML中使用
androidx.compose.ui.platform.ComposeView
<androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="wrap_content" />
findViewById<ComposeView>(R.id.compose_view).setContent { MyComposable() }
- 使用
Compose的性能优化策略有哪些? ⭐⭐⭐⭐
状态提升:
- 将状态提升到只需要它的最低公共祖先
- 避免不必要的重组传播
稳定类型和不可变数据:
- 使用稳定类型(
@Stable
标记的类)和不可变对象 - 使Compose更好地跟踪状态变化
- 使用稳定类型(
使用key:
- 在列表项和动态内容中使用唯一且稳定的key
- 帮助Compose正确跟踪项目身份
使用derivedStateOf:
- 避免中间状态变化触发重组
- 仅在最终结果变化时重组
延迟布局计算:
- 使用
LazyColumn
而不是Column
处理长列表 - 使用
remember { derivedStateOf }
计算派生数据
- 使用
避免不必要的重组:
- 分解大型Composable函数为更小的函数
- 使用remember包装不依赖于状态的计算
Remember + lambda缓存:
- 缓存传递给子组件的lambda以避免重组
val onClick = remember { { performAction() } }
- 缓存传递给子组件的lambda以避免重组
明智地使用CompositionLocal:
- 避免过度使用CompositionLocal,它可能导致整个子树重组
使用Layout Inspector和Compose编译器报告:
- 分析重组原因和频率
- 识别过度重组的组件
避免读取组合期间的全局状态:
- 将全局状态包装为Compose状态
如何在Compose中实现动画?有哪几种动画API? ⭐⭐⭐
Compose提供多种动画API,从简单到复杂:
高级动画API:
animateContentSize
:自动为大小变化添加动画Box(modifier = Modifier.animateContentSize()) { /* 内容 */ }
AnimatedVisibility
:为组件的显示/隐藏添加动画AnimatedVisibility(visible = isVisible) { /* 内容 */ }
animateColorAsState
/animateFloatAsState
:为单个值变化添加动画val color = animateColorAsState(if (isSelected) Color.Red else Color.Black)
中级动画API:
Transition
:协调多个值的动画val transition = updateTransition(targetState) val color = transition.animateColor { state -> if (state) Color.Red else Color.Black } val size = transition.animateFloat { state -> if (state) 100f else 50f }
InfiniteTransition
:无限循环动画val infiniteTransition = rememberInfiniteTransition() val color = infiniteTransition.animateColor( initialValue = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse) )
低级动画API:
Animatable
:命令式动画控制器val position = remember { Animatable(0f) } LaunchedEffect(key1) { position.animateTo(100f) }
Animation
和AnimationVector
:支持向量动画,如位置(x,y)
手势动画:
draggable
,swipeable
,transformable
修饰符- 结合
animateTo
和animateDecay
实现完整交互动画val state = rememberDraggableState { delta -> /* 处理拖动 */ } Modifier.draggable(state, Orientation.Horizontal)
通用动画规格包括
spring()
、tween()
、repeatable()
、keyframes()
,支持不同的曲线和时间配置。Compose中的LazyColumn和RecyclerView有什么区别? ⭐⭐⭐⭐
LazyColumn和RecyclerView都用于高效显示长列表,但有几个关键区别:
声明式vs命令式:
- LazyColumn:声明式API,直接描述每个项目应该是什么样子
- RecyclerView:命令式API,需要Adapter、ViewHolder等配置
代码简洁性:
- LazyColumn大大减少了模板代码量
LazyColumn { items(dataList) { item -> ItemComposable(item) } }
- RecyclerView需要创建多个类(Adapter、ViewHolder)和XML布局
- LazyColumn大大减少了模板代码量
状态管理:
- LazyColumn:自动响应状态变化重组列表项
- RecyclerView:需要手动通知适配器数据变化(notifyItem…)
差异化更新:
- LazyColumn:基于key()函数自动处理差异化更新
- RecyclerView:需要DiffUtil或手动实现差异化更新
滚动状态:
- LazyColumn:使用LazyListState控制和观察滚动状态
- RecyclerView:使用LayoutManager和ScrollListener
项目布局预测:
- LazyColumn:无需预测,基于组合逻辑
- RecyclerView:可能需要预测布局以优化性能
自定义布局:
- LazyColumn:使用Arrangement和自定义item布局
- RecyclerView:通过LayoutManager实现自定义布局
嵌套滚动:
- LazyColumn:更好的嵌套滚动支持,如滚动内部其他可滚动组件
- RecyclerView:嵌套滚动需要额外配置和协调
内部实现:
- LazyColumn实际上在内部使用了类似RecyclerView的回收机制
Compose如何处理生命周期? ⭐⭐⭐⭐
Compose处理生命周期的几个关键方面:
Composition生命周期:
- Compose UI树有自己的生命周期,独立于Activity/Fragment
- 主要阶段:初始组合(Initial Composition)、重组(Recomposition)、离开组合(Leaving Composition)
与Android生命周期集成:
- ComposeView自动跟随宿主View的生命周期
- 在Activity中,setContent{}绑定到Activity生命周期
生命周期感知:
- 使用LocalLifecycleOwner访问Android LifecycleOwner
val lifecycleOwner = LocalLifecycleOwner.current
- 使用LocalLifecycleOwner访问Android LifecycleOwner
副作用与生命周期:
- DisposableEffect:在组件”死亡”时清理资源
DisposableEffect(Unit) { // 设置,类似onCreate onDispose { // 清理,类似onDestroy } }
- LaunchedEffect:协程范围与组件生命周期绑定
- DisposableEffect:在组件”死亡”时清理资源
状态保存:
- rememberSaveable:通过savedInstanceState在配置变化中保存状态
- 类似于Activity.onSaveInstanceState的功能
BackHandler:
- 处理返回按键事件,替代Activity.onBackPressed
BackHandler { /* 处理返回事件 */ }
- 处理返回按键事件,替代Activity.onBackPressed
与ViewModel集成:
- viewModel()函数获取ViewModel,自动与生命周期绑定
val viewModel = viewModel<MyViewModel>()
- viewModel()函数获取ViewModel,自动与生命周期绑定
记忆化与生命周期:
- remember跟随Composable生命周期,而不是Android组件生命周期
- 配置变化时remember的值会丢失,除非使用rememberSaveable
副作用调用时机:
- SideEffect:每次成功重组后调用
- LaunchedEffect:组件进入组合或key变化时调用
- DisposableEffect:组件进入组合时调用,离开组合时清理
Compose生命周期管理更加简洁和声明式,减少了生命周期相关bug的可能性。
34、Room
- 什么是Room持久性库?它与原生SQLite相比有什么优势? ⭐⭐⭐⭐⭐
- Room的三个主要组件是什么?各自的作用是什么? ⭐⭐⭐⭐
- 如何在Room中定义复杂查询?能否举例说明? ⭐⭐⭐
- Room如何处理数据库迁移(Migration)? ⭐⭐⭐⭐
- Room与LiveData、Flow、RxJava如何结合使用? ⭐⭐⭐⭐
- Room的事务处理方式有哪些? ⭐⭐⭐
- TypeConverter在Room中的作用是什么? ⭐⭐⭐
- Room与协程如何结合使用?挂起函数在DAO中有什么特点? ⭐⭐⭐⭐
35、Kotlin语言特性
Kotlin与Java相比有哪些优势? ⭐⭐⭐⭐⭐
Kotlin相比Java的主要优势:
空安全:通过类型系统区分可空和非空类型,在编译时就避免NullPointerException
简洁性:
- 类型推断减少了显式类型声明
- 数据类简化POJO对象创建
- Lambda表达式和高阶函数
- 属性自动生成getter/setter
函数式编程支持:
- 一等公民函数
- 不可变集合
- 丰富的集合操作API
扩展函数:无需继承即可扩展类的功能
协程支持:简化异步编程和并发
互操作性:与Java完全兼容,可以调用Java代码,Java也可以调用Kotlin
安全特性:
- 显式类型转换
- 智能类型转换
- 运算符重载
更现代化的语法:
- 默认参数值和命名参数
- 表达式函数体
- 字符串模板
避免常见错误:
- 没有原始类型
- 默认为final类和方法
- 强制处理异常
Kotlin中的空安全是如何实现的?什么是安全调用操作符和非空断言操作符? ⭐⭐⭐⭐
Kotlin的空安全系统通过类型系统区分可空类型和非空类型实现:
可空和非空类型:
- 默认情况下变量不可为null:
val name: String
- 添加问号表示可空:
val name: String?
- 编译器强制检查可空类型的使用
- 默认情况下变量不可为null:
安全调用操作符(
?.
):- 仅当对象非空时才执行调用,否则返回null
- 例:
person?.name
(如果person为null,则返回null,不执行.name) - 可链式使用:
person?.department?.head?.name
非空断言操作符(
!!
):- 将任何值转换为非空类型,如果为null则抛出NPE
- 例:
val length = str!!.length
(如果str为null,抛出异常) - 应该尽量避免使用,除非确实需要NPE
Elvis操作符(
?:
):- 当左侧表达式为null时提供默认值
- 例:
val name = person?.name ?: "Unknown"
安全类型转换(
as?
):- 尝试转换类型,如果不匹配则返回null而不抛出异常
- 例:
val customer = person as? Customer
平台类型:
- 从Java代码返回的类型被视为”平台类型”,编译器不确定其空安全性
- 可以被当作可空或非空类型处理,但开发者需要小心
什么是Kotlin的扩展函数?它与Java相比有什么不同? ⭐⭐⭐⭐
扩展函数允许给现有类添加新函数,无需继承或装饰器模式。
基本概念:
fun String.addHello(): String { return "Hello, $this" } // 使用 val result = "World".addHello() // 返回 "Hello, World"
与Java区别:
静态解析:
- Kotlin扩展在编译时静态解析,不是运行时动态派发
- 实际编译为带接收者参数的静态方法
- Java需要辅助类或装饰器模式实现类似功能
接收者类型:
- 扩展声明在接收者类型外部
- 可以为任何类型定义扩展,包括第三方库和Java类
可空接收者:
- 可以为可空类型定义扩展,安全处理null
fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()
- 可以为可空类型定义扩展,安全处理null
可见性:
- 扩展函数对整个项目可见,不局限在类内部
- 可以导入特定扩展,避免命名冲突
成员优先原则:
- 如果扩展函数和成员函数签名相同,成员函数优先调用
属性扩展:
- 也可以定义扩展属性
val String.lastChar: Char get() = this[length - 1]
- 也可以定义扩展属性
扩展函数的主要优势是不修改原类的情况下增强其功能,同时保持代码整洁和类型安全。
Kotlin中的数据类(data class)有什么特点? ⭐⭐⭐⭐
数据类是专为保存数据设计的类,仅通过一行代码就能替代传统Java POJO类的大量模板代码。
特点:
自动生成方法:
equals()/hashCode()
:基于主构造函数中声明的所有属性toString()
:返回格式化字符串,包含所有属性componentN()
:用于解构声明copy()
:创建对象副本,可选择修改部分属性
声明简洁:
data class User(val name: String, val age: Int, val email: String)
等同于Java中包含所有属性、getter/setter、equals/hashCode/toString的完整类
使用限制:
- 主构造函数必须至少有一个参数
- 参数必须标记为
val
或var
- 不能是抽象、开放、密封或内部类
- 可以实现接口并继承其他类
解构能力:
val user = User("Alex", 30, "alex@example.com") val (name, age, email) = user // 解构声明
不可变性支持:
- 推荐使用
val
声明属性,创建不可变数据类 - 配合
copy()
方法实现不可变对象的修改
- 推荐使用
复制与修改:
val updatedUser = user.copy(age = 31)
集合操作优化:
- 适合用作映射的键或集合元素,因为有可靠的equals/hashCode
数据类极大简化了处理数据的代码,提高可读性和可维护性,是Kotlin减少样板代码的典型例子。
什么是Kotlin的密封类(sealed class)?它与枚举相比有什么优势? ⭐⭐⭐
密封类是一种限制类继承层次结构的特殊类,提供了类型安全的方式来表示受限的类层次结构。
基本概念:
sealed class Result { data class Success(val data: Any) : Result() data class Error(val message: String, val cause: Exception? = null) : Result() object Loading : Result() }
特点:
受限继承:
- 所有直接子类必须在同一文件中声明
- 密封类自身是抽象的,不能直接实例化
穷举性检查:
when
表达式不需要else
分支,编译器可以验证已覆盖所有可能情况when (result) { is Result.Success -> handleSuccess(result.data) is Result.Error -> handleError(result.message) is Result.Loading -> showLoadingIndicator() }
与枚举相比的优势:
可承载数据:
- 每个子类可以有不同的属性和参数
- 枚举每个常量只能有相同的属性集
灵活性:
- 子类可以是任意类型(data class、object、常规class)
- 枚举常量本质上都是相同类型的单例
扩展性:
- 密封类子类可以有多个实例(除了object)
- 枚举每个常量只有一个实例
层次结构:
- 密封类可以创建复杂类层次,子类还可以有子类
- 枚举是扁平的结构
应用场景:
- 表示受限的类层次结构(如网络结果、UI状态)
- 需要区分多种情况,且每种情况携带不同数据
- 状态机实现
- 代替类型码模式
Kotlin的委托属性(Delegated Properties)是什么?有什么用途? ⭐⭐⭐⭐
委托属性允许将属性的getter/setter逻辑委托给另一个对象,实现属性行为的定制化与复用。
基本语法:
class Example { var p: String by Delegate() }
工作原理:
- 委托对象必须实现
getValue()
和setValue()
方法(对于var属性) - 编译器生成辅助属性和访问器,将操作委托给指定对象
常用内置委托:
lazy:实现延迟初始化
val expensiveValue: String by lazy { println("Computing...") computeExpensiveString() }
- 仅在首次访问时计算并缓存结果
- 线程安全(默认同步锁定)
observable/vetoable:观察/验证属性变化
var name: String by Delegates.observable("Initial") { prop, old, new -> println("Name changed from $old to $new") } var age: Int by Delegates.vetoable(0) { prop, old, new -> new >= 0 // 负值将被拒绝 }
map存储:从Map读写属性
class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map }
notNull:非空属性延迟初始化
var name: String by Delegates.notNull()
主要用途:
行为复用:
- 多个属性共享相同的访问逻辑
- 避免重复代码
关注点分离:
- 将属性存储与访问逻辑从类主逻辑分离
- 使类更专注于核心功能
自定义行为:
- 属性验证
- 通知/事件触发
- 懒加载
- 值映射/转换
框架功能:
- ViewModel中的savedStateHandle属性委托
- 视图绑定
- 依赖注入
委托属性是Kotlin最强大的特性之一,可以显著简化代码并提高可重用性。
- 委托对象必须实现
Kotlin的伴生对象(companion object)与Java的静态成员有什么区别? ⭐⭐⭐
伴生对象是Kotlin实现类级别功能的方式,与Java静态成员概念类似但有显著区别。
基本语法:
class MyClass { companion object { const val MAX_COUNT = 100 fun create(): MyClass = MyClass() } } // 使用 val max = MyClass.MAX_COUNT val instance = MyClass.create()
与Java静态成员的区别:
实例性质:
- 伴生对象是真实的对象实例,而不只是静态成员集合
- 在运行时确实存在,有自己的内存分配
特性和能力:
- 可以实现接口
- 可以保存状态
- 可以有初始化代码块
class MyClass { companion object MyCompanion : SomeInterface { init { println("Companion initialized") } override fun interfaceMethod() {} } }
命名和引用:
- 可以命名(如上例的MyCompanion),也可以匿名
- 可以被独立引用:
val companion = MyClass.Companion
扩展函数:
- 伴生对象可以有扩展函数
fun MyClass.Companion.additionalFunction() { /* ... */ }
- 伴生对象可以有扩展函数
JVM实现:
- 编译为类的私有静态final字段和静态初始化器
- 使用
@JvmStatic
注解可使方法编译为真正的Java静态方法companion object { @JvmStatic fun staticMethod() {} }
作用域:
- 伴生对象可以访问包含类的私有成员
- Java静态方法不能直接访问实例成员
伴生对象最佳实践:
- 工厂方法
- 常量定义
- 单例实现
- 实用工具方法
- 默认值提供
Kotlin中的高阶函数和Lambda表达式有什么特点? ⭐⭐⭐⭐
高阶函数和Lambda表达式是Kotlin函数式编程的核心特性,极大提升了代码简洁性和表达能力。
高阶函数:
- 接受函数作为参数或返回函数的函数
fun <T, R> Collection<T>.mapCustom(transform: (T) -> R): List<R> { val result = mutableListOf<R>() for (item in this) { result.add(transform(item)) } return result }
Lambda表达式特点:
简洁语法:
val sum = { x: Int, y: Int -> x + y } val numbers = listOf(1, 2, 3, 4) val doubled = numbers.map { it * 2 } // 使用it引用单个参数
函数类型:
- 明确的类型表示法:
(Int, Int) -> Int
- 可空函数类型:
((Int) -> String)?
- 带接收者的函数类型:
Int.(Int) -> Int
- 明确的类型表示法:
闭包特性:
- Lambda可以引用和修改外部变量
var sum = 0 listOf(1, 2, 3).forEach { sum += it }
- Lambda可以引用和修改外部变量
尾随Lambda:
- 当函数的最后一个参数是函数时,可以放在括号外
files.filter { it.isFile } .map { it.name }
- 当函数的最后一个参数是函数时,可以放在括号外
隐式参数:
- 单参数lambda可用
it
隐式引用list.filter { it > 0 }
- 单参数lambda可用
解构:
- Lambda参数可以解构
map.forEach { (key, value) -> println("$key = $value") }
- Lambda参数可以解构
常见用途:
集合操作:
val adults = people .filter { it.age >= 18 } .sortedBy { it.name } .map { it.name }
操作构建器:
val dialog = AlertDialog.Builder(context) .setTitle("Title") .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() } .create()
资源管理:
fun <T> withResource(resource: Resource, block: (Resource) -> T): T { try { return block(resource) } finally { resource.close() } }
自定义控制结构:
inline fun transaction(block: () -> Unit) { beginTransaction() try { block() commit() } catch (e: Exception) { rollback() throw e } }
- 接受函数作为参数或返回函数的函数
Kotlin中的内联函数(inline function)有什么优势? ⭐⭐⭐
内联函数通过在编译时将函数调用替换为函数体内容,提高了使用Lambda表达式时的性能和功能。
基本用法:
inline fun repeat(times: Int, action: (Int) -> Unit) { for (index in 0 until times) { action(index) } }
主要优势:
减少运行时开销:
- 避免为Lambda创建匿名类对象
- 消除函数调用开销
- 减少内存分配和垃圾回收压力
// 使用内联函数 repeat(1000) { println(it) } // 编译后无额外对象创建 // 非内联等效 repeat(1000, object : Function1<Int, Unit> { // 创建匿名对象 override fun invoke(it: Int) { println(it) } })
支持非局部返回:
- 可以从Lambda中返回外层函数
fun findPerson(people: List<Person>, predicate: (Person) -> Boolean): Person? { people.forEach { if (predicate(it)) return it // 从findPerson函数返回 } return null }
- 可以从Lambda中返回外层函数
支持reified泛型参数:
- 在运行时保留泛型类型信息
inline fun <reified T> Any.isOfType(): Boolean = this is T // 使用 val isString = obj.isOfType<String>() // 编译为 obj is String
- 在运行时保留泛型类型信息
crossinline与noinline修饰符:
- crossinline:禁止非局部返回但仍内联
- noinline:禁止参数内联,当需要将Lambda作为对象传递时
inline fun example( crossinline c: () -> Unit, noinline n: () -> Unit ) { /* ... */ }
常见应用场景:
- 资源管理模式:如
use
、withLock
、runBlocking
- 控制流构建器:如
run
、let
、apply
、also
- 重复操作:如
repeat
- 条件执行:如
takeIf
、takeUnless
- 集合操作:如
filter
、map
、forEach
注意事项:
- 内联会增加生成代码的大小
- 复杂函数内联可能导致代码膨胀
- 应优先内联短小且频繁调用的函数
什么是Kotlin的解构声明(destructuring declaration)? ⭐⭐⭐
解构声明允许将一个对象同时解包到多个变量中,提高代码可读性和简洁性。
基本语法:
val (name, age, email) = person
工作原理:
- 依赖于命名为
componentN()
的函数 - data class自动生成这些函数
- 可以为任何类手动实现这些函数
支持解构的数据类型:
数据类:自动支持
data class Person(val name: String, val age: Int, val email: String) val person = Person("Alex", 30, "alex@example.com") val (name, age, email) = person // 解构
Pair和Triple:
val pair = Pair("John", 25) val (name, age) = pair
Map.Entry:
for ((key, value) in map) { println("$key -> $value") }
自定义类:通过实现componentN函数
class Point(val x: Int, val y: Int) { operator fun component1() = x operator fun component2() = y } val (x, y) = Point(10, 20)
数组和集合:通过扩展函数支持
val (first, second, third) = listOf(1, 2, 3)
高级用法:
忽略部分值:
val (name, _, email) = person // 忽略age
函数返回值解构:
fun getPersonInfo(): Triple<String, Int, String> = Triple("Alex", 30, "alex@example.com") val (name, age, email) = getPersonInfo()
Lambda参数解构:
map.mapValues { (_, value) -> value.uppercase() }
解构与类型声明结合:
val (name: String, age: Int) = pair
循环中解构:
for ((index, element) in collection.withIndex()) { println("$index: $element") }
解构声明使得处理复合数据结构更加直观和简洁,特别是在处理返回多个值的情况下。
- 依赖于命名为
36、协程(Coroutines)
- 什么是协程?协程与线程的区别? ⭐⭐⭐⭐⭐
- 协程的基本构建块有哪些?(CoroutineScope, CoroutineContext, Job等) ⭐⭐⭐⭐
- 协程的调度器(Dispatchers)有哪些?各自适用于什么场景? ⭐⭐⭐⭐
- launch和async的区别是什么?什么时候使用哪一个? ⭐⭐⭐⭐⭐
- 协程中的异常处理机制是怎样的? ⭐⭐⭐⭐
- 协程的取消机制是如何工作的?如何确保协程可取消? ⭐⭐⭐⭐
- 什么是结构化并发(Structured Concurrency)?它有什么优势? ⭐⭐⭐⭐
- suspend关键字的作用是什么?它与普通函数有什么区别? ⭐⭐⭐⭐⭐
- Flow是什么?它与RxJava、LiveData相比有什么特点? ⭐⭐⭐⭐
- 协程作用域(CoroutineScope)有哪些?lifecycleScope、viewModelScope、GlobalScope分别适用于什么场景? ⭐⭐⭐⭐
37、KAPT与KSP
- 什么是KAPT(Kotlin Annotation Processing Tool)?它与Java的注解处理器有什么区别? ⭐⭐⭐⭐
- KSP(Kotlin Symbol Processing)是什么?它相比KAPT有什么优势? ⭐⭐⭐⭐⭐
- KAPT在Dagger/Hilt、Room等框架中的应用? ⭐⭐⭐
- 如何在项目中配置和使用KAPT/KSP? ⭐⭐⭐
- KSP与编译速度的关系?为什么KSP比KAPT更快? ⭐⭐⭐⭐
- 如何编写兼容KAPT和KSP的注解处理器? ⭐⭐⭐
- KAPT/KSP在多模块项目中的配置和注意事项? ⭐⭐⭐⭐
38、Jetpack库与架构组件
- ViewModel的作用是什么?它如何帮助管理UI相关数据? ⭐⭐⭐⭐⭐
- LiveData是什么?它与Observable、Flow有什么区别? ⭐⭐⭐⭐
- DataBinding和ViewBinding的区别是什么?各自的优缺点? ⭐⭐⭐⭐
- Navigation组件的主要组成部分是什么?它如何简化Fragment之间的导航? ⭐⭐⭐⭐
- 什么是Lifecycle组件?它如何解决生命周期管理问题? ⭐⭐⭐⭐
- WorkManager用于什么场景?它与其他后台处理方案相比有什么优势? ⭐⭐⭐⭐
- Paging 3的主要组件是什么?它如何实现高效的分页加载? ⭐⭐⭐⭐
- Hilt与Dagger 2相比有什么改进?它如何简化依赖注入? ⭐⭐⭐⭐⭐
- DataStore与SharedPreferences相比有什么优势? ⭐⭐⭐
- CameraX API与Camera2 API相比有什么优势? ⭐⭐⭐
39、Kotlin多平台(KMP)与Compose Multiplatform
- 什么是Kotlin多平台(KMP)?它的主要优势是什么? ⭐⭐⭐⭐
- KMP项目架构通常如何组织?共享哪些代码? ⭐⭐⭐⭐
- Compose Multiplatform如何实现跨平台UI开发? ⭐⭐⭐⭐⭐
- 如何在KMP项目中处理平台特定代码?expect/actual关键字的作用? ⭐⭐⭐⭐
- KMP项目如何处理网络请求和数据存储? ⭐⭐⭐
- KMP与Flutter、React Native等跨平台框架相比有什么优劣势? ⭐⭐⭐⭐
40、App模块化与动态交付
- 什么是应用模块化?它有什么优势? ⭐⭐⭐⭐
- Dynamic Feature Module如何工作?它解决了什么问题? ⭐⭐⭐⭐
- 模块间依赖与通信有哪些实现方式? ⭐⭐⭐⭐
- 如何在多模块项目中管理版本依赖? ⭐⭐⭐
- 多模块项目的构建速度优化策略有哪些? ⭐⭐⭐⭐
- 模块化架构中如何实现单模块调试? ⭐⭐⭐
- App Bundle与Split APKs的区别和优势? ⭐⭐⭐⭐