我是 Flywith24,欢迎来到我的 Github 主页 😉
📱 Android Developer | 🍚 懂车干饭人 | 📷 业余 vlogger | 🏀 篮球爱好者 | 🚣🏻 划船器入门选手 |
---|
我的博文列表,按系列分类,方便查找。地址:https://flywith24.gitee.io/
Home Page: https://flywith24.gitee.io/categories/
androidx navigation 2.3.0
加入了对 dynamic feature module
的导航支持,因此我们利用这个来分离出多个功能 module 来实现模块化
Android App Bundle 是官方 18 年推出的动态发布方案,类似国内各种插件化方案。不过它需要 Google Play Store 支持,这导致在国内无法使用
借着 navigation 组件支持 dynamic feature module
间导航的契机,我们可以使用 dynamic feature module
来拆分功能模块以实现模块化
传统的拆分方案大概是这样,feature module 之间相互隔离,app module 依赖各个 feature module 间接依赖 base 库,公共库
而使用 dynamic feature module
,其结构是这样的
dynamic feature module
也可以按需安装,也就是说,它们可能不包含在用户最初下载的 APK 中,而是在运行时安装。而我们可以直接将它们包含到 APK 中
首先我们在 base lib 中引入依赖
dependencies {
def nav_version = "2.3.0-alpha06"
api "androidx.navigation:navigation-fragment-ktx:$nav_version"
api "androidx.navigation:navigation-ui-ktx:$nav_version"
api "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
}
我们在 app module 中的 res/navigation 目录下创建 main_nav.xml
接着我们在 activity_main 中设置默认的 host
这里不同于正常 navigation 的用法,没有使用 NavHostFragment,而是使用 DynamicNavHostFragment
我们创建 dynamic feature module ,取名为 feature1
这里
dynamic feature module
的包名前部分要和 applicationId 即 app module 包名相同,否则后续的 include 操作会有问题
这里我们选择在安装时集成该 module
接着我们在该 module 下创建一个 fragment 取名为 Feature1OneFragment
之后我们直接在 main_nav.xml 中引入 该 fragment 并加入 action
接着我们就可以在 app 下的 MainFragment 打开 Feature1OneFragment
我的 demo 中 feature2 是直接引入 fragment,因此跳转的是 Feature2OneFragment
在 feature1 中创建 activity (demo 中为 feature2)
同样需要指定 moduleName
我们可以为 dynamic feature module 单独配置 navigation graph,这样就可以处理 dynamic feature module 内部的跳转了
在 feature1 中创建 feature1_nav.xml ,其中 startDestination 为 Feature1OneFragment
在 main_nav.xml 我们需要使用另外一种方式来使用该 graph
我们使用了一个新的标签 include-dynamic
,同时我们看到了几个没用过的属性
graphPackage
为 dynamic feature module
的包名graphResName
为 dynamic feature module
内部 graph 的名字moduleName
为 module 名注意:这里的 graphPackage 可以省略
- 如果 module 的包名没用按照前文的格式配置会导致无法找到 graphId 的异常
- include-dynamic 标签的 id 要与
feature1_nav.xml
navigation 标签下的 id 一致,或者后者不设置 id
这样从 app module 导航到 feature1 的 startDestination 后便可使用其内部的逻辑进行后续的导航了
Navigation 组件暂不支持 Dynamic include graph 的 deep link
因此我目前也没有找到特别优雅的方式,已知的方案如下
不会测试的开发不是好开发——鲁迅
一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,那么就从认识 test 开始吧
本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics
作为 Android 开发者我们知道在 Android Studio 的 Android 视图中有三部分代码
test 代码知道所有的 main source set 中的代码,因此可以测试这些类。但是 app 代码不知道 test 中的代码,并且 androidTest 和 test 都不知道对方的存在。事实上,当你构建出 apk 并提交应用市场时,测试代码并没有包含在内
下面标记的依赖 使用了 test 的引用方式 testImplementation
和 androidTestImplementation
注意:这些 test 代码不会打包到最终的 apk 文件中
testImplementation
引用的 JUnit
依赖只能在 test source set 中使用,这种依赖范围的限制是 Gradle
实现的
简单总结下:
main
,test
,androidTest
testImplementation
和 androidTestImplementation
我们打开 test source set ,看到其中有一个 ExampleUnitTest
可以看到其内部只有一个 addition_isCorrect()
方法
有两个要素使它成为一个 test:
@Test
注解有了这两个要素,这个方法就可以独立的作为 test 运行
本示例 test 测试的内容在第 15 行,它被称之为断言(assertion
)
断言是 test 的核心内容,它检查你的代码或者 app 行为是否符合你的预期
本示例中,断言检查 4 是否等于 2 + 2
按照规定,您需要将你的 预期结果 传入到 expected 参数中,将 实际结果 传入到 actual 参数中
@Test
注解和断言语句都是 JUnit
下的
关于 JUnit
的更详细的的信息,请移步 官方文档
让我们开始运行一下这个 test,右击该方法,点击 Run
紧接着,Run 窗口会出现
可以看到该窗口显示了 test 的信息,显示出 test 是否通过以及有多少 test 通过
下面我们尝试一个 test 不通过的情况,我们加入一个断言,如下所示
这次我们点击 Run 窗口的绿色按钮来运行 test
我们可以看到,即使只有一个断言失败,整个 test 失败了
窗口指出了预期的结果为 5,而实际的结果为 4,并且下边标记处错误发生在第 15 行,可以看到这的确是个 bug
解决好 bug ,我们再次运行 test 。这次我们使用一个不一样的方式
下面介绍一些其他运行 test 的方式
可以右击类名选择 Run 选项
也可以在左侧视图中右击 test source set 选择 Run 按钮,该方法会运行所有 test。在顶部可以切换要运行的 test ,点击绿色按钮可以运行。也可切换回 app
下面我们来对比一下 androidTest
和 test
test | androidTest |
---|---|
Local Tests | Instrumented Tests |
Local machine JVM | Real or emulated devices |
Faster | Slower |
我们来运行一个 androidTest
,可以看到启动了模拟器
首先我们针对一个功能来创建 test,如图所示,存在一个 getForkAndOriginRepoStats()
方法用于获取 fork 仓库和原始仓库的数据并返回 StatsResult
,其中 StatsResult
第一个参数为 fork 项目的百分比,第二个参数为原始项目的百分比。我们调用 Generate
,选中 test 选项,在弹出框中选择 JUnit4
,点击 OK 并选择存放在 local test 中。这样我们就创建了一个 test
接下来我们编写 test 。可以看到自动创建出的 test 路径与 app code 中的代码路径的包名是对应的。我们先测试项目列表只有一个 item,并且没有 fork ,然后计算 fork 项目的百分比和原始项目的百分比。理论上讲,fork 项目的百分比为 0 ,而原始项目的百分比为 100%,代码如下图所示,我们编写完毕后点击运行
可以看到测试通过
这是一个正常流程,我们还需要测试异常的流程,比如 repos 为 empty list 或者 repos 变量本身为 null
可以看到我们的代码中没有针对 list 为 empty 或 null 做判断,所以导致了空指针,之后我们修改代码后即可通过测试
事实上,我们上面的编码流程叫做 Test Driven Development(TDD) 有关 TDD
的更多信息,可以移步 Test-Driven Development on Android with the Android Testing Support Library (Google I/O '17)
与写普通代码一样,您需要让您的 test 代码更具可读性,可以从三个方向入手
首先我们来谈谈命名,我们知道 test 方法使用 @test 注解标记,理论上方法名可以随意命名,但随意的命名会导致可读性的降低,因此需要一些特定的命名规范
测试模块_ 动作或输入_ 结果状态
例如上面的例子我们的命名为:getForkAndOriginRepoStats_noForked_returnHundredZero
第一部分显示我们要测试的是 getForkAndOriginRepoStats()
方法,第二部分代表我们需要的是没有 fork 仓库的数据源,第三部分是结果的状态,0%
说完了命名我们来谈谈 Given/When/Then
测试的基本结构是 Given X,When Y,Then Z
还是上面的例子
上面示例最后的断言代码让人看着很别扭,我们可以借助断言库来提高这部分的可读性
// 之前
assertEquals(result.forkPercent, 0f)
// 之后
assertThat(result.forkPercent, `is`(0f))
下面的语句就像人类的一句话,翻译下来就是 断言 forkPercent 是 0f
这样的写法需要引入一个库 Hamcrest
testImplementation "org.hamcrest:hamcrest-all:1.3"
注意:由于
is
是 kotlin 中的关键字,因此使用 `is` 来转义
常用的断言库
测试范围指一个 test 测试多少代码
例如自动化测试根据测试范围可以分为
您的测试策略需要覆盖到所有的类型
上面的示例我们已经写过了 Unit Tests
他们的范围是单个的方法或类
如果 Unit Tests
失败了,您知道您的代码在哪里出了问题。因为它聚焦于很小一段代码
Unit Tests
也意味着可以快速运行,由于您频繁地修改代码会使得它会频繁的运行,因此需要速度。Unit Tests
通常是本地测试
它们有较低的保真度,因为现实世界您的 app 要执行很多代码而不仅仅是一个方法或者类
Unit Tests
就像检查一个链条的每个环节是否能够正常运行
但它不检查这些环节组合在一起是否能够运行,为此您需要 Integration Tests
Integration Tests
拥有更大的范围
就像 Integration
这个词一样,Integration Tests
整合一些类确保他们组合起来的表现符合预期
构建 Integration Tests
的方式是让他们测试单个功能,就像获取指定用户的 Github
仓库
与 Unit Tests
相比,Integration Tests
有着更大的范围,但他们仍运行的很快并且有着很好的保真度
根据具体情况来判断使用本地测试还是机器测试,例如如果您写的 Integration Tests
涉及到了 UI 组件,那么您需要使用真机来测试了
第三种类型是 End to end Tests
,该测试将一些列功能组合起来一起运行
End to end Tests
测试 app 的大部分,它十分接近真实地使用,因此速度上会比较慢
它有着最高的保真度并确保您的应用作为一个整体运行
这些测试应该使用设备测试
推荐的测试比例是 70% 的单元测试,20% 的组装测试,以及10% 的端到端测试
您能否轻松地在各个部分测试您的 app 取决于您的 app 使用的结构
例如,您的应用将所有逻辑都放置在一个 activity 的大的方法中,您可能可以写出端到端测试,但单元测试和组装测试则写不出来
一个更好的架构应该将应用的逻辑拆分为多个方法和类,这允许每部分可以独立的测试
对于单元测试,您可以测试 ViewModel
,Repository
以及 DAO
对于组装测试,您可以组合测试 fragment
和 ViewModel
,或者您可以测试整个数据库代码
端到端测试会测试整个应用
关于测试的原理,可移步 官方文档
test 的 codelab
在学习和使用
jetpack
组件时,总是被其 gradle 依赖搞的晕头转向,故在此整理jetpack
主要组件的依赖,及传递关系
jetpcak
组件源码地址jetpcak
组件 版本: Google's Maven Repository./gradlew :app:dependencies
可直接跳过后续内容前往最后一节查看总结
关于 androidx
下 fragment/activity
的变化,可查看该文, 【译】AdroidX下使用Activity和Fragment的变化
dependencies {
def appcompat_version = "1.1.0"
implementation "androidx.appcompat:appcompat:$appcompat_version"
// For loading and tinting drawables on older versions of the platform
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
}
androidx.annotation:annotation:1.1.0
androidx.core:core:1.1.0
androidx.cursoradapter:cursoradapter:1.0.0
androidx.fragment:fragment:1.1.0
androidx.appcompat:appcompat-resources:1.1.0
androidx.drawerlayout:drawerlayout:1.0.0
androidx.collection:collection:1.0.0
appcompat
中默认引入了fragment
库,如果想使用更新版本的fragment
库,可以单独引用
dependencies {
def fragment_version = "1.2.2"
// Java language implementation
implementation "androidx.fragment:fragment:$fragment_version"
// Kotlin
implementation "androidx.fragment:fragment-ktx:$fragment_version"
// Testing Fragments in Isolation
implementation "androidx.fragment:fragment-testing:$fragment_version"
}
⚠️ Note: The Kotlin dependant libraries of this version (fragment-ktx,fragment-testing) target Java 8 programming language bytecode. Please read Use Java 8 language features to learn how to use it in your project.
org.jetbrains.kotlin:kotlin-stdlib:1.3.50
androidx.activity:activity-ktx:1.1.0
androidx.core:core-ktx:1.1.0
androidx.collection:collection-ktx:1.1.0
androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0
fragment
库默认引入了activity
core-ktx
lifecycle-livedata-core-ktx
lifecycle-viewmodel-ktx
库
dependencies {
def activity_version = "1.1.0"
// Java language implementation
implementation "androidx.activity:activity:$activity_version"
// Kotlin
implementation "androidx.activity:activity-ktx:$activity_version"
}
⚠️ Note: The Kotlin dependant libraries of this version (activity-ktx) target Java 8 programming language bytecode. Please read Use Java 8 language features to learn how to use it in your project.
org.jetbrains.kotlin:kotlin-stdlib:1.3.50
androidx.core:core-ktx:1.1.0
androidx.lifecycle:lifecycle-runtime-ktx:2.2.0
androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0
dependencies {
def core_version = "1.2.0"
// Java language implementation
implementation "androidx.core:core:$core_version"
// Kotlin
implementation "androidx.core:core-ktx:$core_version"
// To use RoleManagerCompat
implementation "androidx.core:core-role:1.0.0-alpha01"
}
dependencies {
def lifecycle_version = "2.2.0"
def arch_version = "2.1.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Saved state module for ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
// Annotation processor
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// alternately - if using Java8, use the following instead of lifecycle-compiler
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// optional - helpers for implementing LifecycleOwner in a Service
implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version"
// optional - ProcessLifecycleOwner provides a lifecycle for the whole application process
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
// optional - ReactiveStreams support for LiveData
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
// optional - Test helpers for LiveData
testImplementation "androidx.arch.core:core-testing:$arch_version"
}
⚠️ lifecycle-extensions
已废弃,如果使用LifecycleService
请依赖lifecycle-service
;如果使用ProcessLifecycleOwner
请依赖lifecycle-process
。lifecycle-extensionsl
不会有2.3.0版本- 2.1.0 后
ViewModelProviders.of()
被废弃。您可以在FragmentActivity
或者Fragment
使用ViewModelProvider(ViewModelStoreOwner)
构造器来实现相同的功能。(Fragment
库 1.2.0以上)
dependencies {
def nav_version = "2.3.0-alpha02"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Dynamic Feature Module Support
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
// Testing Navigation
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}
dependencies {
def paging_version = "2.1.1"
implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
// alternatively - without Android dependencies for testing
testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
// optional - RxJava support
implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}
dependencies {
def room_version = "2.2.4"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
⚠️ Note: For Kotlin-based apps, make sure you use kapt instead of annotationProcessor. You should also add the kotlin-kapt plugin.
androidx
库遵循严格的语义版本控制。版本字符串(例如 1.0.1-beta02)包含三个数字,分别代表 major 级别、minor 级别和问题修复级别。预发布版本也有一个后缀,用于指定预发布阶段(Alpha 版、Beta 版、候选版本)和版本号(01、02 等)。
库的每个版本都要经历三个预发布阶段,才能成为稳定版本。各预发布阶段的标准如下:
Alpha 版
Beta 版
候选版本 (RC)
一个库可以同时具有多个版本。每个版本都具有不同的发布阶段。例如,虽然 androidx.activity 的稳定版可以是 1.0.0,但也可能还有 1.1.0-beta02 版本以及 2.0.0-alpha01 版本。
ViewModel
LiveData
Activity
Fragment
Service
等均可使用协程
ViewModelScope
,请使用 androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01
或更高版本。LifecycleScope
,请使用 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01
或更高版本。liveData
,请使用 androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01
或更高版本。class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
带有-ktx
的库拥有 kotlin
的特性,有很多常用的扩展函数
带有-ktx
的库依赖 java 版本, 带有-ktx
和 java 版本各自单独使用即可
appcompat
库包含 fragmnet
,fragment
包含 activity
,当你引入androidx
appcompat
库便可以使用androidx
fragment
和 androidx
activity
受限于appcompat
稳定版的更新速度,您可以选择单独使用androidx
fragment
/activity
AppCompatActivity
继承自 FragmentActivity
, AppCompatActivity
可以直接使用 fragment
DataBinding
ViewBinding
依赖于 android build gradle
插件 ,无需引入其他依赖
androidx
fragment
/activity
下均依赖的ViewModel
和 LiveData
,您可以直接使用
使用ViewModel
和 LiveData
完整功能需要单独引入,它们都是lifecycle
大家族下的
lifecycle-extensions
已废弃,不要使用它了
如果想使用实现LifecycleOwner
的 Service
,需要引入 lifecycle-service
androidx recyclerview 1.2.0-alpha02
版本添加了新功能 MergeAdapter,帮助开发者更容易地为 RecyclerView 添加 Header 和 Footer。详情参见 【译】MergeAdapter 的使用 使用官方 API 为 Recyclerview 添加 Header 和 Footer
该版本中还有一个改动:RecyclerView.Adapter
lazy state restoration,帮助开发者恢复 RecyclerView 的状态
我对这个功能并没有什么感觉。众所周知,Android 中的 View 内部是有着状态保存和恢复的方法的。RecyclerView 也是如此,它可以恢复自身已滚动的位置
有关状态保存的内容可以参见 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析
真实情况也是如此
最近看到 Florina Muntenescu 的 Restore RecyclerView scroll position ,其中介绍了 RecyclerView.Adapter
lazy state restoration,这勾起了我的兴趣
如文中描述,RecyclerView 在 activity/fragment 重建时失去滚动位置是因为 Adapter 中的数据是 异步 加载的,当 RecyclerView layout 时数据并没有加载,因此也恢复不了之前的位置状态。一个比较简单的例子是使用 Navigation 组件进行导航,返回时 fragment 中的 RecyclerView 由于再次调用接口获取数据,导致其滑动位置失去
有几种方法可以保证 RecyclerView 恢复到正确的滚动位置,最好的办法是借助缓存,ViewModel 或 Repository 中缓存要显示的数据,确保始终在第一个布局传入前在 Adapter 上设置数据。也有一些其他的方案,这些方案要么太复杂,要么不够优雅
recyclerview:1.2.0-alpha02
中的解决方案是提供一个新的 Adapter 方法,该方法允许设置状态恢复策略,它有三个选项
这是 默认 的状态,它会立即恢复 RecyclerView 的状态,该种策略无法解决延迟加载的数据的问题,可以使用 PREVENT_WHEN_EMPTY
仅当 Adapter 不为空(adapter.getItemCount() > 0)时,才恢复 RecyclerView 状态。 如果您的数据是异步加载的,那么 RecyclerView 会一直等到数据加载完毕,然后状态才能恢复。 如果您有默认 item(例如 Header 或 加载指示器)作为适配器的一部分,则应该使用PREVENT
选项,除非使用 MergeAdapter 添加了默认 item。 MergeAdapter 等待所有适配器准备就绪,然后才恢复状态
状态不会恢复,直到配置了 ALLOW
或者 PREVENT_WHEN_EMPTY
使用方式如下:
adapter.stateRestorationPolicy = PREVENT_WHEN_EMPTY
加入了上面的配置后即使是异步加载数据也能恢复 RecyclerView 的位置
老规矩,我们沿着官方的 commit log 来看看其实现原理
首先我们看看 IssueTracker 上提的 Feature
表达的意思也很简单,就是当加载异步数据时 RecyclerView 的位置状态无法恢复,Adapter 应该提供相关的解决方案
有意思的是,实现该功能时还重新实现了前一个版本的逻辑,我在 git commit log 中看到了 revert 操作
为了防止 LayoutManager#onRestore
执行多次,没有采用最开始的实现方式。但 Yigit Boyar (这个提交的开发者) 仍然希望使用最开始的实现方式,但是 LayoutManager#onRestoreInstance
的状态时 public ,因此只能选取一个折中的方案
过去,开发者会无意间调用 onRestoreInstanceState(State)
方法。例如,一些开发者已使用它来手动设置自己更新的状态,这样即使在此状态之前已恢复,在此处传递状态也将导致 LayoutManager 接收它并相应地更新其内部状态。因此,即使看起来好像很奇怪,也必须始终调用 requestLayout
来保留功能
接下来我们来分析这部分源码,内容很少,所以我们详细看下
首先是引入 StateRestorationPolicy
的枚举
然后需要提供 setStateRestorationPolicy
和 getStateRestorationPolicy
方法,此时我们还需要一个方法来判断是否要将 SavedState 传递给 LayoutManager
前面的 setStateRestorationPolicy
方法中 调用了 notifyStateRestorationPolicyChanged
,而 notifyStateRestorationPolicyChanged
为静态类 AdapterDataObservable
中的方法,该类中的其他方法我们也很熟悉,均是刷新 Adapter 中数据的方法。
而 notifyStateRestorationPolicyChanged
中调用了 mObservers list 中元素的 onStateRestorationPolicyChanged
方法,通过源码我们得知该 list 中的元素类型为 AdapterDataObserver
,因此还需要在 AdapterDataObserver
中加入 onStateRestorationPolicyChanged
方法
该方法是个空实现,而 RecyclerViewDataObserver
重写了该方法
配置恢复策略以及恢复策略变化时的监听都有了,接下来要做的就是如果之前有待恢复的装则恢复之前的状态
注意:发布之前
StateRestorationPolicy
叫做StateRestorationStrategy
,后来命名为StateRestorationPolicy
,alpha 版本的库可能随时更改 API 的命名和删除 API,因此查看这部分源码的同学请注意
至此,相关的源码都在这里了
StateRestorationPolicy
提供了 RecyclerView 异步加载数据恢复滚动位置的解决方案。原理就是通过配置 StateRestorationPolicy
来改变恢复策略,同时在策略改变时调用 requestLayout
方法。在 dispatchLayoutStep2()
(该方法会在 onLayout 和 measure 方法中调用) 方法中恢复状态(如果 canRestoreState()
返回 true)
一点思考:我们都知道 ViewPager2 是使用 RecyclerView 实现的,那么借助本文介绍的 API 可以做点什么吗?
欢迎各位小伙伴在评论区留言,说说你的想法
市面上权限请求的库很多,而前段时间官方刚刚将 requestPermissions()
+ onRequestPermissionsResult()
API 弃用,那么官方的替代方案是什么呢?本文将介绍如何借助 Activity Result API 进行权限请求以及如何使用 Kotlin 扩展函数自己封装一个权限请求库
在 Android Jetpack Activity 1.2.0-alpha02
和 Fragment 1.3.0-alpha02
中,Google 提供了全新的 Activity Result API 来替换 startActivityForResult()
+ onActivityResult()
和 requestPermissions()
+ onRequestPermissionsResult()
。详情可移步 官方文档,中文可以参考 秉心说 的 是时候丢掉 onActivityResult 了 !,有些 API 的名字发生了变化,请留意
紧接着在 Activity 1.2.0-alpha04
和 Fragment 1.3.0-alpha04
版本中,
startActivityForResult()
+onActivityResult()
和requestPermissions()
+onRequestPermissionsResult()
被标记为弃用,而在Fragment 1.3.0-alpha05
这些标记弃用的方法内部已改为使用 ActivityResultRegistry
实现
新的 API 使用非常简单,分为单一权限请求,和多权限请求,Activity 和 Fragment 使用方法相同
val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE
registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
// 请求结果,result 为 boolean true 代表已授权,false 代表未授权
}.launch(permission)
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result: MutableMap<String, Boolean> ->
// 请求结果,返回一个map ,其中 key 为权限名称,value 为是否权限是否赋予
}.launch(permissions)
配合 Kotlin 的扩展函数,我们可以将权限请求的逻辑进行封装。
开发过程中,我们申请权限时关注的就是权限是否申请成功,如果未申请成功是否勾选了不再询问
因此我们可以加入「权限申请成功」,「权限申请失败且未勾选不再询问」,「权限申请失败且已勾选不再询问」三种状态的回调
/**
* [permission] 权限名称
* [granted] 申请成功
* [denied] 被拒绝且未勾选不再询问
* [explained] 被拒绝且勾选不再询问
*/
inline fun Fragment.requestPermission(
permission: String,
crossinline granted: (permission: String) -> Unit = {},
crossinline denied: (permission: String) -> Unit = {},
crossinline explained: (permission: String) -> Unit = {}
) {
registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
when {
result -> granted.invoke(permission)
shouldShowRequestPermissionRationale(permission) -> denied.invoke(permission)
else -> explained.invoke(permission)
}
}.launch(permission)
}
/**
* [permissions] 权限数组
* [allGranted] 所有权限均申请成功
* [denied] 被拒绝且未勾选不再询问,同时被拒绝且未勾选不再询问的权限列表
* [explained] 被拒绝且勾选不再询问,同时被拒绝且勾选不再询问的权限列表
*/
inline fun Fragment.requestMultiplePermissions(
vararg permissions: String,
crossinline allGranted: () -> Unit = {},
crossinline denied: (List<String>) -> Unit = {},
crossinline explained: (List<String>) -> Unit = {}
) {
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result: MutableMap<String, Boolean> ->
//过滤 value 为 false 的元素并转换为 list
val deniedList = result.filter { !it.value }.map { it.key }
when {
deniedList.isNotEmpty() -> {
//对被拒绝全选列表进行分组,分组条件为是否勾选不再询问
val map = deniedList.groupBy { permission ->
if (shouldShowRequestPermissionRationale(permission)) DENIED else EXPLAINED
}
//被拒接且没勾选不再询问
map[DENIED]?.let { denied.invoke(it) }
//被拒接且勾选不再询问
map[EXPLAINED]?.let { explained.invoke(it) }
}
else -> allGranted.invoke()
}
}.launch(permissions)
}
使用起来是这样的
requestPermission(Manifest.permission.RECORD_AUDIO,
granted = { permission ->
//权限申请成功
},
denied = { permission ->
//权限申请失败且未勾选不再询问,下次可继续申请
},
explained = { permission ->
//权限申请失败且已勾选不再询问,需要向用户解释原因并引导用户开启权限
})
requestMultiplePermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA,
allGranted = {
//全部权限均已申请成功
},
denied = {list->
//权限申请失败且未勾选不再询问,下次可继续申请
},
explained = {list->
//权限申请失败且已勾选不再询问,需要向用户解释原因并引导用户开启权限
})
配合 Kotlin DSL,代码读起来会更容易
// DSL 模式
permission(Manifest.permission.RECORD_AUDIO) {
granted = { permission ->
//权限申请成功
}
denied = { permission ->
//权限申请失败且未勾选不再询问,下次可继续申请
}
explained = { permission ->
//权限申请失败且已勾选不再询问,需要向用户解释原因并引导用户开启权限
}
}
// DSL 模式
permissions(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
) {
allGranted = {
//全部权限均已申请成功
}
denied = {list->
//权限申请失败且未勾选不再询问,下次可继续申请
}
explained = {list->
//权限申请失败且已勾选不再询问,需要向用户解释原因并引导用户开启权限
}
}
Java 是可以调用 Kotlin 的扩展函数的,为了更方便地调用,可以在此基础上再封装一层
PermissionUtils.requestPermission(this, permission, new PermissionResultListener() {
@Override
public void granted(String permission) {
Log.i(TAG, "granted: ");
}
@Override
public void denied(String permission) {
Log.i(TAG, "denied: ");
}
@Override
public void explained(String permission) {
Log.i(TAG, "explained: ");
}
});
PermissionUtils.requestMultiplePermissions(this, new MultiPermissionResultListener() {
@Override
public void allGranted() {
Log.i(TAG, "allGranted: ");
}
@Override
public void denied(List<String> list) {
Log.i(TAG, "denied: " + list.toString());
}
@Override
public void explained(List<String> list) {
Log.i(TAG, "explained: " + list.toString());
}
}, permissions);
demo 在这里,如果感觉这个思路对你有帮助的话,点一颗小星星吧~ 😉
另外我还将它传到了 JitPack 上,引入姿势如下:
在项目根目录的 build.gradle
加入
allprojects {
repositories {
//...
maven { url 'https://jitpack.io' }
}
}
添加依赖
dependencies {
implementation 'com.github.Flywith24:Flywith24-Permission:$version'
}
androidx Navigation
组件是 Android 中应用内导航的官方库,当前最新的版本为 2.3.0-beta01
(2020.05.20)
很多人不喜欢 Navigation 因为其设计不符合开发者的预期,它在 navigate 时会导致之前的 fragment 重建。网上针对这一问题有一个 重写 Navigator 的方案,大多数人会简单地认为 Navigation 无法保存 fragment 状态是因为使用了 replace(曾经的我也这样认为)
本文的内容为 Navigation 的职能边界,简单使用,高阶使用技巧(例如同一 activity 部分内部分 fragment 共享 ViewModel,模块化)以及关于 Navigation 所谓的「设计问题」的探讨
对 Navigation 的使用及职能边界已经了解的小伙伴可以直接跳过前两部分
由于文章内容较长,已将模块化部分单独成文,地址在这
Android 中,activity 和 fragment 是主要的视图控制器,因此界面间的调转也是围绕 activity / fragment 进行的
// 跳转 activity
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("key", "value")
startActivity(intent)
// 跳转 fragment
supportFragmentManager.commit {
addToBackStack(null)
val args = Bundle().apply { putString("key", "value") }
replace<HomeFragment>(R.id.container, args = args)
}
如果项目比较大,我们可能会将 KEY 抽取为常量,并且在 activity 和 fragment 中填写静态方法以告诉调用者该界面需要什么参数
// SecondActivity
companion object {
@JvmStatic
fun startSecondActivity(context: Context, param: String) {
val intent = Intent(context, SecondActivity::class.java)
intent.putExtra(Constant.KEY, param)
context.startActivity(intent)
}
}
// HomeFragment
companion object {
fun newInstance(param: String) = HomeFragment().apply {
arguments = Bundle().also {
it.putString(Constant.KEY, param)
}
}
}
可以看到,得益于 kotlin 的扩展函数,界面间跳转的代码已足够简洁
但是
Jetpack 导航组件是一套库,工具和指南,为应用内导航提供了强大的导航框架
引入依赖如下
dependencies {
def nav_version = "2.3.0-beta01"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Dynamic Feature Module Support
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
// Testing Navigation
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}
它支持 fragment ,activity,或者是自定义的 destination 间的跳转
Navigation UI 库 支持 Drawer,Toolbar 等 UI 组件
现在我们对 Navigation 有一个初步的认识,接下来我们看看 Navigation 的职能边界
Navigation 主要有三个部分
Navigation Graph
是一种新的 resource type,它是一个集中管理 navigation 的 xml 文件
Navigation Graph 中的每一个界面叫:Destination,它可以使 fragment ,activity,或者自定义的 Destination
Navigation 管理的就是 Destination 间的跳转
点击 Destination,可以在屏幕右侧看见 deep link 等信息的配置
Navigation Graph 中的带箭头的线叫:Action,它代表着 Destination 间不同的路径
点击 Action,可以在屏幕右侧看到 Action 的详细配置,动画,Destination 间跳转传递的参数,操作返回栈,Launch Options
不知道各位小伙伴大学是否学过 图论,个人感觉 Navigation Graph 就像 有向图,而其中的 Destination 和 Action 就像图论中的 点 和 边
NavHost
是一个空容器,用于显示 navigation graph
中的 destination。 导航组件提供一个默认的 NavHost
实现 NavHostFragment
,它显示 fragment destination
NavHostFragment 是 navigation-fragment 中的类
它提供了一个可独立导航的区域,使用时大概是这样
所有的 fragment Destination 都是通过 NavHostFragment 管理,相当于一个容器
每个 NavHostFragment 都有一个 NavController,用于定义 navigation host 中的导航。 它还包括 navigation graph 以及 navigation 状态(例如当前位置和返回栈),它们将与 NavHostFragment 本身一起保存和恢复
NavController
帮助 NavHost
管理导航,其内部持有 NavGraph
和 Navigator
(通过持有 NavigatorProvider
间接持有)
其中 NavGraph 决定了界面间的跳转逻辑,它通常在 Android resource 下创建,同时也支持通过代码动态创建
Navigator 定义了一在应用内导航的机制。它的实现类有 ActivityNavigator, DialogFragmentNavigator, FragmentNavigator, NavGraphNavigator。当然,开发者也可以自定义 Navigator
。每种 Navigator
都有自己的导航策略,例如 ActivityNavigator
使用 starActivity
来进行导航
下面引用一张 KunMinX 的专栏 重学Android Navigation 一文中的配图,帮助大家理解这其中的依赖关系
我们在 res/navigatoin 创建的 xml 文件叫 Navigation Graph
(类似图论中的图)
其内部每个节点叫 Destination
(类似图论中的点) ,它对应着 activity/fragment/dialog,代表着屏幕上的界面
连接 Destination
与 Destination
之间的线叫 Action
(类似图论中的边),它是从一个界面跳转另个一个界面的抽象,可以配置跳转动画,传递参数,以及返回栈等信息
NavHost
是显示 Navigation Graph
的容器,实现类为 NavHostFragment
,每个 NavHostFragment
中都持有一个 NavController
NavController
是导航的大管家,封装着 navigate
navigateUp
popBackStack
等方法
Navigator
是对 Destination
之间跳转的封装。由于 Destination
可以是 activity 或 fragment,因此有了 ActivityNavigator
FragmentNavigator
等实现类,用于实现具体的界面跳转
Navigation 2.1.0 引入,用于实现 navigate 到一个 DialogFragment
使用也很简单,使用 dialog 标签,其他处理的与 fragment 相同
我们都知道 fragment 可以使用 activity 级别共享 ViewModel,但是对于单 activity 项目,这就意味着所有的 fragment 都能拿到这个共享的 ViewModel。本着最少知道原则,这不是一个好的设计
Navigation 2.1.0,官方引入了 navigation graph 内共享的 ViewModel,这使得 ViewModel 的作用域得到了细化,业务之间可以很好地被隔离
使用起来非常简单
// kotlin
val viewModel: MyViewModel by navGraphViewModels(R.id.my_graph)
// java
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.my_graph);
MyViewModel viewModel = new ViewModelProvider(backStackEntry).get(MyViewModel.class);
什么是 Safe Args?
它是一个 Gradle 插件,可以根据 navigation 文件生成代码,帮助开发者安全地在 Destination
之间传递数据
那么为什么要设计这样一个插件呢?
我们知道使用 bundle 或者 intent 传递数据时,如果出现类型不匹配或者其他异常,是在 Runtime 中才能发现的,而 Safe Args
把校验转移到了编译期
使用 Safe Args
需要手动引入插件
buildscript {
repositories {
google()
}
dependencies {
def nav_version = "2.3.0-alpha06"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
前面我们提到 Safe Args
会生成代码
如果要生成 java 代码,则在 app 或其他 module 的 build.gradle 中加入
apply plugin: "androidx.navigation.safeargs"
如果向生产 kotlin 代码,则加入
apply plugin: "androidx.navigation.safeargs.kotlin"
参数是在 action 中配置的,我们只需在加入 argument 标签
<action android:id="@+id/startMyFragment"
app:destination="@+id/myFragment">
<argument
android:name="myArg"
app:argType="integer"
android:defaultValue="1" />
</action>
Navigation 支持以下类型
启用 Safe Args 后,生成的代码将为每个操作以及每个发送和接收 destination 创建以下类型安全的类和方法
fromBundle()
方法可以取出从发送 destination 传来的参数下面的代码展示如何在发送 destination 传递参数
override fun onClick(v: View) {
val amountTv: EditText = view!!.findViewById(R.id.editTextAmount)
val amount = amountTv.text.toString().toInt()
// 在相应的 action 中配置参数
val action = SpecifyAmountFragmentDirections.confirmationAction(amount)
v.findNavController().navigate(action)
}
接下来展示如何在接收destination 中取出传来的参数,kotlin 可以使用 by navArgs()
获取参数
// kotlin
val args: ConfirmationFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tv: TextView = view.findViewById(R.id.textViewAmount)
val amount = args.amount
tv.text = amount.toString()
}
// java
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
TextView tv = view.findViewById(R.id.textViewAmount);
int amount = ConfirmationFragmentArgs.fromBundle(getArguments()).getAmount();
tv.setText(amount + "")
}
有一些 destination 通常是组合使用,并且在多个地方重复调用。例如独立的登录流程,后续的忘记密码,修改密码等 destination 可以看做一个整体来使用
这种情况我们使用嵌套 navigation graph,选中一组可以作为整体的 destination (按住 shift 鼠标点选),然后右击选中 Move to Nested Graph > New Graph 。这样就生成了 嵌套 navigation graph。在 嵌套 navigation graph 上双击即可查看内部的 destination
如果想要引用其他 module 中的 graph,可以使用 include
标签
您可以为多个 destination 创建共用的 action,例如您可能想要在不同的 destination 中导航至相同的界面
对于这种情况您可以使用全局 action
选中一个 destination 并右击,选择 Add Action > Global ,一个箭头会 出现在 destination 的左边
使用也很简单,向 navigate 方法传入全局 action 的资源 id 即可
viewTransactionButton.setOnClickListener { view ->
view.findNavController().navigate(R.id.action_global_mainFragment)
}
在开发过程中,我们可能会遇到一个 destination 根据条件跳转不同的 destination 的情况
例如一些 destination 需要用户处于登录状态才能进入,或者在游戏结束后,胜利和失败跳转不同的 destination
下面我们用一个示例来展示 Navigation 如何处理该种场景
该示例中,用户尝试跳转到资料页中,如果该用户处于未登录状态,则需要跳转到登录界面
我们使用 LoginViewModel 来保存登录状态,从 ProfileFragment 点击按钮跳转到 ProfileDetailFragment,在该界面判断登录的状态,如果是未授权则跳转到 LoginFragment,如果已授权则提示欢迎
在登录界面判断登录状态,如果授权成功则回到 ProfileDetailFragment,授权失败则显示登录失败提示
在登录界面点击返回视为未授权,应该直接返回 ProfileFragment 界面
开发过程中我们可能会遇到这类的需求,我们需要让用户打开 app 时直接空降到某个特定页面(例如点开通知栏跳转到特定文章),亦或者我们需要从一个 destination 跳转到一个在其他流程中比较深的位置的 destination ,如下图,从 FriendList 跳转到 Chat 界面
上面的这种需求叫作 deep link
,Navigation 支持两种 deep link
的跳转。
在 manifest activity -> intent-filter 标签下 加入 action ,category ,data 等标签,满足条件的 intent 可以被打开
详情见 官方文档,这里不再赘述,我们只关注如何通过 Navigation 构建 intent
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.android)
.setArguments(args)
.createPendingIntent()
如果已经存在 NavController,可以通过 NavController.createDeepLink() 方法创建 deep link
在 navigation graph 中支持 deepLink 标签
例如上图从 FriendListFragment 跳转到 ChatFragment,可以在 graph ChatFragment 下加入 deepLink 标签
<fragment
android:id="@+id/chatFragment"
android:name="com.flywith24.bottomtest.ChatFragment
android:label="ChatFragment">
<argument
android:name="userId"
app:argType="string" />
<deepLink
android:id="@+id/deepLink"
app:uri="chat://convention/{userId}" />
</fragment>
NavController#navigate()
方法支持传入 URI
因此可直接调用
val userId = "1111"
findNavController().navigate("chat://convention/$userId".toUri())
参见 【奇技淫巧】使用 Navigation + Dynamic Feature Module 实现模块化
androidx 下 frament replace 的行为你真的了解吗?
该部分内容我们在 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析 和【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析 已有分析
因此我们直接说结论
fragment replace 后 前一个 fragment 会执行 onDestroyView 而不执行 onDestroy ,即 fragment 本身未销毁,其内部 view 被销毁
FragmentManager 的 moveToState 方法在触发 fragment 的 onDestroyView 前根据条件会执行 fragmentStateManager.saveViewState() 方法来保存view状态(1.2.2,旧版本该处方法方法名略有不同)
而由于 fragment 本身没有销毁,其成员也不会被销毁
所以当返回后 view 的状态会被恢复,而成员状态没有改变,所以 replace 后 fragment 能恢复到之前的状态
那么 Navigation 的所谓 「设计问题」是怎么回事?
我们使用navigation 从 HomeFragment 跳转到 DashboardFragment,日志如下
可以看到 HomeFragment 被重建了,原实例(e6c266),新实例(c3e49cc)
fragment 被重建,这就是原因所在!
那么为什么出现这种现象?我们翻一下源码
从源码可以看到,其内部通过反射创建了新的 fragment 实例,这导致 fragment 内部的状态无法恢复
不过如果 navigation 导航的所有 destination 没有平级关系,换句话说在一个返回栈内,这样的设计是没有问题的
但是有些时候我们希望使用 navigation 管理一些平级界面,例如 BottomNavigation
issuetracker 有这样一个 issue,注意提出的时间
主要意思就是现阶段的 bottom tab navigation 不符合 Material Design 的规范
相同类型的 issue 还有这些
Ian Lake 在该条 issue 下给出了详细的解答
我在这里简单介绍一下 Ian Lake,虽然不知道他的职位,但从他的活跃程度看应该是 fragment 和 navigation 的负责人,在 Google I/O 大会 和 Android Dev Summit 多次进行演讲
一句话解释:单个FragmentManager 不支持多个返回栈,以现有的 fragment API 无法做到这一支持,开发者不得不自己实现多返回栈(例如我在 【背上Jetpack之Fragment】从源码的角度看Fragment 返回栈 附多返回栈demo 一文中提供的demo)
但他提供了短期和中期方案
短期:提供一个公开示例,展示使用当前 API 进行多返回栈的实现(即,每个底部导航项都有一个单独的 NavHostFragment 和 navigation graph)。其 demo 在这 ,核心逻辑在 NavigationExtensions.kt
内
中期:在 Fragment 上构建正确的API,以便它们可以正确地支持多个返回栈,包括为所有返回栈上的所有 Fragment 正确保存和恢复已保存的实例状态和非配置实例状态。 这项工作目前处于探索阶段,尽管我希望,但我无法提供时间表或保证这项工作将会成功
以上回复发生在 2019 年2月
在 2019 年10月 Ian Lake 再次回复了开发者的疑问
多返回栈支持计划需要三步走
之后他回复了 70 楼的开发者,明确了如果使用单个 NavHostFragment / navigation graph / FragmentManager 能够支持多返回栈,那么从 A 或 B 导航到 C 就不会有任何问题
接着时间来到了2020年
今年 1月30日,Ian Lake 再次做了回复
大概意思就是原定 Fragment 1.3.0
和 Navigation 2.3.0
提供多返回栈支持的计划跳水了,计划在 Fragment 1.4.0-alpah01
和 Navigation 2.4.0-alpha01
提供
至此关于 Navigation 所谓的 「设计问题」就探讨结束了
这两天在准备写 fragment 返回栈的文章,但是发现必须先介绍一下 OnBackPressedDispatcher ,所以这是一篇介绍 what 的文章,喜欢一手资料的可以移步 官方文档
【背上Jetpack】Jetpack 主要组件的依赖及传递关系
【背上Jetpack】AdroidX下使用Activity和Fragment的变化
【背上Jetpack之Fragment】你真的会用Fragment吗?Fragment常见问题以及androidx下Fragment的使用新姿势
【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析
OnBackPressedDispatcher
在 androidx activity 1.0.0
加入,旨在处理返回逻辑。您不仅可以获得在 Activity
之外处理返回键的便捷方式。 根据您的需要,您可以在任意位置定义 OnBackPressedCallback
,使其可复用,或根据应用程序的架构进行任何操作。 您不再需要重写Activity
中的 onBackPressed
方法,也不必提供自己的抽象的来实现需求的代码。
ComponentActivity
是 FragmentActivity
和 AppCompatActivity
的基类,它使您可以通过使用其 OnBackPressedDispatcher
(可以通过调用 getOnBackPressedDispatcher()
)来控制返回按钮的行为。
OnBackPressedDispatcher
控制如何将返回按钮事件分配给一个或多个OnBackPressedCallback
对象。 OnBackPressedCallback
的构造函数将布尔值用于初始启用状态。 仅当启用了回调(即 isEnabled()
返回true)时,调度程序才会调用回调的handleOnBackPressed()
来处理返回按钮事件。 您可以通过调用 setEnabled()
来更改启用状态。
回调是通过 addCallback
方法添加的。 强烈建议使用采用 LifecycleOwner 的addCallback()
方法。 这样可以确保仅在 LifecycleOwner 为 Lifecycle.State.STARTED 时才添加OnBackPressedCallback
。 当关联的 LifecycleOwner 被销毁时,该 activity 会删除已注册的回调,以防止内存泄漏,并使其适用于寿命比该 activity
短的 fragment
或其他生命周期所有者。
下面是一个示例
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//此 callback 仅当 MyFragment 至少是 Started 状态下调用
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
//拦截返回事件
}
//此 callback 可以在这里或者上面的 lambda 中开启和关闭
}
...
}
您可以通过 addCallback()
提供多个回调。 这样做时,将按照添加回调的相反顺序调用回调,即最后添加的回调是第一个给予处理返回按钮事件的机会的回调。 例如,如果您依次添加了三个分别名为1、2和3的回调,则将分别以3、2和1的顺序调用它们。
回调遵循“责任链”模式。 仅当未启用前一个回调时,才调用链中的每个回调。 这意味着在前面的示例中,仅当未启用回调3时,才会调用回调2。 仅当未启用回调2时,才调用回调1,依此类推。
请注意,通过 addCallback()
添加回调时,直到 LifecycleOwner 进入Lifecycle.State.STARTED 状态,才将回调添加到责任链中。
强烈建议更改 OnBackPressedCallback
的启用状态以进行临时更改(即更改 isEnabled 的值),因为它可以保持上述顺序,如果您在多个不同的嵌套生命周期所有者上注册了回调,这尤其重要。
但是,如果要完全删除 OnBackPressedCallback
,则应调用 remove()。 但是,这通常不是必需的,因为在销毁关联的 LifecycleOwner
时会自动删除其回调。
如果您使用 onBackPressed()
处理返回按钮事件,建议您改用 OnBackPressedCallback
。 但是,如果您无法进行此更改,则适用以下规则:
当您调用 super.onBackPressed()
时,将通过 addCallback
注册的所有回调。
无论 OnBackPressedCallback
的任何注册实例,始终会调用 onBackPressed
。
关于 fragment 返回栈的 demo 已经写好了,感兴趣的小伙伴可以 在这 找到它。
我们下一篇再见。
LiveData 篇 我们提到 Android 开发的主要工作内容是将数据转换为 UI ,同时我们也介绍了数据驱动 UI 的**,使用 ViewModel + LiveData,可以安全地在订阅者的生命周期内分发正确的数据。但是 activity 和 fragment 充斥着大量的模板代码,铺天盖地的 findViewById,以及各种 set (根据数据设置 UI)。如果能够消灭掉这些模板代码就好了
他来了他来了,他欢快地走来了
然而,很多开发者对 DataBinding 存在偏见,「DataBinding 不是个好东西,在声明式编程中书写 UI 逻辑,既不可调试,也不便于察觉和追踪,万一出现问题就麻烦了。」
本文主要介绍 DataBinding 的解决的问题以及其背后的逻辑,带您对 DataBinding 有一个感性的认识。本文末尾会对各个 findViewById 的替代方案进行对比
DataBinding 的相关资源
DataBinding 允许使用声明性格式而不是通过编程方式将布局中的 UI 组件与数据源绑定
// before
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
<TextView
android:text="@{viewmodel.userName}" />
通过在布局文件中绑定组件,您可以删除 activity 中的许多设置 UI 调用,从而使它们更易于维护。 这也可以提高应用程序的性能,并有助于防止内存泄漏和空指针异常
如果仅替换 findViewById 而不需要数据的绑定,可以使用 ViewBinding,它使用起来更简单,性能也更好。
使用方法参见 [译]深入研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用
详细内容参见 官方文档 ,这里只简单介绍 DataBinding
app build.gradle 中加入
android {
...
dataBinding {
enabled = true
}
}
// Android Studio 4.0
android {
...
buildFeatures {
dataBinding = true
}
}
必须在 app module 中声明,声明后其他子 module 可直接使用 DataBinding
使用 DataBinding 无需开发者手动引入库,android build gradle plugin 内部已经引入了
DataBinding 中使用了注解,因此在构建速度上比 ViewBinding 差些(不过功能这么强大要啥自行车)
DataBinding 布局文件略有不同,它们以 layout 的根标记开始,后跟一个 data 元素和一个 view 根元素。 view 元素是您的根将位于非绑定布局文件中的元素。 以下代码显示了一个示例布局文件:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewmodel"
type="com.myapp.data.ViewModel" />
</data>
<ConstraintLayout... /> <!-- UI layout's root element -->
</layout>
DataBinding 会为每个在布局声明 layout 标签的 xml 布局文件生成一个绑定类。 默认情况下,类的名称基于布局文件的名称。 上面的布局文件名是 activity_main.xml,因此相应的生成类是 ActivityMainBinding。 此类包含从布局属性(例如,viewmodel 变量)到布局视图的所有绑定,并且知道如何为绑定表达式分配值
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// before
// setContentView(R.layout.activity_main)
// after
val binding : ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
}
不知道你是否有这些烦恼:activity 和 fragment 中有着大量的模板代码,即使使用 ButterKnife 等工具写起代码来也很繁琐。而且 View id 与 View 的类型不匹配时,只有在运行期才能发现;旋转屏幕后如果新的布局中不存在之前 id 的 view ,可能还导致空指针异常;项目中使用各类 bus 通知 UI 刷新,但是有时 UI 的显示并不符合预期,而排查起来特别困难,因为数据源很多...
不要慌,DataBinding 可以解决以下问题
替换 findViewById ,减少模板代码
解决类型安全问题
解决空安全问题
保证了数据的一致性
com.android.tools.build:gradle 插件中封装了 DataBinding 的魔法
查看 com.android.tools.build:gradle:3.6.2 的源码,找到 DataBinding 配置项的类 DataBindingOptions
// DataBindingOptions.java
@Override
public boolean isEnabled() {
// DataBinding 是否开启,对应上面在 build.gradle 中的配置
return enabled;
}
它的调用者很多,在 TaskManager 中的 createDataBindingTasksIfNecessary
// TaskManager
protected void createDataBindingTasksIfNecessary(@NonNull VariantScope scope) {
// 是否开启 DataBinding
boolean dataBindingEnabled = extension.getDataBinding().isEnabled();
boolean viewBindingEnabled = extension.getViewBinding().isEnabled();
if (!dataBindingEnabled && !viewBindingEnabled) {
// DataBinding 和 ViewBinding 均未开启则直接 return
return;
}
createDataBindingMergeBaseClassesTask(scope);
createDataBindingMergeArtifactsTask(scope);
//...
// 构建 DataBinding 相应绑定类
taskFactory.register(new DataBindingGenBaseClassesTask.CreationAction(scope));
}
// CreationAction
override fun handleProvider(taskProvider: TaskProvider<out DataBindingGenBaseClassesTask>) {
variantScope.artifacts.producesDir(
// DATA_BINDING_BASE_CLASS_SOURCE_OUT
InternalArtifactType.DATA_BINDING_BASE_CLASS_SOURCE_OUT,
BuildArtifactsHolder.OperationType.INITIAL,
taskProvider,
DataBindingGenBaseClassesTask::sourceOutFolder
)
}
可以看到生成 DataBinding 绑定类的 task 为 DataBindingGenBaseClassesTask,而InternalArtifactType.DATA_BINDING_BASE_CLASS_SOURCE_OUT 则对应着 build 目录生成的 DataBinding 类的
data_binding_base_class_source_out 目录
这里可以简单看一下,感兴趣的小伙伴可以自己查看源码
我们可以查看 DataBinding 生成的绑定类
public final class FragmentSingleChildBinding implements ViewBinding {
// NonNull 注解标记
// 如果存在不同配置的不同布局文件(如横竖屏)且该控件不是存在于所有布局,该处使用 Nullable注解标记
@NonNull
public final MaterialButton button;
// 省略...
@NonNull
public static FragmentSingleChildBinding bind(@NonNull View rootView) {
String missingId;
missingId: {
//其内部也是使用 findViewById
MaterialButton button = rootView.findViewById(R.id.button);
if (button == null) {
missingId = "button";
break missingId;
}
return new FragmentSingleChildBinding((MaterialButton) rootView, button);
}
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
Binding 类内部的也是使用 findViewById ,因此 DataBinding 可以代替 findViewById ,并且减少模板代码
View 控件变量类型是固定的,因此不会出现类型安全问题
View 控件变量由空/非空注解修饰,(如果为 Nullable java 中会有 lint 警告,而 kotlin 直接调用时无法通过编译的)因此 不会出现空安全问题
通过声明式的配置,UI 完全来自唯一可信的数据源配置,保证了数据的一致性
注意:以上分析同样适用于 ViewBinding
这里简单展示一下 DataBinding 的「魔法」
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/firstName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/lastName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</layout>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Before Data Binding
//setContentView(R.layout.activity_main);
//TextView firstName = (TextView) findViewById(R.id.firstName);
//TextView lastName = (TextView) findViewById(R.id.lastName);
//firstName.setText("xxx");
//lastName.setText("xxx");
// After Data Binding
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.firstName.setText("xxx");
binding.lastName.setText("xxx");
}
}
上面展示了 DataBinding 的基础操作(单纯的替换 findViewById),如果仅使用 DataBinding 这部分功能,可以考虑使用 ViewBinding
在之前的布局的基础上绑定数据
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/firstName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView
android:id="@+id/lastName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.user = new User("xxx","xxx");
}
}
这种方式也可以用在 recyclerview adapter 中,adapter 中的代码大大减少
您可能会好奇配置 android:text="@{user.firstName}
后内部发生了什么
DataBinding 中使用 Binding Adapter
来处理,它主要处理「属性」和「事件」,前者如 setText()
,后者如 setOnClickListener()
。上面的 android:text
实际上调用的是下面的方法
DataBinding 中提供了很多 Binding Adapter
如果官方提供的 Binding Adapter
不满足您的需求,您还可以自定义 Binding Adapter
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Glide.with(view).load(url).error(error).into(view);
}
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
要将 LiveData 与 DataBinding 一起使用,需要指定生命周期所有者来定义 LiveData 对象的范围
class ViewModelActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);
binding.setLifecycleOwner(this);
}
}
使用单向 DataBinding,可以在属性上设置一个值,并设置一个对该属性的更改做出反应的监听器:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>
使用双向绑定可以简化该过程
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>
使用 @={}
接收对该属性的数据更改,并同时监听用户更新(注意,这里有 =
)
那么究竟什么是双向绑定呢?
所谓的「数据驱动」就是数据驱动视图的变化,而 DataBinding 的单向绑定就是如此。反过来讲,有些时候我们需要视图来驱动数据的变化(例如当我们在 EditText 上输入了文字,我们希望对应的 ViewModel 的 LiveData 的值能够及时响应该变化)
如图,绿色部分为独立的 fragment ,内部存在两个 TextView,用于显示外部 fragment EditText 输入的文字
如果实现上述功能,传统做法可能是使用 activity 级别的 ViewModel 进行两个 fragment 之间的通信,通过监听 EditText 文字的变化改变 ViewModel 中 LiveData 的值,并在绿色 fragment 中观察 LiveData 并显示到 TextView 中
class NormalViewModel : ViewModel() {
val firstName = MutableLiveData<String>()
val lastName = MutableLiveData<String>()
}
class NormalDetailFragment : Fragment(R.layout.fragment_normal_detail) {
private val mViewModel by activityViewModels<NormalViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mViewModel.firstName.observe(viewLifecycleOwner) {
tvFirstName.text = it
}
mViewModel.lastName.observe(viewLifecycleOwner) {
tvLastName.text = it
}
}
}
class NormalFragment : Fragment(R.layout.fragment_normal) {
private val mViewModel by activityViewModels<NormalViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
etFirstName.addTextChangedListener {
mViewModel.firstName.value = it.toString()
}
etLastName.addTextChangedListener {
mViewModel.lastName.value = it.toString()
}
}
}
得益于 kotlin ,上面的代码以及很简洁了,如果使用 java 代码片段只会更长。
不过使用 DataBinding,还可以更简洁
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvFirstName"
android:text="@{vm.firstName}"/>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tvLastName"
android:text="@{vm.lastName}"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etFirstName"
android:text="@={vm.firstName}"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etLastName"
android:text="@={vm.lastName}"/>
只需配置好双向绑定(EditText 驱动 ViewModel 的 LiveData 的值变化,ViewModel 再驱动 TextView 显示数据),并在 fragment 通过固定的模板代码设置好 ViewModel 即可
这里的魔法还是来自 Binding Adapter
// TextViewBindingAdapter.java
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
"android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
final OnTextChanged on, final AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
final TextWatcher newValue;
if (before == null && after == null && on == null && textAttrChanged == null) {
newValue = null;
} else {
newValue = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (before != null) {
before.beforeTextChanged(s, start, count, after);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (on != null) {
on.onTextChanged(s, start, before, count);
}
if (textAttrChanged != null) {
//通知发生变化
textAttrChanged.onChange();
}
}
@Override
public void afterTextChanged(Editable s) {
if (after != null) {
after.afterTextChanged(s);
}
}
};
}
final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
if (oldValue != null) {
view.removeTextChangedListener(oldValue);
}
if (newValue != null) {
view.addTextChangedListener(newValue);
}
}
这里使用 InverseBindingListener (调用 textAttrChanged.onChange()
)来通知 LiveData 数据发生变化
而变化后的值 通过 @InverseBindingAdapter 注解标记的方法处理,这里的 event 与上面的标记匹配(android:textAttrChanged
)
// TextViewBindingAdapter.java
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
view 层变化通知数据变化,数据变化再通知 view 层变化,仿佛是个套娃
因此避免这种死循环十分重要,setText 方法判断了新旧值是否相等来避免死循环
DataBinding 主要提供两部分功能
findViewById 有两个问题
在 compileSdk 的 API 级别 26 中,对该方法的定义稍作更改以消除强制类型转换问题
现在,开发人员无需在代码中手动转换 view 类型。 如果您引用 id 指向类型 TextView 的 View 并将其指定为 Button,则 Android SDK 会尝试查找具有提供的 id 的 Button,并且它将返回 Null,因为它将无法找到它
但是在 Kotlin 中,您仍然需要提供诸如 findViewById(R.id.txtUsername) 之类的类型。 如果您不检查视图是否具有 null 安全,则可能出现 NullPointerException,但是此方法不会像以前那样抛出ClassCastException
Butterknife 是 Jake Wharton 大神写的替代 findViewById 的库,该库使用注解处理并生成 findViewById 代码
它具有与 findViewById 几乎相似的问题。 但是,它在运行时添加了null 安全检查以避免 NullPointerException
由于 DataBinding 和 ViewBinding 的出现,沃神已经宣布弃用该库
Kotlin 引入的最大功能之一是 Kotlin 扩展方法。 在它的帮助下,Kotlin Synthetics 诞生了。 Kotlin Synthetics 通过自动生成的 Kotlin 扩展方法,使开发人员可以从 xml 布局直接访问其内部的 view
Kotlin Synthetics 第一次调用 findViewById 方法,然后默认情况下将 view 实例缓存在 HashMap 中。 可以通过Gradle 设置将此缓存配置更改为 SparseArray 或不缓存
总体而言,Kotlin Synthetics 是一种很好的选择,因为它类型安全,并且通过 Kotlin 的 ?进行空检查。 它不需要开发人员的额外代码。 但这仅适用于 Kotlin 项目
但是,在使用 Kotlin Synthetics 时遇到了一个小问题。 例如,如果将内容视图设置为布局,然后使用仅存在于其他布局中的 id ,则 IDE 可让您自动完成并添加新的 import 语句。 除非您专门检查以确保其 import 语句仅导入正确的 view,否则没有安全的方法来验证这不会导致运行时问题
DataBinding 在功能上比其他方法优越得多,因为它不仅为您提供类型安全和空安全的 view 引用,而且还允许您直接在 xml 布局内使用数据驱动视图变化
最近在 Android Studio 3.6 中引入的 ViewBinding 是 DataBinding 库的子集。 由于不需要注解处理,因此可以缩短构建时间。详细的使用可以参见 这篇文章
findViewById | Butterknife | Kotlin Synthetics | DataBinding | ViewBinding | |
---|---|---|---|---|---|
一直空安全 | ❌ | 部分 | 部分 | ✔️ | ✔️ |
类型安全 | ❌ | ❌ | ✔️ | ✔️ | ✔️ |
样板代码 | 多 | 少 | 少 | 中等 | 少 |
构建时间 | ✔️ | ❌ | ✔️ | ❌ | ✔️ |
支持语音 | java/kotlin | java/kotlin | kotlin | java/kotlin | java/kotlin |
过去的一段时间,AndroidX
软件包下的 Activity/Fragmet
的 API 发生了很多变化。让我们看看它们是如何提升Android 的开发效率以及如何适应当下流行的编程规则和模式。
本文中描述的所有功能现在都可以在稳定的 AndroidX
软件包中使用,它们在去年均已发布或移至稳定版本。
从 AndroidX
AppCompat 1.1.0
和 Fragment 1.1.0
( 译者注:AppCompat 包含 Fragment,且 Fragment 包含 Activity,详情见【整理】Jetpack 主要组件的依赖及传递关系 )开始,您可以使用将 layoutId
作为参数的构造函数:
class MyActivity : AppCompatActivity(R.layout.my_activity)
class MyFragmentActivity: FragmentActivity(R.layout.my_fragment_activity)
class MyFragment : Fragment(R.layout.my_fragment)
这种方法可以减少 Activity/Fragment
中方法重写的数量,并使类更具可读性。 无需在 Activity
中重写 onCreate()
即可调用 setContentView()
方法。 另外,无需手动在Fragment
中重写 onCreateView
即可手动调用 Inflater
来扩展视图。
Activity/Fragment
的灵活性借助 AndroidX
新的 API ,可以减少在 Activity/Fragment
处理某些功能的情况。通常,您可以获取提供某些功能的对象并向其注册您的处理逻辑,而不是重写 Activity / Fragment
中的方法。 这样,您现在可以在屏幕上组成几个独立的类,获得更高的灵活性,复用代码,并且通常在不引入自己的抽象的情况下,对代码结构具有更多控制。 让我们看看这在两个示例中如何工作。
有时,您需要阻止用户返回上一级。 在这种情况下,您需要在 Activity
中重写 onBackPressed()
方法。 但是,当您使用 Fragment
时,没有直接的方法来拦截返回。 在 Fragment
类中没有可用的 onBackPressed()
方法,这是为了防止同时存在多个 Fragment
时发生意外行为。
但是,从 AndroidX
Activity 1.0.0
开始,您可以使用 OnBackPressedDispatcher
在您可以访问该 Activity
的代码的任何位置(例如,在 Fragment
中)注册 OnBackPressedCallback
。
class MyFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Do something
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
}
您可能会在这里注意到另外两个有用的功能:
OnBackPressedCallback
的构造函数中的布尔类型的参数有助于根据当前状态动态 打开/关闭按下的行为addCallback()
方法的可选第一个参数是 LifecycleOwner
,以确保仅在您的生命周期感知对象(例如,Fragment
)至少处于 STARTED
状态时才使用回调。通过使用 OnBackPressedDispatcher
,您不仅可以获得在 Activity
之外处理返回键的便捷方式。 根据您的需要,您可以在任意位置定义 OnBackPressedCallback
,使其可复用,或根据应用程序的架构进行任何操作。 您不再需要重写Activity
中的 onBackPressed
方法,也不必提供自己的抽象的来实现需求的代码。
如果您希望 Activity
在终止并重启后恢复之前的状态,则可能要使用 saved state
功能。 过去,您需要在 Activity
中重写两个方法:onSaveInstanceState
和 onRestoreInstanceState
。 您还可以在 onCreate
方法中访问恢复的状态。 同样,在 Fragment
中,您可以使用 onSaveInstanceState
方法(并且可以在 onCreate
,onCreateView
和onActivityCreated
方法中恢复状态)。
从 AndroidX
SavedState 1.0.0
(它是 AndroidX
Activity
和 AndroidX
Fragment
内部的依赖。译者注:您不需要单独声明它)开始,您可以访问 SavedStateRegistry
,它使用了与前面描述的 OnBackPressedDispatcher
类似的机制:您可以从 Activity / Fragment
中获取 SavedStateRegistry
,然后 注册您的 SavedStateProvider
:
class MyActivity : AppCompatActivity() {
companion object {
private const val MY_SAVED_STATE_KEY = "my_saved_state"
private const val SOME_VALUE_KEY = "some_value"
}
private lateinit var someValue: String
private val savedStateProvider = SavedStateRegistry.SavedStateProvider {
Bundle().apply {
putString(SOME_VALUE_KEY, someValue)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedStateRegistry
.registerSavedStateProvider(MY_SAVED_STATE_KEY, savedStateProvider)
}
fun someMethod() {
someValue = savedStateRegistry
.consumeRestoredStateForKey(MY_SAVED_STATE_KEY)
?.getString(SOME_VALUE_KEY)
?: ""
}
}
如您所见,SavedStateRegistry
强制您将密钥用于数据。 这样可以防止您的数据被 attach 到同一个 Activity/Fragment
的另一个 SavedStateProvider
破坏。 就像在 OnBackPressedDispatcher
中一样,您可以例如将 SavedStateProvider
提取到另一个类,通过使用所需的任何逻辑使其与数据一起使用,从而在应用程序中实现清晰的保存状态行为。
此外,如果您在应用程序中使用 ViewModel
,请考虑使用 AndroidX
ViewModel-SavedState
使你的ViewModel
可以保存其状态。 为了方便起见,从 AndroidX
Activity 1.1.0
和 AndroidX
Fragment 1.2.0
开始,启用 SavedState
的SavedStateViewModelFactory
是在获取 ViewModel
的所有方式中使用的默认工厂:委托 ViewModelProvider
构造函数和 ViewModelProviders.of()
方法。
Fragment
最常提及的问题之一是不能使用带有参数的构造函数。 例如,如果您使用 Dagger2
进行依赖项注入,则无法使用 Inject
注解 Fragment
构造函数并指定参数。 现在,您可以通过指定 FragmentFactory
类来减少 Fragment
创建过程中的类似问题。 通过在 FragmentManager
中注册 FragmentFactory
,可以重写实例化 Fragment
的默认方法:
class MyFragmentFactory : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
// Call loadFragmentClass() to obtain the Class object
val fragmentClass = loadFragmentClass(classLoader, className)
// Now you can use className/fragmentClass to determine your prefered way
// of instantiating the Fragment object and just do it here.
// Or just call regular FragmentFactory to instantiate the Fragment using
// no arguments constructor
return super.instantiate(classLoader, className)
}
}
如您所见,该API非常通用,因此您可以执行想要创建 Fragment
实例的所有操作。 回到 Dagger2
示例,例如,您可以注入FragmentFactory Provider <Fragment>
并使用它来获取 Fragment
对象。
从AndroidX
Fragment 1.1.0
开始,可以使用 Fragment
测试组件提供 FragmentScenario
类,该类可以帮助在测试中实例化 Fragment
并进行单独测试:
// To launch a Fragment with a user interface:
val scenario = launchFragmentInContainer<FirstFragment>()
// To launch a headless Fragment:
val scenario = launchFragment<FirstFragment>()
// To move the fragment to specific lifecycle state:
scenario.moveToState(CREATED)
// Now you can e.g. perform actions using Espresso:
onView(withId(R.id.refresh)).perform(click())
// To obtain a Fragment instance:
scenario.onFragment { fragment ->
...
}
很高兴看到 -ktx
AndroidX
软件包中提供了许多有用的 Kotlin
扩展方法,并且定期添加了新的方法。 例如,在AndroidX
Fragment-KTX 1.2.0
中,使用片段化类型的扩展名可用于 FragmentTransaction
上的 replace()
方法。 将其与 commit()
扩展方法结合使用,我们可以获得以下代码:
// Before
supportFragmentManager
.beginTransaction()
.add(R.id.container, MyFragment::class.java, null)
.commit()
// After
supportFragmentManager.commit {
replace<MyFragment>(R.id.container)
}
一件小而重要的事情。 如果您将 FrameLayout
用作 Fragment
的容器,则应改用 FragmentContainerView
。 它修复了一些动画 z轴索引顺序问题和窗口插入调度。 从 AndroidX
Fragment 1.2.0
开始可以使用 FragmentContainerView
。
我们在使用 LiveData 时可能会遇到「粘性」事件的问题,该问题可以使用包装类的方式解决。解决方案见 [译] 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例)
使用时是这样的
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
不过这样写甚是繁琐,我们可以使用更优雅的方式解决该问题
//为 LiveData<Event<T>>提供类型别名,使用 EventLiveData<T> 即可
typealias EventMutableLiveData<T> = MutableLiveData<Event<T>>
typealias EventLiveData<T> = LiveData<Event<T>>
使用 typealias
关键字,我们可以提供一个类型别名,可以这样使用
//等价于 MutableLiveData<Event<Boolean>>(Event(false))
val eventContent = EventMutableLiveData<Boolean>(Event(false))
现在声明时不用多加一层泛型了,那么使用时还是很繁琐
我们可以借助 kotlin 的 扩展函数更优雅的使用
demo 中封装了两种形式的 LiveData,一种为 LiveData<Boolean>
,一种为 EventLiveData<Boolean>
,当屏幕旋转时,前者会再次回调结果,而后者由于事件已被处理而不执行 onChanged,我们通过 Toast 可观察到这一现象
很多时候我们在获取网络数据时要封装一层网络状态,例如:加载中,成功,失败
在使用时我们遇到了和上面一样的问题,多层泛型用起来很麻烦
我们依然可以使用 typealias + 扩展函数来优雅的处理该问题
demo 截图
demo 在这,如果感觉这个思路对你有帮助的话,点一颗小星星吧~ 😉
另外我还将它传到了 JitPack 上,引入姿势如下:
在项目根目录的 build.gradle
加入
allprojects {
repositories {
//...
maven { url 'https://jitpack.io' }
}
}
添加依赖
dependencies {
implementation 'com.github.Flywith24:WrapperLiveData:$version'
}
原文:Android Styling: Themes Overlay
作者:Nick Butcher
译者:Flywith24
题图来自 Virginia Poltrack
在 Android styling 系列文章中,我们研究了 style 与 theme 的区别 ,讨论了 使用主题和主题属性的优势,并且介绍了 常用的属性
今天,我们将集中讨论主题的实际使用,如何将其应用到您的应用程序中
在 第一篇文章 中我们提到
Theme
可以作为Context
的属性被获取,并且它可以从任何 Context 或 Context 的子类获得,例如Activity
,View
,或者ViewGroup
。这些对象存在于一个「树」中,其中 Activity 包含 ViewGroup,ViewGroup 包含 View。在此树的任何级别上指定主题都会影响到其后代节点,例如在 ViewGroup 上设置 Theme 会作用域其所有子 View(这与只作用于单一 View 的 Style 相反)
在此「树」中的任何一层设置主题都不会 「替换 」当前有效的主题,而是将其 「覆盖」。下面的例子中有一个按钮,该按钮可以选择一个主题,但它的 parent 也可以指定一个主题:
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<ViewGroup …
android:theme="@style/Theme.App.Foo">
<Button …
android:theme="@style/Theme.App.Bar"/>
</ViewGroup>
如果在两个主题中都指定了属性,则最本地的 「获胜」,即 主题Bar
将应用于按钮。 在主题 Foo
中指定但 未 在主题 Bar
中指定的任何属性也将应用于按钮
这可能看起来像是脱离实际的示例,但是有些场景特别有用。例如在浅色屏幕上的深色 Toolbar,或者这个界面(来自 Owl sample app),它大部分是粉色主题,但是底部是一个蓝色主题
这可以通过在蓝色部分的 root 设置主题,它的子 view 都会受此影响
由于主题会覆盖其树中 parent 的主题,确保它不会意外地替换您想要保留的属性十分重要。例如,您可能想要改变 view 的背景色(通常由 colorSurface
控制)不做其他更改,即您想保留当前主题的剩余部分。对于这种场景,我们可以使用一种叫做 theme overlays
的技术
这些主题的作用是与其他主题合并。它们的范围可能很狭窄,即它们仅定义(或继承)尽可能少的属性。theme overlays
经常(并非总是)没有 parent
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<style name="ThemeOverlay.MyApp.DarkSurface" parent="">
<item name="colorSurface">#121212</item>
</style>
theme overlays
的作用范围十分小,它十分克制地定义了尽可能少的属性,目的是为了与其它主题合并共同作用
按照惯例,我们以 ThemeOverlay 作为命名前缀。MDC 和 AppCompat 提供了很多方便的 theme overlays
,您可以使用它们为程序的特定部分的颜色做从浅色到深色的转换
根据定义,theme overlays
不会指定很多内容,因此它不应被单独使用。例如,将其用作 activity 的主题。事实上,您可以在 app 中使用两种类型的主题:
Theme.MaterialComponents
。它们应该在 activity 中使用总会有一个生效的主题,即使您未在应用中的任何地方指定一个主题,也会继承默认主题。 因此,上面的示例只是一种简化,因此您绝对不应在 View 中使用 full theme,而是应该使用 theme overlays
使用主题会在运行期产生一定的开销。每当您声明一个 android:theme
,您都在创建一个新的 ContextThemeWrapper 来分配新的 Theme
和 Resource
实例。它还间接引入了更多层的 styling。请谨慎使用主题,尤其在 RecyclerView 这种重复使用的场景下
我们在前文提到主题与 Context 关联——这意味着如果您正在使用 context 来检索代码中的资源,那么请注意使用正确的 context 。例如您在某个位置获取 Drawable
如果 drawable 引用了主题属性(所有的 Drawable 可以在 API 21+ 使用,VectorDrawable 可以通过 Jetpack 在 API 14+ 中使用),则应确保 Drawable 使用了正确的 context。如果您用错了,则可能发现在子节点设置的主题在 Drawable 中不符合预期。例如如果您使用了 Fragment 或者 Activity 的 Context 加载 Drawable,它不会使用当前树的子节点的主题,而是使用最接近传入 Context 的资源
我们已经讨论的「树」中的主题和上下文:Activity > ViewGroup > View。我们很自然的会将这个模式套用在 Application
类中,毕竟您可以在 标签中指定主题。但别被忽悠了!
Application Context
不包含任何主题信息,您在 manifest 文件中的 标签设置主题仅作用于那些没指定主题的 Activity。因此不要使用 Application Context 加载那些因主题而异的 资源(drawable 或 color)或者解析主题属性
切勿使用 Application Context 来加载可以的资源
这也是为什么我们为 Activity 指定 full theme,而不会覆盖在 标签设置的主题
本文解释了 themes overlay 的原理和使用。在你的布局中使用 android:theme
标签配置主题,并使用 theme overlays 来调整您需要的属性。请注意使用正确的主题和 context 来加载资源并小心 application context!
感谢 Florina Muntenescu 和 Chris Banes
译文完
本系列完
之前写过两篇关于管理项目中依赖本的文章:
什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin
【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本?巧用 includeBuild
Android Detail 项目 目前使用的是第二篇提到的方式。
主工程(Android-Detail)与一个版本控制插件(version)通过组合构建(composing builds)进行管理。
今天我们来谈一谈在这种方式的基础上如何抽取公共配置,使得 build.gradle 文件的内容尽可能少,甚至内容可以为空。
阅读本文,你将了解:
Android Detail 下除了 baselib
是 library module,其它都是 app module。app module 中有很多相同的配置,如下图:
我们可以为 BaseExtension 写一个扩展函数,名字叫 applyBaseCommons。在该方法中,我们配置 android 闭包下的公共配置,例如 compileSdkVersion
,versionCode
等。
接着,我们在 version plugin 中调用该方法,即可为所有 module 配置 android 闭包内的公共配置。
每个 app module 都有着公共的依赖,如 test 相关的依赖,Kotlin 标准库的依赖,并且同时引用了 baselib module
按照上面的思路,我们可以再一个配置依赖的扩展函数
接着在在 version plugin 中调用该方法
每个 app module 都有 kotlin-android
和 kotlin-android-extensions
两个插件
遵循上面的思路,我们再写一个配置公共插件的扩展函数
到目前为止,app module 中只有两行内容
有着严重强迫症的我十分想将这两行消灭掉。
可以实现吗?
必须可以!
我们都知道 project 下的 build.gradle 中有一个 allprojects{}
闭包,我们可以在其中为所有 project 统一配置内容。
其实还有一个 subprojects{}
用于为子 project 统一配置内容。我们可以在该闭包内为所有 module 配置 version plugin,为 baselib 配置 com.android.library
plugin,为其余的 app module 配置 com.android.application
plugin
为了使 project build.gradle 能够找到我们的 version plugin,还需使用 plugins{}
声明一下,apply 设置为 false
当然,判断使用 application/library plugin 的判断条件可以根据自身情况配置,例如所有带 lib_
前缀的 module 使用 library plugin
组合构建可以将多个 project 一起构建,例如我在 Android-Detail 的 settings.gradle 通过 includeBuild
关键字引入了我的 另一个项目,该项目已发布到 Jitpack,可以使用 com.github.Flywith24:Flywith24-Permission:1.0.1
引入
此处根据环境变量 useLocal 来判断是否 includeBuild permission 项目,如果为 false ,则使用远程依赖,如果为 true,则会使用本地 module :library
在 project 使用前文类似的配置,也可实现一键切换远程依赖/本地module 的功能
这样调试阶段就不需要频繁的发快照包啦~
前三篇文章我们介绍了如何写单元测试,从这篇文章开始,我们介绍一下 集成测试
fragment 和 ViewModel 联系很紧密,我们需要确保 ViewModel 在适当时的时机更新 UI,那么该如何测试这部分内容呢?
为了在下面的架构上进行 集成测试 ,我们需要尽可能的屏蔽无关代码
例如我们可以使用 empty activity,它不包含 fragment 或 activity 的其他代码。对于数据层,可以使用 test doubles 来替代
这样就可以聚焦于 fragment 和 ViewModel 的代码
当你需要测试 activity 和 fragment 时,AndroidX test 中的 FragmentScenario
和 ActivityScenario
的 API 可以帮到你
debugImplementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
这些 API 用于为测试提供 fragment 和 activity ,你可以控制它们的启动情况和生命周期状态
以下代码用于启动 fragment 并传入 bundle
val bundle = Bundle().apply { putString("username", "Flywith24") }
val scenario = launchFragmentInContainer<RepoListFragment>(bundle, R.style.AppTheme)
如果想要控制 fragment 的生命周期状态,可以调用
scenario.moveToState(Lifecycle.State.CREATED)
由于 FragmentScenario 是 AndroidX test 一部分,因此在 local test 和 instrumented test 中均可使用
如果想要测试 Android 中的 UI 组件可以使用 Espresso
库,使用该库你可以使用 view 并检查它们的状态
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
一个 Espresso
有四个最基本的部分
onView()
是 Espresso
中一个常用的静态方法,它意味着接下来要对 view 进行操作
ViewMatches
的职责就是寻找 view ,上图中使用的是 withId()
方法,根据 id 匹配相应的 view。还有一些其他的匹配方法,例如 withText
注意:保证
ViewMatches
只能匹配到一个 view,否则会抛出AmbiquousviewMatcher Exception
ViewAction
是 view 执行的动作,示例中是 click 方法
通过 ViewAssertion
我们可以判断 view 的状态是否符合我们的预期
tips:为了提高响应速度,您可以在开发者选项中将动画关闭
我们在 【玩转Test】Test Doubles 的概念及如何测试 Repository 中介绍了 test doubles 的类型,其中我们主要介绍了 Fake。今天,我们来介绍 test doubles 的另一种类型:Mock
不同于 Fake
,Mock
侧重于跟踪方法的调用,这么说可能比较抽象,让我们举个栗子
如上图,有一个方法被调用并改变了 UI(更新 text )
如果使用 Mock
,则验证更新 text 的方法是否被正确地调用
如果不使用 Mock
,我们通常会验证 TextView 中的 text 是否符合预期
为了进行 Mock
测试,我们需要引入 Mockito
库
androidTestImplementation "org.mockito:mockito-core:2.25.0"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.12.1"
有的小伙伴可能会问了,有什么场景需要使用 Mock
吗?
这里有一个例子比较合适,测试使用 navigation 进行 fragment 的跳转
我们有一个 HostFragment ,内部有一个 button,点击可以跳转到 RepoListFragment 并将用户名传递过去
接下来我们来测试这部分的跳转
首先提供第一个 fragment
// GIVEN 显示 fragment
val scenario = launchFragmentInContainer<HostFragment>(Bundle(), R.style.AppTheme)
由于我们使用了 navigation ,因此我们还需要 navigationController
val navController = Mockito.mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
最后我们执行点击按钮动作并验证 navigation 的跳转和参数传递是否符合要求
// WHEN 点击搜索按钮
onView(withId(R.id.button)).perform(click())
// THEN 验证跳转到 repolist 界面
verify(navController).navigate(HostFragmentDirections.actionHostFragmentToRepoListFragment("Flywith24"))
作为 Android 开发者,项目中引入 gradle 依赖是家常便饭,但是 Android Studio 自带的依赖查询工具并不好用,mac 上使用 Alfred 搭配 workflow 可以方便地 copy gradle dependency。但 Alfred 是mac独占的,如果有一个跨平台的插件就好了。
「每当你在感叹,如果有这样一个东西就好了的时候,请注意,其实这是你的机会..」,于是这款插件诞生了。
uTools是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。
当你熟悉它后,能够为你节约大量时间,让你可以更加专注地改变世界。
来自官网介绍
笔者使用 uTools 替代 Wox 半年多,完美切换,没有不适感。
uTools
google
并输入关键词,在 google maven repository
中查询maven
并输入关键词,在 maven center
中查询前往查看B站 演示视频
笔者不擅长 js ,欢迎各位提 PR
jitpack repository
查询maven center
查询防抖优化我们都知道 fragment 中的 setRetainInstance
用于控制是否在 activity 保留 fragment 实例,具体内容可参见 WanAndroid 的每日一问:Fragment 是如何被存储与恢复的?
但是该方法已于 androidx fragment 1.3.0-alpha01
弃用了
老规矩,我们查看一下 commit log
简单概况一下
SetRetainInstance
尝试在 activity 重建时保存状态。但它带来了很多副作用。
随着 ViewModel
的引入,开发者拥有一个特定的 API,用于保留与 Activity,Fragments 和 Navigation 相关联的状态。 这使开发者可以使用正常的,不需要保留 fragment ,从而在保存单个需要的属性时避免了常见的泄漏源,并且可以销毁保存的状态(即 ViewModel
的构造器和 onCleared
回调)
详情可参见 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析 和 【背上Jetpack之ViewModel】即使您不使用MVVM也要了解ViewModel ——ViewModel 的职能边界
从这个改动可以看出官方正致力于保证逻辑的单一性,状态保存交给 ViewModel
,减少这种特殊的例外情况,从而消除一些不符合预期的问题
原文:Android Styling: Common Theme Attributes
作者:Nick Butcher
译者:Flywith24
题图来自 Virginia Poltrack
在 Android styling 系列文章的第一篇,我们研究了主题和样式之间的区别以及主题如何使开发者写出更灵活的样式和布局
具体来说,我们建议您使用主题属性来提供资源的间接访问点,以便您可以改变它们(例如,深色主题)。 也就是说,如果发现自己在布局或样式中编写了直接的资源引用(或更糟糕的是,一个硬编码值😱),请考虑是否应该使用主题属性
但是可以使用哪些主题属性? 本文重点介绍了您应该了解的常见知识; 来自 Material
,AppCompat
或 platform
的内容。 这不是一个完整的列表(为此,我建议您浏览定义在下面链接的 attrs 文件),但是这些都是我一直使用的属性(使用主题属性实现)
这里的很多颜色来自于 Material color system,该系统定义了可在整个应用程序中使用的颜色名
?attr/colorPrimary
app 主色?attr/colorSecondary
app 次级颜色,通常作为主色的补充?attr/colorOn[Primary, Secondary, Surface etc]
与命名颜色形成对比的颜色?attr/color[Primary, Secondary]Variant
给定颜色的阴影?attr/colorSurface
组件界面(卡片,表格,菜单等)的颜色?android:attr/colorBackground
背景?attr/colorPrimarySurface
在浅色主题的 colorPrimary
和深色主题的 colorSurface
间切换?attr/colorError
错误消息的颜色其他常用的颜色
?attr/colorControlNormal
正常状态下图标/控件的颜色?attr/colorControlActivated
激活状态下图标/控件的颜色(例如 checked)?attr/colorControlHighlight
高亮颜色(例如 ripples, list selectors)?android:attr/textColorPrimary
text 突出颜色?android:attr/textColorSecondary
text 次要颜色?attr/listPreferredItemHeight
list item 的标准(最小)高度?attr/actionBarSize
toolbar 的高度?attr/selectableItemBackground
当前交互项的水波纹/高亮(也为前景提供了便利)?attr/selectableItemBackgroundBorderless
无界的水波纹?attr/dividerVertical
一个可绘制对象,可用作元素之间的垂直分隔线?attr/dividerHorizontal
一个可绘制对象,可用作元素之间的水平分隔线Material 定义 了一种类型比例——您应该在整个应用中使用的离散文本样式集,它们作为一个主题属性(textAppearance
) 被提供。使用 Material type scale generator 帮助生成不同字体的比例
?attr/textAppearanceHeadline1
默认的浅色 96sp 文本?attr/textAppearanceHeadline2
默认的浅色 60sp 文本?attr/textAppearanceHeadline3
默认的普通 48sp 文本?attr/textAppearanceHeadline4
默认的普通 34sp 文本?attr/textAppearanceHeadline5
默认的普通 24sp 文本?attr/textAppearanceHeadline6
默认的中等 20sp 文本?attr/textAppearanceSubtitle1
默认的普通 16sp 文本?attr/textAppearanceSubtitle2
默认的中等 14sp 文本?attr/textAppearanceBody1
默认的普通 16sp 文本?attr/textAppearanceBody2
默认的普通 14sp 文本?attr/textAppearanceCaption
默认普通 12sp 文本?attr/textAppearanceButton
默认的中等全大写 14sp 文本?attr/textAppearanceOverline
默认的中等全大写 10sp 文本Material 采用了 shape system,该系统为小型,中型和大型组件 提供 了主题属性。请注意,如果要在自定义组件上设置 shape,则可能要使用 MaterialShapeDrawable
作为其背景,它可以理解并实现 shape
?attr/shapeAppearanceSmallComponent
在 Button ,Chip,Text 的属性中使用,默认 4dp 的圆角?attr/shapeAppearanceMediumComponent
在 Card,Dialog,Date Picker 中使用,默认 4dp 的圆角?attr/shapeAppearanceLargeComponent
在 Bottom Sheet 中使用,默认 0dp 圆角这看起来似乎很具体,但是 Material 定义了三种类型的 button:Contained, Text 以及 Outlined。MDC 提供了主题属性,可用于设置 MaterialButton
的 style
?attr/materialButtonStyle
默认样式,可省略?attr/borderlessButtonStyle
文本样式的 button?attr/materialButtonOutlinedStyle
outline 样式的 button?android:attr/disabledAlpha
为控件禁用透明度?android:attr/primaryContentAlpha
前景元素的透明度?android:attr/secondaryContentAlpha
次级元素的透明度你可能注意到,有些属性由 ?android:attr/foo
引用,而其他的则为 ?attr/bar
。这是因为它们中的部分是由 Android Platform 定义的,因此您需要 android
前缀通过命名空间引用它们(就像 layout 中 view 的属性:android:id
)。那些不是来自静态库(即 AppCompat 或 MDC ),它们已编译到您的应用程序中,因此不需要名称空间(类似于您在布局中使用 app:baz
的方式)。一些元素在 platform 和 library 均有定义(例如 colorPrimary
)。在这种情况下,最好使用非平台版本,这样可以在所有 API 级别上使用。例如它们是在 library 中重复定义恰好目的是为了向后兼容。在这些情况下,我已在上面列出了非平台版本
首选可以在所有API级别上使用的非平台属性
有关可用的主题属性的完整列表,可以直接访问以下链接
Material Design Components :
有时,没有主题属性可以抽象出您希望随主题变化的内容(同样的 attribute 在不同的主题下不同),不必担心,你可以自定义!这是 Google I / O
应用程序中的一个示例,该示例在两个屏幕中显示了会议列表
它们在很大程度上相似,但左屏幕必须为时间标题留出空间,而右屏幕则不能。 我们通过抽象在主题属性后面对齐 item 的位置来实现此目的,以便我们可以根据主题来改变它们并在两个不同的屏幕上使用相同的布局:
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<attr name="sessionListKeyline" format="dimension" />
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<style name="Theme.IOSched.Schedule">
…
<item name="sessionListKeyline">72dp</item>
</style>
<style name="Theme.IOSched.Speaker">
…
<item name="sessionListKeyline">16dp</item>
</style>
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<Guideline …
app:layout_constraintGuide_begin="?attr/sessionListKeyline" />
了解可用的主题属性后,您便可以在编写布局,样式或可绘制对象时使用它们。 使用主题属性使支持主题(如深色主题)和编写更灵活,维护代码变得更加容易。 要对此进行深入研究,请看本系列的下一篇文章
感谢 Florina Muntenescu 和 Chris Banes
译文完
在
Android Jetpack
组件中,fragment
作为视图控制器之一占有很重要的位置。但由于其bug众多,暗坑无数,以至于 Square 有这样一篇博客:Advocating Against Android Fragments。github上的 Fragmentation 有着 9.4k 的star。而现在,
androidx fragment
稳定版已来到 1.2.2,让我们总结一下fragment
有哪些常见问题以及有哪些使用fragment
的新姿势
getSupportFragmentManager , getParentFragmentManager 和 getChildFragmentManager
FragmentStateAdapter 和 FragmentPagerAdapter
add 和 replace
observe LiveData时传入 this 还是 viewLifecycleOwner
使用 simpleName 作为 fragment 的 tag 有何风险?
在 BottomBarNavigation 和 drawer 中如何使用Fragment多次添加?
返回栈
FragmentManager
是androidx.fragment.app
(已弃用的不考虑)下的抽象类,创建用于 添加,移除,替换fragment
的事务(transaction
)
首先要确认一件事,getSupportFragmentManager()
是 FragmentActivity
下的方法
getParentFragmentManager
和 getChildFragmentManager
是 androidx.fragment.app.Fragment
下的方法,其中 androidx.fragment 1.2.0
后 getFragmentManager
与 requireFragmentManager
已弃用
明确了这件事,接下来的就很清晰了
getSupportFragmentManager
与 activity
关联,可以将其视为 activity
的 FragmentManager
getChildFragmentManager
与 fragment
关联,可以将其视为fragment
的FragmentManager
getParentFragmentManager
情况稍微复杂,正常情况返回的是该fragment
依附的activity
的FragmentManager
。如果该fragment是另一个fragment
的子 fragment
,则返回的是其父fragment
的 getChildFragmentManager
如果这么说还不明白的话,我们可以做一个实践。
创建一个 activity
,一个父fragment
,一个子fragment
// activity
class MyActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager.commit {
add<ParentFragment>(R.id.content)
}
Log.i("MyActivity", "supportFragmentManager $supportFragmentManager")
}
}
class ParentFragment : Fragment(R.layout.fragment_parent) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
childFragmentManager.commit {
add<ChildFragment>(R.id.content)
}
Log.i("ParentFragment", "parentFragmentManager $parentFragmentManager")
Log.i("ParentFragment", "childFragmentManager $childFragmentManager")
}
}
class ChildFragment : Fragment(R.layout.fragment_child) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.i("ChildFragment", "parentFragmentManager $parentFragmentManager")
Log.i("ChildFragment", "childFragmentManager $childFragmentManager")
}
}
//log
I/MyActivity: supportFragmentManager FragmentManager{825dcef in HostCallbacks{14a13fc}}}
I/ParentFragment: parentFragmentManager FragmentManager{825dcef in HostCallbacks{14a13fc}}}
I/ParentFragment: childFragmentManager FragmentManager{df5de83 in ParentFragment{7cdd800}}}
I/ChildFragment: parentFragmentManager FragmentManager{df5de83 in ParentFragment{7cdd800}}}
I/ChildFragment: childFragmentManager FragmentManager{aba9afb in ChildFragment{5cea718}}}
因此
在 activity
中使用 ViewPager
,BottomSheetFragment
和DialogFragment
时,都应使用 getSupportFragmentManager
在fragment
中使用 ViewPager
时应该使用getChildFragmentManager
错误的在 fragment
中使用 activity
的 FragmentManager
会引发内存泄露。 为什么呢?假如您的fragment中有一些依靠 ViewPager
管理的子 fragment
,并且所有这些 fragment
都在 activity
中,因为您使用的是activity
的FragmentManager
。 现在,如果关闭您的父fragment
,它将被关闭,但不会被销毁,因为所有子fragment
都处于活动状态,并且它们仍在内存中,从而导致泄漏。 它不仅会泄漏父fragment
,还会泄漏所有子fragment
,因为它们都无法从堆内存中清除。
FragmentPagerAdapter
将整个 fragment
存储在内存中,如果ViewPager
中使用了大量 fragment
,则可能导致内存开销增加。 FragmentStatePagerAdapter
仅存储片段的savedInstanceState
,并在失去焦点时销毁所有 fragment
。
让我们看看常见的两个问题
ViewPager
中的 fragment
是通过 activity
或 fragment
的 FragmentManager
管理的,FragmentManager
包含了viewpager
的所有fragment
的实例
因此,当ViewPager
没有刷新时,它只是FragmentManager
仍保留的旧 fragment
实例。 您需要找出为什么FragmentManger
持有fragment
实例的原因。
这也是我们遇到的一个非常普遍的问题。 如果遇到这种情况,我们一般在 adapter
内部创建 fragment
的数组列表,或者尝试使用某些标签访问fragment
。 不过还有另一种选择。 FragmentStateAdapter
和FragmentPagerAdapter
都提供方法setPrimaryItem
。 可以用来设置当前fragment
,如下所示:
var fragment: ChildFragment? = null
override fun setPrimaryItem(container: ViewGroup, position: Int, any: Any) {
if (getChildFragment() != any)
fragment = any as ChildFragment
super.setPrimaryItem(container, position, any)
}
fun getChildFragment(): ChildFragment? = fragment
//use
mAapter.getChildFragment()
在我们的activity
中,我们有一个容器,其中装有fragment
。
add
只会将一个fragment
添加到容器中。 假设您将FragmentA
和FragmentB
添加到容器中。 容器将具有FragmentA
和FragmentB
,如果容器是FrameLayout
,则将fragment
一个添加在另一个之上。
replace
将简单地替换容器顶部的一个fragment
,因此,如果我创建了 FragmentC
并 replace
顶部的 FragmentB
,则FragmentB
将被从容器中删除(执行onDestroy
,除非您调用addToBackStack
,仅执行onDestroyView
),而FragmentC
将位于顶部。
那么如何选择呢? replace
删除现有fragment
并添加一个新fragment
。 这意味着当您按下返回按钮时,将创建被替换的fragment
,并调用其onCreateView
。 另一方面,add
保留现有fragment
,并添加一个新fragment
,这意味着现有fragment
将处于活动状态,并且它们不会处于 “paused” 状态。 因此,按下返回按钮时,现有fragment
(添加新fragment
之前的fragment
)不会调用onCreateView
。 就fragment
的生命周期事件而言,在replace
的情况下将调用onPause
,onResume
,onCreateView
和其他生命周期事件,在add
的情况下则不会。
如果不需要重新访问当前fragment
并且不再需要当前fragment
,请使用replace
。 另外,如果您的应用有内存限制,请考虑使用replace
。
androidx fragment 1.2.0
起,添加了新的 Lint 检查,以确保您在从 onCreateView()
、onViewCreated()
或 onActivityCreated()
观察 LiveData
时使用 getViewLifecycleOwner()
一般情况下我们会使用calss的simpleName
作为fragment
的tag
supportFragmentManager.commit {
replace(R.id.content,MyFragment.newInstance("Fragment"),
MyFragment::class.java.simpleName)
addToBackStack(null)
}
这样做不会出现什么问题,但是...
val fragment = supportFragmentManager.findFragmentByTag(tag)
这样获取到的fragment可能不是想要的结果。
为什么呢?
加入有两个 fragment,经过混淆,它们变成
com.mypackage.FragmentA → com.mypackage.c.a
com.mypackage.FragmentB → com.mypackage.c.a.a
上面是混淆了 full name,如果是simpleName 呢?
com.mypackage.FragmentA → a
com.mypackage.FragmentB → a
WTF!
所以在设置tag时尽量用全名或者常量
当我们使用BottomBarNavigation
和 NavigationDrawer
时,通常会看到诸如fragment
重建或多次添加相同fragment
之类的问题。
在这种情况下,您可以使用show / hide
而不是 add
或 replace
。
如果您想在fragment
的一系列跳转中按返回键返回上一个fragment
,应该在commit
transaction
之前调用addToBackStack
方法
//使用该扩展 androidx.fragment:fragment-ktx:1.2.0 以上
parentFragmentManager.commit {
addToBackStack(null)
add<SecondFragment>(R.id.content)
}
fragment-ktx 有哪些好用的扩展函数
fragment 之间和与 activity 通信
使用 FragmentContainerView 作为 fragment 容器
FragmentFactory 的使用
Fragment 返回键拦截
Fragment 使用 ViewBinding
Fragment 使用 ViewPager2
不需要重写 onCreateView 了?
使用require_()方法
//before
supportFragmentManager
.beginTransaction()
.add(R.id.content,Fragment1())
.commit()
//after
supportFragmentManager.commit {
add<Fragment1>(R.id.content)
}
//before
//共享范围activity
val mViewMode1l = ViewModelProvider(requireActivity()).get(UpdateAppViewModel::class.java)
//共享范围fragment 内部
val mViewMode1l = ViewModelProvider(this).get(UpdateAppViewModel::class.java)
//after
//共享范围activity
private val mViewModel by activityViewModels<MyViewModel>()
//共享范围fragment 内部
private val mViewModel by viewModel<MyViewModel>()
注意:ViewModelProviders.of(this).get(MyViewModel.class); 的方式已弃用
lifecycle-extensions
依赖包已弃用
fragment 和 fragment之间,fragment 和 activity 之间的通信有很多方法,android jetpack 推荐我们使用 ViewModel + LiveData 处理
同一个activity 内的 fragment 之间通信,可以使用作用范围为activity的ViewModel,activity与 fragment通信同理。详情可移步 Android官方应用架构指南
过去我们使用 FrameLayout
作为 Fragment
的容器,在 AndroidX Fragment 1.2.0
后,可以使用 FragmentContainerView
代替 Fragment
。
它修复了一些动画 z轴索引顺序问题和窗口插入调度,这意味着两个fragment
之间的退出和进入过渡不会互相重叠。使用FragmentContainerView
将先开启退出动画然后才是进入动画。
FragmentContainerView
是专门为 fragment设计的自定义View,它继承自 FrameLayout
android:name
属性允许您添加fragment
,android:tag
属性可以为fragment
设置tag
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.MyFragment"
android:tag="my_tag">
</androidx.fragment.app.FragmentContainerView>
过去,我们只能使用其默认的空构造函数实例化Fragment实例。 这是因为在某些情况下,例如配置更改和应用程序的流程重新创建,系统需要重新初始化。 如果不是默认的构造方法,系统将不知道如何重新初始化Fragment实例。
创建FragmentFactory来解决此限制。 通过向其提供实例化Fragment所需的必要参数/依赖关系,它可以帮助系统创建Fragment实例。
过去我们实例化fragment并传递参数会使用类似下面的代码
class MyFragment : Fragment() {
private lateinit var arg: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.getString(ARG) ?: ""
}
companion object {
fun newInstance(arg: String) =
MyFragment().apply {
arguments = Bundle().apply {
putString(ARG, arg)
}
}
}
}
//use
val fragment = MyFragment.newInstance("my argument")
如果您的Fragment有一个非空的构造函数,则需要创建一个FragmentFactory来处理它的初始化。
class MyFragmentFactory(private val arg: String) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
if (className == MyFragment::class.java.name) {
return MyFragment(arg)
}
return super.instantiate(classLoader, className)
}
}
fragment
由FragmentManager
管理,因此很自然,FragmentFactory
需要添加到FragmentManager
才能使用。
那么什么时候把FragmentFactory
添加到FragmentManager
呢?
父类调用 Activity#onCreate()
和 Fragment#onCreate()
之前
class HostActivity : AppCompatActivity() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
// ...
}
}
class ParentFragment : Fragment() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())
override fun onCreate(savedInstanceState: Bundle?) {
childFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
// ...
}
}
如果您的Fragment
具有默认的空构造函数,则无需使用FragmentFactory
。 但是,如果您的Fragment
在其构造函数中接受参数,则必须使用FragmentFactory
,否则将抛出Fragment.InstantiationException
,因为将使用的默认FragmentFactory
将不知道如何实例化Fragment
的实例。
有时候,您需要阻止用户返回上一级。 在这种情况下,您需要在 Activity
中重写 onBackPressed()
方法。 但是,当您使用 Fragment
时,没有直接的方法来拦截返回。 在 Fragment
类中没有可用的 onBackPressed()
方法,这是为了防止同时存在多个 Fragment
时发生意外行为。
但是,从 AndroidX
Activity 1.0.0
开始,您可以使用 OnBackPressedDispatcher
在您可以访问该 Activity
的代码的任何位置(例如,在 Fragment
中)注册 OnBackPressedCallback
。
class MyFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Do something
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
}
Android Studio 3.6.0
后提供了 ViewBindind
的支持,完整使用流程参见 [译]深入研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
_binding = null
}
}
ViewPager
使用了三个adapter
的抽象类,而ViewPager2
中只有两个
PagerAdaper
,ViewPager2 中使用 Recyclerview.Adapter
FragmentPagerAdapter
,ViewPager2中使用 FragmentStateAdapter
FragmentStatePagerAdapter
,ViewPager2中使用 FragmentStateAdapter
// A simple ViewPager adapter class for paging through fragments
class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
override fun getCount(): Int = NUM_PAGES
override fun getItem(position: Int): Fragment = ScreenSlidePageFragment()
}
// An equivalent ViewPager2 adapter class
class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = NUM_PAGES
override fun createFragment(position: Int): Fragment = ScreenSlidePageFragment()
}
使用 TabLayout
的变化,TabLayout
已从ViewPager2
中解耦,如果使用TabLayout
,需要引入依赖
implementation "com.google.android.material:material:1.1.0"
对于ViewPager2
,TabLayout
布局应与ViewPager2
在同一级别
<!-- A ViewPager element with a TabLayout -->
<androidx.viewpager.widget.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.viewpager.widget.ViewPager>
<!-- A ViewPager2 element with a TabLayout -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
使用ViewPager
时,TabLayout
与ViewPager
联动需要调用 setupWithViewPager
,并重写getPageTitle
方法,而ViewPager2
改为使用TabLayoutMediator
对象
// Integrating TabLayout with ViewPager
class CollectionDemoFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tabLayout = view.findViewById(R.id.tab_layout)
tabLayout.setupWithViewPager(viewPager)
}
...
}
class DemoCollectionPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
override fun getCount(): Int = 4
override fun getPageTitle(position: Int): CharSequence {
return "OBJECT ${(position + 1)}"
}
...
}
// Integrating TabLayout with ViewPager2
class CollectionDemoFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tabLayout = view.findViewById(R.id.tab_layout)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = "OBJECT ${(position + 1)}"
}.attach()
}
...
}
androidx fragment 1.1.0
后,您可以使用将 layoutId
作为参数的构造函数,这样就无需重写 onCreateView
方法了
class MyActivity : AppCompatActivity(R.layout.my_activity)
class MyFragmentActivity: FragmentActivity(R.layout.my_fragment_activity)
class MyFragment : Fragment(R.layout.my_fragment)
androidx fragment 1.2.2
起,新增了一项lint检查,fragment
建议使用关联的require_()
方法获取更多描述性错误消息,而不是使用checkNotNull(get_())
,requireNonNull(get_())
或get()!
适用于所有包含 get 和 require Fragment API
例如:使用 requireActivity()
替代 getActivity()
越来越多的项目使用了组件化,组件之间的通信是一个比较重要的问题。ARouter
等路由方案为我们提供了解决办法。那么如果不使用 Router 如何实现组件间的界面跳转呢?
setClassName
从一个 Activity 跳转到另一个Activity 的最直接方法如下:
val intent = Intent(this, TestActivity::class.java)
startActivity(intent)
但是,采用这种方法,当原 activity 位于一个 module(例如 FeatureA
)中,而目标 activity 位于另一个 module(FeatureB
)中时,该怎么办?
我们可以使用 Intent 的 setClassName
方法
val intent = Intent()
intent.setClassName(this, “com.flywith24.demo.TestActivity”)
startActivity(intent)
但是这种方式硬编码目标 activity 的完整类名,如果 activity 的类名被更改或者移动,而且没有更改硬编码,则编译可以通过,但是运行时崩溃
如果可以自动生成 activity 完整类名就好了
我们知道 activity 作为 Android 的组件之一需要在 Manifest 文件中声明
<activity android:name=”com.flywith24.demo.MainActivity” />
<activity android:name=”com.flywith24.demo.TestActivity” />
如果我们的数据是从 Manifest 中获得的,那么就解决了硬编码的问题了
有这样一个插件 ,在 build 时会将所有在 Manifest 中声明的 activity 的完整类名以静态常量的形式罗列到一个静态类中
object QuadrantConstants {
const val MAIN_ACTIVITY: String = "com.gaelmarhic.quadrant.MainActivity"
const val SECONDARY_ACTIVITY: String = "com.gaelmarhic.quadrant.SecondaryActivity"
const val TERTIARY_ACTIVITY: String = "com.gaelmarhic.quadrant.TertiaryActivity"
}
这样在使用时就避免了硬编码
val intent = Intent()
intent.setClassName(context, QuadrantConstants.MAIN_ACTIVITY)
startActivity(intent)
组件化中 app
module 会依赖所有的功能 module ,因此如果我们使用依赖注入在 app
中将所有的目标 activity 的完整类名声明出来,也能达到解决硬编码的问题
这里以 koin 为例
class MyApplication : Application() {
val myModule = module {
single { Feature2Activity::class.java.name }
}
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(myModule)
}
}
}
这样通过 get() 方法即可拿到 Feature2Activity
的完整类名
val intent = Intent()
.setClassName(this@Feature1Activity, get())
.putExtra("key", "value")
startActivity(intent)
各位有什么想法欢迎在评论区留言
Android 开发中统一不同 module 的依赖版本十分重要,传统的方式是使用 ext 的方式
之前我发过关于使用 buildSrc 简化项目中 gradle 代码的译文:什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin
该种方式可以很好的管理 gradle 的公共配置,这其中当然包括依赖版本
如图,在使用依赖时有代码提示,而且可以点击进入查看
但是由于 buildSrc 是对全局的所有 module 的配置,因此在构建速度上会慢一些。那么有没有一个更纯净的方式来配置依赖版本呢?
今天我们来介绍一种新的方式
使用 Gradle Composite builds 可以很容易解决这一问题
我们新建一个 module,命名为 version ,并将原来的 buildSrc 的代码转移过来
class DependencyVersionPlugin : Plugin<Project> {
override fun apply(project: Project) {
}
}
在 version 的 build.gradle 文件加入
gradlePlugin {
plugins {
version {
id = 'com.flywith24.version'
implementationClass = 'com.flywith24.version.DependencyVersionPlugin'
}
}
}
在 settings.gradle 加入 includeBuild("version")
(重点)
includeBuild("version")
rootProject.name='VersionControlDemo'
include ':app'
include ':lib'
接下来在需要引用的 module 中引入该插件
plugins {
id "com.flywith24.version"
}
之后我们就可以使用了
本系列文章介绍 Jetpack 组件库的更新
一直以来, fragment 的 api 都非常难用,官方也承认这一点。一个月前,fragment 中的onActivityCreated()
被弃用了
fragment 1.3.0-alpha02
中 onActivityCreated()
方法被弃用了
让我们来看一下提交 log
简单翻译一下
onActivityCreated()
最初的目的是让 fragment 的逻辑与其宿主 activity 创建建立关联,我们不鼓励这种耦合
我们应该传递外部依赖来作为 FragmentFactory
参数。view 相关的代码应该放置在 onViewCreated()
完成,其他的初始化代码应该在 onCreate()
中完成。为了在 activity onCreate()
完成后接收回调,可以添加一个 activity 生命周期的 LifecycleObserver
,并且接收到 Lifecycle.State#CREATED
回调时将其移除
override fun onAttach(context: Context) {
super.onAttach(context)
requireActivity().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
// 想做啥做点啥
owner.lifecycle.removeObserver(this)
}
})
}
那么 DialogFragment
怎么办?其 onActivityCreated
变为可选的
简单翻译一下
DialogFragment
使用 onActivityCreated
() 帮助创建 dialog。onActivityCreated() 弃用后我们应当寻找一个更好的方式来执行这部分逻辑
关于 view 相关的代码已经转移至 DialogFragment
的 viewLifecycleOwnerLiveData
,其他初始化逻辑可以放在 onGetLayoutInflater
我们仍支持为自定义 dialog 在 onActivityCreated()
中配置 dialog
查看 Jetpack fragment
的变动,不难看出官方正致力于为 fragment 「减负」,将小的,独立的功能从 fragment 中抽离出去,降低耦合,后续文章我们介绍其他的改动
原文:Bridging the gap between coroutines, JVM threads, and concurrency problems
作者:Manuel Vivo
译者:Flywith24
「协程是轻量级的线程」,是不是经常听到这样的描述?这个描述对你理解协程有实质性的帮助吗?可能没有。阅读本文,您会对 协程在 JVM 中实际的执行方式,协程与线程的关系以及使用 JVM 线程模型时不可避免的 并发问题 有更多的了解。
协程旨在简化执行异步操作的代码。基于 JVM 的协程的本质是:传递给协程构建器的 lambda 代码块最终会在特定的 JVM 线程上执行。如下面这个简单的 斐波那契数列(译者注:链接已改为百度百科)的计算:
// 在后台线程中计算第10个斐波那契数的协程
someScope.launch(Dispatchers.Default) {
val fibonacci10 = synchronousFibonacci(10)
saveFibonacciInMemory(10, fibonacci10)
}
private fun synchronousFibonacci(n: Long): Long { /* ... */ }
上面的 异步
协程代码块执行了同步且阻塞的斐波那契计算并将结果保存至内存。该代码块被 协程库管理的线程池(通过 Dispatchers.Default 配置)分发调度 并且在未来的某个时刻(取决于线程池的策略)在线程池中的线程执行。
请注意,因为没有挂起(suspend),所以上面的代码会在一个线程中执行。如果将执行的逻辑转移至不同的调度器(dispatcher),或者代码块可能在使用线程池的调度器中 yield / suspend,则协程可以在不同的线程中执行。
同样,如果没有协程,也可以使用线程手动执行上述逻辑,如下所示:
// 创建一个四个线程的线程池
val executorService = Executors.newFixedThreadPool(4)
// 在线程池中的线程上调度并执行下面代码
executorService.execute {
val fibonacci10 = synchronousFibonacci(10)
saveFibonacciInMemory(10, fibonacci10)
}
尽管手动管理线程池是可行的,但考虑到协程内置支持取消,更容易处理错误,使用可以降低内存泄露可能性的 结构化并发(structured concurrency) 以及 Jetpack 库的支持,协程是 Android 中异步编程的推荐方案。
开始创建协程到在线程中执行,这过程发生了什么?当使用标准的协程构建器创建协程时,您可以指定在特定的 CoroutineDispatcher 执行代码,默认将使用 Dispatchers.Default
。
CoroutineDispatcher 负责将协程的执行分发给 JVM 线程。原理是:当使用 CoroutineDispatcher
时,它会使用 interceptContinuation 拦截协程,该方法 将 Continuation 包装在 DispatchedContinuation 中。 这是可行的,因为 CoroutineDispatcher
实现了 ContinuationInterceptor 接口。
如果您阅读过我的 协程工作原理 的文章,您已经知道编译器创建一个状态机,状态机的信息(如下一步需要执行的内容)保存在 Continuation 对象中。
如果需要在其它 Dispatcher 中执行 Continuation,DispatchedContinuation
的 resumeWith 方法负责分配给适合的协程!
此外,DispatchedContinuation 是 DispatchedTask
,在 JVM 中它是可在 JVM 线程上运行的 Runnable
对象!这很酷不是吗?当指定 CoroutineDispatcher
时,协程将转换为 DispatchedTask
,该DispatchedTask
会作为一个 Runnable
在 JVM 线程上执行!
在创建协程时 dispatch
方法是如何调用的呢?使用标准的协程构建器创建协程,可以指定协程以 CoroutineStart 类型的 start
参数。例如,您可以使用 CoroutineStart.LAZY
将其配置为仅在需要时启动。 默认情况下,使用 CoroutineStart.DEFAULT
来根据其 CoroutineDispatcher
调度协程执行。
协程中的代码块最终如何在线程中执行的图示
您可以使用 Executor.asCoroutineDispatcher() 扩展函数将协程转换为 CoroutineDispatcher
,从而在您的 app 线程池中执行协程。您也可以使用协程库中的默认 Dispatchers。
您可以在 createDefaultDispatcher 方法中看到如何初始化 Dispatchers.Default
。默认情况下使用 DefaultScheduler。如果您查看 Dispatchers.IO 的实现,它还将使用 DefaultScheduler
并允许根据需要创建至少 64 个线程。Dispatchers.Default
和 Dispatchers.IO
隐式地连接在一起,因为它们使用相同的线程池。下面我们来看看使用不同的 Dispatcher 调用 withContext
的运行时开销是怎样的?
在 JVM 中,如果创建的线程多于可用的 CPU 核心数,则在线程之间进行切换会带来一些运行时开销。上下文切换 的成本并不低!操作系统需要保存和恢复执行上下文,CPU 需要花时间调度线程而不是运行实际的 app 工作。除此之外,如果线程正在运行的代码阻塞了,也可能会发生上下文切换。如果线程是这种情况,将 withContext
与不同的 Dispatchers 配合使用是否会对性能造成损失?
幸运的是,如您所料,线程池为我们管理了这些复杂的场景,并尝试尽可能优化被执行的工作(这就是在线程池上执行工作比手动在线程中执行工作更好的原因)。协程也从中受益(因为它们是在线程池中调度的)!最重要的是,协程不阻塞线程,而是 suspend 工作! 甚至更有效率!
默认情况下,CoroutineScheduler 是 JVM 实现中使用的线程池,它以最有效的方式将分派的协程分配给工作线程。由于 Dispatchers.Default
和 Dispatchers.IO
使用相同的线程池,因此优化了它们之间的切换,以尽可能避免线程切换。协程库可以优化这些调用,保留在相同的调度器(dispatcher)和线程上,并遵循一个快速路径(fast-path)。
由于 Dispatchers.Main
通常是 UI app 中不同的线程,因此在协程中 Dispatchers.Default
和 Dispatchers.Main
之间切换不会带来巨大的性能成本,因为协程只是挂起(即停止在一个线程中执行),并被调度到在另一个线程中执行。
由于不同线程上的调度工作非常简单,协程 确实 使异步编程更容易。另一方面,这种简单性可能是一把双刃剑:由于协程运行在 JVM 线程模型上,它们不能简单地摆脱线程模型带来的并发问题。 因此,您必须注意避免并发问题。
多年来,不可变性(immutability)等良好实践已经缓解了您可能遇到的一些与线程有关的问题。然而,有些场景下不适合不可变性。所有并发问题的根源在于状态管理!特别是在多线程环境中访问 可变状态。
多线程应用中的操作顺序是不可预测的。除了编译优化会带来有序性问题,上下文切换还可能带来原子性问题(译者注:并发问题可参考 译者的笔记)。如果在访问可变状态时未采取必要的预防措施,则线程可能会看到过时的数据,丢失更新或遭受 竞争状况 的困扰。
请注意,可变状态和访问顺序的问题不是 JVM 特有的,这些问题也会影响其它平台的协程。
使用协程的 app 本质上是一个多线程 app。使用协程并且包含可变状态的类必须采取预防措施以确保执行结果符合预期,即确保在协程中执行的代码能看到最新版本的数据。这样,不同的线程不会互相干扰。 并发问题可能会导致非常小的错误,难以调试,甚至是 heisenbug!
这类问题并不罕见。例如可能一个类需要将已登录用户的信息保留在内存中,或者在应用运行时缓存某些值。如不小心,并发问题仍会在协程中发生!使用 withContext(defaultDispatcher)
的挂起函数不能总是在同一线程中执行!
假设我们有一个类可以缓存用户进行的交易。如果无法正确访问缓存,如下示例,则可能会发生并发错误:
class TransactionsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
private val transactionsCache = mutableMapOf<User, List<Transaction>()
private suspend fun addTransaction(user: User, transaction: Transaction) =
// 小心!访问缓存是不受保护的。
// 并发错误可能发生:线程可以看到过时的数据,竞争条件可能发生
withContext(defaultDispatcher) {
if (transactionsCache.contains(user)) {
val oldList = transactionsCache[user]
val newList = oldList!!.toMutableList()
newList.add(transaction)
transactionsCache.put(user, newList)
} else {
transactionsCache.put(user, listOf(transaction))
}
}
}
即使我们讨论的是 Kotlin,《Java 并发编程实践》(作者:Brian Goetz)一书也是了解更多这部分内容和 JVM 系统并发问题的绝佳资源。或者参考 Jetbrains 关于 共享可变状态和并发 的文档。
如何保护可变状态或找到一个好的 同步 策略,完全取决于数据的性质和所涉及的操作。本节旨在使您意识到可能会遇到的并发问题,而不是列出保护可变状态的所有不同方法和 API。尽管如此,您还是可以从这里获得一些技巧和 API,以使得可变变量线程安全。
可变状态应由一个 class 封装并拥有。该类集中对状态的访问,并根据场景使用更适合的同步策略来保护读写操作。
有一种解决方案是限制对一个线程的读/写访问。可以使用队列以 生产者-消费者 的方式完成对可变状态的访问。JetBrains 对此有一个很好的文档。
在 JVM 中,您可以使用线程安全的数据结构来保护可变变量。例如,对于简单计数器,可以使用 AtomicInteger。为了保护上面代码的 Map,可以使用 ConcurrentHashMap。ConcurrentHashMap 是一个线程安全的同步集合,可优化 Map 的读写吞吐量。
请注意,线程安全的数据结构不能防止调用方排序问题,它们只是确保内存访问是原子性的。当逻辑不太复杂时,它们有助于避免使用锁。例如,它们不能在上面显示的 transactionCache
示例中使用,因为操作顺序和它们之间的逻辑需要线程和访问保护。
同样,这些线程安全数据结构中的数据必须是不可变的或受保护的,以防止在修改已存储在其中的对象时出现竞争条件。
如果您有需要同步的复合操作,则 @Volatile
变量或线程安全的数据结构将无济于事!内置的 @Synchronized
注解可能不够精细,无法提高的效率。
在这种场景下,您可能需要使用并发工具(如 latch,信号量 或 屏障)创建自己的同步机制。其它场景,您可以使用锁或互斥锁保护代码的多线程访问。
Kotlin 中的 Mutex 具有 lock 和 unlock 的挂起函数以用来手动保护协程代码。Mutex.withLock 扩展函数使用很简单:
class TransactionsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
// Mutex 保护缓存可变状态
private val cacheMutex = Mutex()
private val transactionsCache = mutableMapOf<User, List<Transaction>()
private suspend fun addTransaction(user: User, transaction: Transaction) =
withContext(defaultDispatcher) {
// Mutex 使 读&写 缓存的操作 线程安全
cacheMutex.withLock {
if (transactionsCache.contains(user)) {
val oldList = transactionsCache[user]
val newList = oldList!!.toMutableList()
newList.add(transaction)
transactionsCache.put(user, newList)
} else {
transactionsCache.put(user, listOf(transaction))
}
}
}
}
由于使用 Mutex
的协程在可以继续执行前会暂停执行,因此它比阻塞线程的 JVM 锁要有效得多。在协程中使用 JVM 同步类时要小心,因为这可能会阻塞在其中执行协程的线程并产生 liveness 问题。
传递给协程构建器的代码块最终在一个或多个 JVM 线程上执行。因此,协程运行在 JVM 线程模型中并受其所有约束。使用协程,仍会写出错误的多线程代码。因此,在代码中访问共享的可变状态要小心!
译文完。
人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~
我是 Flywith24,人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。
原文:Android Styling: Themes vs Styles
作者:Nick Butcher
译者:Flywith24
题图来自 Virginia Poltrack
Android styling system 提供了一种强大的方式来指定应用程序的视觉设计,但很容易被滥用。正确地使用它可以使 theme 和 style 更易于维护,使品牌更好地更新并且直接支持暗黑模式。这是我和 Chris Banes 揭开 Android styling system 神秘面纱系列文章的第一篇,这样您就可以更轻松地打造一款时尚的 app
在第一篇文章中,我们来聊一聊 Android styling system 的组成部分:Theme 和 Style
Theme 和 Style 都使用 <style>
标签,但它们的用途截然不同。您可以将它们视为一个 key-value 模型,其中 key 是属性,而 value 代表资源。
Style 是 view 属性的集合。您可以将 style 视为 Map<view attribute, resource>。这里的 key 是 view 的所有属性,例如控件声明并且开发者可以在布局文件中配置的属性。Style 支持特定类型的控件,因为不同的控件有着不同的属性集:
Style 是 view 属性的集合;特定于单一类型的控件
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<style name="Widget.Plaid.Button.InlineAction" parent="…">
<item name="android:gravity">center_horizontal</item>
<item name="android:textAppearance">@style/TextAppearance.CommentAuthor</item>
<item name="android:drawablePadding">@dimen/spacing_micro</item>
</style>
Style 中的每一个 key 都是可以在 layout 文件中配置的
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<Button …
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.CommentAuthor"
android:drawablePadding="@dimen/spacing_micro"/>
将它们抽取为 style 可以更方便地在多个 view 中复用和维护
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<Button …
style="@style/Widget.Plaid.Button.InlineAction"/>
View 只能使用一种 Style,这与其他的 styling systems 不同(例如 Web 上的 CSS,在该系统中,组件可以设置多个 CSS 样式)
一个 Style 只作用于其应用的 view,不包含它的任何子 view。
例如,存在一个 ViewGroup,其内部有三个 button。为 ViewGroup 配置 style 不会作用于这些 button。Style 提供的值会与在布局中直接设置的值组合(使用 styling precedence order)
Theme 是资源的集合,它可以被 style ,layout 或者其它引用。它为 Android 资源提供了语义明确的命名,例如 colorPrimary
是给定颜色的命名
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<style name="Theme.Plaid" parent="…">
<item name="colorPrimary">@color/teal_500</item>
<item name="colorSecondary">@color/pink_200</item>
<item name="android:windowBackground">@color/white</item>
</style>
这些被命名的资源被称为 theme 属性。Theme 是 Map<theme attribute, resource>。theme 属性不同于 view 属性,因为它们不是特定于单个 view 类型的属性,而是指向值的指针,这些指针在应用中的适用范围更广。theme 为这些已命名的资源提供了正确的值。在上面的示例中,colorPrimary
属性指定此主题的原色为蓝绿色。通过将这些资源抽取到一个 theme 中,我们可以提供不同的具体的值(例如 colorPrimary
=orange 是不同的主题)
theme 是命名资源的集合,广泛应用于整个应用程序
theme 就像接口,面向接口编程能够让开发者将公共协议与具体实现解耦,从而允许开发者提供出不同的实现。theme 扮演类似的角色, 通过针对 theme 属性编写 layout 和 style,我们可以在不同的主题下使用它们,从而提供不同的具体资源
大致等效的伪代码:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
interface ColorPalette {
@ColorInt val colorPrimary
@ColorInt val colorSecondary
}
class MyView(colors: ColorPalette) {
fab.backgroundTint = colors.colorPrimary
}
这使您可以更改MyView的呈现方式,而不必创建它的变体:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
val lightPalette = object : ColorPalette { … }
val darkPalette = object : ColorPalette { … }
val view = MyView(if (isDarkTheme) darkPalette else lightPalette)
您可以在具有 Context 的组件中使用 theme,例如 Activity ,View ,ViewGroup
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<!-- AndroidManifest.xml -->
<application …
android:theme="@style/Theme.Plaid">
<activity …
android:theme="@style/Theme.Plaid.About"/>
<!-- layout/foo.xml -->
<ConstraintLayout …
android:theme="@style/Theme.Plaid.Foo">
您还可以通过用 ContextThemeWrapper 包装现有的 Context 来在代码中设置 theme,然后将其用于 inflate 布局等
Theme
可以作为 Context
的属性被获取,并且它可以从任何 Context 或 Context 的子类获得,例如 Activity
,View
,或者 ViewGroup
。这些对象存在于一个「树」中,其中 Activity 包含 ViewGroup,ViewGroup 包含 View。在此树的任何级别上指定主题都会影响到其后代节点,例如在 ViewGroup 上设置 Theme 会作用域其所有子 View(这与只作用于单一 View 的 Style 相反)
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<ViewGroup …
android:theme="@style/Theme.App.SomeTheme">
<! - 该主题还会作用于所有子 View -->
</ViewGroup>
例如,如果您想让原本暗色的屏幕变暗,则这可能非常有用。 在下一篇文章(即将发布!)中了解有关此行为的更多信息。
请注意,它的行为仅适用于 layout inflation 的时刻。 尽管 Context 提供了 setTheme
方法,Theme 提供了 applyStyle
方法,但都需要在 inflation 之前调用。 在 inflation 之后设置新 theme 或应用 style 不会更新已存在的 view。
理解 Style 和 Theme 的职责和作用有助于我们更好地管理资源
例如我们有一个蓝色主题的 app ,但是在 pro 版本我们想要一个紫色并且花哨的外观,并且需要提供暗黑主题。如果仅使用 Style 实现此需求,则必须为 pro/non-Pro,light/dark 创建四个 Style。由于 Style 只能作用于特定的 View(Button,Switch 等等),您需要为应用中的每种视图类型创建这些组合
如果改为使用 Style 和 Theme,则可以将因 Theme 而变化的抽取为 Theme 属性,因此我们仅需要为每个视图类型定义一个 Style。 对于上面的示例,我们可以定义4个主题,每个主题为colorPrimary 主题属性提供不同的值,然后这些主题并自动反映出正确值
当您需要考虑 Style 和 Theme 的交互时,此方法可能看起来更复杂,但是它的好处是可以隔离每个主题中差异的部分。 因此,如果您的应用程序的主题色从蓝色改为橙色,则只需要在一个地方进行更改,而无需分散在整个 Style 中。 它还有助于避免 Style 的泛滥。 理想情况下,每种视图类型只具有少量样式。 如果您不利用主题,则您的 styles.xml 文件很容易失控并以相似样式的不同变体「爆炸」,这使维护工作变得头疼
感谢 Florina Muntenescu 和 Chris Banes
译文完
最近 activity 1.2.0 正式版发布,除了全新的 Activity Result API (前面已有介绍),还引入了一个新的接口 ContextAware
。
老规矩,我们沿着 git commit 查看该功能的引入过程。
IssueTracker 上提了这样一个需求:
LifecycleOwner
API 提供了达到 CREATED
状态时的回调,但有些场景这个 API 不满足我们的需求。
例如:
setTheme
来配置主题setLocalNightMode()
这些方法都需要在 super.onCreate()
和 inflate 布局前调用。
如果存在一个可组合的回调就好了(无需手动在 onCreate()
中加入代码)。
我们先来看看引入前的状态, LifecycleOwner
API 提供的 Observer 只能在至少达到 CREATED
状态时回调。换言之,回调时 super.onCreate()
已经调用。
class MainActivity : AppCompatActivity(R.layout.activity_main) {
init {
lifecycle.addObserver(LifecycleEventObserver { _, event ->
Log.i(TAG, "LifecycleEventObserver: ${event.name} ${event.targetState}")
})
}
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate: before super")
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: after super")
}
}
我们可以通过调用 addOnContextAvailableListener
设置监听:
class MainActivity : AppCompatActivity(R.layout.activity_main) {
init {
lifecycle.addObserver(LifecycleEventObserver { _, event ->
Log.i(TAG, "LifecycleEventObserver: ${event.name} ${event.targetState}")
})
++ addOnContextAvailableListener { context ->
++ Log.i(TAG, "ContextAvailableListener: $context")
++ // 需要在 super.onCreate() 前进行的操作
++ }
}
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate: before super")
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: after super")
}
}
该方法会在调用 super.onCreate()
前回调,这意味着我们可以将一些需要在 super.onCreate()
前执行的逻辑放置在 OnContextAvailableListener#onContextAvailable() 方法中。
如上图所示,Activity 的继承关系是:
android.app.Activity
→ androidx.core.app.ComponentActivity
→ androidx.activity.ComponentActivity
→ androidx.fragment.app.FragmentActivity
→ androidx.appcompat.app.AppCompatActivity
→ 自定义 Activity
android.app.Activity
是固化在 ROM 中的 framework 层的 Activity(代码逻辑随着 Android 版本发布而固定),最基础的 Activity,拥有 Activity 最核心的逻辑androidx.core.app.ComponentActivity
是 androidx.core library (提供最新的平台功能并兼容旧设备)下的 Activity,其内部逻辑很少,主要作为兼容层存在androidx.activity.ComponentActivity
是 androidx.activity library 下的 Activity,其内部封装者 activity 库最新的 API,例如新的 Activity Result API。主要使用多接口的组合实现,例如实现了 LifecycleOwner
,ViewModelStoreOwner
,ActivityResultRegistryOwner
等接口androidx.fragment.app.FragmentActivity
是 androidx.fragment library 下的 Activity,用于支持 Fragment。其内部持有 FragmentController 以对 Fragment 进行操作。androidx.appcompat.app.AppCompatActivity
是 androidx.appcompat library 下的 Activity,主要用于为旧设备提供高版本的功能,如使用 Toolbar 操作标题栏,将 TextView 等控件映射成 AppcompatTextView 等控件。为了引入 ContextAware 功能,官方在 androidx.activity library
下引入了相关的代码:
代码十分简单,两个接口和一个 Helper 类
让 androidx.activity library
的 ComponentActivity 实现 ContextAware 接口,并在调用 super.onCreate() 方法前调用 dispatchOnContextAvailable() 方法,代码如下,删减无关代码。
人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~
我是 Flywith24,人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。
之前写过 Android Studio 多个项目依赖同一个模块的用法
不过在使用中遇到了几个问题,编译速度慢,总是显示出关联项目。
所以决定将公共模块aar
使用maven
私服管理,在此记录之。
官网
下载后解压,这里以windows为例
打开 D:\nexus-3.20.1-01-win64\nexus-3.20.1-01\bin
目录
在该目录下执行
nexus.exe /run
见到 Started Sonatype Nexus OSS 3.20.1-01
字样即成功
打开 http://localhost:8081/ 进入配置界面
详情参考 Maven私服Nexus 3.x搭建
网上文章很多,下面说一下搭建过程中出现的问题。
正常配置并引入私服的依赖,但是提示无法resolve该依赖
解决:
1. Nexus 允许匿名登录
这种操作很暴力
2. 引用依赖配置账号密码
project 的 build.gradle allprojects->repositories
中配置maven url 的同时配置用户名密码
allprojects {
repositories {
google()
jcenter()
maven {
credentials {
username 'username'
password 'password'
}
url 'http://localhost:8081/repository/Android/'
}
}
}
成功引入依赖后发现找不到aar中的类
详情参考 解决aar混淆后包里是空的问题,android混淆讲解
解决:
打出的aar是release的,所以关闭release的混淆,或者想暴露出的类禁止混淆即可
生成 java doc 时提示错误: 编码GBK的不可映射字符
在module的build.gradle
中配置
tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
options.addStringOption('encoding', 'UTF-8')
}
在 Root Project 下的 build.gradle 文件中 buildscript 下的 dependencies 中添加:
classpath 'org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.17'
module 的 build.gradle 应用插件
apply plugin: 'org.jetbrains.dokka-android'
详情参考 使用Gradle打包Kotlin项目代码、生成Kotlin代码文档
> Failed to deploy artifacts: Could not transfer artifact
cn.example.baselib:library-base:aar:0.0.1
from/to remote (http://localhost:8081/repository/Android/):
Failed to transfer file:
http://localhost:8081/repository/Android/cn/example/baselib/library-base/0.0.1/library-base-0.0.1.aar.
Return code is: 500, ReasonPhrase: Internal Privoxy Error.
解决:
关闭Android Studio代理
windows在C:\Users\Administrator\.gradle\gradle.properties
文件
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Mon Jan 06 13:56:29 CST 2020
移除代理
#systemProp.http.proxyHost=127.0.0.1
#systemProp.http.proxyPort=1080
Android Studio将项目发布到Maven仓库(3种方式最新最全)
Kotlin与Java混编项目的Nexus私有仓库持续交付与集成
Android 11(R)是2020年的下一代 Android,Google 于上周发布了 Android 11: Developer Preview 3
在 Android 11 Toast 的行为发生了变更
禁止后台自定义 Toast
text toast 不允许自定义
setView() 被弃用
新增 Toast.Callback 回调
自定义 Toast 不能 在 app 处于后台时显示,取而代之会显示 "Background custom toast blocked for package [packageName] See g.co/dev/toast." 的文本 toast
普通的 text toast
不受影响
默认的 toast 是 text toast
,如果想使用自定义的 toast ,需要调用 setView() 方法
在 targetSdkVersion 为 R 或更高时,调用 setGravity 和 setMargin 方法将不进行任何操作
官方文档中所述的 Android R 仅影响 text toast ,而自定义的 toast 不受影响
如图,在 test toast 中调用 setGravity 和 setMargin 方法,但 toast 位置并未居中
setView() 方法被标记弃用
Deprecated 表示该功能目前仍可以使用,但可能会在将来的 Android 版本中删除。 建议开发人员避免长期使用此功能
可以看到,官方在一步步禁止自定义 Toast
目前是 targetSdkVersion 为 R 或更高的 app 禁止后台弹出自定义 Toast
同时将 setView() 方法标记弃用,当该方法从源码中移除后,自定 Toast 的方式将被彻底消灭
当然,官方提供了相应的替代品,使用 Snackbar
添加了新的回调(Toast.Callback
),以通知 Toast 显示和隐藏。 可以通过以下方法轻松将其添加到 Toast 中:
val toast = Toast.makeText(this, R.string.simple2_toast, Toast.LENGTH_SHORT)
toast.addCallback(object : Toast.Callback() {
override fun onToastShown() {
super.onToastShown()
Log.d(TAG, "onToastShown")
}
override fun onToastHidden() {
super.onToastHidden()
Log.d(TAG, "onToastHidden")
}
})
toast.show()
demo 在这 ,切换 Flavor 即可指定不同的 targetSdkVersion
在写 demo 时遇到一些小问题
Handler()
无参构造方法和 Handler(Handler.Callback)
构造方法 被弃用了
简单来讲就是在初始化 Handler 时要显示的配置 Looper
Handler 使用不当会有这样一种 bug,例如在子线程通过无参构造函数创建 Handler,您可能会看到这样的异常
详细内容这里就不讲了,这是 Android 开发者的必备知识
官方通过强制使用传入 Looper 的 Handler 构造器来避免使用中的问题
过去使用 Toast 构造器创建 Toast 对象 并调用 setText 方法会崩溃,targetSdkVersion 为 R 时不会崩溃
API 29 中调用 setText() 方法时要保证 mNextView 不为空,而 mNextView 是调用 setView 赋值的
因此过去使用 Toast 构造器创建 toast 对象无法创建普通的 text toast,必须调用 setView 方法
至于 API 30 肯定在这里做了修改,由于现在看不到源码,我也猜测不出官方的用意
如果各位小伙伴有什么想法欢迎评论区留言
不会测试的开发不是好开发——鲁迅
一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,本篇是该系列的第三篇,前面我们介绍了如何测试 ViewModel 和 LiveData,今天我们介绍一下如何测试 Repository
当您为某个类写单元测试,您只想测试该类的代码。测试 Repository 比较棘手的问题是我们只想测试 Repository 中的代码而不测试其下层的代码
我们简单看一下我们 demo 中 Repository 中的代码
很明显,我们无法单独测试 Repository 而不测试 RepoDataSource
中的代码
您可能有疑惑为什么写单元测试时能够单独测试 Repository 中的代码很重要?这里有一些原因
Repository 的部分代码依赖于其他代码,例如数据库代码可能需要运行在真实的设备上
Repository 依赖的代码如数据库代码或者网络数据代码需要运行一段时间,并且网络请求甚至有失败的可能
Repository 依赖的代码中有 bug 会导致测试失败,但由于我们进行的是 Repository 的单元测试,因此您无法定位其位
测试 Repository 我们希望它能运行很快,我们需要的是 local test
Repository 依赖的数据库或网络请求的代码是 long-running and flaky test
,这意味着您的测试是不可靠的
简单来讲,
Flaky Tests
是当重复运行相同的代码,有些时候能通过,有些时候不能通过
测试时应该避免这种情况,因为这样的测试结果是不可靠的
那么我们应如何解决该问题呢?答案是 Test Double
Test Double
是为测试精心准备的类,它可以在测试中替换真实版本的数据。就像电影中替身演员会替代演员去完成一些危险动作一样。因此在 Repository 中,我们可以为数据源制作 Test Double
事实上,存在很多种类的 Test Double
,本系列文章会介绍 Fake
和 Mock
Fake | 类的有效实现,只适用于测试,不适用于生产 |
---|---|
Mock | 用于跟踪方法调用,根据方法是否被正确的调用来判断测试是否通过 |
Stub | 不包含逻辑并只返回开发者编程返回的逻辑 |
Dummy | 用于传递但并不使用,例如只需要它作为一个参数 |
Spy | 可以跟踪一些其他信息; 例如,如果您创建了SpyTaskRepository,它可能会跟踪 addTask 方法被调用的次数 |
如果想了解 Test Double 更详细的信息,请移步 Testing on the Toilet: Know Your Test Doubles
关于 Android 中的 Test Double,可参考 great tips about using test doubles
使用 Fake 意味着数据源不是从网络或者数据库中获取,因此它只适用于测试
我们可以将 LocalDataSource
和 RemoteDataSource
替换为 FakeDataSource
首先我们在 test source set 中 创建 FakeDataSource
并实现 RepoDataSource
接口
如此一来该接口就有了三个实现类
我们在构造器中传入 Repo list,并完成其内部的获取和保存方法
然后我们便可以编写 RepoRepository 的 test 代码了
在 RepoRepository 上唤出 Generate 弹出框选择 Create Test 选项,这样我们便创建了 RepoRepositoryTest
首先 我们需要提供数据源
class RepoRepositoryTest {
private val repo1 = Repo(id = 1, fork = false)
private val repo2 = Repo(id = 2, fork = false)
private val repo3 = Repo(id = 3, fork = true)
private val repo4 = Repo(id = 4, fork = true)
private val remoteRepos = listOf(repo1, repo2)
private val localRepos = listOf(repo3, repo4)
//...
}
之后我们声明出 RepoRepository
localDataSource 和 remoteDataSource
class RepoRepositoryTest {
//...
private lateinit var localReposDataSource: FakeDataSource
private lateinit var remoteReposDataSource: FakeDataSource
private lateinit var repoRepository: RepoRepository
//...
}
然后我们编写初始化 Repository 的代码
@Before
fun initRepository() {
remoteReposDataSource = FakeDataSource(remoteRepos)
localReposDataSource = FakeDataSource(localRepos)
repoRepository = RepoRepository(remoteReposDataSource, localReposDataSource)
}
最后我们编写 Test 代码,由于 getRepos
是挂起函数,因此我们在这里使用了 runBlocking{}
@Test
fun getRepos() = runBlocking {
val result = repoRepository.getRepos("Flywith24", true)
assertThat(result.value, IsEqual(remoteRepos))
}
Coil 是 Instacart 团队研发的新的的图片加载库,它使用了很多高级功能,例如协程,Okhttp
,androidx.lifecycle
。Coil
还包括一些高级功能,例如图像采样,有效的内存使用以及请求的自动取消/暂停
默认情况下 Coil
与 R8 完全兼容,开箱即用,不需要添加额外的规则。如果使用 Proguard ,您可能需要为 Coroutines, OkHttp 和 Okio 添加规则
Coil
进行了很多优化,包括内存和磁盘缓存,对内存中的图像进行采样,重新使用位图,自动暂停/取消请求等等Coil
在您的APK中添加了约 2000 种方法(对于已经使用 OkHttp
和 Coroutines
的应用程序),与 Picasso
相当,远少于 Glide
和 Fresco
Coil
的 API 利用 Kotlin 的特性简化了样板代码Coil
是 Kotlin-first
,使用现代化的库,例如 Coroutines
, OkHttp
, Okio
, 以及 AndroidX Lifecycles
Coil
是以下名称的缩写:Coroutine Image Loader
Coil
拥有 5 个 artifact 并发布在 mavenCentral()
io.coil-kt:coil
:依赖于 io.coil-kt:coil-base
并且包含了 Coil
的单例和 ImageView.load
的扩展函数io.coil-kt:coil-base
:base 库,不包含 Coil
的单例和 ImageView.load
的扩展函数,如果使用依赖注入,则可以使用该库io.coil-kt:coil-gif
:引入一系列解码器以支持解码 gifio.coil-kt:coil-svg
:引入一系列解码器以支持 svgio.coil-kt:coil-video
:包括两个 fetchers ,以支持从 Android 支持的任何视频格式中提取和解码帧// 普通使用引用
implementation "io.coil-kt:coil:0.11.0"
// 使用依赖注入时或者制作基于 coil 的库引用
implementation "io.coil-kt:coil-base:0.11.0"
Coil
要求 Java 8,要通过 D8 启用 Java 8 调试,请将以下内容添加到 Gradle 脚本
Gradle (.gradle
)
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}
Gradle Kotlin DSL (.gradle.kts
)
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
}
io.coil-kt:coil
提供了 类型安全的 ImageView 扩展函数
在 ImageView 中加载图片,只需调用 load 扩展函数
// URL
imageView.load("https://www.example.com/image.jpg")
// Resource
imageView.load(R.drawable.image)
// File
imageView.load(File("/path/to/image.jpg"))
// And more...
上面的请求等价于:
val imageLoader = Coil.imageLoader(context)
val request = LoadRequest.Builder(imageView.context)
.data("https://www.example.com/image.jpg")
.target(imageView)
.build()
imageLoader.execute(request)
可选的请求配置可以通过 lambda 来操作
imageView.load("https://www.example.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}
ImageLoader
是执行请求的服务类。 他们处理缓存,数据获取,图像解码,请求管理,bitmap pool,内存管理等。 可以使用 builder 来创建和配置新实例:
val imageLoader = ImageLoader.Builder(context)
.availableMemoryPercentage(0.25)
.crossfade(true)
.build()
imageView.load 使用单例 ImageLoader
执行 LoadRequest
。 可以使用以下方式访问单例 ImageLoader
:
val imageLoader = Coil.imageLoader(context)
(可选)您可以创建自己的ImageLoader实例,并通过依赖项注入将它们注入:
val imageLoader = ImageLoader(context)
当您创建单个 ImageLoader
并在整个应用程序**享时,Coil
的性能最佳。 这是因为每个 ImageLoader
都有自己的内存缓存,bitmap pool 和网络监听
有两种 Request 类型
LoadRequest
是一个生命周期范围的 request,支持 Target
,Transition
等等GetRequest
挂起并返回 RequestResult
如果要加载到自定义 target 中,可以执行 LoadRequest
val request = LoadRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.target { drawable ->
// Handle the result.
}
.build()
imageLoader.execute(request)
要强制获取图像,请执行GetRequest:
val request = GetRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.build()
val drawable = imageLoader.execute(request).drawable
如果您使用的是 io.coil-kt:coil
,您可以使用以下任意方式设置 ImageLoader
的实例
在 Application 中实现 ImageLoaderFactory
(推荐)
class MyApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(context)
.crossfade(true)
.okHttpClient {
OkHttpClient.Builder()
.cache(CoilUtils.createDefaultCache(context))
.build()
}
.build()
}
}
调用 Coil.setImageLoader
val imageLoader = ImageLoader.Builder(context)
.crossfade(true)
.okHttpClient {
OkHttpClient.Builder()
.cache(CoilUtils.createDefaultCache(context))
.build()
}
.build()
Coil.setImageLoader(imageLoader)
默认的 ImageLoader
可以通过这样取回
val imageLoader = Coil.imageLoader(context)
设置默认的 ImageLoader
是可选的。 如果未设置,则 Coil
会延迟创建具有默认值的 ImageLoader
如果您使用的是 io.coil-kt:coil-base
,您应创建自己的 ImageLoader
实例并通过依赖注入将它注入到 app 中
注意:如果设置自定义OkHttpClient,则必须设置缓存实现,否则ImageLoader将没有磁盘缓存。 可以使用CoilUtils.createDefaultCache 创建默认的 Coil 缓存实例
ImageLoader 支持的数据类型为
android.resource
, content
, file
, http
, and https
schemes only)如果要预加载到内存中,执行一个不带 target 的 LoadRequest
val request = LoadRequest.Builder(context)
.data("https://www.example.com/image.jpg")
// 可选的,但是设置 ViewSizeResolver 可以通过限制预加载的大小来节省内存
.size(ViewSizeResolver(imageView))
.build()
imageLoader.execute(request)
如果只想将网络图片预加载到磁盘中,可以为 request 关闭内存缓存
val request = LoadRequest.Builder(context)
.data("https://www.example.com/image.jpg")
.memoryCachePolicy(CachePolicy.DISABLED)
.build()
imageLoader.execute(request)
LoadRequest
会自动取消在以下几种情况下
关联的 view detached,
关联的 lifecycle destroyed
另一个 request 在相同的 view 中开启
此外,每个 LoadRequest
返回一个 RequestDisposable
,可用于检查请求是否在运行中或处理该请求(有效地取消请求并释放其关联资源)
val disposable = imageView.load("https://www.example.com/image.jpg")
// Cancel the request.
disposable.dispose()
GetRequest
仅当协程的上下文被取消时才会取消
假设磁盘上有一个 500x500 的映像,但是只需要以 100x100 的大小将其加载到内存中即可在视图中显示。 Coil
会将图像加载到内存中,但是如果您需要 500x500 的图像会怎样呢? 从磁盘读取还有更好的「质量」,但是图像已经以 100x100 加载到内存中。 理想情况下,当我们从磁盘以 500x500 读取图像时,我们将使用 100x100 图像作为占位符。
这正是 Coil
所做的,并且 Coil
自动为所有 BitmapDrawables
处理此过程。 与 crossfade(true)
搭配使用时,可以创建视觉效果,使图像细节看起来像淡入淡出,类似于渐进式 JPEG
详细内容移步 官方文档
原文:Merge adapters sequentially with MergeAdapter
作者:Florina Muntenescu
译者:Flywith24
MergeAdapter
是 recyclerview 1.2.0-alpha02
中提供的新类,它使您可以顺序组合多个 adapter,以在单个 RecyclerView 中显示。 这使您可以更好地封装 adapter,而不必将许多数据源组合到单个 adapter 中,从而使它们集中并复用。
一个用例是在 Header 或 Footer 中显示列表加载状态:当列表从网络中检索数据时,我们想显示一个进度条。 如果出现错误,我们要显示错误和重试按钮。
MergeAdapter
允许我们按顺序显示多个 adapter 的内容。 例如,假设我们有以下 3 个adapter:
val firstAdapter: FirstAdapter = …
val secondAdapter: SecondAdapter = …
val thirdAdapter: ThirdAdapter = …
val mergeAdapter = MergeAdapter(firstAdapter, secondAdapter,
thirdAdapter)
recyclerView.adapter = mergeAdapter
recyclerView 将顺序显示每个 adapter 中的 item
使用不同的 adapter 可以使您更好地区分列表的每个顺序部分。 例如,如果要显示标题,则无需将与标题显示相关的逻辑放在处理列表显示的同一 adapter 中,而是可以将其封装在其自己的 adapter 中
我们的 Header/Footer 显示进度或提示错误。 列表成功完成加载后,Header/Footer 不应显示任何内容。 因此,可以使用自己的 adapter 将其表示为包含 0 或 1 个项目的列表:
val mergeAdapter = MergeAdapter(headerAdapter, listAdapter, footerAdapter)
recyclerView.adapter = mergeAdapter
如果 Header/Footer 使用相同的布局,ViewHolder
和 UI 逻辑(例如显示进度以及显示方式),则可以仅实现一个 adapter 类并创建 2 个实例:一个用于 Header,一个用于 Footer
对于完整的实现,请查看该 pull request,其中添加:
LoadState
显示 0 或 1 个 item。 每次 LoadState
更改时,我们都会通知您需要更改,插入或删除该 item(请参见代码)默认情况下,每个 adapter 都维护自己的 ViewHolder pool
,并且 adapter 之间不会复用。 如果多个 adapter 显示相同的 ViewHolder
,我们可能要在它们之间复用同一个实例。 我们可以通过使用 MergeAdapter.Config
对象来创建 MergeAdapter
来实现此目的,其中 isolateViewTypes = false
。 这样,所有合并的 adapter 将使用相同的 view pool
。 在加载状态 Header 和 Footer 示例中,两个 ViewHolder
实际上将显示相同的内容,因此我们可以重复使用它们
⚠️ 若要支持不同的ViewHolder
类型,应实现Adapter.getItemViewType
。 当您重复使用ViewHolder
时,请确保相同的 view 类型不会指向不同的ViewHolder
! 一种最佳实践是将布局 id 作为 view 类型返回。
建议不要使用固定的 id 与 notifyDataSetChanged 一起使用,而是建议使用 adapter 的特定通知事件,该事件为 RecyclerView 提供有关数据集更改的更多信息。 这使 RecyclerView 可以更有效地更新 UI 并具有更好的动画。 如果您使用的是 ListAdapter,则在 DiffUtil 回调的帮助下,将在后台为您处理 notify 事件。 但是,如果确实需要使用 固定的 id,则 MergeAdapter.Config
为固定的 id 提供 3 种不同的配置:NO_STABLE_IDS,ISOLATED_STABLE_IDS 和 SHARED_STABLE_IDS。 最后两个要求您处理 adapter 中的 固定的 id。 请查看 StableIdMode 文档以获取有关其工作方式的更多信息
当 MergeAdapter
的 adapter 部分调用通知功能时,MergeAdapter
会在更新RecyclerView 之前计算新 item 的位置。
从 RecyclerView 的角度来看,notifyItemRangeChanged
表示 item 相同,只是内容有所更改。 notifyDataSetChanged
表示之前和之后之间没有任何关系。 因此,我们无法将 notifyDataSetChanged
映射到 notifyItemRangeChanged
中
如果 adapter 调用 Adapter.notifyDataSetChanged
,则 MergeAdapter
还将调用Adapter.notifyDataSetChanged
,而不是 Adapter.notifyItemRangeChanged
。 与 RecyclerView 一样,通常避免调用 Adapter.notifyDataSetChanged()
,而是选择更精细的更新,或者使用自动执行此操作的 adapter 实现,例如 ListAdapter 或 SortedList
您过去可能曾经使用过 ViewHolder.getAdapterPosition
来获取 ViewHolder
在 adapter 中的位置。 现在,因为我们要合并多个 adapter,所以请使用 ViewHolder.getBindingAdapterPosition()
。 如果要获取最后绑定 ViewHolder
的 adapter,则在共享 ViewHolder
的情况下,使用 ViewHolder.getBindingAdapter()
如果要顺序显示不同类型的数据,这些数据将从封装在其自己的 adapter 中受益,请开始使用 MergeAdapter
。 要对 ViewHolder pool
和 固定的 id 进行高级控制,请使用 MergeAdapter.Config
很高兴见到你 👋,我是 Flywith24 。
最近 Android 官方针对 Fragment 文档进行了重新编写,使其适应 2020 年最佳实践的快速发展。
Fragment 的确是一个让开发者头疼的组件,它是一个很好的设计,但一直处于可改进的状态,随着 AndroidX Fragment 的快速更新,Fragment 已不同往日,虽然仍有改进的空间(单个 FragmentManager 不支持多返回栈,Fragment 自身和其 view 的生命周期不一致)。考虑到该文档的确有很多新知识以及官方文档的极慢的汉化速度,本文将 2020 版 Fragment 的官方文档翻译成中文,喜欢一手信息的小伙伴可直奔 官方原文。限于篇幅原因,该文档分上下两部分。
本文将介绍以下内容:
第二部分将介绍:
我是一个「强迫症晚期患者」,为了移动端更好阅读的体验,我经常将代码以图片的形式插入到文内。但随之而来出现一个问题:没办法 copy 代码(这对 cv 开发者很重要的 🤣)。
前些天,我在 github 某个项目的 README
文档中看到一个技巧,便是把较长且有些影响阅读的内容折叠,读者可以自由地选择展开。
这也是这个「彩蛋」的显示方式。后文中关于代码的部分我都会提供图片和可复制的源码两部分,其中后者处于折叠状态。您可以点击 「点击查看代码详情」以展开源码。
彩蛋结束。🥳
一个 Fragment
代表了 开发者 app UI 可重用的部分。Fragment 定义和管理了自己的布局,拥有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独自存在——它们必须有一个 activity 或 fragment 作 宿主。Fragment 的视图树是其宿主视图树的一部分,或者附加到其宿主的视图树上。
🌟 注意:某些 Android Jetpack 库,如 Navigation,
BottomNavigationView
和ViewPager2
是被设计为与 Fragment 配合使用的。
Fragment 允许您将 UI 分成分散的块,从而将 模块化 和 可重用性 引入到您的 activity UI 中。Activity 是放置 app UI 全局元素(如 navigation drawer)的理想场所。相对的,Fragment 更适合于定义和管理整个屏幕或部分屏幕的 UI。
思考设计一个适用各种屏幕尺寸的 app。在较大尺寸的屏幕上,该 app 应该以静态 navigation drawer 和网格 list 的形式展示。在较小尺寸的屏幕上,该 app 应该显示 bootom navigation bar 和线性 list 的形式。管理 activity 中的这些变化可能很麻烦。将导航元素与内容分开可以使此过程更易于管理。之后 activity 负责显示正确的 navigation UI,fragment 负责显示正确布局的列表。
上图展示了同一个界面的两个版本。左侧的大尺寸屏幕包含一个由 activity 控制的 navigation drawer 和 一个由 fragment 控制的网格列表。右侧小尺寸屏幕包含一个由 activity 控制的 bottom navigation bar 和一个由 fragment 控制的线性列表。
将 UI 分离成多个 fragment 有助于更轻松地在运行时修改 activity 的外观。当您的 activity 处于 STARTED
lifecycle state 或更高的状态时,可以添加/替换/移除 fragment。你可以将这些更改记录到一个由 activity 管理的 返回栈 中,以允许恢复之前的状态。
您可以在一个 activity 、多个 activity 甚至是 fragment 中(嵌套场景)使用相同的 fragment 实例。考虑到这一点,您应该在 fragment 中仅提供管理自己 UI 所需的逻辑,避免在 fragment 内部依赖或操作另一个 fragment。
本节介绍如何创建一个 fragment 并将其添加到 activity 中。
Fragment 要求依赖 AndroidX Fragment library。您需要在 project 的 build.gradle
文件中添加 Google Maven repository
buildscript {
...
repositories {
google()
...
}
}
allprojects {
repositories {
google()
...
}
}
欲将 AndroidX Fragment library 添加到您的项目中,请在您的 app 的 build.gradle 文件中添加以下依赖:
dependencies {
def fragment_version = "1.2.5"
// Java 语言使用
implementation "androidx.fragment:fragment:$fragment_version"
// Kotlin 语言使用
implementation "androidx.fragment:fragment-ktx:$fragment_version"
}
创建 fragment,继承 AndroidX Fragment,重写其方法并插入 app 的逻辑与创建 Activity 的方式类似。想要创建定义其自己的布局的最小 fragment,请将您的 fragment 的布局资源提供给主构造器,如以下所示:
// Kotlin
class ExampleFragment : Fragment(R.layout.example_fragment)
// Java
class ExampleFragment extends Fragment {
public ExampleFragment() {
super(R.layout.example_fragment);
}
}
Fragment 库也提供了一些特殊作用的 fragment 类:
展示一个浮动 dialog。使用该类创建一个 dialog 是一种在 Activity 中使用 dialog 的很好替代方法,因为 fragment 会自动处理 dialog 的创建与清除。
将 Preference
对象的层次结构显示为列表。你可以使用 PreferenceFragmentCompat 来为您的 app 创建一个设置界面。
通常,您的 fragment 必须嵌入一个 AndroidX FragmentActivity
中才能作为 activity layout 的部分 UI。FragmentActivity 是 AppCompatActivity
的父类,因此如果您已经继承了 AppCompatActivity
则无需做任何更改。
将 fragment 添加到 activity 的视图树中有两种方式:
无论哪种方式,您都需要添加一个 FragmentContainerView
来定义 fragment 在 activity 视图树中的位置。强烈建议始终使用 FragmentContainerView
作为 fragment 的容器,因为 FragmentContainerView
修复了一些 fragment 的 bug,其它 ViewGroup(如 FrameLayout)并没提供修复。(译者注:之前 fragment 在 Z 轴的顺序有些问题,FragmentContainerView
修复了该问题)
请使用 FragmentContainerView 标签来在 XML 中声明 fragment。下面是包含单个 FragmentContainerView 的 activity 布局文件:
<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.ExampleFragment" />
android:name
属性指定了待实例化的 Fragment 类名。当 activity 的 layout 被 inflated 时,将实例化指定的 fragment,在新的已实例化的 fragment 中调用 onInflate()
,并创建一个 FragmentTransaction
将 fragment 添加到 FragmentManager
上。
🌟 注意:您可以使用 class 来替换 android:name 用来指明待实例化的 fragment
若要以代码的方式将 fragment 添加到 activity 布局中,该布局应该包含一个 FragmentContainerView 用于作为 fragment 的容器,如下面示例:
<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
与使用 XML 的方式不同,此处的 FragmentContainerView 上并未使用 android:name
属性,因此不会自动实例化特定的 fragment 而是在代码中使用 FragmentTransaction
实例化一个 fragment 并将其添加到 activity 的布局中。
当 activity 运行时,您可以使用 fragment 事务 添加/移除/替换 一个 fragment。在 FragmentActivity 中,您可以获取一个 FragmentManager
实例,该实例可用于创建一个 FragmentTransaction
。然后您可以在 activity 的 onCreate()
方法中使用 FragmentTransaction.add()
实例化 fragment,接着传入容器的 ID 和 fragment class,然后提交事务。如下面的示例:
// Kotlin
class ExampleActivity : AppCompatActivity(R.layout.example_activity) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 防止系统资源回收或配置发生变化 fragment 发生重叠的问题
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<ExampleFragment>(R.id.fragment_container_view)
}
}
}
}
// Java
public class ExampleActivity extends AppCompatActivity {
public ExampleActivity() {
super(R.layout.example_activity);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 防止系统资源回收或配置发生变化 fragment 发生重叠的问题
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.setReorderingAllowed(true)
.add(R.id.fragment_container_view, ExampleFragment.class, null)
.commit();
}
}
}
🌟 注意:执行 FragmentTransaction 时,应 始终 使用 setReorderingAllowed(true),更多关于事务重新排序的内容,请移步 Fragment 事务一节。
在上一个示例中,仅当 saveInstanceState 为 null 时才创建 fragment 事务。这是为了确保当 activity 第一次 create 时 fragment 仅被添加一次。当系统资源回收或配置发生变化时 saveInstanceState
不再 为 null 且无需再次添加该 fragment,因为 fragment 会自动从 savedInstanceState
中恢复。
如果 fragment 需要初始化数据,则可以通过在 FragmentTransaction.add() 当调用中提供 Bundle 将参数传递给 fragment,如下所示:
// Kotlin
class ExampleActivity : AppCompatActivity(R.layout.example_activity) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val bundle = bundleOf("some_int" to 0)
supportFragmentManager.commit {
setReorderingAllowed(true)
add<ExampleFragment>(R.id.fragment_container_view, bundle)
}
}
}
}
// Java
public class ExampleActivity extends AppCompatActivity {
public ExampleActivity() {
super(R.layout.example_activity);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
Bundle bundle = new Bundle();
bundle.putInt("some_int", 0);
getSupportFragmentManager().beginTransaction()
.setReorderingAllowed(true)
.add(R.id.fragment_container_view, ExampleFragment.class, bundle)
.commit();
}
}
}
然后,可以通过调用 requireArguments()
从 fragment 中获得 Bundle,并且可以使用相应的 Bundle getter 方法来取出每个参数:
// Kotlin
class ExampleFragment : Fragment(R.layout.example_fragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val someInt = requireArguments().getInt("some_int")
...
}
}
// Java
class ExampleFragment extends Fragment {
public ExampleFragment() {
super(R.layout.example_fragment);
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
int someInt = requireArguments().getInt("some_int");
...
}
}
🌟 注意:我们强烈推荐使用 Navigation library 来管理您 app 的导航。该框架遵循有关处理 fragment,返回栈以及 fragment manager 的最佳实践。想要获取更多关于 Navigation 的信息,参见: Get started with the Navigation component 和 Migrate to the Navigation component。
FragmentManager
是负责对 app 的 fragment 执行操作的类,如添加/移除/替换 fragment 并将这些操作加入到返回栈中。
如果您使用的是 Jetpack Navigation library,则可能永远不会直接与 FragmentManager 进行交互,因为它将 FragmentManager 使用的部分封装了起来。换句话说,任何 app 使用 fragment 都在某种层次上使用 FragmentManager,因此了解它的含义和工作方式非常重要。
本节内容介绍如何访问 FragmentManager,与 activity 和 fragment 相关的 FragmentManager 的角色,使用 FragmentManager 管理返回栈以及为 fragment 提供数据和依赖。
每个 FragmentActivity 及其子类(如 AppCompatActivity)都可以通过 getSupportFragmentManager()
来访问 FragmentManager
Fragment 也能管理一个或多个子 fragment(译者注:嵌套 fragment,即一个 fragment 的直接宿主可能是 activity 或另一个 fragment)。在 fragment 中,您可以通过 getChildFragmentManager()
来获取管理子 fragment 的 FragmentManager 实例。如果需要访问该 fragment 宿主的 FragmentManager,可以使用 getParentFragmentManager()
。
我们来看几个示例,以展示 fragment 及其宿主和每个 fragment 相关联的 FragmentManager 实例之间的关系:
绿色代表宿主 activity
蓝色代表宿主 fragment
白色代表子 fragment
上图显示了两个示例,每个示例都有一个 activity 宿主。在这两个示例中,宿主 activity 均以 BottomNavigationView
的形式向用户显示顶级导航,该视图负责将 host fragment 替换为 app 中的不同界面,每个界面都是独立的 fragment。
Example 1 中的 host fragment 管理着两个子 fragment,这两个 fragment 构成了一个左右分离的两个界面。
Example 2 中的 host fragment 管理着一个单独的 子 fragment,它构成了滑动视图显示的 fragment。
基于上述设定,您可以认为每个宿主都关联着一个 FragmentManager 来管理其子 fragment。下图对此进行了说明:
使用合适的 FragmentManager 取决于调用者所在 fragment 层次结构中的位置以及您想要访问的 fragment manager。
一旦有了 FragmentManager 的引用,便可以使用它来操作显示给用户的 fragment。
一般而言,您的 app 应由一个或少量 activity 构成,每个 activity 代表一组相关的界面。Activity 可能提供 顶级导航,ViewModel 和 其它 fragment 间的 view-state。app 中每个独立的 目的地(destination)应该由一个 fragment 表示。
如果想要一次显示多个 fragment(如在一个拆分视图或仪表板中),则应使用由 destination fragment 及其 childFragmentManager 管理的 子 fragment。
其它使用子 fragment 的场景可能是:
NavHostFragment
并且使用不同的子 destination fragment 填充其 parent 的位置。FragmentManager 管理着 fragment 的返回栈。在运行时,FragmentManager 可以执行返回栈操作(例如响应用户操作而添加/移除 fragment)。每组更改作为一个被称为 FragmentTransaction
的独立单元一起提交。有关事务更深入的讨论,请参见下一节。
但用户按下设备上的返回键时,或者当开发者调用 FragmentManager.popBackStack() 时,最顶部的 fragment 事务将从栈中弹出。换句话说,事务被撤销。如果栈中没有更多的 fragment 事务并且没有使用 子 fragment,那么返回事件将传递给 activity。如果使用了子 fragment,请参阅 子 fragment 和兄弟 fragment 的注意事项 一节。
在事务调用 addToBackStack() 时,请注意,该事务可以包含任意数量的操作,如添加多个 fragment,替换多个容器中的 fragment 等等。当返回栈弹出时,所有的这些操作都将作为单独的原子操作被撤销。如果在 popBackStack() 调用之前已经提交了其它事务,并且未对事务使用 addToBackStack(),则这些操作将不会撤销。因此,在一个 FragmentTransaction 中,请避免将影响返回栈的事务与不影响的事务混合使用。
要在布局容器中显示一个 fragment,请使用 FragmentManager 创建 FragmentTransaction。然后,在事务中,您可以在容器上执行 add()
或 replace()
操作。
一个简单的事务可能如下示例:
// Kotlin
supportFragmentManager.commit {
replace<ExampleFragment>(R.id.fragment_container)
setReorderingAllowed(true)
addToBackStack("name") // name 可以为 null
}
// Java
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, ExampleFragment.class, null)
.setReorderingAllowed(true)
.addToBackStack("name") // name 可以为 nulll
.commit();
在上面的示例中,ExampleFragment
替换了当前由 R.id.fragment_container
ID 标识的布局容器中的 fragment(如果有)。将 fragment class 提供 replace() 方法允许 FragmentManager 使用其 FragmentFactory 处理实例化。有关更多信息,请参考下节。
setReorderingAllowed(true)
优化事务中涉及的 fragment 的状态更改,以便动画和过渡正常工作。有关使用动画和过渡进行导航的更多信息,请参见 Fragment 事务一节和转场动画一节。
调用 addToBackStack()
会将事务提交到返回栈。用户稍后可以撤回事务并通过安返回按钮返回上一个 fragment。如果您在单个事务添加或移除了多个fragment,则弹出返回栈时,所有这些操作都将被撤销。addToBackStack() 提供的可选名称使您能够使用 popBackStack()
弹出该特定事务。
如果在执行移除 fragment 事务时未调用 addToBackStack() ,这提交事务后被移除的 fragment 将被销毁(destroyed),并且用户无法导航回该 fragment。如果在删除 fragment 时调用了 addToBackStack(),则该 fragment 仅处于 STOPPED
状态,稍后当用户向返回时处于 RESUMED
状态。请注意,在这种情况下,其 view 已经 destroyed。(译者注:就是执行了 onDestroyView() 但没执行 onDestroy() 有关更多信息,请参见生命周期一节。
您可以使用 findFragmentById()
获得对布局容器中当前 fragment 的应用。使用 findFragmentById() 从 XML inflate 时或在 FragmentTransaction 中添加时通过给定 ID 来查找 fragment。下面是一个示例:
// 👇 Kotlin
supportFragmentManager.commit {
replace<ExampleFragment>(R.id.fragment_container)
setReorderingAllowed(true)
addToBackStack(null)
}
...
val fragment: ExampleFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment
// 👇 Java
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, ExampleFragment.class, null)
.setReorderingAllowed(true)
.addToBackStack(null)
.commit();
...
ExampleFragment fragment =
(ExampleFragment) fragmentManager.findFragmentById(R.id.fragment_container);
此外,您可以为 fragment 分配唯一的标签,并使用 findFragmentByTag()
获取引用。您可以在布局内定义的 fragment 上使用 android:tag
XML 属性或在 FragmentTransaction 中的 add() 或 replace() 操作时分配标签。
// 👇 Kotlin
supportFragmentManager.commit {
replace<ExampleFragment>(R.id.fragment_container, "tag")
setReorderingAllowed(true)
addToBackStack(null)
}
...
val fragment: ExampleFragment =
supportFragmentManager.findFragmentByTag("tag") as ExampleFragment
// 👇 Java
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, ExampleFragment.class, null, "tag")
.setReorderingAllowed(true)
.addToBackStack(null)
.commit();
...
ExampleFragment fragment = (ExampleFragment) fragmentManager.findFragmentByTag("tag");
在任何给定时间,仅允许一个 FragmentManager 来控制 fragment 的返回栈。如果您的 app 同时在屏幕上显示多个同级 fragment,或者您的 app 使用了子 fragment,则必须指定一个 FragmentManager 来处理 app 的主导航。
在 fragment 事务中定义主导航,请在事务上调用 setPrimaryNavigationFragment()
方法并传入拥有 childFragmentManager 主控制权的 fragment 实例。
将导航结构看作一系列层,activity 作为最外层,将子 fragment 的每一层包裹在下面。每一层都必须有一个主导航 fragment。但发生返回事件时,最内层控制导航行为,一旦最内层不再有要从返回栈中弹出的事务,控制权就会回到下一层,然后重复此过程,直到事件到达 activity 。
请注意,当同时显示两个或多个 fragment 时,其中只有一个可以是主导航 fragment。将 fragment 设置为主导航会删除上一个 fragment designation。使用上面的例子,如果设置 detail fragment 作为主导航 fragment,那么main fragment 的 designation 会被移除。
添加 fragment 时,可以手动实例化 fragment 并将其添加到 FragmentTransaction 中。
// 👇 Kotlin
fragmentManager.commit {
// 在 add 前实例化
val myFragment = ExampleFragment()
add(R.id.fragment_view_container, myFragment)
setReorderingAllowed(true)
}
// 👇 Java
// 在 add 前实例化
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
.add(R.id.fragment_view_container, myFragment)
.setReorderingAllowed(true)
.commit();
提交 fragment 事务时,创建的 fragment 实例就是所使用的实例。但是在系统资源回收或配置发生变化时,您的 activity 及其所有 fragment 都会被销毁,然后使用最适合的 Android 资源进行重新创建。FragmentManager 为您处理所有的这一切。它重新创建 fragment 的实例,然后将其 attach 到宿主并重新创建返回栈。
默认情况下,FragmentManager 使用框架提供的 FragmentFactory 实例化 fragment 的新实例。此默认工厂使用反射为 fragment 查找和调用无参构造器。这意味着您不能使用此默认工厂来提供对 fragment 的依赖性。这也意味着默认情况下,在重新创建过程中不会使用您用于首次创建 fragment 的任何自定义构造器。
若要提供对 fragment 的依赖关系或使用任何自定义构造器,必须创建 FragmentFactory 子类,然后重写 [FragmentFactory.instantiate
](https://developer.android.com/reference/androidx/fragment/app/FragmentFactory#instantiate(java.lang.ClassLoader, java.lang.String))。接着您可以使用自定义工厂重写 FragmentManager 的默认工厂,随后将其用于实例化 fragment。
假设您有一个 DessertsFragment,负责在您的家乡展示受欢迎的甜点。假设 DessertsFragment 依赖于 DessertsRepository 类,该类为其提供向用户显示正确的 UI 所需的信息。
您可能定义在 DessertsFragment 的构造器中请求 DessertsRepository 实例:
// 👇 Kotlin
class DessertsFragment(val dessertsRepository: DessertsRepository) : Fragment() {
...
}
// 👇 Java
public class DessertsFragment extends Fragment {
private DessertsRepository dessertsRepository;
public DessertsFragment(DessertsRepository dessertsRepository) {
super();
this.dessertsRepository = dessertsRepository;
}
...
}
FragmentFactory 的简单实现可能类似如下内容:
// 👇 Kotlin
class MyFragmentFactory(val repository: DessertsRepository) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
when (loadFragmentClass(classLoader, className)) {
//👇 此处更改了默认实现
DessertsFragment::class.java -> DessertsFragment(repository)
else -> super.instantiate(classLoader, className)
}
}
// 👇 Java
public class MyFragmentFactory extends FragmentFactory {
private DessertsRepository repository;
public MyFragmentFactory(DessertsRepository repository) {
super();
this.repository = repository;
}
@NonNull
@Override
public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
Class<? extends Fragment> fragmentClass = loadFragmentClass(classLoader, className);
if (fragmentClass == DessertsFragment.class) {
//👇 此处更改了默认实现
return new DessertsFragment(repository);
} else {
return super.instantiate(classLoader, className);
}
}
}
上面的示例创建了 FragmentFactory 的子类 MyFragmentFactory
。该类重写了 instantiate()
方法以为 DessertsFragment 提供自定义 fragment 创建逻辑,其它 fragment 由 FragmentFactory 的默认行为通过 super.instantiat()
处理。
然后,可以通过在 FragmentManager 上设置属性,将 MyFragmentFactory
指定为构造 app fragment 时要使用的工厂。您必须先在 activity 的 super.onCreate() 之前设置该属性,以确保在重新创建片段时使用 MyFragmentFactory
。
// 👇 Kotlin
class MealActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = MyFragmentFactory(DessertsRepository.getInstance())
super.onCreate(savedInstanceState)
}
}
// 👇 Java
public class MealActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
DessertsRepository repository = DessertsRepository.getInstance();
getSupportFragmentManager().setFragmentFactory(new MyFragmentFactory(repository));
super.onCreate(savedInstanceState);
}
}
请注意,在 activity 设置 FragmentFactory 会重写整个 activity 的 fragment 层次结构中的 fragment 创建。换句话说,您添加的任何子 fragment 的 childFragmentManager 均使用此处设置的自定义 fragment 工厂,除非在较低的级别被重写。
在单一 activity 体系结构中,应该使用 FragmentScenario
隔离地测试 fragment。 由于您不能依赖于 activity 的自定义 onCreate 逻辑,因此可以将 FragmentFactory 作为参数传递给 fragment 测试,如下:
// 测试内部
val dessertRepository = mock(DessertsRepository::class.java)
launchFragment<DessertsFragment>(factory = MyFragmentFactory(dessertRepository)).onFragment {
// 测试 Fragment 逻辑
}
有关测试过程的详细信息以及完整示例,请参考测试一节。
在运行时,FragmentManager
可以添加/移除/替换 fragment 并执行其它操作以响应用户的交互。开发者提交的每组对 fragment 的更改被称为「事务」。您可以使用 FragmentTransaction
提供对 API 指定在事务内执行的操作,您可以将多个操作组织到一个事务中。例如,一个事务可以添加或替换多个 fragment。当您在同一个屏幕上显示多个同级 fragment 时,这十分有用。
您可以将每个事务保存到 FragmentManager
管理的返回栈中,从而允许用户在返回上一次 fragment 的状态,类似于从一个 activity 可以返回到之前的 activity。
您可以通过调用 FragmentManager
的 beginTransaction()
方法获得一个 FragmentTransaction
实例,如下面的例子:
// 👇 Kotlin
val fragmentManager = ...
val fragmentTransaction = fragmentManager.beginTransaction()
// 👇 Java
FragmentManager fragmentManager = ...
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
每个 FragmentTransaction
最后都必须提交事务。commit() 方法向 FragmentManager 发出信号,表示所有操作均已添加到事务中。
// 👇 Kotlin
val fragmentManager = ...
// 这是由 fragment-ktx 提供的扩展函数
// 自动开启并提交事务
fragmentManager.commit {
// 在这加入操作
}
// 👇 Java
FragmentManager fragmentManager = ...
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
// 在这加入操作
fragmentTransaction.commit();
每个 FragmentTransaction
应该使用 setReorderingAllowed(true)
:
// 👇 Kotlin
supportFragmentManager.commit {
...
setReorderingAllowed(true)
}
// 👇 Java
FragmentManager fragmentManager = ...
fragmentManager.beginTransaction()
...
.setReorderingAllowed(true)
.commit();
为了兼容,默认不开启重排序。但是如果需要允许 FragmentManager 在返回栈上运行并运行动画和过渡时能够正确执行 FragmentTransaction,启用重排序可确保一起执行多个事务时,任何中间 fragment(如添加并立即替换的中间 fragment)都不会经历生命周期的更改或执行其它动画或过渡。请注意,重排序影响了事务的开启和事务的撤销。
要将 fragment 添加到 FragmentManager,请在事务上调用 add() 方法。该方法接收 fragment 容器的 ID 和待添加 fragment 的类名。添加的 fragment 将移至 RESUMED
状态。强烈建议容器使用 FragmentContainerView。
要从宿主中移除 fragment,请调用 remove() 方法并传入一个 fragment 实例。该 fragment 实例是通过 findFragmentById()
或 findFragmentByTag
从 fragment manager 中检索到的。如果 fragment 已经添加到容器中,则其视图也将从容器中移除。被移除的 fragment 将移至 DESTROYED
状态。
使用 replace() 将新的 fragment 实例替换容器中现有的 fragment。调用 replace() 等效于对容器中的一个 fragment 调用 remove() 并将一个新的 fragment 添加到同一容器中。
以下代码显示了如果使用一个 fragment 替换另一个 fragment:
// 👇 Kotlin
val fragmentManager = // ...
fragmentManager.commit {
setReorderingAllowed(true)
replace<ExampleFragment>(R.id.fragment_container)
}
// 👇 Java
FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);
transaction.replace(R.id.fragment_container, ExampleFragment.class, null);
transaction.commit();
在上面示例中,ExampleFragment 的新实例将替换当前由 R.id.fragment_container
标识的布局容器的 fragment(如果有)。
🌟 注意:强烈推荐操作 fragment 时使用 Class 而不是 fragment 实例以确保 fragment 通过 saved state 恢复时使用相同的机制。有关更多详细信息,请参见 Fragment manager 一节。
默认情况下,在 FragmentTransaction 中所做的更改不会添加的返回栈中,要保存这些更改,可以在 FragmentTransaction 上调用 addToBackStack() 方法。有关更多信息,请参考 Fragment manager 一节。
调用 commit()
不会立即执行事务,该事务会被安排为能够在主线程上尽快运行。如果需要可以调用 commitNow()
立即在主线程上运行 fragment 事务。
请注意,commitNow
于 addToBackStack
不兼容。不过您可以通过调用 executePendingTransactions()
执行已经调用 commit() 方法但没运行的事务,该方法与addToBackStack
兼容。
对于绝大多数场景,使用 commit()
即可。
在 FragmentTransaction
中执行操作的顺序很重要,尤其是在使用 setCustomAnimations() 方法时。 此方法将给定的动画作用给稍后所有的 fragment 操作。
// 👇 Kotlin
supportFragmentManager.commit {
setCustomAnimations(enter1, exit1, popEnter1, popExit1)
add<ExampleFragment>(R.id.container) // 第一个动画
setCustomAnimations(enter2, exit2, popEnter2, popExit2)
add<ExampleFragment>(R.id.container) // 第二个动画
}
// 👇 Java
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(enter1, exit1, popEnter1, popExit1)
.add(R.id.container, ExampleFragment.class, null) // 第一个动画
.setCustomAnimations(enter2, exit2, popEnter2, popExit2)
.add(R.id.container, ExampleFragment.class, null) // 第二个动画
.commit()
FragmentTransaction 可以影响在事务范围内添加的各个 fragment 的生命周期状态。创建 FragmentTransaction 时,[setMaxLifecycle()
](https://developer.android.com/reference/androidx/fragment/app/FragmentTransaction#setMaxLifecycle(androidx.fragment.app.Fragment, androidx.lifecycle.Lifecycle.State)) 方法可以为给定 fragment 设置最大状态。例如, ViewPager2
使用 setMaxLifecycle() 方法限制屏幕外的 fragment 为 STARTED
状态。
使用 FragmentTransaction
的 show() 和 hide() 方法来显示和隐藏已添加到容器的 fragment 的 view。这些方法设置 fragment view 的可见性而不影响 fragment 的生命周期。
尽管您不需要使用 fragment 事务来切换 fragment view 的可见性,但是这些方法对于改变返回栈上事务的可见性的场景很有用。
FragmentTransaction
的 detach()
方法将 fragment 与 UI 分离,销毁 fragment 的 view。该 fragment 保持与返回栈中相同的状态(STOPPED
)。这意味着 fragment 已从 UI 中删除,但仍由 fragment manager 管理。
attach()
方法重新连接之前分离的 fragment。这将导致其视图树重新创建,添加到 UI 上并显示。
由于将 FragmentTransaction
视为单个原子操作集,因此在同一事务中对分离和连接到同一 fragment 实例的调用彼此间进行了抵消,从而避免了 fragment UI 的销毁和立即重建。如果要分离然后立即重新连接 fragment,请使用单独的事务,如果使用了 commit()
提交事务,则使用 executePendingOperations()
方法进行分离。
🌟 注意:
attach()
和detach()
方法与 Fragment 的onAttach()
和onDetach()
方法无关。有关这些 Fragment 方法的更多信息,参考生命周期一节。
Fragment API 提供了两种在连接 fragment 导航的切换效果。一个是 Animation 框架,包括 Animation
和 Animator
。另一个是 Transition 框架,包含共享元素转换。
🌟 注意:在本节中,我们使用 animation 来描述 Animation 框架中的效果,使用 transition 来描述 Transition 框架的效果。这两个框架是互斥的,不应同时使用。
您可以将自定义效果看成进入和退出 fragment 以及 fragment 共享元素的过渡效果。
首先,您需要为进入和退出效果创建动画,这些动画将在导航到新 fragment 时运行。 您可以将动画定义为补间动画资源。 这些资源使您可以定义动画期间 fragment 应如何旋转,拉伸,淡入淡出和移动。 例如,您可能希望当前 fragment 淡出,而新 fragment 从屏幕的右边缘滑入,如下图:
这些动画可以在 res/anim
目录中定义:
<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1"
android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="100%"
android:toXDelta="0%" />
🌟 注意:强烈建议对涉及多种动画类型的效果使用 transition ,因为使用嵌套
AnimationSet
存在已知问题。
您还可以为弹出返回栈时运行的进入和退出效果指定动画。 这些被称为 popEnter
和 popExit
动画。 例如,当用户跳回到上一个屏幕时,您可能希望当前 fragment 滑出屏幕的右边缘,而前一个 fragment 淡入:
这些动画可以这样定义:
<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromXDelta="0%"
android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0"
android:toAlpha="1" />
定义动画后,可以通过调用 [FragmentTransaction.setCustomAnimations()
](https://developer.android.com/reference/androidx/fragment/app/FragmentTransaction#setCustomAnimations(int, int)) 来使用它们并通过其资源 id 传递动画资源,如下示例:
// 👇 Kotlin
val fragment = FragmentB()
supportFragmentManager.commit {
setCustomAnimations(
enter = R.anim.slide_in,
exit = R.anim.fade_out,
popEnter = R.anim.fade_in,
popExit = R.anim.slide_out
)
replace(R.id.fragment_container, fragment)
addToBackStack(null)
}
// 👇 Java
Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(
R.anim.slide_in, // enter
R.anim.fade_out, // exit
R.anim.fade_in, // popEnter
R.anim.slide_out // popExit
)
.replace(R.id.fragment_container, fragment)
.addToBackStack(null)
.commit();
🌟 注意:
FragmentTransaction.setCustomAnimations()
将自定义动画应用于FragmentTransaction
中所有未来的 fragment 操作。事务中之前的操作不受影响。
您也可以使用 transition 来定义进入和退出效果。 可以在 XML 资源文件中定义这些 transition。例如,您可能希望当前 fragment 淡出,而新 fragment 从屏幕的右边缘滑入。 这些 transition 可以定义如下:
<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_shortAnimTime"
android:slideEdge="right" />
定义 transition 后,通过在进入 fragment 调用 setEnterTransition()
以及在退出 fragment 调用 setExitTransition()
来应用 transition 并通过其资源 id 传递资源,如下所示:
// 👇 Kotlin
class FragmentA : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
}
}
class FragmentB : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
}
}
// 👇 Java
public class FragmentA extends Fragment {
@Override
public View onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TransitionInflater inflater = TransitionInflater.from(requireContext());
setExitTransition(inflater.inflateTransition(R.transition.fade));
}
}
public class FragmentB extends Fragment {
@Override
public View onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TransitionInflater inflater = TransitionInflater.from(requireContext());
setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
}
}
Fragment 支持 AndroidX transitions。尽管 Fragment 也支持 framework transitions,但我们强烈建议使用 AndroidX transitions,因为它支持 API 14 及以上版本并包含了一些错误修复。
作为 transition 框架的一部分,共享元素过渡决定了 fragment 切换期间相应视图如何在两个 fragment 之间移动。 例如,您可能希望一旦 B 变得可见,在 fragment A 的 ImageView 中显示的图像就过渡到 fragment B。如下图:
这是使用共享元素进行 fragment 切换的步骤:
首先,必须为每个共享元素视图分配唯一的 transition 名称以允许将 view 从一个 fragment 映射到下一个 fragment。使用 [ViewCompat.setTransitionName()
](https://developer.android.com/reference/androidx/core/view/ViewCompat#setTransitionName(android.view.View, java.lang.String)) 在每个 fragment 布局中的共享元素上设置 transition 名称,该方法可兼容 API 14 及以上的版本。例如,fragment A 和 fragment B 中的 ImageView 的 transition 可以这样分配:
// 👇 Kotlin
class FragmentA : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
val itemImageView = view.findViewById<ImageView>(R.id.item_image)
ViewCompat.setTransitionName(itemImageView, “item_image”)
}
}
class FragmentB : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
ViewCompat.setTransitionName(heroImageView, “hero_image”)
}
}
// 👇 Java
public class FragmentA extends Fragment {
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
...
ImageView itemImageView = view.findViewById(R.id.item_image);
ViewCompat.setTransitionName(itemImageView, “item_image”);
}
}
public class FragmentB extends Fragment {
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
...
ImageView heroImageView = view.findViewById(R.id.hero_image);
ViewCompat.setTransitionName(heroImageView, “hero_image”);
}
}
🌟 注意:对于minSdkVersion API 21 或更高版本的 app,可以选择使用 XML 布局内的
android:transitionName
属性将 transition 名称分配给特定视图。
要将共享元素加入到 fragment 切换中,您的 FragmentTransaction 必须知道每个共享元素的视图如何从一个 fragment 映射到下一个 fragment。通过调用 FragmentTransaction.addSharedElement() 将每个共享元素添加到 FragmentTransaction,在下一个 fragment 中传入视图和相应视图的 transition 名称,如下:
// 👇 Kotlin
val fragment = FragmentB()
supportFragmentManager.commit {
setCustomAnimations(...)
addSharedElement(itemImageView, “hero_image”)
replace(R.id.fragment_container, fragment)
addToBackStack(null)
}
// 👇 Java
Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(...)
.addSharedElement(itemImageView, “hero_image”)
.replace(R.id.fragment_container, fragment)
.addToBackStack(null)
.commit();
要指定共享元素如何从一个 fragment 过渡到下一个 fragment,必须在要导航到的 fragment 上设置 enter transition。 调用 fragment 的 onCreate()
方法中的 Fragment.setSharedElementEnterTransition()
,如下:
// 👇 Kotlin
class FragmentB : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = TransitionInflater.from(requireContext())
.inflateTransition(R.transition.shared_image)
}
}
// 👇 Java
public class FragmentB extends Fragment {
@Override
public View onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Transition transition = TransitionInflater.from(requireContext())
.inflateTransition(R.transition.shared_image);
setSharedElementEnterTransition(transition);
}
}
shared_image
transition 定义如下:
<!-- res/transition/shared_image.xml -->
<transitionSet>
<changeImageTransform />
</transitionSet>
所有 Transition 的子类都支持共享元素的过渡。如果你想要创建一个自定义的 Transition,参见 创建自定义转场动画。上一个示例中使用的 changeImageTransform
是一个定义好的转换。 您可以在 Transition
的 API 文档中找到其它 Transition
的子类。
默认情况下,共享元素的 enter transition 也用作共享元素的 return transition。 return transition 确定了 fragment 事务从返回栈中弹出时共享元素如何转换回上一个 fragment。 如果要指定其它返回转换,则可以在 fragment 的 onCreate() 方法中使用 Fragment.setSharedElementReturnTransition()
进行指定。
在某些情况下,您可能需要将 fragment 转换延迟一小段时间。 例如,您可能需要等到对进入的 fragment 中的所有视图进行测量和布局后,系统才能准确捕捉其过渡的开始和结束状态。
此外,您的 transition 可能需要延迟直到加载了一些必要的数据。例如在共享元素加了图像之前需要一直等待,否则如果图像在过渡期间或过渡之后完成加载,过渡可能会受到干扰。
要延迟过渡,必须确保 fragment 事务允许 fragment 状态更改的重排序。请调用 FragmentTransaction.setReorderingAllowed()
,如下:
要延迟转换,在进入 fragment 的 onViewCreated()
方法中调用 Fragment.postponeEnterTransition()
。
加载数据并准备开始转换后,请调用 Fragment.startPostponedEnterTransition()
方法。 以下示例使用 Glide 库将图像加载到共享的 ImageView 中,将相应的 transition 推迟到图像加载完成。
在处理网速慢的情况时,您可能需要延迟一定时间后才开始过渡,而不是等待所有数据加载完毕再开始。 在这种场景下,您可以改为在进入 fragment 的 onViewCreated()
方法中调用 [Fragment.postponeEnterTransition(long, TimeUnit)
](https://developer.android.com/reference/androidx/fragment/app/Fragment#postponeEnterTransition(long, java.util.concurrent.TimeUnit)) 并传入持续时间和时间单位。 经过指定的时间后,过渡将自动开始。
在测量并布局了进入 fragment 中的所有视图之前,不应开始延迟的 enter
transition。 使用 RecyclerView 时,必须等待任何数据加载并准备好绘制 RecyclerView item 再开始 transition。 这是一个例子:
// 👇 Kotlin
class FragmentA : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
postponeEnterTransition()
// 等待数据加载
viewModel.data.observe(viewLifecycleOwner) {
// 在 RecyclerView adapter 上设置数据
adapter.setData(it)
// 所有 view 测量和布局完毕后开始 transition
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
}
}
// 👇 Java
public class FragmentA extends Fragment {
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
postponeEnterTransition();
final ViewGroup parentView = (ViewGroup) view.getParent();
// 等待数据加载
viewModel.getData()
.observe(getViewLifecycleOwner(), new Observer<List<String>>() {
@Override
public void onChanged(List<String> list) {
// 在 RecyclerView adapter 上设置数据
adapter.setData(it);
// 所有 view 测量和布局完毕后开始 transition
parentView.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
parentView.getViewTreeObserver()
.removeOnPreDrawListener(this);
startPostponedEnterTransition();
return true;
});
}
});
}
}
请注意,在 fragment 视图的父级上设置了 ViewTreeObserver.OnPreDrawListener
。 这是为了确保在开始延迟 enter transition 之前,已测量并布置了所有 fragment 的视图并准备好绘制。
🌟 注意:从使用 RecyclerView 的 fragment 使用共享元素过渡到另一个 fragment 时,您仍然 必须 使用 RecyclerView 延迟该 fragment,以确保返回的共享元素过渡在弹出到 RecyclerView 时能够正常运行。
将共享元素转换与 RecyclerView 一起使用时要考虑的另一点是,由于任意数量的 item 共享该布局,因此无法在 RecyclerView item 的 XML 布局中设置 transition 名称。 必须分配唯一的 transition 名称,以便过渡动画使用正确的视图。
通过绑定 ViewHolder,可以为每个 item 的共享元素赋予唯一的 transition 名称。 例如,如果每个 item 的数据都包含唯一的 ID,则可以将其用作 transition 名称,如下示例:
// 👇 Kotlin
class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val image = itemView.findViewById<ImageView>(R.id.item_image)
fun bind(id: String) {
ViewCompat.setTransitionName(image, id)
...
}
}
// 👇 Java
public class ExampleViewHolder extends RecyclerView.ViewHolder {
private final ImageView image;
ExampleViewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.item_image);
}
public void bind(String id) {
ViewCompat.setTransitionName(image, id);
...
}
}
要了解有关片段过渡的更多信息,请参见以下其他资源。
每个 Fragment 实例都有其自己的生命周期。 当用户导航并与您的 app 交互时(如添加,移除以及进入或退出屏幕),您的 fragment 会在其生命周期中的各种状态之间转换。
为了管理生命周期,Fragment 实现了 LifecycleOwner
,公开了可以通过 getLifecycle()
方法获取 Lifecycle
对象。
每个可能的生命周期状态都在 Lifecycle.State
枚举中表示:
通过在 Lifecycle
之上构建 Fragment,您可以使用 可感知生命周期组件 处理生命周期。例如,您可以使用生命周期感知组件在屏幕上显示设备的位置。 当 fragment 变为活跃状态时,此组件可以自动开始监听,而当 fragment 变为非活跃状态时,该组件可以停止。
作为使用 LifecycleObserver
的替代方法,Fragment 类包括与 fragment 生命周期中每个更改相对应的回调方法。这些方法是:onCreate()
, onStart()
, onResume()
, onPause()
, onStop()
和 onDestroy()
。
fragment 的 view 具有单独的生命周期,该生命周期独立于 fragment 的生命周期进行管理(译者注:官方正在考虑将二者合二为一)。fragment 为 view 维护着一个 LifecycleOwner,它可以通过 getViewLifecycleOwner() 或 getViewLifecycleOwnerLiveData() 对其进行访问。有权访问视图的生命周期对于仅在 fragment view 存在时执行工作的生命周期感知组件十分有用,例如观察 LiveData 仅在屏幕显示时执行工作。
本节详细讨论的 fragment 的生命周期,解释了确定 fragment 生命周期状态的一些规则,并展示了生命周期状态与 fragment 生命周期回调之间的关系。
当 fragment 被实例化时,它以 INITIALIZED
状态开始。 为了使 fragment 在其整个生命周期中切换,必须将其添加到 FragmentManager。 FragmentManager 负责确定其 fragment 应处于什么状态,然后将其移入该状态。
在 fragment 生命周期之外,FragmentManager 还负责将 fragment attach 到其 宿主 activity,并在不再使用该 fragment 时将其 detach。 Fragment 类具有两个回调方法 onAttach()
和 onDetach()
,当这些事件中的任何一个发生时,您都可以重写它们以执行自己的逻辑。
将 fragment 添加到 FragmentManager 并将其 attach 到其宿主 activity 时,将调用 onAttach()
回调,fragment 处于活跃状态,并且 FragmentManager 正在管理其生命周期状态。 此时,FragmentManager 方法(如 findFragmentById()
)将返回此 fragment。
onAttach()
方法永远在任何生命周期状态改变前调用。
当 fragment 已从 FragmentManager 中移除并与其宿主 activity detach 时,将调用 onDetach()
回调。 该 fragment 不再活跃,无法再使用 findFragmentById()
进行检索。
onDetach()
方法永远在任何生命周期状态改变后调用。
请注意:这些回调与 FragmentTransaction 的 attach() 和 detach() 方法无关。有关这些方法的更多信息,请参见 Fragment 事务一节。
⚠️ 警告:避免从 FragmentManager 中移除 Fragment 实例后重用它们。 当 fragment 处理自己的内部状态清除时,您可能会无意间将自己的状态转移到重用实例中。
在确定 fragment 的生命周期状态时,FragmentManager 考虑以下因素:
⚠️ 警告:避免在 XML 中使用<fragment>
标签添加 fragment。因为<fragment>
标签允许 fragment 移出其 FragmentManager 的状态。 请始终使用FragmentContainerView
通过 XML 添加 fragment。
上图显示了每个 fragment 的生命周期状态,以及它们与 fragment 的生命周期回调和 fragment 的视图生命周期之间的关系。
随着 fragment 在其生命周期中的切换,它会在其状态之间上下移动。 例如,添加到返回栈的顶部的 fragment 从 CREATED
向上移动到 STARTED
再到 RESUMED
。 相反,当一个 fragment 从返回栈中弹出时,它将在这些状态中向下移动,从 RESUMED
到 STARTED
再到 CREATED
,最后到 DESTROYED
。
当向上移动其生命周期状态时,fragment 首先为其新状态调用关联的生命周期回调。 回调完成后,相关的 Lifecycle.Event 发出给观察者,然后由 fragment 的 view Lifecycle(如果已实例化)跟随。
当您的 fragment 达到 CREATED
状态时,已将其添加到 FragmentManager 中并且已经调用了 onAttach()
方法。
这将是通过 fragment 的 SavedStateRegistry
恢复与 fragment 本身关联的所有保存状态的合适位置。 请注意,此时尚未创建 fragment 的 view,并且只有在创建 view 之后,才应还原与 fragment 的 view 关联的任何状态。
这个过程将 调用 onCreate()
回调。 回调还将接收一个 saveInstanceStateState
的 Bundle
参数,其中包含先前由 onSaveInstanceState() 保存的状态。 请注意,第一次创建该 fragment 时,savedInstanceState
的值为 null,但 对于之后的重新创建,即使未重写 onSaveInstanceState
,它也始终为非 null。 有关更多详细信息,请参见使状态保存一节。
仅当您的 fragment 提供有效的 View 实例时,才创建 fragment 的 view 生命周期。 在大多数情况下,您可以使用带有 @LayoutId
的 fragment 构造器,该构造器会在适当的时间自动 inflate view。 您还可以重写 onCreateView()
以编程方式 inflate 或创建 fragment 的 view。
当且仅当使用非 null view 实例化 fragment 的视图时,该视图才设置在 fragment 上,并且可以使用 getView()
进行检索。 然后,使用与 fragment 视图相对应的新的 INITIALIZED
LifecycleOwner
更新 getViewLifecycleOwnerLiveData()
。 此时也会 调用 onViewCreated()
生命周期回调。
这里是设置视图初始状态位置,开始观察其回调更新 fragment 视图的 LiveData 实例以及在 fragment 视图中的任何 RecyclerView
或 ViewPager2
实例上设置 adapter 的适当位置。
创建 fragment 的视图之后,将还原先前的视图状态(如果有),然后将视图的生命周期移至 CREATED
状态。 视图生命周期所有者还向其观察者发出 ON_CREATE
事件。 在这里,您应该还原与 fragment 视图关联的所有其他状态。
此过程还将 调用 onViewStateRestored()
回调。
强烈建议将支持生命周期的组件绑定到 fragment 的 STARTED
状态,因为这种状态可以确保该 fragment 的视图可用(如果已创建),并且可以安全地对该 fragment 的子 FragmentManager
执行 FragmentTransaction
。 如果 fragment 的视图为非 null,则在 fragment 的生命周期移至 STARTED
后立即将 fragment 的视图 Lifecycle
移至 STARTED
。
当 fragment 变为 STARTED
状态时,将 调用 onStart()
回调。
🌟 注意:诸如
ViewPager2
之类的组件将屏幕外 fragment 的最大生命周期设置为STARTED
当 fragment 可见时,所有 Animator
和 Transition
效果均已完成,并且该 fragment 已准备就绪,可以与用户进行交互。 fragment 的生命周期移至 RESUMED
状态,并调用 onResume()
回调。
切换到 RESUMED
状态是指示用户现在可以与您的 fragment 进行交互的状态。 未 RESUMED
的 fragment 不应手动设置 view 的焦点或尝试 处理输入法的可见性。
当 fragment 向下移动到较低的生命周期状态时,相关的 Lifecycle.Event
将通过 fragment 的 view Lifecycle
(如果已实例化)发送给观察者,然后是 fragment 的 Lifecycle
。 发出 fragment 的生命周期事件后,fragment 将调用关联的生命周期回调。
随着用户开始离开 fragment 并且在 fragment 仍可见的时候,fragment 及其 view 的生命周期将移回到 STARTED
状态,并向其观察者发出 ON_PAUSE
事件。 然后,该 fragment 调用其 onPause()
回调。
一旦该 fragment 不再可见,该 fragment 及其 view 的生命周期便进入 CREATED
状态,并向其观察者发出 ON_STOP
事件。 状态转换不仅由父 activity 或 fragment 停止而触发,还由父 activity 或 fragment 保存状态而触发。此行为可确保在保存 fragment 状态之前调用 ON_STOP
事件。 这使 ON_STOP
事件成为在子 FragmentManager
上安全执行 FragmentTransaction
的最后位置。
如上图 所示,onStop()
回调的顺序以及使用 onSaveInstanceState()
保存状态的过程根据 API 级别而有所不同。 对于 API 28 之前的所有版本,在 onStop()
之前调用 onSaveInstanceState()
。 对于 API 级别 28 及以后版本,调用顺序相反。
在所有退出动画和过渡都完成并且 fragment 的 view 已从窗口中 detach 出来之后,fragment 的 view Lifecycle
被移到 DESTROYED
状态,并向其观察者发出 ON_DESTROY
事件。 然后,该 fragment 调用其 onDestroyView()
回调。 此时,fragment 的 view 已到达其生命周期的尽头,并且 getViewLifecycleOwnerLiveData()
返回 null。
此时,应该删除对 fragment view 的所有引用,从而可以垃圾回收 fragment 的 view。
如果移除了 fragment,或者 FragmentManager
被销毁,则 fragment 的生命周期将进入 DESTROYED
状态,并将 ON_DESTROY
事件发送给其观察者。 然后,该 fragment 调用其 onDestroy()
回调。 至此,该 fragment 已达到其生命周期的尽头。
有关 fragment 生命周期的更多信息,请参见以下其他资源。
我是 Flywith24,Android App/Rom 层开发者。目前专注于 Android 体系化文章的写作。
Android Detail 专栏 正在更新中,想要建立系统化知识体系的小伙伴可以去看看哦。我的所有博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉
Android 开发时,我们使用 activity 和 fragment 作为视图控制器, 可能还会使用有一些类可以存储和提供 UI 数据(例如MVP中的
Presenter
)
但是 当配置更改时(如旋转屏幕),activity 会重建,但对于 UI 数据的持有者呢?
如何解决上述问题?ViewModel
本文重点介绍 ViewModel 的职责(what)以及重点功能的实现原理(how),即使您不使用 Jetpack MVVM
架构,也要了解一下 ViewModel
ViewModel 的原理部分要求您了解 activity 的启动流程,这部分内容网上文章很多,本文不再赘述
我先上个 视频 ,这个小姐姐表述的比文字更形象
ViewModel
主要用于存储 UI 数据以及生命周期感知的数据
ViewModel
的生命周期 ,图片来自 官方文档
ViewModel
能够实时进行配置更改。 这意味着即使在手机旋转后销毁并重新创建 activity 之后,您仍然拥有相同的 ViewModel
和相同的数据。 因此:
ViewModel
将由工厂自动创建,您无需自行创建和销毁一个 activity 中的两个或更多 fragment 需要相互通信是很常见的。例如您有一个片段,用户在其中从列表中选择一个 item,另一个片段显示了所选 item 的内容。 传统做法两个 fragment 都需要定义一些接口,并且宿主 activity 必须将两者绑定在一起。 此外,两个 fragment 都必须处理另一个 fragment 尚未创建或不可见的情况。
可以通过使用 ViewModel
对象解决此问题。 这些 fragment 可以使用 activity 范围内共享一个 ViewModel
来处理此通信,如以下示例代码所示:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
model.getSelected().observe(getViewLifecycleOwner(), { item ->
// Update the UI.
});
}
}
由于 两个 fragment 使用的都是 activity 范围的
ViewModel
(ViewModelProvider
构造器传入的 activity ),因此它们获得了相同的 ViewModel 实例,自然其持有的数据也是相同的,这也 保证了数据的一致性
这种方法具有以下优点:
宿主 activity 无需执行任何操作,也无需了解此通信。
除 SharedViewModel
外,fragment 不需要彼此了解。 如果其中一个 fragment 消失了,则另一个继续照常工作。
每个 fragment 都有其自己的生命周期,并且不受另一个 fragment 的生命周期影响。 如果一个 fragment 替换了另一个 fragment,则 UI 可以继续正常工作而不会出现任何问题。
CursorLoader
这样的 Loader 类经常用于使应用程序 UI 中的数据与数据库保持同步。您可以使用 ViewModel
和其他一些类来替换 Loader。 使用 ViewModel
可将视图控制器与数据加载操作分开,这意味着您在类之间的强引用较少。
在使用 Loader 的一种常见方法中,应用程序可能会使用 CursorLoader
来观察数据库的内容。 当数据库中的值更改时,加载程序会自动触发数据的重新加载并更新 UI
图片来自 官方文档
ViewModel
与 Room
和 LiveData
一起使用以替换 Loader。 ViewModel
确保数据在设备配置更改后仍然存在。 当数据库发生更改时,Room
会通知 LiveData
,然后 LiveData
会使用修改后的数据更新 UI
图片来自 官方文档
分析源码时我们可以不计较细枝末节,只分析主要的逻辑即可。因此我们来思考几个问题,并从源码中寻找答案
如何做到 activity 重建后 ViewModel
仍然存在?
如何做到 fragment 重建后 ViewModel
仍然存在?
如何控制作用域?(即保证相同作用域获取的 ViewModel
实例相同)
如何避免内存泄漏?
维持我们一贯的风格,我们先来大胆地猜一猜
对于问题1 :activity 有着 saveInstanceState
机制,因此可能通过该机制来处理(事实证明不是)
对于问题2:可能 fragment 通过 宿主 activity 或 父 fragment 的帮助来确保 ViewModel
实例在重建后仍然存在
对于问题3:实现一个类似单例的效果,相同作用域获取的对象是相同的
对于问题4:避免 ViewModel
持有 view 或 context 的引用
首先我们要先了解一下 ViewModel
的结构
ViewModel
:抽象类,主要有 clear 方法,它是 final 级,不可修改,clear 方法中包含 onClear 钩子,开发者可重写 onClear 方法来自定义数据的清空
ViewModelStore
:内部维护一个 HashMap 以管理 ViewModel
ViewModelStoreOwner
:接口,ViewModelStore
的作用域,实现类为 ComponentActivity
和 Fragment
,此外还有 FragmentActivity.HostCallbacks
ViewModelProvider
:用于创建 ViewModel
,其构造方法有两个参数,第一个参数传入 ViewModelStoreOwner
,确定了 ViewModelStore
的作用域,第二个参数为 ViewModelProvider.Factory
,用于初始化 ViewModel
对象,默认为 getDefaultViewModelProviderFactory()
方法获取的 factory
简单来说 ViewModelStoreOwner 持有 ViewModelStore 持有 ViewModel
在 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析 中我们提到了 androidx.core.app.ComponentActivity 的引入并探讨了其作为中间层的作用
我们已经讲过 SavedStateRegistryOwner
和 OnBackPressedDispatcherOwner
这两种角色,而今天我们来聊一下
ViewModelStoreOwner
和 HasDefaultViewModelProviderFactory
。其中前者代表着 ViewModelStore
的作用域,后者来标记 ViewModelStoreOwner
拥有默认的 ViewModelProvider.Factory
那么 ViewModel
的逻辑肯定就在该类了
ComponentActivity
实现了 ViewModelStoreOwner
接口,意味着需要重写 getViewModelStore()
方法,该方法为 ComponentActivity
的 mViewModelStore
变量赋值。activity 重建后 ViewModel 仍然存在,只要保证 activity 重建后 mViewModelStore 变量值不变即可
顺着这个思路,我们来看一下 getViewModelStore()
的实现
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
//核心,在该位置重置 mViewModelStore
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
即 mViewModelStore
的值由 getLastNonConfigurationInstance()
返回的 NonConfigurationInstances
对象中的 viewModelStore
赋值,如果此时还为空才去 new ViewModelStore 对象。因此我们只需找到
getLastNonConfigurationInstance
中的 NonConfigurationInstances
在哪里保存的即可
getLastNonConfigurationInstance
为平台 activity 中的方法,返回 mLastNonConfigurationInstances.activity
public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}
那么我们看一下 mLastNonConfigurationInstances
的赋值位置
//省略其他参数
final void attach(NonConfigurationInstances lastNonConfigurationInstances){
mLastNonConfigurationInstances = lastNonConfigurationInstances;
//...
}
了解过 activity 的启动流程的小伙伴肯定知道,这个 attach 方法是 ActivityThread
中的 performLaunchActivity
调用的
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
//省略其他参数
activity.attach(r.lastNonConfigurationInstances);
r.lastNonConfigurationInstances = null;
//...
}
深入追踪源码我们整理一下调用流程
由于 ActivityThread
中的 ActivityClientRecord
不受 activity 重建的影响,所以 activity 重建时 mLastNonConfigurationInstances
能够得到上一次的值,使得 ViewModelStore
值不变 ,问题1就解决了
对于问题2,有了上面的思路我们可以认定 fragment 重建后其内部的 getViewModelStore()
方法返回的对象是相同的。
// Fragment.java
public ViewModelStore getViewModelStore() {
return mFragmentManager.getViewModelStore(this);
}
可以看到 getViewModelStore()
内部调用的是 mFragmentManager
(普通fragment 对应 activity 中的 FragmentManager
,子 fragment 则对应父 fragment 的 childFragmentManager
)的 getViewModelStore()
方法
// FragmentManager.java
private FragmentManagerViewModel mNonConfig;
ViewModelStore getViewModelStore(@NonNull Fragment f) {
return mNonConfig.getViewModelStore(f);
}
而 FragmentManager 中的 getViewModelStore 使用的是 mNonConfig ,mNonConfig 竟然是个 ViewModel!
// FragmentManagerViewModel.java
private final HashMap<String, FragmentManagerViewModel> mChildNonConfigs = new HashMap<>();
private final HashMap<String, ViewModelStore> mViewModelStores = new HashMap<>();
FragmentManagerViewModel
管理着内部的 ViewModelStore
和 child 的 FragmentManagerViewModel
。因此保证 mNonConfig 值不变即能确保 fragment 中的 getViewModelStore()
不变。那么看看 mNonConfig 赋值的位置
// FragmentManager.java
void attachController(@NonNull FragmentHostCallback<?> host, @NonNull FragmentContainer container, @Nullable final Fragment parent) {
//...
if (parent != null) {
// 嵌套 fragment 的情况,有父 fragment
mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
} else if (host instanceof ViewModelStoreOwner) {
// host 是 FragmentActivity.HostCallbacks
ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
} else {
mNonConfig = new FragmentManagerViewModel(false);
}
}
// FragmentManagerViewModel.java
static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
FACTORY);
return viewModelProvider.get(FragmentManagerViewModel.class);
}
我们先看 fragment 的直接宿主是 activity (即没有嵌套)的情况,mNonConfig 由FragmentManagerViewModel.getInstance(viewModelStore)
赋值,而 getInstance 中使用的是 ViewModelProvider
获取 ViewModel
,根据我们上面的分析,只要保证作用域(viewModelStore)相同,即可获取相同的 ViewModel
实例,因此我们需要看一下 host 的 getViewModelStore 方法。经过一番寻找,host 是 FragmentActivity.HostCallbacks
// FragmentActivity.java 内部类
class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements ViewModelStoreOwner, OnBackPressedDispatcherOwner {
public ViewModelStore getViewModelStore() {
// 宿主 activity 的 getViewModelStore
return FragmentActivity.this.getViewModelStore();
}
}
host 的 getViewModelStore 方法返回的是宿主 activity 的 getViewModelStore()
,而 activity 重建后其内部的 mViewModelStore
是不变的,因此即使 activity 重建,其内部的 FragmentManager 对象变化,但 FragmentManager 内部的 FragmentManagerViewModel 的实例(mNonConfig
)不变,mNonConfig.getViewModelStore 不变,fragment 的 getViewModelStore()
亦不变,fragment 重建后其内部的 ViewModel
仍然存在
对于嵌套 fragment ,mNonConfig 通过 parent.mFragmentManager.getChildNonConfig(parent) 获取
// FragmentManager.java
private FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) {
return mNonConfig.getChildNonConfig(f);
}
上文提到 FragmentManagerViewModel
管理着 mChildNonConfigs Map,因此子 fragment 重置后其内部的 mNonConfig 对象也是相同的
至此问题 2 就解决了
对于问题3,我们知道 ViewModelStoreOwner
代表着作用域,其内部唯一的方法返回 ViewModelStore
对象,也即不同的作用域对应不同的 ViewModelStore
,而 ViewModelStore
内部维护着 ViewModel
的 HashMap ,因此只要保证相同作用域的 ViewModelStore
对象相同就能保证相同作用域获取到相同的 ViewModel
对象,而问题1我们已经解释了重建时如何保证 ViewModelStore
对象不变。
因此问题3也解决了。
对于问题4,由于 ViewModel
的设计,使得 activity/fragment 依赖它,而 ViewModel
不依赖视图控制器。因此只要不让 ViewModel
持有 context 或 view 的引用,就不会造成内存泄漏
简单的总结一下:
activity 重建后 mViewModelStore 通过 ActivityThread 的一系列方法能够保持不变,从而当 activity 重建时 ViewModel 中的数据不受影响
通过宿主 activity 范围内共享的 FragmentManagerViewModel 来存储 fragment 的 ViewModelStore 和子fragment 的 FragmentManagerViewModel ,而 activity 重建后 FragmentManagerViewModel 中的数据不受影响,因此 fragment 内部的 ViewModel 的数据也不受影响
通过同一 ViewModelStoreOwner 获取的 ViewModelStore 相同,从而保证同一作用域通过 ViewModelProvider 获取的ViewModel 对象是相同的
通过单向依赖(视图控制器持有 ViewModel )来解决内存泄漏的问题
ViewModel
和 onSaveInstanceState
的功能有些类似,但它们也有很多差异
从存储位置上来说,ViewModel
是在内存中,因此其读写速度更快,但当进程被系统杀死后,ViewModel
中的数据也不存在了。从数据存储的类型上来看,ViewModel
适合存储相对较重的数据,例如网络请求到的 list 数据,而 onSaveInstanceState
适合存储轻量可序列化的数据
那么我们该如何使用呢?可以使用 viewmodel-savedstate
库,详情参考 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析
大家都知道 activity 有着一套
onSaveInstanceState-onRestoreInstanceState
状态保存机制,旨在「系统资源回收」或「配置发生变化」保存状态,为用户提供更好的体验在 androidx 下,提供了 SavedState 库帮助 activity 和 fragment 处理状态保存和恢复
本文默认您对状态保存机制有一定了解,这部分内容请移步 Saving UI States
此外,关于 android 下的进程管理,推荐 Ian Lake 的 Who lives and who dies? Process priorities on Android
本文介绍了 androidx 下 SavedState
如何帮助 activity 和 fragment 处理状态的保存和恢复,同时介绍 viewmodel-savedstate
库,以及在开发过程中正确使用状态保存的姿势
在分析 SavedState 库之前我们需要简单聊一聊 ComponentActivity
androidx activity 1.0.0 时,ComponentActivity
成为了 FragmentActivity
和 AppCompatActivity
的基类。
俗话说「百因必有果」,带着强烈的好奇心,我查了一下 ComponentActivity 引入的原因。
可以看到 ComponentActivity
继承了 androidx.core.app.ComponentActivity(在fragment库中),并且最初仅实现了LifecycleOwner
接口
我们创建的 activity 的继承关系现在变成了这样:
那么回到最初的问题,为什么要引入 ComponentActivity
?其实看看现在 ComponentActivity
的类结构答案就很清楚了
ComponentActivity
实现了五个接口,代表着其除了 activity 还充当着五种角色。本着职能单一原则,官方通过建立一个中间层将部分功能分别交于专门的类来负责,OnBackPressedDispatcherOwner 就是我们讲 fragment 返回栈(【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇)时提到的结构,而其中的 SavedStateRegistryOwner
则是我们今天要讲的主角 SavedState 中的成员
引入 SavedState
implementation "androidx.savedstate:savedstate:1.0.0"
其实您不需要显示地声明,因为 activity 库内部已经引入了。jetpack 组件依赖关系可参考 【背上Jetpack】Jetpack 主要组件的依赖及传递关系
这是一个很小的库
保存状态的组件,此状态将在以后恢复并使用
public interface SavedStateProvider {
@NonNull
Bundle saveState();
}
管理 SavedStateProvider
列表的组件,此注册表绑定了其所有者的生命周期(即 activity 或 fragment)。每次创建生命周期所有者都会创建一个新的实例
创建注册表的所有者后(例如,在调用 activity 的 onCreate(savedInstanceState)
方法之后),将调用其 performRestore(state)
方法,以恢复系统杀死其所有者之前保存的任何状态。
void performRestore(@NonNull Lifecycle lifecycle, @Nullable Bundle savedState) {
// ...
if (savedState != null) {
mRestoredState = savedState.getBundle(SAVED_COMPONENTS_KEY);
}
// ...
}
每个注册表的 SavedStateProvider
都由用于注册它的唯一密钥标识
private SafeIterableMap<String, SavedStateProvider> mComponents = new SafeIterableMap<>();
public void registerSavedStateProvider(@NonNull String key, @NonNull SavedStateProvider provider) {
SavedStateProvider previous = mComponents.putIfAbsent(key, provider);
if (previous != null) {
throw new IllegalArgumentException("SavedStateProvider with the given key is already registered");
}
}
public void unregisterSavedStateProvider(@NonNull String key) {
mComponents.remove(key);
}
一旦完成注册,就可以通过consumeRestoredStateForKey(key)
来使用特定密钥的还原状态
public Bundle consumeRestoredStateForKey(@NonNull String key) {
if (mRestoredState != null) {
Bundle result = mRestoredState.getBundle(key);
//调用后就会清空,第二次调用返回null
mRestoredState.remove(key);
if (mRestoredState.isEmpty()) {
mRestoredState = null;
}
return result;
}
return null;
}
请注意,此方法检索保存的状态,然后清除其内部引用,这意味着用相同的键调用它两次将在第二次调用中返回 null
一旦注册表恢复了其保存状态,则由提供者决定是否要求其恢复的数据。 如果没有,下次注册表的所有者被系统杀死时,未使用的还原数据将再次保存到保存状态
已注册的 provider 能够在其所有者被系统杀死之前保存状态。 发生这种情况时,将调用其 Bundle saveState()
方法。 对于每个已注册的 SavedStateProvider
,都可以像这样保存状态。
savedState.putBundle(savedStateProviderKey, savedStateProvider.saveState());
performSave(outBundle)
方法的源码如下
void performSave(@NonNull Bundle outBundle) {
Bundle components = new Bundle();
// 1.保存未使用的状态
if (mRestoredState != null) {
components.putAll(mRestoredState);
}
// 2. 通过 SavedStateProvider 保存状态
for (Iterator<Map.Entry<String, SavedStateProvider>> it = mComponents.iteratorWithAdditions(); it.hasNext(); ) {
Map.Entry<String, SavedStateProvider> entry1 = it.next();
components.putBundle(entry1.getKey(), entry1.getValue().saveState());
}
// 3. 将bundle 保存到 outBundle 对象中
outBundle.putBundle(SAVED_COMPONENTS_KEY, components);
}
执行状态保存将所有未使用的状态与注册表提供的状态合并。 此 outBundle 是 activity 的 onSaveInstanceState
中传入的 bundle 。
一个包装 SavedStateRegistry
并允许通过其2个主要方法对其进行控制的组件:performRestore(savedState) 和 performSave(outBundle )
。 这两个方法将内部通过 SavedStateRegistry
中的方法处理 。
public final class SavedStateRegistryController {
private final SavedStateRegistryOwner mOwner;
private final SavedStateRegistry mRegistry;
public void performRestore(@Nullable Bundle savedState) {
// ...
mRegistry.performRestore(lifecycle, savedState);
}
public void performSave(@NonNull Bundle outBundle) {
mRegistry.performSave(outBundle);
}
}
持有 SavedStateRegistry
的组件。 默认情况下,androidx 包中的ComponentActivity
和 Fragment
都实现此接口。
public interface SavedStateRegistryOwner extends LifecycleOwner {
@NonNull
SavedStateRegistry getSavedStateRegistry();
}
这里我们要明确一件事情,activity 保存的状态究竟都有什么?
这部分内容可以参见 官方文档
简单来说,activity 的状态保存分为 view 状态和成员状态
默认情况下,系统使用 Bundle 实例状态来保存有关 activity 布局中每个 View 对象的信息(例如,输入到 EditText 中的文本值或 recyclerview 的滚动位置)。 因此,如果 activity 实例被销毁并重新创建,则布局状态将恢复为之前的状态,而无需您执行任何代码。(注意,需要恢复状态的 view 需要配置 id )
这部分逻辑在 activity 中的 onSaveInstanceState
方法内实现
不同平台
onSaveInstanceState
方法的执行时机稍有不同,android P 之前onSaveInstanceState
执行在onStop
之前,但不限于在onPause
之前或之后。android P 及之后该方法在onStop
后执行
前面我们提到 ComponentActivity
实现了 SavedStateRegistryOwner
,下面我们来看一看 activity 如何利用该库来实现状态的保存与恢复
public class ComponentActivity extends androidx.core.app.ComponentActivity implements SavedStateRegistryOwner {
private final SavedStateRegistryController mSavedStateRegistryController = SavedStateRegistryController.create(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSavedStateRegistryController.performRestore(savedInstanceState);
// ...
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
// ...
//这里先调用父类的 onSaveInstanceState 保存 view 状态
super.onSaveInstanceState(outState);
mSavedStateRegistryController.performSave(outState);
}
@NonNull
@Override
public final SavedStateRegistry getSavedStateRegistry() {
return mSavedStateRegistryController.getSavedStateRegistry();
}
}
其内部持有 SavedStateRegistryController
的实例 mSavedStateRegistryController
,在 activity 的 onCreate
方法中 通过 controller 的 performRestore
方法来查询已保存的状态,在 onSaveInstanceState
中 使用 controller 的 performSave
方法来保存状态
除了 view 状态和成员状态,activity 还负责保存其内部的 fragment 的状态。FragmentActivity
的 onSaveInstanceState
方法有对其内部 fragment 的状态进行保存,并在 onCreate 方法中对已保存的 fragment 进行恢复。这解释了如果操作不当会导致 fragment 重叠的问题
androidx fragment 使用 FragmentStateManager
来处理 fragment 的状态保存
其内部有四个保存相关的方法
saveState
saveBasicState
saveViewState
saveInstanceState
其调用链为 activity 通过 FragmentController
间接 调用 FragmentManager
的 saveAllState
,接着依次调用后面的save 方法
Fragment 的状态保存可分为 view 状态,成员状态,child fragment 状态
关于 view 状态 , FragmentStateManager
提供了 saveViewSate
方法,它的调用有两处:
onDestroyView
生命周期时调用,其位置在 FragmentManager
moveToState 方法内部,这解释了为什么加入返回栈的 replace 操作在返回时 view 状态可以自动恢复关于成员状态,由 activity 中的状态机制处理,即上节内容
关于 child fragment 状态,fragment 的 onCreate
方法会调用 restoreChildFragmentState
来恢复 child fragment 的状态,并在 FragmentStateManager
中的 saveBasicState
方法中 调用 performSaveInstanceState
来保存 child fragment 的状态
2020-01-22,ViewModel-SavedState 1.0.0
正式版发布,02-05 发布了 2.2.0
正式版
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0"
您不需要手动引入该库,因为 fragment 库以及内部引入该库
Jetpack MVVM
下 UI State 通常被 ViewModel
持有并存储,因此该模块出现了,配置该模块后,ViewModel
对象将通过其构造函数接收 SavedStateHandle
对象(键值映射),可让您保存状态并查询已保存的状态。 这些值将在系统终止进程后继续存在,并可以通过同一对象使用。
内部持有已保存状态 key-value 的 map,允许读取和写入状态,这些状态在应用进程被杀死后仍然存在
SavedStateHandle
通过 ViewModel
的构造器传入,下面是其主要的主要的几个方法
SavedStateHandle
还包含 SavedStateProvider
的实例,用于帮助 ViewModel
的 owner 保存状态
一个实现 ViewModelFactory.KeyedFactory
的 ViewModel Factory
,它会创建一个与实例化的请求的 ViewModel 关联的 SavedStateHandle
public abstract class AbstractSavedStateViewModelFactory extends ViewModelProvider.KeyedFactory {
private final SavedStateRegistry mSavedStateRegistry;
// Default state used when the saved state is empty
private final Bundle mDefaultArgs;
@Override
public final <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
// 读取保存的状态
Bundle restoredState = mSavedStateRegistry.consumeRestoredStateForKey(key);
// 创建保存状态的 handle
SavedStateHandle handle = SavedStateHandle.createHandle(restoredState, mDefaultArgs);
// ...
// 创建 viewModel
T viewmodel = create(key, modelClass, handle);
// ...
return viewmodel;
}
}
AbstractSavedStateViewModelFactory
的具体实现
public final class SavedStateViewModelFactory extends AbstractSavedStateVMFactory {
public SavedStateViewModelFactory(@NonNull Application application,
@NonNull SavedStateRegistryOwner owner) {
this(application, owner, null);
}
public SavedStateViewModelFactory(@NonNull Application application, @NonNull SavedStateRegistryOwner owner, @Nullable Bundle defaultArgs) {
mSavedStateRegistry = owner.getSavedStateRegistry();
mLifecycle = owner.getLifecycle();
mDefaultArgs = defaultArgs;
mApplication = application;
mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}
public <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass) {
boolean isAndroidViewModel = AndroidViewModel.class.isAssignableFrom(modelClass);
Constructor<T> constructor;
if (isAndroidViewModel) {
constructor = findMatchingConstructor(modelClass, ANDROID_VIEWMODEL_SIGNATURE);
} else {
constructor = findMatchingConstructor(modelClass, VIEWMODEL_SIGNATURE);
}
// doesn't need SavedStateHandle
if (constructor == null) {
return mFactory.create(modelClass);
}
SavedStateHandleController controller = SavedStateHandleController.create(
mSavedStateRegistry, mLifecycle, key, mDefaultArgs);
T viewmodel;
if (isAndroidViewModel) {
viewmodel = constructor.newInstance(mApplication, controller.getHandle());
} else {
viewmodel = constructor.newInstance(controller.getHandle());
}
viewmodel.setTagIfAbsent(TAG_SAVED_STATE_HANDLE_CONTROLLER, controller);
return viewmodel;
//...
}
}
ViewModelProvider(this).get(MyViewModel::class.java)
在 activity 中创建 ViewModel 实例,传入 this (SavedStateRegistryOwner
)作为参数,该参数可以访问其 SavedStateRegistry
,如果没有传入 factory 会通过 activity 重写的 getDefaultViewModelProviderFactory
方法来获取默认的 factory 。然后 factory 将使用保存的状态, 将其包装在 SavedStateHandle
中,并将其传递给 ViewModel。 ViewModel 可以读取和写入该 handle
当 activity 的 onSaveInstanceState(outState)
方法被调用,其 SavedStateRegistry
的 performSave(outState)
方法将被执行,其内部的所有 SavedStateProvider
的 saveState
方法均被执行,一旦执行完毕,outState
就包含了已保存的状态
当 app 被重启后,activity 和新的 registry 将被创建,activity 的 onCreate(savedInstanceState)
方法会被调用,然后 registry 的 performRestore(savedInstanceState)
将被调用以便恢复之前保存的状态
ViewModel
构造器加入 SavedStateHandle
参数,并将想要保存的数据使用该 handle 保存
class WithSavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
private val key = "key"
fun setValue(value: String) = state.set(key, value)
fun getValue(): LiveData<String> = state.getLiveData(key)
}
无需重写 onSaveInstanceState/onRestoreInstanceState
方法
SavedState 仅适合保存轻量级的数据,重量级操作请考虑持sp,数据库等持久化方案
很多情况下,fragment 的生命周期上限应该低于 FragmentManager/Activity。例如,ViewPager
屏幕外的界面不应被 resumed
理想状态下,可以通过以下 API 实现
supportFragmentManager
.beginTransaction()
.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
.commit()
将最大生命周期设置为 Lifecycle.State.RESUMED
将有效地消除限制(因为这是最高生命周期状态)
这将允许废弃 setUserVisibleHint()
API
该功能应如何实现的?我们沿着 commit log
来理一下官方的思路
将 BackStackRecord
的部分逻辑转移至父类 FragmentTransaction
中
在 FragmentTransaction
中添加 setMaxLifecycle
API
保存 fragment maxState
弃用 setUserVisibleHint
FragmentPagerAdapter
构造器新增参数,使用 setMaxLifecycle()
API 确保 fragment resumed
时对用户可见
弃用 FragmentStatePagerAdapter
原来的单参构造器,推荐使用新的构造
随着 ViewPager2 1.0.0
正式版发布,与 ViewPager
交互的FragmentPagerAdapter
和 FragmentStatePagerAdapter
被弃用了
至此我们捋顺了 setMaxLifecycle
的出现,setUserVisibleHint
的弃用以及与ViewPager
相关的 FragmentPagerAdapter
和 FragmentStatePagerAdapter
的弃用
接下来我们看看 setMaxLifecycle
是如何发挥作用的
首先我们要研究一下 fragment 的状态管理,为了更好的管理 fragment 的状态,官方添加了 FragmentStateManager
类来专门管理 fragment 的状态,职能单一原则哈
接着在该类中添加了计算 fragment 最大生命周期的方法 computeMaxState()
后来该方法改名为 computeExpectedState()
并加入了 moveToExpectedState()
方法
computeExpectedState()
方法会根据 fragment mMaxState
计算 fragment 应该所处的生命周期
而 fragment 的 mMaxState
是通过 FragmentManager
的 setMaxLifecycle()
方法设置的 ,而该方法是 BackStackRecord
执行 OP 时调用的,而 OP 值正是通过 FragmentTransaction
的 setMaxLifecycle()
设置的
至此,我们理清了 setMaxLifecycle()
的内部逻辑
我们可以看到官方为了使 fragment 能够在正确的生命周期上,引入了 setMaxLifecycle()
方法,同时为了更好的管理 fragment 的状态,抽象出了 FragmentStateManager
。更少的代码,更少的职责,fragment 的内部逻辑会越来越清晰
关于如何迁移至 ViewPager2 ,请移步 官方视频
关于新的 API 下懒加载实现,请移步 Androidx 下 Fragment 懒加载的新实现
很高兴见到你!
基于搞一个多人协作项目的想法,我们首先找到一个切入点:提供了 开源项目:Jetpack 从 Java 到 Kotlin 无痛上车指南
而今天,我们的多人协作的想法又向前迈进一步,就是这—— Motion 挑战
Android Studio 4.0 为我们提供了全新的工具:MotionEditor
,得益于它,我们创建动画变得十分简单
在此我们发起一个 Motion 挑战,欢迎小伙伴发挥自己的想象力,将原创作品与我们分享
可以在微信公众号内给我们留言,或者通过邮箱联系我们
公众号:KunMinX
公众号:Flywith24
我先来抛砖引玉
感觉如何?😉
其实该动画的编写十分简单。B 站有位声音好听的小哥哥录制了一份超详细的 教学视频
如果各位小伙伴对组织一次 「使用 Jetpack MVVM 多人协作开发」 有什么好的想法,也欢迎联系我们
按投稿先后顺序排列
原文:Android Styling: Prefer Theme Attributes
作者:Nick Butcher
译者:Flywith24
题图来自 Virginia Poltrack
Android Styling 系列的前几篇文章中,我们研究了主题和样式之间的区别,以及使用主题和常用的主题属性的优势
这使我们创建更少地布局和样式,隔离主题内的变化。在实践中,您应 始终* (此处有星号,后文解释) 通过主题属性来控制颜色
Always refer to colors via theme attributes*
传统做法是这样的
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<View …
android:background="@color/white"/>
事实上,您应该引用主题属性,它允许您通过主题来控制颜色,例如,在暗黑主题下提供不同的颜色
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<View …
android:background="?attr/colorSurface"/>
即使您当前不支持其他主题(什么?没有暗黑主题?),我还是建议您采用这种方法,因为这样会使采用主题更加容易
您可以在不同的配置下使用不同的色值(例如,@color/foo
在 res/values/colors.xml
和 res/values-night/colors.xml
下均有定义),但我建议您使用主题属性
颜色层级的变化是您必须给颜色一个语义明确的名字,例如您可能不会命名一个颜色为 @color/white
并在暗黑主题内提供一个变体(这会很奇怪并且混乱)。取而代之的是您可能会使用一个语义明确的名字,例如 @color/background
。但这样做的问题是它既代表了语义又代表了色值,它没有展示出使用有能够跟随主题变化的能力
使用 @colors
会导致你创建出更多的颜色。如果您在某些场景下需要一个新的语义值,但该色值之前却定义过(例如,你需要一个和 background 相同色值的颜色,但是会将其命名为其他名字),您需要在 colors 文件中再创建一条
通过使用主题属性,我们将色值与颜色的语义名字分离,使调用更加清晰(使用 ?attr/
语句调用),颜色将随主题而变化。将颜色声明保持为字面值,使您定义了一个「调色板」,您可以在主题层级上进行更改,从而使 color 文件较小且更易维护
Define a palette of colors used by your app and vary them at the theme level
定义 app 的调色板,并在主题层级进行更改
这种方法的另一个好处是,引用这些颜色的 layouts/styles 变得可复用。 由于主题可以覆盖或变化,因此间接表示您无需创建其他布局或样式就可以仅更改某些颜色——您可以将相同的布局用于不同的主题
前文的 「始终* 通过主题属性来控制颜色」 中的(always)带有星号,因为在某些情况下,您很明确不想按主题更改颜色。例如 Material Design guidelines 中指出有些场景您可能希望在浅色和深色主题使用相同的「品牌色」
在这种特殊场景下,直接引用颜色资源也是可以的
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<FloatingActionButton …
app:backgroundTint="@color/owl_pink_500"/>
不使用主题属性的另一种情况是使用 ColorStateList
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<View …
android:background="@color/primary_20"/>
如果 primary_20
是一个 ColorStateList
并且它本身通过主题属性引用色值,则这可能是有效的(请参见下文)。 虽然通常用于在不同状态(按下,禁用等)下提供不同的颜色,但 ColorStateLists
具有另一种可用于主题化的功能。 它可以帮你为一个颜色设置透明度
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
<selector …
<item android:alpha="0.20" android:color="?attr/colorPrimary" />
</selector>
这种单项 ColorStateList
(即,仅提供一种默认颜色,所有状态都一样)有助于减少需要维护的颜色资源数量。它不是通过手动的创建一个新的颜色资源来为 app 的主色设置透明度(每个配置),取而代之的是通过当前主题 colorPrimary
来处理。如果您的 主色 发生了变化,则只需要在一个地方进行更新,而无需跟踪对其进行了调整的所有实例
尽管有用,但要注意此技术的一些注意事项
出于此原因,最好将主题颜色指定为完全不透明,并使用
ColorStateLists
修改其 Alpha
alpha 组件是 API 23 后引入的,因此您的 app min sdk 版本比这个低,请确保使用兼容的 [AppCompatResources.getColorStateList](https://developer.android.com/reference/androidx/appcompat/content/res/AppCompatResources.html#getColorStateList(android.content.Context, int)) 并一直使用 android:alpha
命名空间,不要使用 app:alpha
命名空间
将普通的颜色作为 drawable
View 的 background 属性 需要一个 drawable,我们使用普通的颜色设置 background 是可以的,其内部会把 color 转换为 ColorDrawable
,然而 ColorStateList
是无法转换为 Drawable 的(直到 API 29 ColorStateListDrawable 的出现解决了这一问题)。我们可以曲线救国解决此限制。
因此,您应该使用主题属性和 ColorStateList
,但是如何在整个代码库或团队中实施呢? 您可以在 code review 时关注,但这并不是个好办法。 更好的方法是依靠工具来解决此问题。这篇文章介绍了通过 lint 检查对不符合规范的用法给出更好的建议。文章在这
使用主题属性和 ColorStateList
可以将颜色与主题分离,可以使布局和样式更加灵活,方便复用用并保持代码库精简和可维护性
感谢 Florina Muntenescu 和 Chris Banes
译文完
原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities
作者:Somesh Kumar
译者:Fly_with24
谷歌在2019 I/O 大会中的 What’s New in Architecture Components 介绍了 view binding
在 What’s New in Architecture Components 中,有一个简短的关于view binding 的演讲,演讲中将 view binding 与现有解决方案进行了比较,并进一步讨论了为什么view binding 比 data binding
或 Kotlin synthetics
等现有解决方案更好。
对我而言,Kotlin synthetics
运行良好,但是没有编译时的安全性,这意味着所有 ID 都位于全局命名空间中。因此,如果您使用的 ID 具有相同的名称,并且从错误的布局导入 ID, 由于ID不是当前布局的一部分,导致崩溃,除非您将应用程序运行到该布局,否则无法提前知道这一点。
这篇文章很好地概述了 Kotlin synthetics
的问题
The Argument Over Kotlin Synthetics
View Binding
将在 Android Studio 3.6 稳定版中提供(译者注:当前Android Studio稳定版版本为3.5.3),如果您想要使用它,您可以下载 Android Studio 3.6(2020.02.25更新) 或者 Android Studio 4.0 Canary
view binding 的主要优点是所有绑定类都是由Gradle插件生成的,因此它对构建时间没有影响,并且具有编译时安全性(我们将在示例中看到)。
首先,启用 view binding, 我们需要在 module 的build.gradle文件中添加以下内容:
// Android Studio 3.6
android {
viewBinding {
enabled = true
}
}
// Android Studio 4.0
android {
buildFeatures {
viewBinding = true
}
}
注意:视图绑定是逐模块启用的,因此,如果您具有多模块项目设置,则需要在每个 build.gradle
文件中添加以上代码。
如果要在特定的布局禁用 view binding,则需要在布局文件的根视图中添加 tools:viewBindingIgnore = “true”
。
启用后,我们可以立即开始使用它,并且当您完成同步 build.gradle 文件时,默认情况下会生成所有绑定类。
它通过将XML布局文件名转换为驼峰式大小写并在其末尾添加 Binding
来生成绑定类。 例如,如果您的布局文件名为 activity_splash
,则它将生成绑定类为 ActivitySplashBinding
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvVersionName.text = getString(R.string.version)
}
我们有一个名为 activity_splash
的布局文件,里面有一个ID为 tvVersionName
的 TextView
,因此在使用view binding 时,我们要做的就是获取绑定类的引用,例如:
val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)
在 setContentView()
方法中使用 getRoot()
,该方法将返回布局的根布局。可以从我们创建的绑定类对象访问视图,并且可以在创建对象后立即使用它,如下所示:
binding.tvVersionName.text = getString(R.string.version)
在这里,绑定类知道 tvVersionName
是TextView
,因此我们不必担心类型转换。
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
_binding = null
}
}
在 fragment 中,使用 view binding 有些不同。 我们需要传递 LayoutInflator
,ViewGroup
和一个 attachToRoot
布尔变量,这些变量是通过覆盖 onCreateView
获得的。
我们可以通过调用 binding.root
返回 view。您还注意到,我们使用了两个不同的变量 binding
和 _binding
,并且 _binding
变量在 onDestroyView()
中设置为null。
这是因为该 fragment 的生命周期与 activity 的生命周期不同,并且该fragment 可以超出其视图的生命周期,因此如果不将其设置为null,则可能会发生内存泄漏。
另一个变量通过 !!
使一个变量为可空值而使另一个变量为非空值避免了空检查。 。
class PaymentAdapter(private val paymentList: List<PaymentBean>) : RecyclerView.Adapter<PaymentAdapter.PaymentHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentHolder {
val itemBinding = RowPaymentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PaymentHolder(itemBinding)
}
override fun onBindViewHolder(holder: PaymentHolder, position: Int) {
val paymentBean: PaymentBean = paymentList[position]
holder.bind(paymentBean)
}
override fun getItemCount(): Int = paymentList.size
class PaymentHolder(private val itemBinding: RowPaymentBinding) : RecyclerView.ViewHolder(itemBinding.root) {
fun bind(paymentBean: PaymentBean) {
itemBinding.tvPaymentInvoiceNumber.text = paymentBean.invoiceNumber
itemBinding.tvPaymentAmount.text = paymentBean.totalAmount
}
}
}
row_payment.xml
是我们用于 RecyclerView
item 的布局文件,对应生成的绑定类 RowPaymentBinding
。
现在,我们所需要做的就是在onCreateViewHolder()
中调用 inflate()
方法生成 RowPaymentBinding
对象并传递到 PaymentHolder
主构造器中,并将 itemBinding.root
传递给 RecyclerView .ViewHolder()
构造函数。
<include>
标签view binding 可以与 <include>
标签一起使用。 布局中通常包含两种 <include>
标签,带或不带<merge>
标签。
<inlude>
不带 <merge>
标签我们需要为<include>
分配一个 ID,然后使用该 ID 来访问包含布局中的视图。让我们来看一个例子。
app_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:background="?colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
main_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/appbar"
layout="@layout/app_bar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
在上面的代码中,我们在布局文件中包括了一个通用工具栏,<include>
有一个 android:id=“@+id/appbar”
ID,我们将使用它从 app_bar.xml
中访问工具栏并将其设置为我们的 action bar
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MainLayoutBinding = MainLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appbar.toolbar)
}
<inlude>
带 <merge>
标签当在一个布局中包含另一个布局时,我们通常使用一个带有 <merge>
标记的布局,这有助于消除布局嵌套。
placeholder.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/tvPlaceholder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</merge>
fragment_order.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/placeholder" />
</androidx.constraintlayout.widget.ConstraintLayout>
如果我们尝试为该 <include>
提供ID,view binding 不会在绑定类中生成ID,因此我们无法像使用普通 include
那样访问视图。
在这种情况下,我们有 PlaceholderBinding
,它是 placeholder.xml
(<merge>
布局文件)的自动生成的类。我们可以调用其bind()
方法并传递包含它的布局的根视图。
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentOrderBinding.inflate(layoutInflater, container, false)
placeholderBinding = PlaceholderBinding.bind(binding.root)
placeholderBinding.tvPlaceholder.text = getString(R.string.please_wait)
return binding.root
}
然后,我们可以从我们的类(如 placeholderBinding.tvPlaceholder.text
)访问 placeholder.xml
内部的视图。
感谢阅读。希望收到您的评论。
在 fragment 中使用 view binding 比较麻烦,译者提供一个 BaseFragment
的封装供大家参考
abstract class BaseFragment<T : ViewBinding>(layoutId: Int) : Fragment(layoutId) {
private var _binding: T? = null
val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = initBinding(view)
init()
}
/**
* 初始化 [_binding]
*/
abstract fun initBinding(view: View): T
abstract fun init()
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {
override fun initBinding(view: View): FragmentHomeBinding = FragmentHomeBinding.bind(view)
override fun init() {
binding.viewPager.adapter = SectionsPagerAdapter(this)
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
tab.text = TAB_TITLES[position]
}.attach()
}
}
关于ViewBinding旋转屏幕等状态的空安全问题,译者进行了测试,步骤如下:
TextView
,id 分别为 hello1
hello2
kotlin
和 java
创建 activity
,使用 ViewBinding
将 hello1
text
设置为 “你好”结论:kotlin
语言项目编译不通过,java
语言项目正常运行,旋转后空指针异常
分析:打开 ViewBinding
生成的 Binding
类
/**
* This binding is not available in all configurations.
* <p>
* Present:
* <ul>
* <li>layout/</li>
* </ul>
*
* Absent:
* <ul>
* <li>layout-land/</li>
* </ul>
*/
@Nullable
public final TextView hello1;
/**
* This binding is not available in all configurations.
* <p>
* Present:
* <ul>
* <li>layout-land/</li>
* </ul>
*
* Absent:
* <ul>
* <li>layout/</li>
* </ul>
*/
@Nullable
public final TextView hello2;
@Nullable
注解 解释了 kotlin
为什么编译不通过,而对于 java
该注解仅会 lint
提示开发者该位置可能出现空指针
不会测试的开发不是好开发——鲁迅
一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,本篇是该系列的第二篇,我们来介绍一下 AndroidX Test
以及如何对 ViewModel 和 LiveData 进行测试
本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics
再开始介绍 ViewModel
和 LiveData
的 test 之前,我们先来介绍一下 AndroidX Test
AndroidX Test
是一个测试库的集合,它提供了测试版本的组件,例如 application 和 activity。
如果想在 local test 使用 application context
怎么办?有了 AndroidX Test
,您可以使用 ApplicationProvider.getApplicationContext()
方法来获取
AndroidX Test
不仅可以提供 application 。使用 AndroidX Test
,您可以只写一次 test 代码,然后在 local test 和 instrumentd test 中运行
在不使用 AndroidX Test
之前,local test 和 instrumented test 使用不同的库。例如 application context ,它们是不同的写法
使用 Android Test
,您只需要学习一套 API 便可以使用 local test 或 instrumented test
使用 AndroidX Test,您需要引入依赖
dependencies {
// Other dependencies
// AndroidX Test - JVM testing
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
// AndroidX Test - Instrumented testing
androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
}
如果需要使用 application 等资源,需要在测试类上使用 @RunWith(AndroidJUnit4::class)
标记
Android JUnit4 Runner
允许 AndroidX Test
在 local test 和 instrumented test 使用不同的依赖
我们来创建 ViewModel 的 test,在 ViewModel 类名上唤出 Generate 菜单,选择 test ,选择存储在 local test source 中
接下来我们开始编码,我们创建 getReposByUser_loadReposEvent
方法用于测试获取仓库数据
首先,我们要提供 ViewModel,不同于在 app 编码使用 ViewModelProvider
,test 中可以直接创建 ViewModel 实例
接着我们调用 ViewModel 中待测试的方法
最后我们需要检查结果,这里暂时不写
这里我们没使用 application context ,因此可以不加入
@RunWith(AndroidJUnit4::class)
注解标记
对于 LiveData,我们主要注意两件事
第一步是添加 InstantTaskExecutorRule
,它是一个 JUnit rule
JUnit rule 允许在测试运行前后定义代码
如果您要测试 LiveData,则需要使用它
// 使用架构组件同步执行每个任务
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
使用 InstantTaskExecutorRule 需要引入依赖
testImplementation "androidx.arch.core:core-testing:2.1.0"
observe LiveData 也很重要,我们在 activity 和 fragment 中 observe LiveData 时需要传入 LifecycleOwner
。而在 test 中我们是拿不到 LifecycleOwner
的,所以我们需要使用 observeForever
方法,但是要注意需要在合适的位置 remove observer
我们基于上一节的代码进行补充
我们通过断言判断 repos 包裹的 List<Repo> 是否是 null 或者 empty
这样的写法有些繁琐,我们每次测试 LiveData 都有加入这么一大段代码,我们可以通过编写一个扩展函数来简化
扩展函数源码如下
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
前面我们提到 JUnit rule 允许在测试运行前后定义代码,例如示例中的 repoViewModel 变量,如果多个 test 都需要它,我们可以将其单独抽离出来
lateinit var repoViewModel: RepoViewModel
@Before
fun initRepoViewModel() {
repoViewModel = RepoViewModel(RepoRepository.getRepository())
}
注意:不要将 ViewModel 声明后立刻赋值,像下面这样
// 错误写法
val repoViewModel = RepoViewModel(RepoRepository.getRepository())
这样会导致所有测试都使用同一个 ViewModel 实例,您应该避免这样。每个测试都应有一个新的测试实例
最终的代码如下
class RepoViewModelTest {
// 使用架构组件同步执行每个任务
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
lateinit var repoViewModel: RepoViewModel
@Before
fun initRepoViewModel() {
repoViewModel = RepoViewModel(RepoRepository.getRepository())
}
@Test
fun getReposByUser_loadReposEmpty() {
// When load repos by user
repoViewModel.getReposByUser("Flywith24")
// Then 检查 repos 是否为 null 或 empty
val value = repoViewModel.userRepos.getOrAwaitValue()
assertThat(value.isNullOrEmpty(), `is`(true))
}
@Test
fun getReposByOrg_loadReposEmpty() {
// When load repos by organization
repoViewModel.getReposByOrg("Android")
// Then 检查 repos 是否为 null 或 empty
val value = repoViewModel.orgRepos.getOrAwaitValue()
assertThat(value.isNullOrEmpty(), `is`(true))
}
}
上一篇 我们介绍了
OnBackPressedDispather
,那么今天我们来正式地从源码的角度看看 fragment 的返回栈吧。由于其主流程和生命周期差不多,因此本文将详细地分析返回栈相关的源码,并插入大量源码。建议将生命周期流程熟悉后阅读本文。文末提供单返回栈和多返回栈的 demo
如果您对 activity 对任务栈和返回栈不是很了解,可以移步 Tasks and the Back Stack
在分析源码之前,我们先来思考几个问题。
返回栈,顾名思义,是一个栈结构。所以我们要搞清楚,这个栈结构到底存的是什么。
我们都知道,使用 fragment 的返回栈需要调用 addToBackStack("")
方法
在 从源码角度看 Fragment 生命周期 一文中,我们提到了 FragmentTransaction ,它是一个「事务」的模型,事务可以回滚到之前的状态。所以当触发返回操作时,就是将之前提交的事务进行回滚。
FragmentTransaction
的实现类为 BackStackRecord
,所以 fragment 的返回栈其实存放的就是 BackStackRecord
作为返回栈的元素,BackStackRecord 实现了FragmentManager.BackStackEntry 接口
从 BackStackRecord
的定义我们可以发现 BackStackRecord
有三种身份
FragmentTransaction
,即是事务,保存了整个事务的全部操作FragmentManager.BackStackEntry
,作为回退栈的元素OpGenerator
,可以生成 BackStackRecord
列表,后文详细介绍我们已经知道 fragment 的返回栈其实存放的是 BackSrackRecord , 那么谁来管理 fragment 的返回栈?
FragmentManager
用于管理 fragment ,所以 fragment 返回栈也应该由 FragmentManager 管理
//FragmentManager.java
ArrayList<BackStackRecord> mBackStack;
其实触发 fragment 的返回逻辑有两种途径
开发主动调用 fragment 的返回方法
用户按返回键触发
后文我们会从这两个角度分析一下 fragment 中的返回栈逻辑究竟是怎样的
我们已经知道返回栈中的元素是 BackStackRecord
,也清楚了是 FragmentManager
来管理返回栈。那么如果让我们来实现「返回」逻辑,应该如何做?
首先我们要清楚所谓的「返回」是对事务的回滚,即 对 commit 事务的内部逻辑执行相应的「逆操作」。
例如
addFragment←→removeFragment
showFragment←→hideFragment
attachFragment←→detachFragment
有的小伙伴可能会疑惑 replace 呢?
expandReplaceOps
方法会把 replace 替换(目标 fragment 已经被 add )成相应的 remove 和 add 两个操作,或者(目标 fragment 没有被 add )只替换成 add 操作
FragmentManager
中提供了popBackStack
系列方法
是否觉得很眼熟?提交事务也有类似的api,commit 系列方法
这里分别提供了同步和异步的方法,可能有读者会疑惑,同样是对事务的操作,一个为提交,一个为回滚,为什么一个封装到了 FragmentManager
中,一个却在 FragmentTransaction
中。既然都是对事务的操作,应该都放在FragmentManager 中。我认为可能为了api使用的方便,使得 FragmentManager
开启事务的链式调用一气呵成。各位有什么想法欢迎在评论区留言。
这里主要介绍一下 popBackStack(String name, int flag)
name 为 addToBackStack(String name) 的参数,通过 name 能找到回退栈的特定元素,flag可以为 0 或者FragmentManager.POP_BACK_STACK_INCLUSIVE
,0 表示只弹出该元素以上的所有元素,POP_BACK_STACK_INCLUSIVE
表示弹出包含该元素及以上的所有元素。这里说的弹出所有元素包含回退这些事务。如果这么说比较抽象的话,看图
//flag 传入0,弹出 ♥2 上的所有元素
childFragmentManager.popBackStack("♥", 0)
//flag 为 POP_BACK_STACK_INCLUSIVE 弹出包括该元素及及以上的元素
childFragmentManager.popBackStack("♥", androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)
在分析返回栈源码之前我们回顾一下 FragmentManager 提交事务到 fragment 各个生命周期的流程
下面我们看看 popBackStack 的源码
等等,这个 enqueueAction 有些眼熟...
看来提交事务和回滚事务的流程基本是相同的,只是传递的 action 不同
由源码可知,OpGenerator
是一个接口,其内只有一个 generateOps
方法,用于生成事务列表以及对应的该事务是否是弹出的。有两个实现类
由此可见 commit 调用的为 BackStackRecord
的 generateOps
方法,popBackStack
调用的是 PopBackStackState
中的 generateOps
前者的逻辑很简单,向 records list 中添加数据, isRecordPop list 全部传入 false
records.add(this);
isRecordPop.add(false);
后者的逻辑稍微复杂些,其内部调用了 popBackStackState
方法
如果是 popBackStack
方法 ,则将 FragmentManager
的返回栈列表(mBackStack
)的栈顶移除, isRecordPop
list 全部传入 true
int last = mBackStack.size() - 1;
records.add(mBackStack.remove(last));
isRecordPop.add(true);
如果传入的 name 或 id 有值,且 flag 为 0,则找到返回栈(倒序遍历)中第一个符合 name 或 id 的位置,并将该位置上方的所有 BackStackRecord
并添加到 record
list 中,同时 isRecordPop
list 全部传入 true
index = mBackStack.size() - 1;
while (index >= 0) {
BackStackRecord bss = mBackStack.get(index);
if (name != null && name.equals(bss.getName())) {
break;
}
if (id >= 0 && id == bss.mIndex) {
break;
}
index--;
}
for (int i = mBackStack.size() - 1; i > index; i--) {
records.add(mBackStack.remove(i));
isRecordPop.add(true);
}
如果传入的 name 或 id 有值,且 flag 为 POP_BACK_STACK_INCLUSIVE
,则在上一条获取位置的基础上继续遍历,直至栈底或者遇到不匹配的跳出循环,接着出栈所有 BackStackRecord
//index 操作与上方相同,先找到返回栈(倒序遍历)中第一个符合 name 或 id 的位置
if ((flags & POP_BACK_STACK_INCLUSIVE) != 0) {
index--;
// 继续遍历 mBackStack 直至栈底或者遇到不匹配的跳出循环
while (index >= 0) {
BackStackRecord bss = mBackStack.get(index);
if ((name != null && name.equals(bss.getName()))
|| (id >= 0 && id == bss.mIndex)) {
index--;
continue;
}
break;
}
}
//后续出栈逻辑与上方相同
可以配合上面的动图理解
入栈和出栈后续的逻辑大体是相同的,只是根据 isPop 的正负出现了分支,出栈调用的是 executePopOps
上文我们有提到,「返回」逻辑实际上就是执行提交事务内部操作逻辑的「逆操作」
那么接下的逻辑就很清晰了,根据不同的 mCmd 执行相应的逆操作
void executePopOps(boolean moveToState) {
for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
final Op op = mOps.get(opNum);
Fragment f = op.mFragment;
switch (op.mCmd) {
case OP_ADD:
mManager.removeFragment(f);
break;
case OP_REMOVE:
mManager.addFragment(f);
break;
case OP_HIDE:
mManager.showFragment(f);
break;
case OP_SHOW:
mManager.hideFragment(f);
break;
case OP_DETACH:
mManager.attachFragment(f);
break;
case OP_ATTACH:
mManager.detachFragment(f);
break;
case OP_SET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(null);
break;
case OP_UNSET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(f);
break;
case OP_SET_MAX_LIFECYCLE:
mManager.setMaxLifecycle(f, op.mOldMaxState);
break;
default:
throw new IllegalArgumentException("Unknown cmd: " + op.mCmd);
}
if (!mReorderingAllowed && op.mCmd != OP_REMOVE && f != null) {
mManager.moveFragmentToExpectedState(f);
}
}
if (!mReorderingAllowed && moveToState) {
mManager.moveToState(mManager.mCurState, true);
}
}
后面的逻辑就完全一样了
在 【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇 一文中我们介绍了 OnBackPressedDispatcher
activity 的 onBackPressed
的逻辑主要分为两部分,判断所有注册的 OnBackPressedCallback
是否有 enabled 的,如果有则拦截,不执行后续逻辑;
否则着执行 mFallbackOnBackPressed.run() ,其内部逻辑为调用 ComponentActivity 父类的 onBackPressed
方法
所以我们只需看 mOnBackPressedCallbacks(ArrayDeque<OnBackPressedCallback) 是怎样被添加的以及 isEnabled 何时赋值为 true
经过查找我们发现它是在 FragmentManager 的 attachController 调用 addCallback
mOnBackPressedDispatcher.addCallback(owner,mOnBackPressedCallback)
进而执行了
而 mOnBackPressedCallback
在初始化时 enabled 赋值为 false
isEnadbled
会在返回栈数量大于 0 且其 mParent 为 PrimaryNavigation
时赋值为true
而返回栈(mBackStack
)的赋值在 BackStackRecord
的 generateOps
方法中,且是否添加到返回栈由 mAddToBackStack
这个布尔类型的属性控制
mAddToBackStack 的赋值在 addToBackStack 方法中,这也解释了为何调用 addToBackStack 方法就能将事务加入返回栈
我们来总结一下,fragment 拦截 activity 返回栈是通过
OnBackPressedDispatcher
实现的,如果开启事务调用了addToBackStack
方法,则mOnBackPressedCallback
的isEnabled
属性会赋值为 true,进而起到拦截 activity 返回逻辑的作用。拦截后执行popBackStackImmediate
方法而 popBackStack系列方法会调用 popBackStackState 构造
records
和isRecordPop
列表,isRecordPop
的内部元素的值均为true 后续流程和提交事务是一样的,根据isRecordPop
值的不同选择执行executePopOps
或executeOps
方法
Ian Lake 在 Fragments: Past, Present, and Future (Android Dev Summit '19)
有提到未来会提供多返回栈的 api
那么以现有的 api 如何实现多返回栈呢?
首先我要弄清楚怎样才会有多返回栈,根据上文我们知道 FragmentManager
内部持有mBackStack
list,这对应着一个返回栈,如果想要实现多返回栈,则需要多个 FragmentManager,而多 FragmentManager
则对应多个 fragment
因此我们可以创建多个宿主 frament 作为导航 fragment 这样就可以用不同的宿主 fragment 的 独立的FragmentManager
分别管理各自的返回栈,如果这样说比较抽象,可以参考下图
图中有四个返回栈,其中最外部有一个宿主 fragment ,内部有四个负责导航的 fragment 管理其内部的返回栈,外部的宿主负责协调各个返回栈为空后如何切换至其他返回栈
单返回栈就很容易了,我们只需在同一个 FragmentManager
上添加返回栈即可
详情参照 demo
之前我们讨论过 ViewModel 的职能边界 ,得益于 ViewModel 的生命周期更长,我们可以在 activity 重建后将数据传递给 activity ,也可以避免内存泄漏。但是如果不是每次需要就获取数据,而是当每次有新数据时通知我们,应该怎么办?
本文介绍 LiveData
,一个 生命周期感知的,可观察的,数据持有者。同时还会简单分析 LiveData
的源码实现
在谈 LiveData
前我们来思考一个问题
Android 开发(亦或者说前端开发)的本质工作内容是什么?
对于应用层 app 开发者,开发者的工作主要工作就是 Adapter
什么是 Adapter ,下图可能比较直观
图片来自 google image
我们的工作本质是 将数据转换成 UI
数据可能来自网络,来自本地数据库,来自内存,而 UI 可能是 activity 或 fragment。
上面我们提到 Android 开发者的核心工作就是将数据转换为 UI 。这个过程比较理想的状态是:当数据发生变化时,UI 跟随变化。我们还可以进一步展开:当 UI 对用户可见时,数据发生变化时 UI 跟随变化;当 UI 对用户不可见时,我们希望数据变化时什么都不做,当 UI 再次对用户可见时根据最新的数据进行 UI 的处理。
而 LiveData
就是我们理想中的数据模型
LiveData 可以三个关键词概括
lifecycle-aware
observable
data holder
Android 中不同的组件有着不同的生命周期,不同的存活时间
因此我们不会在 ViewModel
中持有 Activity
的引用,因为这会导致当 Activity
重建时内存泄漏,甚至出现空指针的情况
通常我们会在 Activity
中持有 ViewModel
的引用,那么如何进行二者间的通信,如何向 Activity
发送 ViewModel
中的数据?
答案是让 Activity
观察 ViewModel
LiveData
是 observable
当观察者观察着某个数据时,该数据必须保留对观察者的引用才能调用它,为了解决这个问题,LiveData
被设计成可感知生命周期
当 activity / fragment 被销毁后,它会自动的取消订阅
LiveData
仅持有 单个且最新 的数据
上图中,最右侧是在 ViewModel
中的 LiveData
,左侧为观察这个 LiveData
的 activity / fragment 。一旦我们为 LiveData
设值,该值会传递到 activity。简而言之,LiveData
值改变,activity 收到最新的值的变化。但是当观察者不再处于活动状态(STARTED 到 RESUMED ),数据 C 不会被发送到 activity 。当 activity 回到前台,它将收到最新的值,数据 D。LiveData 仅持有单个且最新的数据。当 activity 执行销毁流程时,此时的数据 E 也不会产生任何影响
LiveData
提供 两种 transformation ,map
和 switch map
。开发者也可以创建自定义的 MediatorLiveData
我们都知道 LiveData
可以为 View 和 ViewModel 提供通信,但如果有一个第三方组件(例如 repository )也持有 LiveData
。那么它应该如何在 ViewModel
中订阅?该组件并没有 lifecycle
一旦我们的应用愈发复杂,repository 可能会观察数据源
那么 view 如何获取 repository 中的 LiveData
?
在上面的示例中,ViewModel
仅将数据从 repository 转发到 view,然后将其转换为 UI Model。 每当 repository 中有新数据时,ViewModel
只需 map
class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
convertDataToMainUIModel(data)
}
}
第一个参数为 LiveData
源(来自 repository ),第二个参数是一个转换函数。
// 这里的转换为将 X 转换为 Y
inline fun <X, Y> LiveData<X>.map(crossinline transform: (X) -> Y): LiveData<Y> =
Transformations.map(this) { transform(it) }
假如您正在观察一个提供用户的用户管理器,并且需要提供用户的 id 才能开始观察 repository
您不能将其写到 ViewModel
初始化的过程中,因为此时用户的 id 还不可用
这时 switchMap
就派上用场了
class MainViewModel {
val repositoryResult = Transformations.switchMap(userManager.userId) { userId ->
repository.getDataForUser(userId)
}
}
switchMap
在内部使用 MediatorLiveData
,因此了解它非常重要,因为当您要组合多个 LiveData
源时需要使用它
// 这里的转换为将 X 转换为 LiveData<Y>
inline fun <X, Y> LiveData<X>.switchMap(
crossinline transform: (X) -> LiveData<Y>
): LiveData<Y> = Transformations.switchMap(this) { transform(it) }
MediatorLiveData
允许您将一个或多个数据源添加到单个可观察的 LiveData
中
val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...
val result = MediatorLiveData<Int>()
result.addSource(liveData1) { value ->
result.setValue(value)
}
result.addSource(liveData2) { value ->
result.setValue(value)
}
在上面的例子中,当任何一个数据源变化时,result 会更新。
注意:数据并不是合并,MediatorLiveData 只是处理通知
为了实现示例中的转换,我们需要将两个不同的 LiveData 组合为一个
图片来自 LiveData beyond the ViewModel — Reactive patterns using Transformations and MediatorLiveData
使用 MediatorLiveData
合并数据的一种方法是添加源并以其他方法设置值:
fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {
val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
val liveData2 = userCheckinsDataSource.getCheckins(newUser)
val result = MediatorLiveData<UserDataResult>()
result.addSource(liveData1) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
result.addSource(liveData2) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
return result
}
数据的实际组合是在 combineLatestData
方法中完成的
private fun combineLatestData(
onlineTimeResult: LiveData<Long>,
checkinsResult: LiveData<CheckinsResult>
): UserDataResult {
val onlineTime = onlineTimeResult.value
val checkins = checkinsResult.value
// Don't send a success until we have both results
if (onlineTime == null || checkins == null) {
return UserDataLoading()
}
// TODO: Check for errors and return UserDataError if any.
return UserDataSuccess(timeOnline = onlineTime, checkins = checkins)
}
检查值是否准备好并发出结果(加载中,失败或成功)
var lateinit randomNumber: LiveData<Int>
fun onGetNumber() {
randomNumber = Transformations.map(numberGenerator.getNumber()) {
it
}
}
这里有一个重要的问题需要理解:转换会在调用时(map
和 switchMap
)会创建一个新的 LiveData
。 在此示例中,randomNumber 公开给 View ,但是每次用户单击按钮时都会对其进行重新赋值。 观察者只会在订阅时收到分配给 var 的 LiveData
更新的信息
// 只会收到第一次分配的值
viewmodel.randomNumber.observe(this, Observer { number ->
numberTv.text = resources.getString(R.string.random_text, number)
})
如果 viewmodel.randomNumber LiveData
实例发生更改,这里永远不会回调。而且这里泄漏了之前的 LiveData
,这些 LiveData
不会再发送更新
一言以蔽之,不要在 var 中使用 Livedata
正确示例见 demo
一般来说我们使用 LiveData 持有 UI 数据和状态,但是如果通过它来发送事件,可能会出现一些问题。这些问题及解决方案 在这
androidx fragment 1.2.0
起,添加了新的 Lint 检查,以确保您在从 onCreateView()、onViewCreated() 或 onActivityCreated() 观察 LiveData
时使用 getViewLifecycleOwner()
如图,我们有一个 fragment ,onCreate 观察 LiveData
,通过正常的生命周期创建了 View ,接着进入了 resume 状态。此时你使用了 LiveData
,UI 将开始展示它。之后,用户点击了按钮,由于跳转了另一个 fragment,所以要 detach 该 fragment,一旦 fragment stop 我们就不需要其中的 view 了,因此 destroyView 。之后用户点击了返回按钮回到了上一个 fragment,由于我们已经 destroyView,因此我们需要创建一个新的 view ,接着进入正常的生命周期,但此时,出现了一个 bug 。这个新 View 不会恢复 LiveData
的状态,因为我们使用的是 fragment 的 lifecycle observe 的 LiveData
我们有两种选择,在 onCreate 或者在 onCreateView 中使用 fragment 的 lifecycle observe LiveData
前者的优点是一次注册,缺点是当 recreate 时有bug;后者优点是能够解决 recreate 的 bug,但会导致重复注册
该问题的核心是 fragment 拥有两个生命周期:fragment 自身和 fragment 内部 view 的生命周期
androidx fragment 1.0
和 support library 28
了 viewLifecycle
因此,当需要观察 view 相关的 LiveData ,可以在 onCreateView()、onViewCreated() 或 onActivityCreated() 中 LiveData observe 方法中传入 viewLifecycleOwner 而不是传入 this
首先来看 LiveData
主要的源码结构
LiveData
是可以在给定生命周期内观察到的数据持有者类。 这意味着可以将一个Observer
与 LifecycleOwner
成对添加,并且只有在配对的 LifecycleOwner
处于活动状态时,才会向该观察者通知有关包装数据的修改。 如果 LifecycleOwner 的状态为 Lifecycle.State.STARTED
或 Lifecycle.State.RESUMED
,则将其视为活动状态。 通过 observeForever
(Observer)添加的观察者被视为始终处于活动状态,因此将始终收到有关修改的通知。 对于那些观察者,需要手动调用 removeObserver
(Observer)
如果相应的生命周期移至 Lifecycle.State.DESTROYED
状态,则添加了生命周期的观察者将被自动删除。 这对于 activity 和 fragment 可以安全地观察 LiveData
而不用担心泄漏
此外,LiveData
具有 onActive() 和 onInactive() 方法,以便在活动观察者的数量在 0 到 1 之间变化时得到通知。这使 LiveData
在没有任何活动观察者的情况下可以释放大量资源。
主要方法有:
LiveData
实现类,公开了 setValue
和 postValue
方法
接口,内部只有 onChanged(T t) 方法,在数据变化时该方法会被调用
我们通过源码来看看 LiveData
如何实现它的特性的
LiveData
持有最新的数据?我们查看 LiveData
的 observe 方法
// LiveData.java
@MainThread
public void observe(LifecycleOwner owner, Observer<? super T> observer) {
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// 如果 owner 已经是 DESTROYED 状态,则忽略
return;
}
// 使用 LifecycleBoundObserver 包装 owner 和 observer
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
// 如果已经添加过直接 return
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}
// LifecycleBoundObserver.java
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
@NonNull
final LifecycleOwner mOwner;
LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) {
super(observer);
mOwner = owner;
}
}
通过源码我们知道,当我们调用 observe 方法时,内部是通过 LifecycleBoundObserver
将 owner 和 observer 包裹起来并通过 addObserver
方法添加观察者的,因而当数据变化时,会调用 LifecycleBoundObserver
的 onStateChanged
方法
// LiveData.LifecycleBoundObserver.java
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
// 自动移除观察者,问题 2 得到解释
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}
当什么周期所有者处于 DESTROYED
状态时,会调用 removeObserver
方法,因此问题 2 得到解释
我们继续向下看,activeStateChanged
方法调用时传入了 shouldBeActive()
@Override
boolean shouldBeActive() {
// 至少是 STARTED 状态 返回 true
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
// 与上次值相同,则直接 return (两次均为活动状态或均为非活动状态)
return;
}
mActive = newActive;
boolean wasInactive = LiveData.this.mActiveCount == 0;
// 根据 mActive 修改活动状态观察者的数量(加 1 或减 1 )
LiveData.this.mActiveCount += mActive ? 1 : -1;
if (wasInactive && mActive) {
onActive();
}
if (LiveData.this.mActiveCount == 0 && !mActive) {
onInactive();
}
if (mActive) {
// 如果是活动状态,则发送数据,问题 1 得到解释
dispatchingValue(this);
}
}
这里牵扯了 Lifecycle State
比较的知识,详情在这
只有 STARTED
和 RESUMED
状态 shouldBeActive()
才返回 true,至此问题 1 得到解释
dispatchingValue
方法内部调用了 considerNotify
方法
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// 再次判断生命周期所有者状态
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
// 比较版本号
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
// 调用我们传入的 mObserver 的 onChanged 方法
observer.mObserver.onChanged((T) mData);
}
可以看到 considerNotify
中比较了 observer 的版本号,如果是最新的数据,直接 return
而 mVersion
在 setValue
方法中 进行更改
@MainThread
protected void setValue(T value) {
// 每次设置对 mVersion 进行++
mVersion++;
mData = value;
dispatchingValue(null);
}
因此 LiveData
每次都持有最新的数据,问题 3 得到解释
回到本文开头的思考,Android 开发者的主要工作是将数据转换成 UI ,而 LiveData
本质上是一种「数据驱动」,即通过改变状态数据,来驱动视图树中绑定了相应状态数据的控件重新发生绘制。Flutter 和未来的 Jetpack Compose 采用的都是这种机制。使用 ViewModel + LiveData,可以 安全地在订阅者的生命周期内分发正确的数据,使开发者不知不觉中完成了 UI -> ViewModel -> Data
的单向依赖。
所谓架构,很多时候不是使用它能做什么,更多的是不要做什么,使用它时开发者能够得到约束,以便产出更健壮的代码
在开发过程中我们可能遇到自家应用间共享账号的场景。例如 APP1 登录成功后,启动 APP2 时自动完成登录并与 APP1 共享账号信息。
Android 为我们提供了AccountManager 来管理账号信息。
AccountManager是一个面向应用程序开发的组件,它提供了一套对应于 IAccountManager 协议的应用程序接口;这组接口通过Binder机制与系统服务AccountManagerService进行通信,协作完成帐号相关的操作。同时,AccountManager接收authenticators 提供的回调,以便在帐号操作完成之后向调用此帐号服务的业务返回对应的接口,同时触发这个业务对结果的处理。
- authenticators 即注册帐号服务的app;
- 业务调用方 即使用authenticators提供的帐号服务的第三方,也可以是authenticator自己
该项目中有两个 module ,app 对应注册账号服务的app,app1 对应使用账号服务的第三方应用
在两个应用的 manifest 中加入 <uses-permission android:name="android.permission.GET_ACCOUNTS" />
权限
在 app 中创建 authenticator.xml
文件,注意 accountType 的配置,这里的应用名及 icon 会在设备的 设置 -> 账号 中显示
在 app 中注册一个 action 为“android.accounts.AccountAuthenticator”的 authenticator service,引入上一步创建的 xml 文件
在 app 中创建 authenticator
很高兴见到你 👋
本文不是面经
本文不是面经
本文不是面经
本文的目标读者是想要换城市/换工作的小伙伴(无论何种技术栈),喜欢「吃快餐」的小伙伴可以关闭窗口了。
分割线开始
分割线结束
很高兴您能读到这里,我想通过这样的方式过滤一部分非目标读者。我保证接下来的内容会干货满满~ 😉
本文将分享一下我跨城市换工作的经历以及心得(面试了快手和字节两家公司,拿到快手 offer)。
我将从 面试前准备 -> 面试过程 -> 面试后总结 三个阶段进行阐述。
这是我 2021 年的第一篇文章,让我们开始吧~
之前的文章 我提到了建立系统化知识体系的重要性。
一点建议:把知识看做一个语义树十分重要,在你进入到叶子/细节之前,确保自己清楚基本原理(即树干和大树枝),否则这些叶子将无处安放。—— 埃隆·马斯克
最近我想到了一个更好的词来描述,我把它称之为「技术人的基本盘」。
人的基本盘指个人长期的竞争力。
每个人由于拥有不同技术栈,有着不同的项目经历,因此会拥有不同的知识体系。「技术人的基本盘」大致是这样的:
掌握扎实的计算机领域的通用知识(操作系统,网络,数据结构等)
拥抱变化并拥有较强的学习能力(技术领域更新快)
拥有良好的沟通协作能力(工作中大都是多人协作的场景)
掌握扎实的自身技术栈相关的知识(吃饭的家伙)
贴标签的含义是:用最快的速度将人/事归类。
生活中贴标签的场景比比皆是。
例如根据年代贴标签
游戏中也有贴标签:
上图是篮球游戏球员建模的画面,该建模被贴了「篮筐冲击者」的标签,玩家可快速了解该建模在球场上的位置以及技术长项。
图片摘自 B 站 2k 游戏 up 主 油管小王子Terry
贴标签是人类认识世界、进行社会交往最便捷的手段之一。
企业面试候选人,希望能够最快速地将候选人分类,因此我认为 面试是面试官给候选人贴标签的过程。当然这个过程有一定的局限性,但它是筛选候选人最快速便捷的手段。有些人会认为「贴标签」是一个贬义词,而我认为正常情况下它是一个中性词,当它表示人们简单粗暴地对某个标签进行不全面的评价时,是一个贬义词。
既然面试过程是面试官给候选人贴标签的过程,那么作为候选人应努力向面试官展示自己积极意义的标签。
如果这么说比较抽象的话,举一个我自身的例子:
我没有「优秀学历」,「优秀项目经验」,「大厂经历」等标签,但我拥有其他的标签,并努力在面试过程中展示:
我想说:知道自己想要什么很重要。
人每个阶段都有自己的需求。上图是 马斯洛需求层次理论 的简单模型。
图片摘自网络。
明确了自己想要什么,便很容易去选择城市、公司。例如:
我是一个计划性很强的人,这次换城市工作是我在大学时期已经规划好的:
我的 Android 学习经历已在 这篇文章 介绍,感兴趣的小伙伴可以移步查看。
明确了自己的需求,便明确了选择,接下来就能向心仪公司撒简历啦~🥳
不过有一个更高效的方式。
我是一个敏感并十分在意他人对自己看法的人,东北话叫「脸儿小」。
有句俗语:脸皮厚吃个够,脸皮薄吃不着。
「脸皮薄」很可能错过很重要的机会。我举一个自身的例子。
我刚毕业时便听过 南尘 大佬在 HencoderPlus 中的课程,也知道他的联系方式,但直到去年 11 月份我才鼓起勇气加他的微信好友,请他帮忙内推字节,后来我又主动在 v2ex 上找了快手的内推(又结识了一个优秀的小伙伴)。
截图已得到 南尘 同意。
我发现 扔物线(朱凯),南尘 等大佬都很随和,甚至有些可爱。因此我十分后悔大学时期没有与他们多多交流,如果那样做的话我可能会进步得更快。
但是生活哪有如果呢?希望各位能做一个「厚脸皮」的人,抓住能够提升自己的机会。与优秀的人交流真的是如沐春风(丝毫没有夸张)。
在此郑重感谢 南尘 对我的帮助!(虽然最后没能入职字节🤣)
南尘 的主页有他的微信号哦😉
快手和字节一般是三轮技术面+一轮 HR 面
每轮技术面均有算法题
算法题难度基本上是 leetcode 简单/中等(低概率是困难题,遇到过🙃)
快手和字节下班晚,因此可以将面试时间约到晚上 8 点左右
远程视频面试需要带有摄像头的电脑
快手和字节远程面试使用牛客网,遇到问题可以找人工客服(很 kind)解决
据说在面试前,女友的鼓励 kiss 有魔法加成哦🙈
之前一直疑惑为什么别人的面经记录得那么完整。后来释然了:可以使用录音/录像的方式记录每轮的面试。我没使用录屏软件,因为不清楚是否会被判定为作弊。我使用的是手机录音,面试过程中让电脑处于外放状态。可以结合自身情况选择合适的方式。
面试经验不足的候选人一般很难保持平和的心态。
我之前的面试经验很少,一只手都数得过来。但随着面试次数的不断增加(从去年 7 月开始一共与字节的 10 位面试官交流过🤣),心态便渐渐平和下来。
一点建议:在面试心仪公司前,多刷几次「高质量」的面试(不仅仅是数量上的增加,还要对每次面试进行复盘,进而提高自己),这真的很重要!
视频面试时关闭 VPN 等程序,否则会产生回声!(否则会被吵到心态爆炸😠)
如果未设备调试好可以约其他时间重新面试,千万不要忍受回声然后继续面试,这很影响状态(深受其苦😭)
视频面试过程发现画面变卡,可以刷新界面重新连接(与面试官打好招呼)
字节 HR 约面试使用座机(快手一般使用手机),注意不要被自己的手机拦截
视频面试牛客网的链接会以邮箱的方式发出,注意简历上的邮箱的准确性
有时我们在面试后会陷入自我感觉良好但实际却并非如此的怪圈,这导致我们很难通过复盘来提升自己。
在尝试解决这个问题前我们可以思考一下这个的问题:
在获取知识如此便捷的今天,我们为什么要通过上学来接受教育?
柏拉图给出了一个明确的答案,他认为,人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的。(摘自吴军博士的《硅谷来信》)
我们可以通过他人的帮助来验证自己面试过程中的表达是否准确,持有的观点是否正确(当然,最好找一个比自己水平高的小伙伴帮忙分析,什么?找不到?参考「脸皮厚吃个够」一节)。
在此郑重感谢 扔物线(朱凯)!感谢凯哥每轮面试后帮我复盘并指出存在的问题。
可以抓住反问面试官的机会,让其帮忙复盘总结自己的面试表现(面试官没有义务这么做,做好遭到拒绝的准备)
针对性学习面试过程中未回答好的内容,或者拓展已掌握内容的深度
如果面试结果不理想调整好心态
换城市/换工作前想好自己想要什么,分析好自己目前的需求,并根据需求定制计划
面试过程中实力才是硬道理,其他技巧只是辅助,因此要保证自己的「基本盘」牢固
机会总是留给有准备的人,但很多时候机会是自己争取的,做一个「厚脸皮」的人
每次面试都是一个提升自己的机会,做好记录和复盘
面试要保持平和的心态,如何保持?多多练习/刷面试
祝愿各位小伙伴能够入职自己心仪的公司~
刀口漫谈 是一个非技术类的系列,主要分享一些我的思考。何为刀口?我的名字最后一个字是「召」。惊不惊喜,意不意外~🤪
人总是喜欢做能够获得正反馈(成就感)的事情,如果本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~
我是 Flywith24,人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。
很高兴见到你 👋
我不是大佬,但我相信我在通往成为大佬的路上 | 掘金年度征文 中我结合自身经历阐述了自己对学习方法的思考与实践。简单来说,我认为学习需要建立一套系统的知识体系(知识树),在此基础上可将学习分为 通用学习 与 需求学习。本文将简单讨论掌握知识的过程,并基于这一过程的各阶段进行内容创作实践。
个人认为,对知识的掌握,需要经历以下过程:
感性认识 可以看作是一个 从 0 到 1 的过程,它是认识的初级阶段。在这一过程学习者通常会寻找已有的素材学习(如书籍,论坛博客,视频等)。而创作感性认识相关的内容看似简单,实则非常困难。这里列举一些优质的从 0 到 1 的内容的创作者:
主要输出内容有 HenCoder (自定义 View),码上开学(Kotlin 基础、进阶、协程),付费视频课程 HenCoder Plus 等。其内容形式为文章 + 视频,超强的语言表达能力以及自然的上镜表现。
主要输出内容有 是让人耳目一新的 Jetpack MVVM 精讲啊!,你用不惯 RxJava,只因缺了这把钥匙,重学安卓 专栏等。行文简练,很有「大道至简」的感觉。
主要输出内容有 反思 系列,该系列站在设计者的角度,对知识体系进行了深度思考。
理性认识 建立在 感性认识 的基础上,在这一过程学习者通常会在头脑中建立起相应的知识体系并进行从 1 到 N 的扩展思考。具体些便是通过阅读源码等手段对原理进行分析并结合已有知识体系进行总结归纳。这部份内容的创作很容易陷入创作者「自说自话」的怪圈,而读者会觉得「不知所云」。
实践阶段 是整个过程的终点同时又是起点。之所以这么说是因为该过程并非简单的线性的结构,而是一个循环的过程。
通过具体的实践,不仅可以避免「一听就会,一做全废」的尴尬局面,还是对前面认识阶段的升华,是进入更高层级认识阶段的起点。
今天我将以 View 的事件分发机制 这一知识为切入点来进行内容创作。本文的定位为建立感性认识阶段。
关于 View 的事件分发机制,个人认为写的最好的两篇文章分别是:
KunMinX 的文风简洁干净,清楚地表述出 View 事件分发机制 的本质以及这一过程中的「消费」这一概念的理解。
却把清梅嗅 则站在设计者的角度,阐述了事件分发的全貌,其中 View 的事件分发机制 只是 UI 层事件分发的一个环节。
基于前一节的理论,掌握 View 的事件分发这一内容的过程可以这样拆分:
确认该知识的在「知识树」的位置:是 Android 中 UI 层事件分发的一个环节
在脑海中建立该内容的模型:
感性认识的深度与学习者所处知识层级有关,感性认识 → 理性认识 → 实践 是一个循环的过程,因此不同阶段的学习者感性认识的深度不同。
脑中建立模型后可以进行一些细节的学习,该过程学习者会进行从 1 到 N 到扩展思考与学习:
例如在查看源码实现之前,如果不清楚 N 叉树的遍历方式或者不理解递归算法的流程,很可能会被「消费」,「返回 true」等描述搞得晕头转向。
同样,处于不同知识层级的学习者对理性认识的深度也不同。因此对于源码的阅读,开始可以过滤掉细节,理清核心逻辑(简单的分发 + 拦截),后续关注之前忽略掉的细节(如多指的处理)。
对于前面的认识进行实践,例如自定义 ViewGroup 和 View,通过打日志等手段验证之前的学习内容,并在这一过程加深之前的认识并进入下一次循环。
本文的定位为感性认识阶段,适合对 View 事件分发机制理解不清晰,或者之前不知道该内容的读者阅读。
正文开始。
某公司有着严格的等级制度,模型类似 N 叉树:
该公司的员工可分为两种职位:View 和 ViewGroup。
除了上述通用的职位,还有一些特殊的「大佬级别」的职位:
该公司不仅有着严格的 等级 制度,还有着严格的 保密 制度。
每个员工只能和自己的上级/下级通信,并且上级并不了解下级的能力以及擅长的方向。
因此,当一个任务来临时,上级不知道这项工作下级能不能处理。
Android 的应用世界有着若干个这样的公司。
由于前面的各种制度制约,该公司接到任务后的处理流程可以抽象为:
从 N 叉树的根节点开始,遍历整个 N 叉树,寻找一个能够处理该任务的节点。
该公司处理任务的具体流程如下:
ViewGroup 拥有拦截任务指令的权限,拦截分为以下情况;
由于 ViewGroup 拥有拦截任务的权限,这样会导致下级没有机会表现,因此公司赋予了下属可以「拒绝上级拦截」的权限
一个任务由多个任务指令组成,如果每个任务指令来临都要遍历一次整个公司的所有员工,处理时间太长。
因此可以进行一些优化,减少遍历的次数。
如上图所示,如果一个任务的「任务开始」ViewGroup4 的处理结果 OK,那么该任务的后续指令可以无需遍历完整的树,而直接将该任务的后续指令直接交予 ViewGroup1 → ViewGruop4 这一分支(红色标识)。
上图演示了没有拦截并且遍历所有节点的情况。
DecorView -> Activity ->PhoneWindow 这一流程可参考 Android 事件分发机制的设计与实现 DecorView 的双重职责 一节。
任务开始
— ACTION_DOWN
任务进行
— ACTION_MOVE
任务结束
— ACTION_UP
任务取消
— ACTION_CANCEL
任务开始
— ACTION_DOWN
便拦截了 任务/事件,则 下级 ViewGroup/View 没机会让上级取消拦截下期内容将进入理性认识阶段,介绍 递归遍历 N 叉树的方式,责任链模式的常用实现,以及 View 事件分发的源码简析。
我是 Flywith24,我的博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉
目前我正专注于建立系统化的知识体系,内容更新在语雀上。感兴趣的小伙伴可以在相关文档底部评论区补充优质的资源以及技术细节。点击查看 。
Android 中有一个比较重要的概念:「生命周期」。刚毕业去面试,总会被问到「四大组件的生命周期」这类的问题。17年的 IO 大会上,Google 推出了 Lifecycle-Aware Components(生命周期感知组件),帮助开发者组织更好,更轻量,易于维护的代码
本文介绍 Lifecycle
的职责以及简单分析 lifecycle 如何感知 activity 和 fragment ,帮助您对 Lifecycle
有一个感性的认识
鲁迅曾说过:万物基于 Lifecycle
哦不对
Android 中的视图控制器就有这么多生命周期的情况,所以处理好生命周期十分重要,否则会导致内存泄漏甚至是程序崩溃。这里引用 官方文档 的例子
class MyLocationListener {
public MyLocationListener(Context context, Callback callback) {
// ...
}
void start() {
// 连接系统的定位服务
}
void stop() {
// 与系统的定位服务断开连接
}
}
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
@Override
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, (location) -> {
// 更新 UI
});
}
@Override
public void onStart() {
super.onStart();
myLocationListener.start();
//管理其他需要响应 activity 生命周期的组件
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
//管理其他需要响应 activity 生命周期的组件
}
}
此示例看起来不错,在实际的应用程序中,您仍然会响应生命周期的当前状态而进行过多的调用来管理 UI 和其他组件。 管理多个组件会在生命周期方法中放置大量代码,例如 onStart() 和 onStop(),这使它们难以维护
而且,不能保证组件在 activity 或 fragment 停止之前就已启动。 如果我们需要执行长时间运行的操作(例如onStart() 中的某些配置检查),则可能会导致争用情况,其中onStop() 方法在 onStart() 之前完成,从而使组件的生存期超过了所需的生存期。
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, location -> {
// 更新 UI
});
}
@Override
public void onStart() {
super.onStart();
Util.checkUserStatus(result -> {
// 如果在 activity 停止后调用此回调怎么办?
if (result) {
myLocationListener.start();
}
});
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
}
}
如果有所有的组件,都能感知外部的生命周期,能在相应的时机释放资源,并且在错过生命周期时能及时叫停异步的任务就好了,
我们不妨先思考一下,如果实现这样的想法,应该如何做
首先我们先来整理一下我们的需求
针对需求1,可以用观察者模式,内部组件能够在外部生命周期变化时做出相应
针对需求2,可以将依赖组件的代码移出生命周期方法内,然后移入组件本身,这样只需修改组件内部逻辑即可
针对需求3,可以在合适的时机移除观察者
关于开发者模式,我第一次比较详细的了解是在 扔物线 的 给 Android 开发者的 RxJava 详解。
观察者模式面向的需求是:A 对象(观察者)对 B 对象(被观察者)的某种变化高度敏感,需要在 B 变化的一瞬间做出反应。举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者(例如 A 不需要每过 2ms 就检查一次 B 的状态),而是采用注册(Register)或者称为订阅**(Subscribe)**的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 Android 开发中一个比较典型的例子是点击监听器
OnClickListener
。对设置OnClickListener
来说,View
是被观察者,OnClickListener
是观察者,二者通过setOnClickListener()
方法达成订阅关系。订阅之后用户点击按钮的瞬间,Android Framework 就会将点击事件发送给已经注册的OnClickListener
。采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。
OnClickListener 的模式大致如下图:
上述描述及图片均来自 给 Android 开发者的 RxJava 详解
因此在生命周期组件的生命周期发生变化时告诉观察者,内部组件即可感知外部的生命周期
public class MyObserver implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void connectListener() {
...
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void disconnectListener() {
...
}
}
myLifecycleOwner.getLifecycle().addObserver(new MyObserver());
这是 Lifecycle 的结构,抽象类,其内部有两个枚举,分别代表着「事件」和「状态」,此外还有三个方法,添加/移除观察者,获取当前状态
注意,这里 State 中的枚举顺序是有意义的,后文详细介绍
其实现类为 LifecycleRegistry ,可以处理多个观察者
其内部持有当前的状态 mState ,LifecycleOwner 以及观察者的自定义列表,同时重写了父类的添加/删除观察者的方法
LifecycleOwner ,具有 Android 的生命周期,定制组件可以使用这些事件来处理生命周期更改,而无需在 Activity 或 Fragment 中实现任何代码
LifecycleObserver ,将一个类标记为 LifecycleObserver
。 它没有任何方法,而是依赖于 OnLifecycleEvent 注解的方法
LifecycleEventObserver ,可以接收任何生命周期更改并将其分派给接收方。
如果一个类实现此接口并同时使用 OnLifecycleEvent,则注解将被忽略
DefaultLifecycleObserver ,用于监听 LifecycleOwner 状态更改的回调接口。
如果一个类同时实现了此接口和 LifecycleEventObserver,则将首先调用DefaultLifecycleObserver
的方法,然后再调用LifecycleEventObserver.onStateChanged(LifecycleOwner,Lifecycle.Event)
注意:使用 DefaultLifecycleObserver 需引入
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
首先我们还是来看 androidx.activity.ComponentActivity ,这个类我们这个系列的文章里提到多次,第一次提及是在 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析 ,感兴趣的小伙伴可以看看。
其实现的接口大多数我们都已经探讨过了,今天我们来看看 LifecycleOwner
ActivityResultCaller 为 activity 1.2.0-alpha02 推出的,旨在统一 onActivityResult ,这里暂时不讨论它
既然实现了 LifecycleOwner
接口,必定重写 getLifecycle() 方法
// androidx.activity.ComponentActivity.java
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
其返回的 Lifecycle
为 实现类 LifecycleRegistry
的实例
而 activity 操作生命周期是通过 ReportFragment
处理的
// androidx.activity.ComponentActivity.java
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ReportFragment.injectIfNeededIn(this);
//...
}
// ReportFragment
public static void injectIfNeededIn(Activity activity) {
if (Build.VERSION.SDK_INT >= 29) {
// api 29 及以上 直接注册正确的生命周期回调
activity.registerActivityLifecycleCallbacks(
new LifecycleCallbacks());
}
android.app.FragmentManager manager = activity.getFragmentManager();
if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
manager.executePendingTransactions();
}
}
// ReportFragment.java
static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
if (activity instanceof LifecycleRegistryOwner) {
((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
return;
}
if (activity instanceof LifecycleOwner) {
Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
if (lifecycle instanceof LifecycleRegistry) {
((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
}
}
}
private void dispatch(@NonNull Lifecycle.Event event) {
if (Build.VERSION.SDK_INT < 29) {
dispatch(getActivity(), event);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
dispatch(Lifecycle.Event.ON_CREATE);
}
@Override
public void onStart() {
super.onStart();
dispatch(Lifecycle.Event.ON_START);
}
@Override
public void onResume() {
super.onResume();
dispatch(Lifecycle.Event.ON_RESUME);
}
@Override
public void onPause() {
super.onPause();
dispatch(Lifecycle.Event.ON_PAUSE);
}
@Override
public void onStop() {
super.onStop();
dispatch(Lifecycle.Event.ON_STOP);
}
@Override
public void onDestroy() {
super.onDestroy();
dispatch(Lifecycle.Event.ON_DESTROY);
}
// LifecycleCallbacks
static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityPostCreated(@NonNull Activity activity,
@Nullable Bundle savedInstanceState) {
dispatch(activity, Lifecycle.Event.ON_CREATE);
}
@Override
public void onActivityPostStarted(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_START);
}
@Override
public void onActivityPostResumed(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_RESUME);
}
@Override
public void onActivityPrePaused(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_PAUSE);
}
@Override
public void onActivityPreStopped(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_STOP);
}
@Override
public void onActivityPreDestroyed(@NonNull Activity activity) {
dispatch(activity, Lifecycle.Event.ON_DESTROY);
}
//...
}
在 activity 的 onCreate 方法中,调用了 ReportFragment
中的静态方法 injectIfNeededIn()
。而其内部,如果 api 29 及以上的设备上直接注册正确的生命周期回调,低版本通过启动 ReportFragment ,借助 fragment 各个生命周期来处理生命周期回调
在 fragment 内部,每个生命周期节点调用 handleLifecycleEvent
方法
// Fragment.java
public Fragment() {
initLifecycle();
}
private void initLifecycle() {
mLifecycleRegistry = new LifecycleRegistry(this);
}
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
void performCreate(Bundle savedInstanceState) {
onCreate(savedInstanceState);
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
void performStart() {
onStart();
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
}
void performResume() {
onResume();
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
}
void performPause() {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
onPause();
}
void performStop() {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
onStop();
}
void performDestroy() {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
onDestroy();
}
Lifecycle.State
中有一个 isAtLeast
方法,用于判断当前状态是否不小于传入的状态
// Lifecycle.State
public boolean isAtLeast(@NonNull State state) {
return compareTo(state) >= 0;
}
枚举的 compareTo 方法其实是比较的枚举声明的顺序
而 State 的顺序为 DESTROYED -> INITIALIZED -> CREATED -> STARTED -> RESUMED
如果传入的 state 为 STARTED,则当前状态为 STARTED 或 RESUMED 时返回 true ,否则返回 false
LiveData 篇会用到这个知识点
笔者看过不少源码分析类的文章,动辄贴上大段代码,这种方式很容易打断读者的思路,所以很多时候看过这类文章感叹好文好文,却感觉什么都没记住,亦或者默默加入收藏却不知何时能去细心地研读。
所以本文不会过多介绍源码的细节,更多地是抛砖引玉,如果您看过本文后能够跟着本文的思路自己翻一下源码相信您就不会有我上述的体验了。
本文默认您已对 fragment 的生命周期有所了解,并清楚fragment的缘起与职责。这部分基础内容可移步 fragment 官方文档
也即本文不会介绍 “what”,而是介绍 “how” 并且探讨一下 “why”
这里贴一下 androidx fragment 源码地址
本文基于 androidx fragment 1.2.2 源码分析
implementation "androidx.fragment:fragment-ktx:1.2.2"
本文主要介绍fragment的启动流程,其他内容例如返回栈,会后续更新,敬请关注。欢迎在评论区下讨论。本文demo
既然我们都知道 “what”,不妨我们来思考一下 “how”
请大家思考一个问题,我们知道fragment 的生命周期是与其宿主 activity 的生命周期息息相关的,也即 activity 的每次生命周期回调都会引发每个fragment的类似回调。
那么,如果让我们来实现这样的操作,应该怎么做?
猜测:在activity每个生命周期的节点,去操作fragment,让其执行相应的生命周期方法。
思路有了,下面进行一些细节的确认。
对于第一条,我们抽象出一个可以管理 fragment 的模型,加入上下级的关系,即 activity 可管理其内部的 fragment,fragment 亦可管理其内部的 fragment。因此 fragment 同时充当着管理者与被管理者两种角色
对于后两条,相信在大学学过数据库的人会想到一种结构:事务(Transaction)
事务是指一组原子性的操作,这些操作是不可分割的整体,要么全完成,要么全不完成,完成后可以回滚到完成前的状态
因此,fragment 中两个最重要的概念出现了,FragmentManager
和 FragmentTransaction
FragmentManager
封装着对 fragment 操作的各种方法,addFragment
removeFragment
等等,而 FragmentActivity
通过 FragmentController
来操作 FragmentManager
FragmentTransaction
封装对 fragment 容器进行的 fragment 操作,例如在容器1内添加一个 fragment,同时在容器2内替换fragment。
它们均为抽象类,需要具体的实现类。
FragmentManager
的实现类为 FragmentManagerImpl
,其内部逻辑已全部移至 FragmentManager
中,是个空实现。
FragmentTransaction
的实现类为 BackStackRecord
,其内部引用了 FragmentManager
的实例 ,同时重写了父类的 四个 commit
相关的方法。
现在让我们看一部分代码,平时在activity中我们是这样填充一个fragment的
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//避免旋转屏幕等场景 fragment 重叠的问题
if (savedInstanceState == null) {
supportFragmentManager//步骤1
.beginTransaction()//步骤2
.add(R.id.container, BlankFragment.newInstance())//步骤3
.commitNow()//步骤4
}
}
步骤1,实例化 FragmentManagerImpl
对象 (内部经历了一些转换,详情参见源码或查看demo注释)
步骤2,实例化 BackStackRecord
对象,并在构造器中传入 FragmentManager
实例
步骤3,调用事务方法,对 fragment 容器进行相应的操作,本例表示在 id 为 container
容器内添加 BlankFragment
步骤4,提交事务,交于 FragmentManager
处理
在 terminal 敲入 adb shell setprop log.tag.FragmentManager VERBOSE
可开启FragmentManager
的日志功能,过滤 FragmentManager
,日志如下:
绿色部分为笔者手动添加的log,灰色和蓝色部分为 fragment 源码中的log
根据日志显示的流程,我们的猜测看似是正确的,“在 activity 每个生命周期的节点,去操作 fragment ,让其执行相应的生命周期方法”
其实这里是有干扰的,因为我们是在activity 的 onCreate
方法里 创建并提交 FragmentTransaction
,如果在 onResume
里调用呢?
WTF!
或许,我们的猜测有问题?看似调用 commitNow
后 fragment 的生命流程是自发进行的
那如果我们把调用挪到 onPause
呢?
打开 activity 并按下 home 键
我知道好奇的读者会尝试在 onStop
中尝试一下,有惊喜。手动滑稽。
从这几段日志上来看,fragment 在提交事务后会自发进入自己的生命周期流程,而当其宿主 activity 生命周期发生变化时,fragment 的生命周期也跟随变化。
如果这么说比较抽象的话,我们可以看在 onPause 中显示fragment 的日志,当 Fragment 进入 onStart 生命周期后,如果是正常流程应该进入 onResume,但由于按下 home 键 activity进入onStop,fragment 也进入了 onStop 状态
因此,我们将之前的猜测进行扩展:
- 在activity每个生命周期的节点,去操作fragment,让其执行相应的生命周期方法
- FragmentTransaction 被提交后 fragment 会进入自己的生命周期流程,但受 1 约束
那么我们的源码解读就从两个方向入手
activity 是通过 FragmentController
操作 FragmentManager
进而操作 fragment 的。
具体点就是在 activity 各个生命周期节点通过调用 FragmentController
中的各个 dispatch-
方法进而调用 FragmentManager
中的各个 dispatch-
方法
//FragmentActivity.java
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
//以下代码省略部分逻辑
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.dispatchCreate();
}
@Override
protected void onStart() {
mFragments.dispatchStart();
}
//onResume 彻底执行完毕的回调
@Override
protected void onPostResume() {
mFragments.dispatchResume();
}
@Override
protected void onPause() {
mFragments.dispatchPause();
}
@Override
protected void onStop() {
mFragments.dispatchStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
mFragments.dispatchDestroy();
}
这样猜测 1 就被证实了
activity 会在各个生命周期节点通过 FragmentController
间接调用 FragmentManager
中的 各种 dispatch-
方法,进而影响 fragment 的生命周期
那么嵌套 fragment 呢?
嵌套 fragment 也应该是宿主使用 FragmentManager
中的各种 dispatch-
方法,基于这个想法我们可以看一下 FragmentManager
中 dispatch-
方法的调用
可以看到这里有两处调用,第二处为activity 通过 FragmentController
间接调用,第一处使用的是 mChildFragmentManager
这里引出 fragment 中另外两个比较重要的概念,getParentFragmentManager()
和 getChildFragmentManager()
注意:
requireFragmentManager()
和getFragmentManager
已弃用
getChildFragmentManager()
获取的是fragment 中的 mChildFragmentManager
getParentFragmentManager()
获取的是fragment 中的 mFragmentManager
mChildFragmentManager
为fragment内部的 fragmentManager
// Private fragment manager for child fragments inside of this one.
@NonNull
FragmentManager mChildFragmentManager = new FragmentManagerImpl();
mFragmentManager
稍显复杂,
getSupportFragmentManager()
返回的 fragmentManager
getChildFragmentManager
所以 嵌套fragment 的生命周期是父 fragment 在各个生命周期节点上通过 mChildFragmentManager
调用 dispatch-
以影响其子 fragment 的生命周期
这样我们第一部分的解读就告一段落了, 这里点到为止,一些细节需要您自己亲自看看源码
在 看似最简单的启动流程
一节中我们分别在 activity 的 onCreate ,onResume,onPause 中分别开启并提交事务,来观察 fragment 的生命周期日志。
在没有 activity 干扰的情况下,fragment 的生命周期是自治的。
那么我们继续思考一个问题
Fragment 的生命周期是如何一环扣一环的执行的?
从上面的日志,我们看到很多 “moveto-” 的日志,
我们可以继续大胆地猜测,一个生命周期节点结束后调用进入另一个生命周期节点的方法
基于这个猜测,我们确认一些细节
fragment 应该有自己的状态,它可能自己管理内部的状态,也可能会有封装着状态转移的逻辑的专门管理状态的抽象
这里引出另外一个概念 FragmentStateManager
FragmentStateManager
中持有 fragment 的引用 mFragment
以及 FragmentManager
的状态 mFragmentManagerState
这里fragment的状态值为:
static final int INITIALIZING = -1; // Not yet attached.
static final int ATTACHED = 0; // Attached to the host.
static final int CREATED = 1; // Created.
static final int ACTIVITY_CREATED = 2; // Fully created, not started.
static final int STARTED = 3; // Created and started, not resumed.
static final int RESUMED = 4; // Created started and resumed.
FragmentStateManager
还封装着 fragment 状态转移的方法,例如:
void activityCreated() {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "moveto ACTIVITY_CREATED: " + mFragment);
}
mFragment.performActivityCreated(mFragment.mSavedFragmentState);
}
void start() {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "moveto STARTED: " + mFragment);
}
mFragment.performStart();
}
fragment 生命周期自治的核心逻辑封装在 FragmentManager
中的 void moveToState(@NonNull Fragment f, int newState)
内,主要代码为(精简后):
void moveToState(@NonNull Fragment f, int newState) {
FragmentStateManager fragmentStateManager = mFragmentStore.getFragmentStateManager(f.mWho);
newState = Math.min(newState, fragmentStateManager.computeMaxState());
if (f.mState <= newState) {
switch (f.mState) {
case Fragment.INITIALIZING:
if (newState > Fragment.INITIALIZING) {
fragmentStateManager.attach(mHost, this, mParent);
}
case Fragment.ATTACHED:
if (newState > Fragment.ATTACHED) {
fragmentStateManager.create();
}
case Fragment.CREATED:
if (newState > Fragment.INITIALIZING) {
fragmentStateManager.ensureInflatedView();
}
if (newState > Fragment.CREATED) {
fragmentStateManager.createView(mContainer);
fragmentStateManager.activityCreated();
fragmentStateManager.restoreViewState();
}
case Fragment.ACTIVITY_CREATED:
if (newState > Fragment.ACTIVITY_CREATED) {
fragmentStateManager.start();
}
case Fragment.STARTED:
if (newState > Fragment.STARTED) {
fragmentStateManager.resume();
}
}
}
}
注意:这里的switch 没有 break
细心的读者可能发现了,fragment 中的状态怎么只到 resume ,后续的状态呢?
我们可以看一下 FragmentManager
中的 dispatchPause
方法
void dispatchPause() {
dispatchStateChange(Fragment.STARTED);
}
为什么 dispatch 了 STARTED
的状态?其实刚刚 moveToState
方法我精简掉了一部分代码,留下的只有 f.mState <= newState
的逻辑,即 dispatch 的新状态大于等于当前的状态
而现在dispatch 的新状态比当前状态值小,则走了下面的逻辑,例如当前状态为 RESUMED ,新传递的状态为 STARTED,执行了 fragmentStateManager.pause();
void moveToState(@NonNull Fragment f, int newState) {
FragmentStateManager fragmentStateManager = mFragmentStore.getFragmentStateManager(f.mWho);
newState = Math.min(newState, fragmentStateManager.computeMaxState());
if (f.mState <= newState) {
//省略...
}else if (f.mState > newState) {
switch (f.mState) {
case Fragment.RESUMED:
if (newState < Fragment.RESUMED) {
fragmentStateManager.pause();
}
case Fragment.STARTED:
if (newState < Fragment.STARTED) {
fragmentStateManager.stop();
}
case Fragment.ACTIVITY_CREATED:
if (newState < Fragment.ACTIVITY_CREATED) {
if (isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "movefrom ACTIVITY_CREATED: " + f);
}
if (f.mView != null) {
if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {
fragmentStateManager.saveViewState();
}
}
if (mExitAnimationCancellationSignals.get(f) == null) {
destroyFragmentView(f);
} else {
f.setStateAfterAnimating(newState);
}
}
case Fragment.CREATED:
if (newState < Fragment.CREATED) {
boolean beingRemoved = f.mRemoving && !f.isInBackStack();
if (beingRemoved || mNonConfig.shouldDestroy(f)) {
makeInactive(fragmentStateManager);
} else {
if (f.mTargetWho != null) {
Fragment target = findActiveFragment(f.mTargetWho);
if (target != null && target.getRetainInstance()) {
f.mTarget = target;
}
}
}
if (mExitAnimationCancellationSignals.get(f) != null) {
f.setStateAfterAnimating(newState);
newState = Fragment.CREATED;
} else {
fragmentStateManager.destroy(mHost, mNonConfig);
}
}
case Fragment.ATTACHED:
if (newState < Fragment.ATTACHED) {
fragmentStateManager.detach(mNonConfig);
}
}
}
}
注意:这里的switch 还是没有 break
这里有个细节,由于activity没有 onDestroyView 的生命周期,所以 FragmentController
中的 dispatchDestroyView
是没有调用的
在 activity 中的 destroy 方法中通过 fragmentController
调用了 dispatchDestroy
内部调用 dispatchStateChange(Fragment.INITIALIZING)
,而此时的fragment 的 mState 为 ACTIVITY_CREATED
,所以 moveToState
方法会走到 ACTIVITY_CREATED
的 case 并执行到底
这样 fragment 最简单场景的生命周期就结束了
我们做一个总结:activity 和 fragment 会在各个生命周期节点通过被调用 fragment 的 parentFragmentManager
(或者说父 fragment 的 childFragmentManager
和 activity 的 supportFragmentManager
)中的各种 dispatch-
方法以影响子 fragment 的 生命周期,同时子 fragment 也拥有自己生命周期的调用链(从状态A转移至状态B)
不得不说 fragment 的很多 API 并不是很好用,从 androidx fragment 的更新频率也可以看出。比如 fragment 中的 view 和 fragment本身的生命周期是不一致的,存在onDestroyView 但 fragment没有销毁的情况
Ian Lake 在 Fragments: Past, Present, and Future (Android Dev Summit '19) 中提到未来官方会将二者合并,届时 fragment 的使用会更加简洁
这里引用 The Android Lifecycle cheat sheet — part III : Fragments 文中的图片 ,和我画的commit FragmentTransaction
的脑图(略简陋),帮您更好的理解
强烈建议您自己亲自看一看源码,不然就变为我文章开头时说的状态了。
原文:Flow: an intro for an RxJava user
作者:Mohamed Ibrahim
译者:Flywith24
RxJava
可能是我使用的最重要的库,Rx 通常是编写代码的另一种范式,Kotlin
作为一种新的编程语言,使它可以轻松实现将协程驱动的 flow 实现为自己的 Rx 实现。 我可能在 Hello Kotlin Coroutines 中介绍了协程,这对于理解 flow 很有必要
Kotlin
具有一组扩展,以方便使用集合。 但它不是响应式的
listOf("Madara", "Kakashi", "Naruto", "Jiraya", "Itachi")
.map { it.length }
.filter { it > 4 }
.forEach {
println(it)
}
在此示例中,如果您深入研究 map 函数源代码,您将发现这里没有魔法,它只是列表的循环,进行了一些转换然后为您提供了一个新列表。 过滤器也一样。 这种机制称为 eager evaluation
,该函数在整个列表中进行操作并提供一个新列表。 但是如果我们不需要创建这些临时列表以节省一些内存,那我们可以使用 Sequences
listOf("Madara", "Kakashi", "Naruto", "Jiraya", "Itachi")
// 使用 Sequence
.asSequence()
.map { it.length }
.filter { it > 4 }
.forEach {
println(it)
}
这里的区别就是先调用 asSequence
方法,然后使用我们的操作,再次 查看 map 方法后,我们发现了一些不同之处,它只是 sequence 的修饰符,返回值类型也是 sequence
。 使用 sequence map
时,只能一项一项地进行操作。列表较大时,sequence
比普通集合要好得多。sequence
可以同步完成其工作,有没有办法异步使用那些转换运算符呢?答案是 flow
如果我们尝试获取列表并将其用作 flow ,并在流的末尾调用 collect {..}
,则会收到编译错误。 由于 flow 是基于协程构建的,因此默认情况下它具有异步功能,因此您可以在代码中使用协程时使用它
collect {…}
运算符,您可以将其想像为 Rxjava
中的 subscribe
流也是 cold stream
,这意味着,直到您调用操作符(如 collect)后,flow 才会被执行。 如果您重复调用 collect ,每次您将获得相同的结果
因此,Collections 扩展功能仅适用于小数据,sequence
可以节省您不必要的工作(不创建临时列表),而使用 flow,您可以用协程的强大功能来编写代码。 因此,让我们学习如何构建它
我们看到 asFlow
方法,它是 Collections 上的扩展函数,可将其转换为 flow,我们查看一下源码
public fun <T> Iterable<T>.asFlow(): Flow<T> = flow {
forEach { value ->
emit(value)
}
}
如果我们要编写前面的示例在数据源中添加一些逻辑,则只需使用 flow{…}
或者 flowof()
flow 拥有一些列的用于转换的运算符,例如 map
, filter
, groupBy
,scan
等等
在由 Coroutines 提供支持的 flow
中,您可以自然地在您的操作符中使用异步代码,假设我们想要做一些耗时的操作,这里使用延迟一秒钟表示。 使用 RxJava
时,您可以使用 flatmap
这里想表达的是 flow 具有更简单的设计,并且与以其陡峭的学习曲线而闻名的 RxJava
相比易于学习,我在此使用 flow 将它简化一下
我已经提到 collect()
是 terminal operator,当您在调用它时得到结果,在 RxJava
中,您可以通过调用 subscribe()
来启动它,或者使用阻塞的方式,调用 blockingGet
flow 中的 terminal operator 是需要作用域操作的挂起函数,其他的 operator 例如
toList(),toSet -> 返回集合中的所有 item
first() -> 仅返回第一个发射
reduce(),fold() -> 使用特定操作获取结果
为了发射数据,您需要使用一个挂起函数
//fire a coroutine
someScope.launch {
//fire flow with a terminal operator
flowSampleData().collect { }
}
上面的花括号让人想起了回调,您可以使用 launchIn
函数,处理结果可以使用 onEach{...}
flowSampleData()
.onEach {
//handle emissions
}
.launchIn(someScope)
每次设置 RxJava
订阅时,我们都必须取消这些订阅以避免内存泄漏或过期的任务在后台运行,RxJava
提供对订阅的引用(disposable
)来取消订阅,disposable().dispose()
。如果您在 CompositeDisposable
使用了多个对象,则调用 clear()
或 dispose()
对于 flow 使用特定 scope 的协程则可以无需进行额外的工作来达到此目的
RxJava
最有用的功能之一就是处理错误的方式,您可以使用此 onError()
函数捕获工作流中的任何错误。 flow 有一个类似的称为 catch {…}
,如果不使用 catch {…}
,则您的代码可能会引发异常或应用崩溃。 您就可以选择使用常规 try catch 或使用 atch {…} 以声明方式进行编码
让我们模拟一个错误
private fun flowOfAnimeCharacters() = flow {
emit("Madara")
emit("Kakashi")
// 抛出异常
throw IllegalStateException()
emit("Jiraya")
emit("Itachi")
emit("Naruto")
}
使用
runBlocking {
flowOfAnimeCharacters()
.map { stringToLength(it) }
.filter { it > 4 }
.collect {
println(it)
}
}
如果我们运行此代码,它将引发异常,并且如我们所说,您有两个选项可以处理错误,即常规 try-catch
和 catch {…}
。 这是两种情况下的修改代码
// 使用 try-catch
runBlocking {
try {
flowOfAnimeCharacters()
.map { stringToLength(it) }
.filter { it > 4 }
.collect {
println(it)
}
} catch (e: Exception) {
println(e.stackTrace)
} finally {
println("Beat it")
}
}
// 使用 catch{}
runBlocking {
flowOfAnimeCharacters()
.map { stringToLength(it) }
.filter { it > 4 }
// catch
.catch { println(it) }
.collect {
println(it)
}
}
使用 catch{}
需要注意的是 catch{}
操作符的放置顺序,它要放置在 terminal operator 之前,这样您才可以捕获想要的异常
如果错误中断了流,并且我们打算使用完整备份或默认数据恢复流,在 Rxjava
中使用 onErrorResumeNext()
或 onErrorReturn()
,在 flow 中,我们还是使用 catch {…}
,但我们在其中调用了 emit()
来逐个生成备份,甚至我们可以使用 emitAll()
引入一个全新的 flow,例如如果中途出现了异常,我们需要“ Minato” 和 “ Hashirama”
runBlocking {
flowOfAnimeCharacters()
.catch {
emitAll(flowOf("Minato", "Hashirama"))
}
.collect {
println(it)
}
}
那么得到的结果是
Madara
Kakashi
Minato
Hashirama
默认情况下,flow 数据源将在调用者上下文中运行,如果要更改它,例如,要使 flow 在 IO 而不是 Main 上运行,则使用 flowOn()
,并更改上游的上下文,上游是调用 flowOn
之前的全部操作符。 这是一个很好的文档示例
这里的 flowOn()
充当 RxJava
中的两个角色 [subscribeOn() — observeOn()]
,您可以编写流然后确定将在哪个上下文中进行操作
当 flow 完成发射时,您可能需要执行一些操作,onCompletion {…}
可以解决这一问题,并且它确定 flow 是正常完成还是异常完成
已知数据源如下
private fun flowOfAnimeCharacters() = flow {
emit("Madara")
emit("Kakashi")
throw IllegalStateException()
emit("Jiraya")
emit("Itachi")
emit("Naruto")
}
catch {…}
的工作就是捕获 IllegalStateException()
并重新开始新流程,这使我们从源头上留下“ Madara”,“ Kakashi”,在后面留下“ Minato”,“ Hashirama”。 但是 onCompletion {…}
会显示错误吗?
答案是否定的,catch 捕获了所有错误,接下来是全新的事情,请记住 onCompletion {…}
和 catch {…}
只是中介程序运算符。 它们的顺序很重要
您可以使用 Flow builders 构建 flow,其中最基本的是 flow{…}
。 如果要开始该 flow,请调用诸如 collect {…}
之类的 terminal operator,并且由于 terminal operator 是挂起函数,因此需要使用协程构建器 launch {…}
的作用域,或者如果您想要以优雅的风格进行操作, 您可以结合使用 launchIn()
和 onEach {…}
。 使用 catch {…}
捕获上游错误,并根据需要提供回退流程。 onCompletion {..}
将在上游完成所有发射之后或发生错误时触发。 默认情况下,所有这些方法都适用于调用程序协程上下文,如果要更改上游上下文,请使用flowOn()
作为 Android 开发者,不知你是否也有这样的体验,随着项目变得越来越大,各种不同圆角的 shape,不同透明度的 color,不同大小的阴影效果,它们使资源文件越来越多
我认为造成这种问题的原因有两个:一个是产品设计的不规范,整个 app 没有统一的设计风格;第二个便是开发者在开发过程中编码的不规范
Android Dev Summit '19 有一场关于 Style 与 Theme 的演讲,它的 中文字幕视频在这里
我为你整理了每个主题所在的位置
时间 | 内容 |
---|---|
02:14 | Styling vs Theme |
08:55 | Theme Overlay |
12:36 | Color |
17:35 | 使用及三个技巧 |
24:00 | Material 颜色 |
28:06 | Material 排版 |
30:07 | Material 形状 |
34:41 | Dark Theme |
在视频下方的评论区,点击相应时间即可跳转到指定内容
与之对应的有一个 Styling 的系列文章,我最近翻译成了中文
本文整理了视频与文章中的内容,介绍在开发过程中,我们应如何利用 theme 与 style 更优雅地管理资源文件,并提供了很多实用的技巧,在标题中找到技巧相关的查看即可
并且提供了演示 demo,效果如下
关于 recreate 黑屏闪烁的问题,请见文章底部 20200705 更新
这部分内容在视频的 02.14 处
Style 是 View 属性的集合,可以将 Style 视为 Map<View Attribute, Resource>,其中 key 为 View 的属性,value 为资源
Resource 可以为以下类型
而 Theme 则不同,它的 key 是 「主题属性」,很显然下图中的 colorPrimary 不是任何 View 中的属性
主题属性有点像把配置抽象为语义化的命名的变量,并把它们塞到 map 中,以便未来使用,主题属性 与 View 属性很像,它们在 attr 中定义的方式以及对应的类型都是类似的,但二者仍有差异
在引用主题属性时,可以使用 ?.attr
语法,其中 ?代表在当前主题中搜索
如果我们 app 需要支持普通版本和 Pro 版本,它们的主色不同,我们只需定义两个主题,配置不同的 colorPrimary。接着我们需要适配深色主题,那么只需提供不同的数值即可
这就好比我们有一个 Theme 抽象类,而其中有一个抽象属性 colorPrimary,它有四个实现类,分别重写了 colorPrimary 属性,这样我们便得到了 四个变体,未来想加入新的变体,只需继承该抽象类并重写属性即可。看过 第一篇译文 的小伙伴知道,主题的作用范围是 「树」中的所有子节点。这样我们便很轻松地实现了更改程序主色的功能
如果要使用 Style 实现这一功能,首先,我们需要定义四种 Style。由于 Style 的作用范围是特定 View,因此我们要为每个 View 均定义四套 Style
简单来说,Style 与 Theme 的作用范围不同
Style 只会作用于单一的 View 中,使用时用 style
标签
Theme 会作用于「树」中的所有子节点,使用时用 theme
标签
在任意时刻,程序都是运行在某一特定主题下的,例如 activity 被设置了特定主题
我们在使用时应该注意 theme 与 style 各自的优势,灵活运用二者
这部分内容在视频的 08.55 处
主题是有继承关系的,当该继承关系链中有多个主题配置了同一属性,那么最继承链最底部的内容会生效,在下图中,如果多个主题都声明了 colorPrimary,那么 Theme.Owl.Pink 中的内容会生效(这有点像 Java 的继承关系)
利用这种继承关系我们可以实现在粉色主题下将部分界面使用蓝色主题
我们看一下两种主题的继承关系,这两种主题的父级应该是比较相似的,这看起来比较浪费,因为很多属性是相同的
另外,当你把一个主题设置在另一个主题之上,你需要注意不能将自己想要保留的东西被覆盖掉
Theme Overlay
可以很好地解决这一问题,它并不是一项新技术,而是属于一种技巧
接下来我们只需关注想要更改的东西,应用下图的声明的主题,则会只改变 colorPrimary 和 colorSecondary 两项属性的值,而其它的所有属性均不变
MaterialComponents 提供了 暗色的 Theme Overlay
使用该 Theme Overlay
可以将浅色主题中的某部分做成暗色主题
我们知道主题与 Context 相关,由于上文我们提到的主题的继承关系,使用正确的 Context 很重要
记住:使用距离最近 View 所在的 Context
当然更好的做法是使用主题属性
如果想要在代码中使用 Theme Overlay
,可以将其包裹为 ContextThemeWrapper,这也是 android:theme
标签内部做的事情
这部分内容在视频的 12.36 处
程序内最重要的资源便是 Color,Android 定义 Color 有很多种方式,比如 Color tag,它主要由 ARGB 色值组成
Color tag 也可以引用其他的 Color tag。需要注意的是,你不能在 Color tag 引用主题属性
以上是静态的颜色,下面我们谈一谈有状态的颜色
Color state list 允许你在不同状态下定义不同的颜色,它们也可以用作 drawable,例如下面的例子,在 button 按下或禁用时都有相应的颜色
下面我们来看一下 Color state list 是如何定义的,我们看到这里使用了 selector
标签,本示例中有两个 item,第一项定义了一种颜色,并指定它是在选中状态下才被使用;第二项 没有定义任何状态,这意味着它是默认颜色。如果没有其他颜色可以匹配当前状态时,它就会被使用
这里有一个小技巧,可以将 Color state list 按照最容易出现——最不容易出现的顺序进行排序,这由其背后的实现原理决定,系统会遍历每个 item 直至找出匹配项
在 API 21 官方引入了 android:alpha
tag 来设置透明度,并在 API 23 中可以引用主题属性。如果你使用 AppCompat 的话,它可以向下兼容到 API 14
我们在开发中可能会遇到这种情况:对于同一个颜色,我们需要不同的透明度。因此我们可能会复制不同透明度的色值
你可能不想在更改一个颜色后然后再逐一更改其相应透明度的色值,此处我们可以使用 ColorStateList,我们可以使用默认颜色的功能,只配置一个 item,并在此处配置透明度(从 0 到 1)
我们在 View 配置 background 等属性时可以直接传入 color,在内部系统会填充该颜色并将其包装为 ColorDrawable
但如果你将 ColorStateList 传入是不行的,在 API 28 及之前的设备会崩溃
这是因为 ColorDrawable 是无状态的,在 Android 10 中,官方加入了 ColorStateListDrawable 解决了这一问题
为了在所有 API 中获得相同的体验,我们可以使用一种变通的做法,使用 backgroundTint
此处使用纯色设置了一个矩形,接着使用 backgroundTint
指向了 ColorStateList
这部分内容在视频的 17.35 处
我们的项目中肯定有这样命名的资源,它们是按照主题属性命名的。Android Studio 新建项目默认的资源就是这样命名的
而你的主题大概是这样,主题属性指向同名的颜色资源
这样做是不推荐的
我们需要的不是一个语义的命名,而是需要一个文字的命名,我们可以用品牌颜色命名,也可以像 Material Color System,根据色调命名
大家可能见过这样的样式,一个叫 AppTheme,一个叫 Toolbar,从命名便可以看出它们的用途
但是如果我们加入了第三种样式,它的用途不是很明显,我们无法区分它是一个主题还是样式
为此我们可以约定一个命名规则
第一部分为 Style type:主题,样式,文本外观,Theme Overlay,形状外观等等
第二部分为 Group name:通常采用应用名称,如果是多 module 也可以为 module 名
第三部分为 Sub-group name:这名字通常用于 Widget,也就是使用样式的 View 的名称
第四部分为 Variant name:这是可选的,它是主题的变量
回到最初的例子,按照我们约定的命名规范改造就变成了这样
这里有一个值得注意的地方,在 Android System 中,.
是一个十分神奇的符号,这里有一个基于它的隐含的继承系统
上图的 Widget.MyApp.Toolbar.Blue 实际上继承了中间的这个主题
这种命名规范可以在 code review 时直观地判断出 style 或 theme 是否用错。如下图,很明显这里使用了 style 标签,却传入了一个主题
你甚至可以使用 Lint 来解决此问题,详情移步
将资源类型文件进行标准的分类
theme.xml
:Theme 和 Theme Overlaytype.xml
:字体,文本外观,文本尺寸,字体文件等style.xml
:只有 Widget styledimens.xml
colors.xml
strings.xml
:其它类型归类于实际的资源类型复杂模式是按照逻辑进行分类,例如形状相关的放入 shape.xml
,如果想要实现全屏的UI,可以在 sys_ui.xml
中控制状态栏/导航栏颜色,以及是否显示等等
在 Android Studio 的 Android 视图下,这样做的效果是很好的。如下图,可以很清晰的看到 light 主题和 dark 主题的主题文件
这部分内容在视频的 24:00 处
该系统构建基础是大量使用语义命名的变量,这些变量都属于「主题属性」。它的运作原理是 library 展示与这些使用语义命名的颜色相关的主题属性,而开发者负责为这些颜色提供数值。在 library 内,用这些颜色构建所有的 Widget
对于颜色系统,开发者需要了解一些常用的颜色
colorPrimary
和 colorSecondary
是 app 品牌的主要颜色,Variant 为主色的对比色;colorSurface
十分有用,它负责在某些控件表面的颜色;colorError
是错误的警示色,因此你没必要在使用时硬编码这些颜色
颜色系统还会提供一些 On
命名的颜色,这种颜色会保证拥有和类似名称颜色形成对比的颜色,例如 colorOnPrimary
永远会和 colorPrimary
形成对比
你可以在自己的主题中配置这些颜色,注意这里不必配置所有的颜色,如果你继承了一些 Material 主题,它们会提供所有颜色的默认色,比如下图中没有设置 colorSurface
,则会使用 Material Light 主题内定义的 colorSurface
之后你便可以在 layout 或 style 中使用这些颜色了
一个有用的技巧是可以将这些颜色与 ColorStateList 结合
比如我们想做一个分割线,不必创建名为 colorDivider 的新颜色,直接从 colorOnSurface 中取一个颜色即可,这个颜色肯定会和背景色形成对比
而且它会响应不同的主题,在浅色主题下,20% colorOnSurface
是一种黑中带白的颜色。在暗色主题下,colorOnSurface
会变成白色,此时的 20% colorOnSurface
会提供合适的对比
以语义命名的颜色是十分有用的,你可以省去大量的颜色定义
这部分内容在视频的 28:06 处
在设计中,通常使用固定的几种字号进行排版,例如大标题1,大标题2,文本主体,副标题,按钮等等
而这些都是作为主题属性实现的
然后便可以在应用中引用这些主题属性
Material 的 Text 十分强大,它可以设置行高,如果你遇到了设置行高却不生效的问题,使用 Material 组件可以解决这一问题
这部分内容在视频的 30:07 处
Material 采用了 shape system,该系统为小型,中型和大型组件 提供 了主题属性。请注意,如果要在自定义组件上设置 shape,则可能要使用 MaterialShapeDrawable
作为其背景,它可以理解并实现 shape
这里是通过 ShapeAppearance 来定义的,ShapeAppearance 和 TextAppearance 类似,是一种针对形状系统的配置
它由几个组件组成
首先是 cornerFamily,支持圆角和切角,圆角的方向等等
ShapeAppearance 还支持 overlay,可以更改特定的 Widget
在使用 overlay 时要注意的是很多 Material 组件是有着自己的 ShapeAppearance overlay,例如 BottomSheet,它会取消底部的圆角
形状系统是由 MaterialShapeDrawable 实现的
而 MaterialShapeDrawable 有一个强大的功能就是它有着一个属性叫 interpolation
使用它可以为形状系统做动画,如果它的值为0,那么所配置形状不会生效,如果值为1,那么形状系统会完整地应用到 drawable 上
这部分内容在视频的 34:41 处
暗黑主题的适配很简单,可以通过代码设置当前主题和获取当前主题
可以使用 Material 组件的 DayNight,这样在打开/关闭 暗黑主题时相应的主题属性的色值都会跟随变化
很多时候只做上面的两步并不能很好地适配暗黑主题,例如我们的应用在浅色主题下是这样的,深色的内容在浅色的背景上
而使用了夜间主题,可能会变成这样
而我们想要的效果是这样的
这是由于设置颜色时硬编码导致的
实际上,这种情况使用主题属性会有更好的效果
如果想要 colorPrimary
在不同的主题下使用不同的颜色,我们应该如何设置?
或许你会在 values-night/colors.xml
为暗色主题定义色值,但不建议这样做!
最好的做法是抽取公共部分到基础主题,然后在此基础上对浅色和深色主题分别配置差异化的属性
有些时候我们的 colorPrimary 是一种亮色,例如下图中的蓝色,但在暗黑主题下我们想使用相对较暗的颜色,例如 ?attr/colorSurface
,Material 组件内部为我们做好了转换,直接使用 ?attr/colorPrimarySurface
即可
demo地址在这里,如果感觉对你有帮助的话,点一颗小星星吧~ 😉
评论区有不少小伙伴觉得重新创建 activity 时有一个黑屏的效果,切换非常生硬。为了解决这一问题,我找了相关的资料,在 issuetracker 有人提过 activity.create() 方法导致黑屏的问题,但官方只是标记了「已分配」,并没有提供解决的明确的时间
因此我们只能寻找其它的方式,考虑到 recreate 会使 activity 重建,因此我们可以考虑在动画上 做些文章,比如将透明度柔和地过渡
这是之前的效果
虽然过渡效果不是特别完美,但比最开始的生硬切换要好一些
还有一种方式是不使用 recreate ,在重启 activity 时使用 finish + startActivity 并关闭动画来处理,但这样会导致原来的数据丢失,甚至 ViewModel 都非同一实例,因此我不是很喜欢这个方式
至于评论区提到的使用 view.getDrawingCache() 的方式,这篇文章有介绍 ,此处不再赘述
本文内容来自博文 I hated Gradle! Kotlin and the buildSrc Plugin made me love it
英文好的可直接前往
注意科学上网
gradle是如何帮助我们构建的?对于这个问题大所数人可能是这样的
图片摘自上述博文 I hated Gradle! Kotlin and the buildSrc Plugin made me love it
.
首先要明确的事情:Android-Gradle-Plugin != Gradle
造成上述问题的原因是 Groovy
大多数 Android 开发者并没有在“真正的项目”中使用 groovy ,所以对于我们来说 Android 中的 build.gradle
文件就像是魔术师手中的魔法。
.
2019年1月23日,Kotlin 1.3.20
发布,并在多平台项目中提供了对Kotlin DSL构建脚本的支持。
构建脚本中可以完成自动补全
我们可以像之前一样写代码,阅读文档,点击进去看到里面的实现,之前的烦恼不再有了
使用 buildSrc 文件夹
关于 builSrc
的职能和使用这里不再赘述,网上文章很多
Gradle dependency management with Kotlin (buildSrc)
Kotlin + buildSrc for Better Gradle Dependency Management
随着时间的推移,我们的 build.gradle
中的代码越来越长
配置 product flavor
,格式化apk文件命名以及路径,动态生成版本号,如果 jenkins 还需要配置一些独立的逻辑那么这个文件会越来越长
我们同一个项目的多个Android Module 大多是公共的样本代码,如果需要更改配置信息,则需要在所有的文件中修改
虽然我们可以使用apply form "..." 将部分逻辑抽取到 xx.gralde
中,可以使用 google 推荐的 ext
配置版本号,targetVersion 等信息。
但是这样真的足够灵活并方便拓展吗?
那么如何使用 kotlin + buildSrc 更改构建文件?
让我们看一下修改之前,这是一个非常庞大的文件,包含近200行代码
.
使用上述方法仅用34行代码即可重构为一段易于理解的代码
.
那么这段新的脚本文件都做了什么?
com.quickbirdstudios.bluesqaure
extension
.
下面我们来看看究竟是如何实现的吧
buildSrc
文件夹buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
/* Example Dependency */
/* Depend on the android gradle plugin, since we want to access it in our plugin */
implementation("com.android.tools.build:gradle:3.5.3")
/* Example Dependency */
/* Depend on the kotlin plugin, since we want to access it in our plugin */
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61")
/* Depend on the default Gradle API's since we want to build a custom plugin */
implementation(gradleApi())
implementation(localGroovy())
}
现在我们实现我们自己的插件,例如:MyPlugin
.
.
这里使用 kotlin 语言
.
首先创建 MyPlugin
class文件 implements Plugin<Project>
import org.gradle.api.Plugin
import org.gradle.api.Project
open class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
}
}
注意导包不要导错
配置自定义插件的id,例如 com.plugin.test
创建这样的文件 buildSrc/src/main/resources/META-INF/gradle-plugins/com.plugin.test.properties
com.plugin.test.properties 该文件名对应着插件id
在该文件中加入MyPlugin
的全路径(包括包名)
implementation-class=com.test.MyPlugin
现在我们可以在所有的module中使用该插件了
plugins {
// ...
id("com.android.library")
id("com.plugin.test")
}
这样的处理会使我们插件内部的 apply
函数(方法)得到调用
在我们配置Android 配置文件时我们会有这样的代码
android {
compileSdkVersion(29)
// ...
}
android{}代码块被称为 扩展(Extension)
这个函数的receiver(不知道如何翻译更合适) 实现了 AppExtension
这里贴一下源码
/**
* Retrieves the [android][com.android.build.gradle.AppExtension] extension.
*/
val org.gradle.api.Project.`android`: com.android.build.gradle.AppExtension get() =
(this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("android") as com.android.build.gradle.AppExtension
/**
* Configures the [android][com.android.build.gradle.AppExtension] extension.
*/
fun org.gradle.api.Project.`android`(configure: com.android.build.gradle.AppExtension.() -> Unit): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)
.
我们没必要在所有的module的build.gradle.kts
中都配置一次,可以将这段逻辑抽取到一个当我们的插件被应用时的函数中:
// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.configureAndroid()
}
}
// Android.kt
internal fun Project.configureAndroid() = this.extensions.getByType<AppExtension>().run {
compileSdkVersion(29)
defaultConfig {
minSdkVersion(21)
targetSdkVersion(28)
versionCode = 2
versionName = "1.0.1"
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
getByName("debug") {
isTestCoverageEnabled = true
}
}
packagingOptions {
exclude("META-INF/NOTICE.txt")
// ...
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
.
之后我们创建一个新的Android module 就很简单了,我们只需应用我们的插件com.test.MyPlugin
即可自动配置android{}代码块
plugins {
id("com.android.library")
id("com.test.MyPlugin")
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
testImplementation("junit:junit:4.12")
andriodTestImplementation("com.android.support.test:runner:1.0.2")
androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")
}
注意:只需使用android{}代码块再次配置即可覆盖插件中预置的配置
·
一般来说,我们每个android 都需使用下列依赖
我们可以在插件中添加另一个函数 configureDependencies
// MyPlugin.kt
open class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.configureAndroid()
project.configureDependencies()
}
}
// Dependencies.kt
const val jUnit = "junit:junit:4.12"
const val androidTestRunner = "com.android.support.test:runner:1.0.2"
const val androidTestRules = "com.android.support.test:rules:1.0.2"
const val mockkAndroid = "io.mockk:mockk-android:1.9"
const val mockk = "io.mockk:mockk:1.9"
const val espressoCore = "com.android.support.test.espresso:espresso-core:3.0.2"
internal fun Project.configureDependencies() = dependencies {
add("testImplementation", jUnit)
if (project.containsAndroidPlugin()) {
add("androidTestImplementation", androidTestRunner)
add("androidTestImplementation", androidTestRules)
add("androidTestImplementation", espressoCore)
}
}
internal fun Project.containsAndroidPlugin(): Boolean {
return project.plugins.toList().any { plugin -> plugin is AndroidBasePlugin }
}
.
现在创建Android module只需
plugins {
id("com.android.library")
id("com.test.MyPlugin")
}
.
上文的配置有一个缺陷:在Android plugin加载之后我们配置的插件才会正常工作。
但如果我们的所有module都是Android module,我们可以用自定义插件去管理
// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.configurePlugins()
project.configureAndroid()
project.configureDependencies()
}
}
//Plugins.kt
internal fun Project.configurePlugins() {
plugins.apply("com.android.library")
plugins.apply("org.gradle.maven-publish")
//其他公共插件
}
.
现在创建新的Android module只需
plugins {
id("com.test.MyPlugin")
}
上述配置可以为我们应用 Android Library Plugin
并配置了android代码块和默认依赖
.
如果想创建不加载Android plguin的纯 java module怎么办?
很简单,我们可以多创建几个插件(不要命名为buildSrc)
例如我可以创建 MyBasePlugin
, MyAndroidPlugin
, MyJavaPlugin
, MyMultiplatformPlugin
.
.
我们可以在应用了 Android Library
或者 Android Application
插件的module中使用android{}代码块配置
android {
compileSdkVersion(29)
}
.
Gradle API 将此定义为 Extension
我们也可以定义一些自定义的扩展
.
例如我们想处理两个配置
.
配置好后我们可以这样使用
plugins {
id("com.plugin.test")
}
test {
publish = true
packageName = "my-package"
}
.
我们只需创建一个类
open class TestExtension {
var publish: Boolean = false
var packageName: String = ""
}
.
接下来告诉Gradle有哪些配置
// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
override fun apply(project: Project) {
val testExtension: TestExtension = project.extensions.create(
"test", TestExtension::class.java
)
project.configurePlugins()
project.configureAndroid()
project.configureDependencies()
}
}
.
原文作者总结了他们公司使用 buildSrc plugin
只用7行代码便实现了以下工作
.
以上为原作者表达的主要信息,原文请移步
.
.
英文水平有限,如有纰漏请指教。
fragment 1.3.0-alpha04
发布了,其中有很多变动,其中提供了 fragment 间传递数据的新方式
首先我们介绍一下 API 更改
startActivityForResult()
/onActivityResult()
和 requestPermissions()
/onRequestPermissionsResult()
弃用prepareCall()
重命名为 registerForActivityResult()
target fragment API
被弃用由于官方提供了 Activity Result API 来替换 onActivityResult 机制,因此 fragment 的 startActivityForResult()
/onActivityResult()
和 requestPermissions()
/onRequestPermissionsResult()
方法被标记弃用了
Activity Result API 详情可参考 秉心说 的 是时候丢掉 onActivityResult 了 !
文章介绍的很详尽,这里不再赘述
值得注意的地方是 prepareCall()
被命名为 registerForActivityResult()
注意:在版本处于 Alpha 版状态时,可以添加、移除或更改 API。因此 Alpha 版本不适合在生产上使用
其实 target fragment API
早已被弃用
target fragment
需要直接访问另一个 fragment 的实例,这是十分危险的,因为你不知道目标 fragment 处于什么状态。而且 target fragment
不支持 Navigation
那么,fragment 之间传递数据更干净的方式是什么呢?
前文提到,在相同的 FragmentManager 中可以使用 target fragment API 来在 fragment 间传递数据,但这种方式需要直接访问目标fragment 的实例,这很危险,因为目标 fragment 的状态是未知的
因此官方提供了这样的 API,它允许在一个 fragment 上设置结果,并将该结果在 fragment 的适当的生命周期中使用。
这种传递数据的方式适用于 DialogFragment ,Navigation 中的 fragment
此更改还包括 -ktx 扩展功能以确保 kotlin 用户可以将 FragmentResultListener 作为 lambda 传递
老规矩,我们沿着官方的 commit log 来看看官方实现该功能的思路
首先,添加了 FragmentResultOwner 这样的的抽象,用于处理 fragment result,其内部有两个方法
前者用于发送数据,后者用于接收数据
而其实现类为 FragmentManager
我们来看看 FragmentManager 两个方法的具体实现
public final void setFragmentResultListener(@NonNull final String requestKey,
@NonNull final LifecycleOwner lifecycleOwner,
@Nullable final FragmentResultListener listener) {
// 设置的 listener 为空时将 requestKey 对应的 listener 移除
if (listener == null) {
mResultListeners.remove(requestKey);
return;
}
// 当fragment 处于DESTROYED 状态时 直接 return ,避免了异常
final Lifecycle lifecycle = lifecycleOwner.getLifecycle();
if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
return;
}
// 观察生命周期,fragment started 后接收回调,destroyed 移除回调
LifecycleEventObserver observer = new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_START) {
// once we are started, check for any stored results
Bundle storedResult = mResults.get(requestKey);
if (storedResult != null) {
// if there is a result, fire the callback
listener.onFragmentResult(requestKey, storedResult);
// and clear the result
setFragmentResult(requestKey, null);
}
}
if (event == Lifecycle.Event.ON_DESTROY) {
lifecycle.removeObserver(this);
mResultListeners.remove(requestKey);
}
}
};
lifecycle.addObserver(observer);
mResultListeners.put(requestKey, new LifecycleAwareResultListener(lifecycle, listener));
}
以上便是这部分的源码
这里要注意一点的是
fragment result api
是基于同一FragmentManager
的
官方一直致力于将 fragment 的 api 变得更好用
Ian Lake 在 Fragments: Past, Present, and Future (Android Dev Summit '19) 中提到了 fragment 间通信的问题,未来 fragment 会整合 fragment 自身和其内部 view 的生命周期,提供同一 FragmentManager 多返回栈的支持
看到 fragment result API
,我突然有个想法,如果将其应用到 Navigation 中是否是解决 Navigation 跳转返回后状态重置的一个方法呢?
各位小伙伴有什么想法欢迎评论区留言
原文:Fragments: Rebuilding the Internals. Introducing: the new state manager
作者:Ian Lake
译者:Flywith24
多年以来,Fragment 要比大多数 Android API 更新得更多。它们最初是 Android platform 的一部分,后来成为 Android Support Library 的一部分,现在以 AndroidX Fragments 的形式成为了 Jetpack 组件的一部分。
注意:您绝不应该使用 Android framework 版本的 Fragment。不仅因为它已经在 Android 10 中完全弃用了,还因为它已经长时间没有被修复 bug,而且保证不同设备和 API 级别之间的一致性。
虽然 Architecture Components 接管了很多过去需要使用 Fragment 的场景(例如 使用 LifecycleObserver 处理生命周期的回调,或者使用 ViewModel 来保留状态),如果您使用 Fragment 来处理这些场景,则需要通过 FragmentManager
来进行 add / remove 操作来与之交互。
随着 Fragment 1.3.0-alpha08
的发布,FragmentManager
内部的重大重构已经完成。该版本使用 更小的,可测试的,可维护的(内部)类替换了许多 FragmentManager
中的逻辑。其核心类是 FragmentStateManager。
注意:我将在这篇文章中讨论 FragmentManager 的内部原理。如果在使用 1.3.0-alpha08 遇到任何问题,请尽快提交 file issues 协助我们修复。
新的 state manager 的职责:
我们彻底回顾了这些系统过去的工作方式,发现 它们需要被从头重写 ,于是我们重写了它们。它们比以往的任何时候都更好,我们能够关闭至少 10 个长期存在的相关 issues,并且这个内部重构为单 FragmentManager 支持 多返回栈 扫清了道路(译者注:Bottom Navigation 管理平级界面的问题),并简化了 Fragment 的生命周期。
每个 FragmentManager
都与一个 host 关联,对于大多数 fragment,host 为 FragmentActivity(使用 FragmentController
和 FragmentHostCallback
可以自定义 host,但不在本文的讨论范围)。当 activity 转移到 CREATED
,STARTED
,以及RESUMED
, FragmentManager 派发这些更改到它的 Fragments。这是 moveToState()
的职责。(译者注:源码分析参见 【背上Jetpack】从源码角度看 Fragment 生命周期)。
当然,它没有那么简单。有很多条件逻辑来确定 fragment 该处于什么状态—— activity 的生命周期状态(如果是嵌套 Fragment 则是其 parent fragment 的生命周期状态)只是第一部分,它被称为 fragment 能够处于的 max state。最大状态保证了 activity ,fragment,以及它们的 child fragment 能够正确地嵌套。
因此 简化 moveToState()
的首要任务是将所有逻辑抽离到一个位置。于是 FragmentStateManager 诞生了。每个 fragment 实例与一个 FragmentStateManager
绑定。通过引入这个内部类,我们能够从 FragmentManager
中抽取与 fragment 交互的大量代码(例如调用 fragment 的 onCreateView
方法以及其他生命周期方法)。
该拆分还使我们能够编写一个方法,该方法将使用所有向后兼容的所需逻辑来确定 fragment 实际应处于的状态,并将其集中在一个位置:computeExpectedState()。该方法追踪当前的所有状态并且确定 fragment 应处于什么状态。98% 的时间,他们与 host / parent fragment 处于同一状态,但是剩下的 2% 与那些建立在 fragment 上的 app 有很大不同。
但是有一种我们无法确定正确状态的情况:postponed fragments。
无论是好是坏,Fragment 都继承了许多与 Activity 相同的命名的 API。这种继承的一部分是围绕转换以及推迟转场直到目标准备好的能力。这对共享元素转场非常重要,与此同时还要确保在转场的同时不会产生更密集的数据加载(译者注:为了转场时先完成动画,再完成大数据的渲染)。
延迟 fragment 拥有两个重要的特性:
STARTED
当您调用 startPostponedEnterTransition()
后,fragment 的转场便会执行,view 将变得可见,并且 fragment 将会移动到 RESUMED
。实际上,这正是新的 state manager 做的,过去的 fragment 不是这样工作的,详情参考 Postponed Fragments leave the Fragments 和 FragmentManager in an inconsistent state 这两个bug。
当 fragment 被使用
postponeEnterTransition()
推迟了,预期的行为是:fragment 被添加到的 container 不会运行任何进入动画或者之前排队的退出动画(例如replace
操作)直到 Fragment 调用startPostponedEnterTransition()
。同时当 fragment 的 container 被延迟时,fragment 不会达到RESUMED
状态。然而,似乎
FragmentManager
没有执行上述操作,反而将 Fragment 和整个 FragmentManager 转移至一个怪异的,不一致的状态。也就是说,任何与 postponed Fragment 的 container 相关的 FragmentTransaction 被「回滚」(如返回上一 fragment),但实际上,这些 fragment 没有移至其正确的状态。
这导致了一些列的问题;
实际上解决这些问题中的任何一个都意味着需要一个系统替换 postponed fragment 使用的整个回滚过程,该系统保证 FragmentManager 的一致性,最新状态,同时保留 postponed fragment 的主要特性。
FragmentManager
具有 container 这个不错的属性(读起来:很方便,但作为维护者却不那么有趣),在该属性中,您可以为要放置 Fragment 传入任何 container id 。甚至在一个 FragmentTransaction
中,您可以 add
一个 fragment 到一个 container,从一个不同的 container remove
另一个,replace
第三个container 最顶端的 fragment,等等。
fragment 动画进/出会产生接触,这仅发生在 container 层。
Fragment 支持一些列的动画系统:
Animation
APIAnimator
APITransition
API(仅支持 21+,同样很烂)Transition
API众所周知,命名是计算机科学中最难的问题之一,因此当我们去构建一个可以控制所有这些 API 的类时,花了一些时间才决定使用 SpecialEffectsController(该类不是 public API,因此命名可能更改)。该类存在于 container 层,并且协调进入和退出 fragment 相关的所有特效("special effects") 。
SpecialEffectsController
是 container 中真实情况的 唯一信源。这意味着如果最顶部被 add 的 fragment 被推迟了,整个 container 也将被推迟。不再需要 FragmentManager 层的逻辑,也不需要 transaction 的任何回滚(如我们前文提到的,它将影响多个 container)。因此, FragmentManager 处于正确的状态,我们仍可以获得被延迟 fragment 的所有属性。
这个 API 还允许我们将 fragment 的所有疯狂的特效 API 集中到一个 DefaultSpecialEffectsController
中,该 controller 负责运行 transition,animation,以及 animator。再次过去分散在 FragmentManager 中的逻辑移动到了一个地方。
它意味着要替代这种架构:
旧的 state manager:所有逻辑都在
FragmentManager
看起来更像这样:
新的 state manager:
FragmentManager
与各个FragmentStateManager
实例进行通信,这些实例通过SpecialEffectsController
与 container 中的其它 fragment 进行协调。
通过拆分 FragmentManager 的内部结构,每一层的逻辑都得到了极大简化:
FragmentManager
仅具有适用于所有 fragment 的状态FragmentStateManager
在 fragment 层管理状态SpecialEffectsController
在 container 层 管理状态职责的分离使我们的测试套件扩大了近 30%,涵盖了几乎无法单独测试的更多场景。
不。事实上,我们对新旧状态管理者都进行了很大一部分的 fragment 测试,专门用于确保我们有一套强大的回归测试。
但是,如果您依赖于不一致的状态,则可以将 FragmentManager 放入延迟的 fragment 中,然后,是的,您会发现实际上已经获得了正确的状态。 在 发行说明 中,您会找到与新状态管理器相关的错误修复列表,因此请仔细阅读以确保您的问题不是由您自己的解决方法引起的,而该解决方法是您可以现在删除的旧行为 。
与 Fragment 1.2.0 中的 onDestroyView 计时更改 类似,新的状态管理器将使 fragment 保持在 STARTED 状态,直到其 transitions/animations/animators/special effects 全部完成为止,从而使所有 fragment 保持一致,无论它们是否是直接 postponed 或由于同一 container 中的其它 fragment 引起的 postponed 。
新的状态管理器会默认开启。如果你在你的 app 中看到了与之前不同的行为,首先使用以下新的实验性的 API 来排查是否是由于新的状态管理器导致的。
当你更新了 Fragment 1.3.0-alpha08
后,新的状态管理器会默认开启。如果你在你的 app 中看到了与之前不同的行为,首先使用以下新的实验性的 API 来排查是否是由于新的状态管理器导致的。
我国启新的状态管理器。如果您发现应用程序和过去存在差异,首先可以使用新的实验性的 API 来排查是否与新的状态管理器相关:
FragmentManager.enableNewStateManager(false)
该 API 可以让您重温旧世界,让您验证所看到的的任何与之前不一致的改变是否由于新的状态管理器导致。如果确认是由于新的状态管理器导致,您可以构建一个示例项目重现您遇到的问题,并 在此提 Issue。
注意:
FragmentManager.enableNewStateManager()
API 是 实验性的。这意味着它不被视为 Fragment 稳定API 的一部分,可以随时删除。 删除所有旧代码可以节省大量代码,但是鉴于正确处理代码的重要性,我们可能要等到 Fragment 1.3.0 的稳定版本发布后才能删除 API,比如考虑在 fragment 1.3.1 发布时移除。
在 11 个月内进行了 100 多次个人更改,这绝对是一段时间内 Fragment 的最大内部更改,它使我们建立了更加可维护,可持续和可理解的代码。 这意味着跨 Fragment 的行为更加一致,并且它成为您在构建应用程序时可以依靠的坚实基础。 我们非常感谢您继续提出问题并 提供反馈。
感谢 Jeremy Woods,Chet Haase,和 Nick Butcher。
译文完。
在 Fragments: Past, Present, and Future (Android Dev Summit '19) 演讲中 Ian Lake 阐述了 fragment 的未来。掘金官方文章在这。
其中 弃用 targetFragment API 使用新的 FragmentResult API 已经发布:详情移步【Jetpack更新之Fragment】1.3.0-alpha04 来袭,Fragment 间通信的新姿势
Fragment 的生命周期正在被简化,例如 onActivityCreated 被弃用了,详情移步【Jetpack更新之Fragment】终于动手了,onActivityCreated 被弃用。不过距离合并 Fragment 自身与其内部 View 的生命周期还有很长一段路(不知官方是否会放弃)。
多返回栈一直被本文解决的问题阻塞,文中所说的过去 11 个月便是去年 9 月 Android Dev Summit '19 到现在。而下面的 issue 是2018 年 5 月创建的。
好在多返回栈的绊脚石已经解决,不过这里我们也可以看到 SDK 级别代码难以维护后的优化成本,文中重写了 fragment 状态管理逻辑,即使做了大量测试,仍要保留旧版逻辑并留出切换的入口。
查看 Fragment 代码变化我们能很明显的认识到「职能单一」重要性,这对构建可维护的,可理解的,可测试的,健壮的代码十分重要。
关于 fragment 的其他内容,可参考
很高兴见到你 👋,我是 Flywith24 。
最近 Android 官方针对 Fragment 文档进行了重新编写,使其适应 2020 年最佳实践的快速发展。
Fragment 的确是一个让开发者头疼的组件,它是一个很好的设计,但一直处于可改进的状态,随着 AndroidX Fragment 的快速更新,Fragment 已不同往日,虽然仍有改进的空间(单个 FragmentManager 不支持多返回栈,Fragment 自身和其 view 的生命周期不一致)。考虑到该文档的确有很多新知识以及官方文档的极慢的汉化速度,本文将 2020 版 Fragment 的官方文档翻译成中文,喜欢一手信息的小伙伴可直奔 官方原文。如果只想关注新文档中的变化,可 点此直达。限于篇幅原因,该文档分上下两部分。
【译】2020 年 Fragment 最新文档(上),该更新知识库啦
【译】2020 年 Fragment 最新文档(下),该更新知识库啦
本文为下半部分,将介绍以下内容:
上半部分介绍:
我是一个「强迫症晚期患者」,为了移动端更好阅读的体验,我经常将代码以图片的形式插入到文内。但随之而来出现一个问题:没办法 copy 代码(这对 cv 开发者很重要的 🤣)。
前些天,我在 github 某个项目的 README
文档中看到一个技巧,便是把较长且有些影响阅读的内容折叠,读者可以自由地选择展开。
这也是这个「彩蛋」的显示方式。后文中关于代码的部分我都会提供图片和可复制的源码两部分,其中后者处于折叠状态。您可以点击 「点击查看代码详情」以展开源码。
彩蛋结束。🥳
各种 Android 系统操作可能会影响 fragment 的状态。 为了确保用户状态得到保存,Android 会自动保存并还原 fragment 的返回栈。因此,您需要确保 fragment 中的所有数据也被保存和还原。
下表罗列了导致 fragment 丢失状态的操作,以及各种状态是否被保存。表中提到的状态类型如下:
onSaveInstanceState()
中通常,Variables 与 SavedState 的处理方式相同,但下表将两者进行了区分,以展示各种操作对它们的影响:
*NonConfig state 在进程死亡时可以使用 Saved State module for ViewModel 保存状态。
让我们看一个具体的例子。我们生成一个随机字符串将其显示在 TextView
中,并提供一个发送给朋友之前编辑该字符串的选项:
用户按下编辑按钮后,将显示一个 EditText 视图,用户可以在其中编辑消息。如果用户点击CANCEL,则应清除 EditText 视图,并将其可见性设置为 View.GONE
。为了保持良好的体验,该示例需要管理 4 个数据:
以下各节介绍如何正确管理数据状态。
View 负责管理自己的状态。例如,当 view 接受用户输入时,view 负责保存该输入以确保配置变化时能够恢复状态。所有 Android 官方提供的 view 均重写了 onSaveInstanceState()
和 onRestoreInstanceState()
方法,因此您不必管理 fragment 中的 View State。
🌟 注意:为了确保配置更改是能够正确处理状态,您的自定义 View 应该重写
onSaveInstanceState()
和onRestoreInstanceState()
方法
例如,在前面的场景中,已编辑的字符串保存在 EditText 中。EditText 知道其显示的文本的值以及其它详细信息(如选定文本的开头和结尾)。
View 需要一个 ID 来恢复状态。这个 ID 必须在其所在的 fragment 视图树中唯一。没有 ID 的 View 不能恢复状态。
如表 1 所示,除了 fragment 被移除且没加入到返回栈和宿主销毁这两种情况,view 可以保存和恢复其 ViewState
。
您的 fragment 负责管理少量动态状态,这些动态状态对于 fragment 的功能至关重要。 您可以使用 Fragment.onSaveInstanceState(Bundle)
保存便于序列化的数据。与 Activity.onSaveInstanceState(Bundle)
相似,Bundle 中的数据将在 配置发生变化和系统资源回收 时保存,并且该 Bundle 在 fragment 的 onCreate(Bundle)
,onCreateView(LayoutInflater, ViewGroup, Bundle)
和 onViewCreated(View, Bundle)
方法中可用。
⚠️ 注意:fragment 的onSaveInstanceState(Bundle)
仅在其宿主 activity 的onSaveInstanceState(Bundle)
调用时调用。
Tips:当使用 ViewModel
时,可用直接使用 SavedStateHandle
保存数据。更多的信息请参考:Saved State module for ViewModel(译者注:也可参考译者文章 绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析)。
继续前面的示例,randomGoodDeed
是显示给用户的数据,isEditing
是确定 fragment 显示或隐藏 EditText
的标志。这种 save state 应使用 onSaveInstanceState(Bundle)
保存,如下所示:
// 👇 Kotlin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(IS_EDITING_KEY, isEditing)
outState.putString(RANDOM_GOOD_DEED_KEY, randomGoodDeed)
}
// 👇 Java
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(IS_EDITING_KEY, isEditing);
outState.putString(RANDOM_GOOD_DEED_KEY, randomGoodDeed);
}
要在 onCreate(Bundle)
中恢复状态,可用从 Bundle 中取值:
// 👇 Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isEditing = savedInstanceState?.getBoolean(IS_EDITING_KEY, false)
randomGoodDeed = savedInstanceState?.getString(RANDOM_GOOD_DEED_KEY)
?: viewModel.generateRandomGoodDeed()
}
// 👇 Java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
isEditing = savedInstanceState.getBoolean(IS_EDITING_KEY, false);
randomGoodDeed = savedInstanceState.getString(RANDOM_GOOD_DEED_KEY);
} else {
randomGoodDeed = viewModel.generateRandomGoodDeed();
}
}
如表 1 所示,请注意,当 fragment 被加入到返回栈时 Variables 会被保存,将 Variables 看作 成 SavedState 来处理可以确保在所有场景下都能保存这些变量。
NonConfig 数据应放在 fragment 之外,例如在 ViewModel
中。在上面的示例中,seed
(NonConfig sate)在 ViewModel
中生成,由 ViewModel
负责保存其状态。
// 👇 Kotlin
public class RandomGoodDeedViewModel : ViewModel() {
private val seed = ... // 生成 seed(种子)
private fun generateRandomGoodDeed(): String {
val goodDeed = ... // 使用 seed 生成 goodDeed
return goodDeed
}
}
// 👇 Java
public class RandomGoodDeedViewModel extends ViewModel {
private Long seed = ... // 生成 seed(种子)
private String generateRandomGoodDeed() {
String goodDeed = ... // 使用 seed 生成 goodDeed
return goodDeed;
}
}
ViewModel
类本质上允许数据在配置发生变化(例如屏幕旋转)中幸存下来,并且在将 fragment 放回返回栈中时仍保留在内存中。在系统资源回收(进程死亡并重新创建)之后,将重新创建 ViewModel
,并生成一个新种子。 在 ViewModel
中添加 SavedState
模块可以使 ViewModel
在系统资源回收的场景下保留其内部数据。
为了复用 fragment,需要将每个 fragment 构建为完全独立的组件并定义自己的布局和行为。定义可复用的 fragment 并将它们与 activity 关联便可为 app 建立复合型 UI。
为了正确响应用户事件或共享状态信息,开发者通常需要在 activity 和它的 fragment 之间或两个到多个 fragment 之间建立通信。为了保证 fragment 的独立性,您 不应 让 fragment 与其它 fragment 或其宿主直接通信。
Fragment 库提供了两个通信选项:共享 ViewModel
和 Fragment Result API
。如何选择应视场景而定: 要与任何自定义 API 共享持久数据,应使用 ViewModel
。对于可以放入 Bundle 的一次性的结果类数据,应使用 Fragment Result API
。
下文介绍如何使用 ViewModel
和 Fragment Result API
在 fragment 和 activity 之间通信。
ViewModel 是多个 fragment 或 fragment 与其宿主之间共享数据的理想选择。ViewModel 对象存储并管理 UI 数据。关于 ViewModel 的更多信息,请参考 ViewModel overview(译者注:也可参考译者文章 即使您不使用 MVVM 也要了解 ViewModel)。
在某些场景下,您可能需要在 fragment 及其宿主 activity 之间共享数据。例如,您可能在 fragment 中操作全局的 UI 组件。
看看下面的 ItemViewViewModel
:
// 👇 Kotlin
class ItemViewModel : ViewModel() {
private val mutableSelectedItem = MutableLiveData<Item>()
val selectedItem: LiveData<Item> get() = mutableSelectedItem
fun selectItem(item: Item) {
mutableSelectedItem.value = item
}
}
// 👇 Java
public class ItemViewModel extends ViewModel {
private final MutableLiveData<Item> selectedItem = new MutableLiveData<Item>();
public void selectItem(Item item) {
selectedItem.setValue(item);
}
public LiveData<Item> getSelectedItem() {
return selectedItem;
}
}
在上面的示例中,要存储的数据包装在 MutableLiveData
类中。LiveData
是可感知生命周期的可观察的数据持有者类。MutableLiveData
有着公开的更改值的方法。有关 LiveData 的更多信息,请参见 LiveData overview(译者注:也可参考译者文章 ViewModel 的左膀右臂 数据驱动真的香)。
通过将 activity 传递给 ViewModelProvider
构造器,您的 fragment 及其宿主 activity 都可以获取 activity 范围内共享的 ViewModel
实例,ViewModelProvider
负责实例化 ViewModel
或获取它(如果已经存在)。activity 和 fragment 都可以观察和修改该数据:
// 👇 Kotlin
class MainActivity : AppCompatActivity() {
// activity-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ItemViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.selectedItem.observe(this) { item ->
// 根据最新的数据执行操作
})
}
}
class ListFragment : Fragment() {
// fragment-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ItemViewModel by activityViewModels()
// 当 item 点击时调用
fun onItemClicked(item: Item) {
// 设置新的 item
viewModel.selectItem(item)
}
}
// 👇 Java
public class MainActivity extends AppCompatActivity {
private ItemViewModel viewModel;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 👇 传入 activity,得到 activity 范围共享的 ViewMoel
viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
viewModel.getSelectedItem().observe(this, item -> {
// 根据最新的数据执行操作
});
}
}
public class ListFragment extends Fragment {
private ItemViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 👇 传入 activity,得到 activity 范围共享的 ViewMoel
viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class);
...
items.setOnClickListener(item -> {
// 设置新的 item
viewModel.select(item);
});
}
}
⚠️ 警告:请确保在ViewModelProvider
中 使用合适的作用域。在上面的示例中,MainActivity
是MainActivity
和ListFragment
的作用域,因此它们能够获得相同的ViewModel
对象。如果ListFragment
改用自身的作用域,则将获得与MainActivity
不同的ViewModel
对象。
同一 activity 中的两个或多个 fragment 通常需要相互通信。例如,一个 fragment 显示列表,另一个 fragment 允许用户将各种过滤选项筛选列表内容。如果没有 fragment 之间的直接通信,那么实现这种功能可能并不容易,这意味着这两个 fragment 不再是独立的。此外,两个 fragment 都必须处理另一个 fragment 尚未创建或不可见的情况。
这些 fragment 可以 使用所在 activity 范围内共享的 ViewModel
来处理通信。通过以这种方式共享 ViewModel
,fragment 之间无需彼此了解,并且 activity 无需执行任何操作即可完成通信。
以下示例显示两个 fragment 如何使用共享的 ViewModel
进行通信:
// 👇 Kotlin
class ListViewModel : ViewModel() {
val filters = MutableLiveData<Set<Filter>>()
private val originalList: LiveData<List<Item>>() = ...
val filteredList: LiveData<List<Item>> = ...
fun addFilter(filter: Filter) { ... }
fun removeFilter(filter: Filter) { ... }
}
class ListFragment : Fragment() {
// fragment-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ListViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// 👇 注意这里的 lifecycleOwner 是 view 的
viewModel.filteredList.observe(viewLifecycleOwner) { list ->
// 更新 list UI
}
}
}
class FilterFragment : Fragment() {
// fragment-ktx 提供的属性代理,获取 activity 范围内共享的 ViewModel
private val viewModel: ListViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// 👇 注意这里的 lifecycleOwner 是 view 的
viewModel.filters.observe(viewLifecycleOwner) { set ->
// 根据选中的过滤条件更新 UI
}
}
fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter)
fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter)
}
// 👇 Java
public class ListViewModel extends ViewModel {
private final MutableLiveData<Set<Filter>> filters = new MutableLiveData<>();
private final LiveData<List<Item>> originalList = ...;
private final LiveData<List<Item>> filteredList = ...;
public LiveData<List<Item>> getFilteredList() {
return filteredList;
}
public LiveData<Set<Filter>> getFilters() {
return filters;
}
public void addFilter(Filter filter) { ... }
public void removeFilter(Filter filter) { ... }
}
public class ListFragment extends Fragment {
private ListViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
// 更新 list UI
});
}
}
public class FilterFragment extends Fragment {
private ListViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
viewModel.getFilters().observe(getViewLifecycleOwner(), set -> {
// 根据选中的过滤条件更新 UI
});
}
public void onFilterSelected(Filter filter) {
viewModel.addFilter(filter);
}
public void onFilterDeselected(Filter filter) {
viewModel.removeFilter(filter);
}
}
请注意,两个 fragment 都将其宿主 activity 作为 ViewModelProvider
的作用域。因为 fragment 使用相同的作用域,所以它们会获得相同的 ViewModel
实例,这使它们可以相互通信。
⚠️ 警告:ViewModel
会保留在内存中,直到其作用域所在的ViewModelStoreOwner
永久消失。在单 activity 体系结构中,如果ViewModel
的作用域为 activity,则它实质上是一个单例。首次实例化ViewModel
之后,使用 activity 作用域获取ViewModel
将始终返回相同的现有ViewModel
实例和现有数据,直到 activity 的生命周期永久结束。
使用子 fragment 时,您的父 fragment 及其子 fragment 可能需要彼此共享数据。要在这些 fragment 之间共享数据,请 使用父 fragment 作为 ViewModel 的作用域。
// 👇 Kotlin
class ListFragment: Fragment() {
// 使用 fragment-ktx 提供的属性代理获取 ViewModel(作用域为当前 fragment)
private val viewModel: ListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner) { list ->
// 更新 list UI
}
}
}
class ChildFragment: Fragment() {
// 使用 fragment-ktx 提供的属性代理获取 ViewModel(作用域为父 fragment)
private val viewModel: ListViewModel by viewModels({requireParentFragment()})
...
}
// 👇 Java
public class ListFragment extends Fragment {
private ListViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
// 👇 作用域为当前 fragment
viewModel = new ViewModelProvider(this).get(ListViewModel.class);
viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
// 更新 list UI
}
}
}
public class ChildFragment extends Fragment {
private ListViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
// 👇 作用域为父 fragment
viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class);
...
}
}
如果您正在使用 Navigation library,还可以将 ViewModel
的作用域限定为目的地的 NavBackStackEntry
的生命周期。例如,可以将 ViewModel
的作用域限定为 ListFragment
的 NavBackStackEntry
:
// 👇 Kotlin
class ListFragment: Fragment() {
// 使用 fragment-ktx 提供的 NavBackStackEntry 范围内共享的 ViewMoel
private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.filteredList.observe(viewLifecycleOwner) { item ->
// 更新 list UI
}
}
}
// 👇 Java
public class ListFragment extends Fragment {
private ListViewModel viewModel;
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
NavController navController = NavHostFragment.findNavController(this);
NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.list_fragment)
viewModel = new ViewModelProvider(backStackEntry).get(ListViewModel.class);
viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
// 更新 list UI
}
}
}
有关将 ViewModel
作用域限定在 NavBackStackEntry
的更多信息,请参考 Interact programmatically with the Navigation component(译者注:也可以参考译者文章 想去哪就去哪,Android 世界的指南针)。
在某些情况下,您可能希望在两个 fragment 之间或 fragment 与其宿主 activity 之间传递一次性值。例如,您可能有一个读取二维码的 fragment,将数据传递回前一个 fragment。从 Fragment 1.3.0-alpha04 开始,每个 FragmentManager
都实现 FragmentResultOwner
。这意味着 FragmentManager
可以充当 fragment 结果的**存储。此更改允许组件通过设置 fragment 结果并监听那些结果进而彼此通信,而无需那些组件彼此直接引用(译者注:Fragment Result API
引入的原因以及源码分析可参考 1.3.0-alpha04 来袭,Fragment 间通信的新姿势)。
要将数据从 fragment B 传递回 fragment A,请首先在 fragment A 上设置一个结果 listener,该 fragment 将接收结果。在 fragment A 的 FragmentManager
上调用 setFragmentResultListener()
,如下所示:
// 👇 Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 fragment-ktx 提供的扩展函数
setFragmentResultListener("requestKey") { requestKey, bundle ->
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
val result = bundle.getString("bundleKey")
// 根据结果执行后续逻辑
}
}
// 👇 Java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
@Override
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
String result = bundle.getString("bundleKey");
// 根据结果执行后续逻辑
}
});
}
在 fragment B(产生结果的 fragment)中,必须使用相同的 requestKey
在相同的 FragmentManager
上设置结果。您可以使用 setFragmentResult()
API 来做到这一点:
// 👇 Kotlin
button.setOnClickListener {
val result = "result"
// 使用 fragment-ktx 提供的扩展函数
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
// 👇 Java
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bundle result = new Bundle();
result.putString("bundleKey", "result");
getParentFragmentManager().setFragmentResult("requestKey", result);
}
});
然后,fragment A 接收到结果,并在 fragment STARTED 后执行 listener 回调。
您只能有一个 listener 和给定 key 的结果。如果为同一 key 多次调用 setFragmentResult()
,并且 listener 未启动,则系统会将所有待处理的结果替换为更新的结果。如果设置的结果没有相应的 listener 接收,则结果将存储在 FragmentManager
中,直到您使用相同的 key 设置 listener 为止。listener 收到结果并触发 onFragmentResult()
回调后,该结果将被清除。此行为有两个主要含义:
STARTED
才能接收结果STARTED
状态的结果,当结果被设置则立即触发 listener 的回调🌟 注意:由于 fragment 结果存储在
FragmentManager
层级上,因此必须将 fragment attach 到父FragmentManager
来调用setFragmentResultListener()
或setFragmentResult()
。
使用 FragmentScenario 测试 setFragmentResult()
和 setFragmentResultListener()
的调用。使用 launchFragmentInContainer 或 launchFragment 为被测 fragment 创建一个场景,然后手动调用待测试的方法。
要测试 setFragmentResultListener()
,请创建带有 fragment 的场景,该 fragment 将调用 setFragmentResultListener()
。接下来,直接调用 setFragmentResult()
并验证结果:
@Test
fun testFragmentResultListener() {
val scenario = launchFragmentInContainer<ResultListenerFragment>()
scenario.onFragment { fragment ->
val expectedResult = "result"
fragment.parentFragmentManager.setFragmentResult("requestKey", bundleOf("bundleKey" to expectedResult))
assertThat(fragment.result).isEqualTo(expectedResult)
}
}
class ResultListenerFragment : Fragment() {
var result : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 fragment-ktx 提供但扩展函数
setFragmentResultListener("requestKey") { requestKey, bundle ->
result = bundle.getString("bundleKey")
}
}
}
要将结果从子 fragment 传递给父 fragment,在调用 setFragmentResultListener()
时,父 fragment 应使用 getChildFragmentManager()
而不是 getParentFragmentManager()
。
// 👇 Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在 child fragmentManager 设置 listener
childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
val result = bundle.getString("bundleKey")
// 处理结果
}
}
// 👇 Java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在 child fragmentManager 设置 listener
getChildFragmentManager()
.setFragmentResultListener("requestKey", this, new FragmentResultListener() {
@Override
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
String result = bundle.getString("bundleKey");
// 处理结果
}
});
}
要在宿主 activity 中接收 fragment 结果,请使用 getSupportFragmentManager()
在 FragmentManager
上设置结果 listener。
// 👇 Kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager.setFragmentResultListener("requestKey", this) { requestKey, bundle ->
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
val result = bundle.getString("bundleKey")
// 处理结果
}
}
}
// 👇 Java
class MainActivity extends AppCompatActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
@Override
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
// 这里使用了 String,但此处使用 Bundle 支持的数据类型均可
String result = bundle.getString("bundleKey");
// 处理结果
}
});
}
}
顶部 app bar 在 app 窗口顶部提供了统一的界面,用于显示当前屏幕上的信息和操作。
使用 fragment 时,app bar 可以作为宿主 activity 的 ActionBar 或 fragment 布局中的 toolbar。app bar 的所属权取决于您的应用需求。
如果所有屏幕都使用始终位于顶部并填满屏幕宽度的同一 app bar,则应使用由该 activity 托管的主题提供的 action bar。使用主题 app bar 有助于保持一致的外观,并提供了一个存放选项菜单和返回按钮的地方。
如果要在多个屏幕上对 ap bar 的大小,位置和动画进行更多控制,请使用由 fragment 托管的 toolbar。例如,您可能需要折叠的 app bar 或宽度为屏幕一半且垂直居中的 app bar。
了解不同的方式并采用正确的方法可以节省您的时间,并助于确保您的 app 正常运行。对于加载菜单和响应用户交互的操作要根据不同场景使用不同的方法处理。
下面的示例包含可编辑配置文件的 ExampleFragment
。该 fragment 在其 app bar 中加载了以下 XML-defined menu:
<!-- sample_menu.xml -->
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_done"
android:icon="@drawable/ic_done"
android:title="@string/done"
app:showAsAction="ifRoom|withText"/>
</menu>
该菜单包含两个选项:一个用于导航到配置文件界面,另一个用于保存对配置文件所做的所有更改。
app bar 通常由宿主 activity 持有。当 activity 持有 app bar 时,fragment 可以通过重写在 fragment 创建期间调用的 framework 方法来与 app bar 进行交互。
🌟 注意:本节内容仅在 activity 持有 app bar 时才适用。 如果您的 app bar 是 fragment 布局中包含的 toolbar,请参见 Fragment 拥有的 app bar 一节。
您必须通知系统您的 app bar fragment 正在参与选项菜单的加载。为此,请在 fragment 的 onCreate(Bundle)
方法中调用 setHasOptionsMenu(true)
,如下所示:
// 👇 Kotlin
class ExampleFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
}
setHasOptionsMenu(true)
告诉系统您的 fragment 想接收菜单相关的回调。当发生与菜单相关的事件(创建,点击等)时,首先在 activity 上调用事件处理方法,然后再在 fragment 上调用该事件处理方法。请注意,您的应用程序逻辑不应依赖于此顺序。如果同一 activity 托管多个 fragment,则每个 fragment 都可以提供菜单选项。在这种情况下,回调顺序取决于 fragment 的添加顺序。
要将菜单合并到 app bar 的选项菜单中,请在 fragment 中重写 onCreateOptionsMenu()
。此方法接收当前 app bar 菜单和 MenuInflater
作为参数。使用 menu inflater 创建 fragment 菜单的实例,然后将其合并到当前菜单中,如下所示:
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.sample_menu, menu)
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.sample_menu, menu);
}
}
参与选项菜单的每个 activity 和 fragment都能够响应触摸事件。Fragment的 onOptionsItemSelected()
接收选定的菜单 item 作为参数,并返回一个布尔值以指示是否已消费了触摸。一旦 activity 或 fragment 从 onOptionsItemSelected()
返回 true,其它任何参与的 fragment 将不会收到回调。
在 onOptionsItemSelected()
的实现中,在菜单 item 的 itemId 上使用 switch
语句(Kotlin 使用 when
关键字)。如果所选 item 属于您,则处理触摸并返回 true 表示已处理 click 事件。 如果所选项目不是您的,请调用 super 方法。默认情况下,super 方法返回 false 以允许菜单被后续处理。
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> {
// 跳转到设置界面
true
}
R.id.action_done -> {
// 保存配置文件更改
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings: {
// 跳转到设置界面
return true;
}
case R.id.action_done: {
// 保存配置文件更改
return true;
}
default:
return super.onOptionsItemSelected(item);
}
}
}
🌟 注意:fragment 只能处理通过
onCreateOptionsMenu()
调用添加的菜单 item 。使用 activity 拥有的 app bar 时,activity 应处理返回上一级按钮和未被 fragment 添加的菜单 item 的点击事件。
隐藏/显示按钮或更改图标的逻辑应放在 onPrepareOptionsMenu()
中。在显示菜单的每个实例之前立即调用此方法。
继续前面的示例,在用户开始编辑之前,保存按钮应该是不可见的,并且在用户保存后应该消失。将此逻辑添加到 onPrepareOptionsMenu()
可以确保始终正确显示菜单:
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onPrepareOptionsMenu(menu: Menu){
super.onPrepareOptionsMenu(menu)
val item = menu.findItem(R.id.action_done)
item.isVisible = isEditing
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuItem item = menu.findItem(R.id.action_done);
item.setVisible(isEditing);
}
}
当您需要更新菜单时(例如,当用户按下编辑按钮以编辑配置文件信息时),您必须在宿主 activity 上调用 invalidateOptionsMenu()
以请求系统调用 onCreateOptionsMenu()
。无效时,您可以在 onCreateOptionsMenu()
中进行更新。菜单加载后,系统将调用 onPrepareOptionsMenu()
并更新菜单以响应 fragment 的当前状态。
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
fun updateOptionsMenu() {
isEditing = !isEditing
requireActivity().invalidateOptionsMenu()
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
public void updateOptionsMenu() {
isEditing = !isEditing;
requireActivity().invalidateOptionsMenu();
}
}
如果您的 app 中的大多数屏幕都不需要应用 app bar,或者一个屏幕可能需要一个截然不同的 app bar,则可以在 fragment 布局中添加 Toolbar。尽管您可以在 fragment 的视图树中的任何位置添加 Toolbar
,但通常应将其放置在屏幕顶部。要在片段中使用 Toolbar
,请提供一个 ID 并在 fragment 中获得对其的引用,就像在其他任何视图中一样。
使用 fragment 拥有的 app bar 时,强烈建议直接使用 Toolbar API。不要使用 setSupportActionBar()
和 Fragment menu API,它们仅适用于 activity 拥有的 app bar。
Toolbar
有一个便捷方法 inflateMenu(int)
,它需要菜单资源的 ID 作为参数。要将 XML 菜单资源加载到 toolbar 中,请将 resId
传递给此方法,如下所示:
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewBinding.myToolbar.inflateMenu(R.menu.sample_menu)
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
viewBinding.myToolbar.inflateMenu(R.menu.sample_menu);
}
}
要加载另一个 XML 菜单资源,请使用新菜单的 resId
再次调用该方法。新菜单 item 将添加到菜单,并且现有菜单 item 不会被修改或删除。
如果要替换现有菜单集,请在使用新菜单 ID 调用 inflateMenu(int)
之前清除菜单。
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
fun clearToolbarMenu() {
viewBinding.myToolbar.menu.clear()
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
public void clearToolbarMenu() {
viewBinding.myToolbar.getMenu().clear()
}
}
您可以使用 setOnMenuItemClickListener()
方法将 OnMenuItemClickListener
直接传递到 toolbar。每当用户从 toolbar 操作菜单 item 时,就会调用此 listener。选定的 MenuItem
传递给 listener 的 onMenuItemClick()
方法,并消费这个事件,如下所示:
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewBinding.myToolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_settings -> {
// 跳转设置界面
true
}
R.id.action_done -> {
// 保存配置更改
true
}
else -> false
}
}
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
viewBinding.myToolbar.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) {
case R.id.action_settings:
// 跳转设置界面
return true;
case R.id.action_done:
// 保存配置更改
return true;
default:
return false;
}
});
}
}
当 fragment 拥有 app bar 时,您可以在运行时像操作其他 view 一样修改 Toolbar
。
继续前面的示例,在用户开始编辑之前,保存菜单 item 应该是不可见的,并且在点击保存后应再次消失:
/ 👇 Kotlin
class ExampleFragment : Fragment() {
...
fun updateToolbar() {
isEditing = !isEditing
val saveItem = viewBinding.myToolbar.menu.findItem(R.id.action_done)
saveItem.isVisible = isEditing
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
public void updateToolbar() {
isEditing = !isEditing;
MenuItem saveItem = viewBinding.myToolbar.getMenu().findItem(R.id.action_done);
saveItem.setVisible(isEditing);
}
}
如果存在,导航按钮将出现在工具栏的起始位置,在 toolbar 上设置导航图标并使其可见。您还可以设置 navigation 特有的 onClickListener()
,只要用户点击导航按钮,就会调用该 onClickListener
,如下所示:
// 👇 Kotlin
class ExampleFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
myToolbar.setNavigationIcon(R.drawable.ic_back)
myToolbar.setNavigationOnClickListener { view ->
// 跳转到某个地方
}
}
}
// 👇 Java
public class ExampleFragment extends Fragment {
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
viewBinding.myToolbar.setNavigationIcon(R.drawable.ic_back);
viewBinding.myToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 跳转到某个地方
}
});
}
}
🌟 注意:使用
Toolbar
API 处理导航图标时,不会触发默认 activity 的行为。您可以使用requireActivity().onSupportNavigateUp()
触发返回到 manifest 中定义的 父 activity 的行为。
DialogFragment
是专门用于创建和托管 dialog 的特殊 fragment 子类。严格来说,您不需要在 fragment 中托管 dialog,但是这样做可以使 FragmentManager
管理 dialog 的状态并在配置发生变化时自动还原 dialog 。
🌟 注意:本节假定您熟悉创建 dialog 。有关更多信息,请参见 dialog 指南。
要创建 DialogFragment
,请首先创建一个继承 DialogFragment
的类,并重写 onCreateDialog()
,如下所示:
// 👇 Kotlin
class PurchaseConfirmationDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.order_confirmation))
.setPositiveButton(getString(R.string.ok)) { _,_ -> }
.create()
companion object {
const val TAG = "PurchaseConfirmationDialog"
}
}
// 👇 Java
public class PurchaseConfirmationDialogFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.order_confirmation))
.setPositiveButton(getString(R.string.ok), (dialog, which) -> {} )
.create();
}
public static String TAG = "PurchaseConfirmationDialog";
}
与 onCreateView()
在普通 fragment 中创建根视图的方式类似,onCreateDialog()
应该创建一个 Dialog
来显示为 DialogFragment
的一部分。DialogFragment
可以在 fragment 的生命周期中的适当状态下显示 Dialog
。
🌟 注意:
DialogFragment
拥有Dialog.setOnCancelListener()
和Dialog.setOnDismissListener()
回调。您不能自己设置它们。要了解有关这些事件的信息,请重写onCancel()
和onDismiss()
。
就像 onCreateView()
一样,您可以从 onCreateDialog()
返回 Dialog
的任何子类,而不仅限于使用 AlertDialog
。
无需手动创建 FragmentTransaction
即可显示 DialogFragment
,使用 show()
方法显示 dialog 。您可以向该方法传递一个 FragmentManager
对象和 FragmentTransaction
的 tag(String 类型)。从 Fragment 中创建 DialogFragment
时,必须使用 Fragment 的子 FragmentManager
来确保在配置发生变化后正确恢复状态。非空标记允许您在以后使用 findFragmentByTag()
来获取 DialogFragment
。
为了更好地控制 FragmentTransaction
,可以使用 show()
的重载方法传入一个 FragmentTransaction
。
🌟 注意:因为
DialogFragment
是在配置发生变化后自动恢复的,所以请考虑仅根据用户操作或findFragmentByTag()
返回 null(代表 dialog 不存在)时才调用show()
。
DialogFragment
遵循标准的 fragment 生命周期。此外,DialogFragment
还有一些其它的生命周期回调。常见的如下:
onCreateDialog()
- 重写此回调,为 fragment 提供一个管理和显示的 dialogonDismiss()
- 如果在关闭 Dialog 时需要执行自定义逻辑(例如释放资源,取消订阅可观察的资源等),请重写此回调onCancel()
- 如果在取消 Dialog 时需要执行自定义逻辑,则重写该方法DialogFragment
还包含用于关闭或设置 DialogFragment
可取消的方法:
dismiss()
- 关闭 fragment 及其 dialog 。如果该 fragment 加入到了返回栈,则弹出该 fragment 及其顶部的所有 entry。否则,将提交一个新的事务 remove 该 fragment。setCancellable()
- 控制当前显示的 dialog 是否可以取消。应该使用 DialogFragment
的该方法而不是直接调用 Dialog.setCancelable(boolean)
。请注意,在将 DialogFragment
与 Dialog
一起使用时,您不要重写 onCreateView()
或 onViewCreated()
。dialog 不仅是 view,它还具有自己的 Windiow。因此,重写 onCreateView()
是不行的。此外,除非您已重写 onCreateView()
并提供了非 null 的 view,否则永远不会在自定义 DialogFragment
上调用 onViewCreated()
。
🌟 注意:订阅支持生命周期的组件(如 LiveData)时,切勿在使用
Dialog
的DialogFragment
中将viewLifecycleOwner
用作LifecycleOwner
。相反,请使用DialogFragment
本身,或者如果您使用的是 Jetpack Navigation,请使用NavBackStackEntry
。
您可以通过 重写 onCreateView()
来创建 DialogFragment
并显示 dialog ,可以像使用正常的 fragment 一样为其提供 layoutId
,也可以使用 Fragment 1.3.0-alpha02 中引入的 DialogFragment
构造器。
由 onCreateView()
返回的 View 将自动添加到 dialog 中。在大多数情况下,这意味着您不需要重写 onCreateDialog()
,因为默认的空 dialog 是用 传入的 view 填充的。
某些 DialogFragment
的子类,例如 BottomSheetDialogFragment
,会将您的 view 嵌入到一个样式为底部弹窗的 dialog 中。
本节内容介绍如何使用框架提供的 API 测试 fragment 的行为。
Fragment 作为 app 中的可复用的容器,使您可以在各种 activity 和布局配置中呈现相同的 UI 界面。考虑到 fragment 的通用性,重要的是要验证它们是否提供了一致且资源高效的体验。请注意以下几点:
为了提供这些测试条件,AndroidX fragment-testing 库提供了 FragmentScenario
创建 fragment 并 改变它们的 Lifecycle.State
。
🌟 注意:要成功运行包含
FragmentScenario
对象的测试,请在测试的 instrumentation 线程中运行 API 的方法。要了解有关 Android 测试中使用的不同线程的更多信息,请参阅 Understand threads in tests。
要使用 FragmentScenario
,请使用 debugImplementation
在 app 的 build.gradle
文件中定义 fragment 测试工件,如下所示:
dependencies {
def fragment_version = "1.2.5"
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"
}
本节示例使用的断言来自 Espresso 和 Truth。关于测试和断言库的更多信息,请参考 Set up project for AndroidX Test。
FragmentScenario
包括以下用于在测试中启动 fragment 的方法:
launchInContainer()
,用于测试 fragment 的 UI。FragmentScenario
将 fragment attach 到 activity root view 容器内,该 activity 除此之外啥都没有。launch()
,用于测试没有 UI 的 fragment。FragmentScenario
将 这种类型的 fragment attach 到一个没有 root view 的空 activity 中。启动其中一种 fragment 时,FragmentScenario
将被 fragment 驱动为 RESUMED
状态。此状态代表该 fragment 正在运行并且对用户可见。您可以使用 Espresso UI tests 测试相关 UI 元素的信息。
以下代码示例演示如何使用每种方法启动 fragment:
🌟 注意:您的 fragment 可能要求测试 activity 不使用默认的主题。您可以提供自己的主题作为
launch()
和launchInContainer()
的参数。
launchInContainer() 示例
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
// fragmentArgs 是可选的.
val fragmentArgs = bundleOf(“selectedListItem” to 0)
val scenario = launchFragmentInContainer<EventFragment>(fragmentArgs)
...
}
}
launch() 示例
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
// fragmentArgs 是可选的.
val fragmentArgs = bundleOf("numElements" to 0)
val scenario = launchFragment<EventFragment>(fragmentArgs)
...
}
}
如果您的 fragment 具有依赖项,则可以通过向 launchInContainer()
或 launch()
方法提供自定义 FragmentFactory
来提供这些依赖项的测试版本:
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val someDependency = TestDependency()
launchFragmentInContainer {
EventFragment(someDependency)
}
...
}
}
有关使用 FragmentFactory
为 Fragment 提供依赖项,请参考 FragmentManager
一节。
在 app 的 UI 测试中,通常需要 fragment 处于 RESUMED
状态时开始测试。但是,在更细粒度的单元测试中,当 fragment 从一种生命周期状态转换为另一种生命周期状态时,您可能也需要测试其行为。
要将 fragment 驱动到不同的生命周期状态,请调用 moveToState()
。 此方法支持以下状态作为参数:CREATED
,STARTED
,RESUMED
和 DESTROYED
。 该方法模拟了该 fragment 或其宿主 activity 由于一些原因驱动 fragment 状态更改的场景。
🌟 注意:如果将 fragment 切换为
DESTROYED
状态,则无法将该 fragment 驱动为另一状态,也不能将该 fragment attach 到其它 activity。
以下示例将测试 fragment 移至 CREATED
状态:
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
scenario.moveToState(Lifecycle.State.CREATED)
// EventFragment moves from RESUMED -> STARTED -> CREATED
...
}
}
⚠️ 警告:如果您尝试将被 fragment 段转换为当前状态,则FragmentScenario
将忽略该请求而不会引发异常。特别是,API 允许您连续多次将 fragment 转换为 DESTROYED 状态。
如果您的 app 在资源不足的设备上运行,则系统可能会销毁包含您的 fragment 的 activity。这种情况要求您的 app 在用户返回 fragment 时重新创建该 fragment。为了模拟这种情况,请调用 recreate()
:
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
scenario.recreate()
...
}
}
FragmentScenario.recreate()
销毁 fragment 及其宿主activity,然后重新创建它们。当 FragmentScenario
类重新创建被测试的 fragment 时,该 fragment 将返回其在销毁之前所处的生命周期状态。
要在被测 fragment 中触发 UI 操作,请使用 Espresso view matchers 与视图中的元素进行交互:
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
onView(withId(R.id.refresh)).perform(click())
// 断言预期的行为
...
}
}
如果需要调用 fragment 自身的方法,例如响应选项菜单中的选择,则可以使用 FragmentScenario.onFragment()
获取对 fragment 的引用并传递 FragmentAction
来安全地进行操作:
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testEventFragment() {
val scenario = launchFragmentInContainer<EventFragment>()
scenario.onFragment { fragment ->
fragment.myInstanceMethod()
}
}
}
🌟 注意:不要保留对传递给
onFragment()
的 fragment 的引用。这些引用消耗系统资源,并且引用本身可能已过时,因为框架可以重新创建 fragment。
FragmentScenario
还支持测试 dialog fragment。尽管 dialog fragment 具有 UI 元素,但它们的布局填充在单独的 Window 中,而不是 activity 本身。因此,请使用 FragmentScenario.launch()
测试 dialog fragment。
下面的示例测试 dialog 的关闭过程:
@RunWith(AndroidJUnit4::class)
class MyTestSuite {
@Test fun testDismissDialogFragment() {
// 假设 MyDialogFragment 继承了 DialogFragment
with(launchFragment<MyDialogFragment>()) {
onFragment { fragment ->
assertThat(fragment.dialog).isNotNull()
assertThat(fragment.requireDialog().isShowing).isTrue()
fragment.dismiss()
fragment.parentFragmentManager.executePendingTransactions()
assertThat(fragment.dialog).isNull()
}
}
// 假设 dialog 存在一个按钮,text 为 "Cancel"
onView(withText("Cancel")).check(doesNotExist())
}
}
FragmentContainerView
作为 fragment 的容器,不要使用 <fragment>
标签或 FrameLayoutgetSupportFragmentManager()
获取 FragmentManager
getChildFragmentManager()
获取管理子 fragment 的 FragmentManager
getParentFragmentManager()
获取其宿主的 FragmentManager
setReorderingAllowed(true)
layoutId
参数的构造器,无需调用 onCreateView
设置布局setMaxLifecycle()
限制了 Fragment 的最大生命周期,因此 setUserVisibleHint 被弃用了,保证了 ViewPager 中 fragment 可见性判断与正常情况一致AnimationSet
存在已知问题。lifecycleOwner
和 viewLifecycleOwner
的区别ViewModel
和 Fragment Result API
DialogFragment
来显示 Dialog,能够更好的处理配置发生变化和系统资源回收的场景fragment-ktx
库有很多方便的扩展函数和属性代理fragment library
中包含了 activity library
我是 Flywith24,Android App/Rom 层开发者。目前专注于 Android 体系化文章的写作。
Android Detail 专栏 正在更新中,想要建立系统化知识体系的小伙伴可以去看看哦。我的所有博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.