Android Weekly Notes #483
Android Weekly Issue #483
Hands on Jetpack AppSearch
Android 12的新feature: App Search.
MAD Skills series: Hilt under the hood
在Hilt中, 三个最重要的注解:
-
@AndroidEntryPoint
. -
@InstallIn
. -
@HiltAndroidApp
.
解释hilt是如何工作的.
对多个module的Classpath aggregation有图片说明.
Effective Kotlin Item 49: Consider using inline value classes
Kotlin 1.3是inline class, 1.5改为value class, 作为更大的功能点.
inline class Name(private val value: String) {
// ...
}
这段代码编译后变为:
// Code
val name: Name = Name("Marcin")
// During compilation replaced with code similar to:
val name: String = "Marcin"
方法都会被作为静态方法:
@JvmInline
value class Name(private val value: String) {
// ...
fun greet() {
print("Hello, I am $value")
}
}
// Code
val name: Name = Name("Marcin")
name.greet()
// During compilation replaced with code similar to:
val name: String = "Marcin"
Name.`greet-impl`(name)
两种流行的使用场景:
- To indicate a unit of measure. 测量单位. 举例: 时间单位.
@JvmInline
value class Minutes(val minutes: Int) {
fun toMillis(): Millis = Millis(minutes * 60 * 1000)
// ...
}
@JvmInline
value class Millis(val milliseconds: Int) {
// ...
}
interface User {
fun decideAboutTime(): Minutes
fun wakeUp()
}
interface Timer {
fun callAfter(timeMillis: Millis, callback: () -> Unit)
}
fun setUpUserWakeUpUser(user: User, timer: Timer) {
val time: Minutes = user.decideAboutTime()
timer.callAfter(time) { // ERROR: Type mismatch
user.wakeUp()
}
}
这样就不会搞混. 强制我们使用正确的单位.
我们可以定义这样的extension properties:
inline val Int.min
get() = Minutes(this)
inline val Int.ms
get() = Millis(this)
val timeMin: Minutes = 10.min
- To use types to protect user from value misuse.
使用场景, 把id包装在inline class里:
@JvmInline
value class StudentId(val studentId: Int)
@JvmInline
value class TeacherId(val teacherId: Int)
@JvmInline
value class SchoolId(val studentId: Int)
@Entity(tableName = "grades")
class Grades(
@ColumnInfo(name = "studentId")
val studentId: StudentId,
@ColumnInfo(name = "teacherId")
val teacherId: TeacherId,
@ColumnInfo(name = "schoolId")
val schoolId: SchoolId,
// ...
)
需要注意的是当inline class实现了接口或者接受可空参数以后, 它就不被inline了.
Testing Hybrid Jetpack Compose Apps
View和Compose混合应用的UI测试.
Thinking functionally in Kotlin
函数式: 基本的积木块是函数.
举的例子: 判断一个点是否在射程以内.
A Simple Framework For Mobile System Design Interviews
mobile开发面试.
面试流程和每个环节的设计.
Utilizing ADB for daily tasks
日常使用adb.
自动填写两个字段实现登录:
adb shell input text user1 && adb shell input tap x y && adb shell input text password1 && adb shell input tap x y
截图:
adb shell screencap $PATH_IN_DEVICE
录屏:
adb shell screenrecord $PATH_IN_DEVICE
测试deep link:
adb shell am start
-W -a android.intent.action.VIEW
-d <URI> <PACKAGE>
adb shell am start
-W -a android.intent.action.VIEW
-d "example://gizmos" com.example.android
Jetpack Compose Side-Effects II — rememberCoroutineScope
LaunchedEffect可以在composable内部启动一个协程. 并且在composition退出时清理.
LaunchedEffect的一些限制:
- LaunchedEffect是一个composable, 它只能在composable方法里调用.
- 用LaunchedEffect, 你不能控制coroutine的生命周期.
对于这样的场景, 我们有rememberCoroutineScope
.
rememberCoroutineScope
是一个返回scope的方法, 在composable中调用, 在composable离开composition的时候自动取消.
利用它我们就可以在callback里做事情.
我们还可以手动取消.
Trackr comes to the Big Screen
一个任务管理应用的大屏适配.
- 导航: navigation-rail
- List-Detail: SlidingPaneLayout
- Edit: Dialog.
Code: https://github.com/android/trackr
Don't mock static: test Timber Logger with trees
如何测试log.
import timber.log.Timber
class TestTree : Timber.Tree() {
val logs = mutableListOf<Log>()
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
logs.add(Log(priority, tag, message, t))
}
data class Log(val priority: Int, val tag: String?, val message: String, val t: Throwable?)
}
加上这个辅助方法:
fun withTestTree(body: TestTree.() -> Unit) {
val testTree = TestTree()
Timber.plant(testTree)
body(testTree)
Timber.uproot(testTree)
}
最后是这样:
"given service error when get all called then log warn" {
//setup system under test
withTestTree {
val service = mockk<ItemsService> {
every { getAll() } throws Exception("Something failed :(")
}
val systemUnderTest = SystemUnderTest(service)
//execute system under test
systemUnderTest.fetchData()
//capture last logged event
val lastLoggedEvent = logs.last()
assertSoftly {
lastLoggedEvent.message shouldContain "fetchData returned exception instead of empty list"
lastLoggedEvent.priority shouldBe Log.WARN
}
}
}
文章所在的这个网站叫Kotlin Testing
Mocking Matchers API
mock library, 作者用的是mockito-kotlin
这个方法:
val product = mock<Product> {
on { calculatePrice(any())} doAnswer { i ->
when (val discountId = i.getArgument<String>(0)) {
null -> Price(10)
discountId1 -> Price(5)
discountId2 -> Price(4)
else -> throw UnsupportedOperationException("$discountId is not mocked")
}
}
}
传null的时候并不生效, 因为应该用anyOrNull
而不是any
.
然后作者对Mockito
, mockito-kotlin
, 和 mockk
的argument matcher进行了比较.
MockWebServer + HTTPS
MockWebServer的配置问题.
用到了: okhttp-tls
Code
- https://rbusarow.github.io/Tangle/ 一个依赖注入的工具.