Giter Club home page Giter Club logo

bloglist's Introduction

很高兴见到你 👋

我是 Flywith24,欢迎来到我的 Github 主页 😉

📱 Android Developer 🍚 懂车干饭人 📷 业余 vlogger 🏀 篮球爱好者 🚣🏻 划船器入门选手

目前专注于写 Android 体系化文章,欢迎订阅我的专栏

小专栏  掘金  自建博客  语雀  CSDN  Bilibili

最近文章

bloglist's People

Contributors

flywith24 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

rohitnotes

bloglist's Issues

【奇技淫巧】使用 Navigation + Dynamic Feature Module 实现模块化

androidx navigation 2.3.0 加入了对 dynamic feature module 的导航支持,因此我们利用这个来分离出多个功能 module 来实现模块化

navigation 2.3.0 更新

国内基本不用的 dynamic feature 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 架构

dynamic feature module 也可以按需安装,也就是说,它们可能不包含在用户最初下载的 APK 中,而是在运行时安装。而我们可以直接将它们包含到 APK 中

使用 dynamic feature module

首先我们在 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

main_nav.xml

接着我们在 activity_main 中设置默认的 host

默认 host

这里不同于正常 navigation 的用法,没有使用 NavHostFragment,而是使用 DynamicNavHostFragment

直接跳转 fragment

我们创建 dynamic feature module ,取名为 feature1

创建 dynamic feature module

包名前部分需保证与 applicationId 相同

这里 dynamic feature module 的包名前部分要和 applicationId 即 app module 包名相同,否则后续的 include 操作会有问题

选择加载模式

这里我们选择在安装时集成该 module

接着我们在该 module 下创建一个 fragment 取名为 Feature1OneFragment

之后我们直接在 main_nav.xml 中引入 该 fragment 并加入 action

直接引入 fragment

接着我们就可以在 app 下的 MainFragment 打开 Feature1OneFragment

启动 fragment

我的 demo 中 feature2 是直接引入 fragment,因此跳转的是 Feature2OneFragment

直接跳转 activity

在 feature1 中创建 activity (demo 中为 feature2)

跳转 activity

同样需要指定 moduleName

启动activity

使用 dynamic feature module 内部的 graph

我们可以为 dynamic feature module 单独配置 navigation graph,这样就可以处理 dynamic feature module 内部的跳转了

在 feature1 中创建 feature1_nav.xml ,其中 startDestination 为 Feature1OneFragment

feature1_nav.xml

在 main_nav.xml 我们需要使用另外一种方式来使用该 graph

include-dynamic

我们使用了一个新的标签 include-dynamic,同时我们看到了几个没用过的属性

  • graphPackagedynamic feature module 的包名
  • graphResNamedynamic feature module 内部 graph 的名字
  • moduleName 为 module 名

注意:这里的 graphPackage 可以省略

  1. 如果 module 的包名没用按照前文的格式配置会导致无法找到 graphId 的异常
  2. include-dynamic 标签的 id 要与 feature1_nav.xml navigation 标签下的 id 一致,或者后者不设置 id

这样从 app module 导航到 feature1 的 startDestination 后便可使用其内部的逻辑进行后续的导航了

include 跳转

feature module 间跳转

暂不支持 deep link

Navigation 组件暂不支持 Dynamic include graph 的 deep link

因此我目前也没有找到特别优雅的方式,已知的方案如下

demo

【玩转Test】开篇-Android test 介绍

不会测试的开发不是好开发——鲁迅

一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,那么就从认识 test 开始吧

本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics

结构

作为 Android 开发者我们知道在 Android Studio 的 Android 视图中有三部分代码

  • app 的逻辑代码(main source set)
  • androidTest 代码
  • local test 代码

test 代码知道所有的 main source set 中的代码,因此可以测试这些类。但是 app 代码不知道 test 中的代码,并且 androidTest 和 test 都不知道对方的存在。事实上,当你构建出 apk 并提交应用市场时,测试代码并没有包含在内

依赖引用

下面标记的依赖 使用了 test 的引用方式 testImplementationandroidTestImplementation

注意:这些 test 代码不会打包到最终的 apk 文件中

testImplementation 引用的 JUnit 依赖只能在 test source set 中使用,这种依赖范围的限制是 Gradle 实现的

简单总结下:

  • 三种 source setsmaintestandroidTest
  • 测试代码能访问 app 代码
  • app 代码 不能 访问测试代码
  • 测试不会被打入到 Apk 中
  • 依赖范围包括:testImplementationandroidTestImplementation

运行第一个 test

我们打开 test source set ,看到其中有一个 ExampleUnitTest

ExampleUnitTest

可以看到其内部只有一个 addition_isCorrect() 方法

有两个要素使它成为一个 test:

  • 使用了 @Test 注解
  • 它存在于两个 test source set 之一

有了这两个要素,这个方法就可以独立的作为 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 VS test

下面我们来对比一下 androidTesttest

test androidTest
Local Tests Instrumented Tests
Local machine JVM Real or emulated devices
Faster Slower

我们来运行一个 androidTest,可以看到启动了模拟器

写一个 test

首先我们针对一个功能来创建 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 代码更具可读性,可以从三个方向入手

  • 优秀的命名
  • Given/When/Then
  • 借助断言库

优秀的命名

首先我们来谈谈命名,我们知道 test 方法使用 @test 注解标记,理论上方法名可以随意命名,但随意的命名会导致可读性的降低,因此需要一些特定的命名规范

测试模块_ 动作或输入_ 结果状态

例如上面的例子我们的命名为:getForkAndOriginRepoStats_noForked_returnHundredZero

第一部分显示我们要测试的是 getForkAndOriginRepoStats() 方法,第二部分代表我们需要的是没有 fork 仓库的数据源,第三部分是结果的状态,0%

Given/When/Then

说完了命名我们来谈谈 Given/When/Then

测试的基本结构是 Given X,When Y,Then Z

还是上面的例子

  • Given 为你的测试逻辑提供数据源
  • When 是你的实际操作
  • Then 检查 test 是否通过

借助断言库

上面示例最后的断言代码让人看着很别扭,我们可以借助断言库来提高这部分的可读性

// 之前
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(单元测试)
  • Integration Tests(组装测试)
  • End to end Tests(端到端测试)

您的测试策略需要覆盖到所有的类型

Unit Tests

上面的示例我们已经写过了 Unit Tests

  • 范围是单个方法或类
  • 帮助查明失败原因
  • 应该运行的很快,通常是本地测试
  • 低保真度

他们的范围是单个的方法或类

如果 Unit Tests 失败了,您知道您的代码在哪里出了问题。因为它聚焦于很小一段代码

Unit Tests 也意味着可以快速运行,由于您频繁地修改代码会使得它会频繁的运行,因此需要速度。Unit Tests 通常是本地测试

它们有较低的保真度,因为现实世界您的 app 要执行很多代码而不仅仅是一个方法或者类

Unit Tests 就像检查一个链条的每个环节是否能够正常运行

但它不检查这些环节组合在一起是否能够运行,为此您需要 Integration 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 的大部分
  • 高保真度
  • 将 app 作为整体来测试
  • 接近真实地使用,应该使用设备测试

End to end Tests 测试 app 的大部分,它十分接近真实地使用,因此速度上会比较慢

它有着最高的保真度并确保您的应用作为一个整体运行

这些测试应该使用设备测试

测试比重

推荐的测试比例是 70% 的单元测试,20% 的组装测试,以及10% 的端到端测试

您能否轻松地在各个部分测试您的 app 取决于您的 app 使用的结构

例如,您的应用将所有逻辑都放置在一个 activity 的大的方法中,您可能可以写出端到端测试,但单元测试和组装测试则写不出来

一个更好的架构应该将应用的逻辑拆分为多个方法和类,这允许每部分可以独立的测试

对于单元测试,您可以测试 ViewModelRepository 以及 DAO

对于组装测试,您可以组合测试 fragmentViewModel ,或者您可以测试整个数据库代码

端到端测试会测试整个应用

关于测试的原理,可移步 官方文档

test 的 codelab

【背上Jetpack】Jetpack 主要组件的依赖及传递关系

在学习和使用 jetpack 组件时,总是被其 gradle 依赖搞的晕头转向,故在此整理 jetpack 主要组件的依赖,及传递关系

可直接跳过后续内容前往最后一节查看总结

关于 androidxfragment/activity的变化,可查看该文, 【译】AdroidX下使用Activity和Fragment的变化

Appcompat

引入

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 库,可以单独引用

appcompat build.gradle 源码地址

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

fragment build.grdle 源码地址

Activity

引入

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

activity build.gradle 源码地址

Core

引入

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"
}

依赖树

Lifecycle

引入

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-processlifecycle-extensionsl不会有2.3.0版本
  • 2.1.0 后 ViewModelProviders.of() 被废弃。您可以在 FragmentActivity 或者 Fragment 使用 ViewModelProvider(ViewModelStoreOwner) 构造器来实现相同的功能。(Fragment 库 1.2.0以上)

依赖树

livedata

viewmodel

Navigation

引入

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"
}

依赖树

navigation

navigation-ui

Paging

引入

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
}

依赖树

paging

Room

引入

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.

依赖树

room

总结

版本说明

androidx库遵循严格的语义版本控制。版本字符串(例如 1.0.1-beta02)包含三个数字,分别代表 major 级别、minor 级别和问题修复级别。预发布版本也有一个后缀,用于指定预发布阶段(Alpha 版、Beta 版、候选版本)和版本号(01、02 等)。

库的每个版本都要经历三个预发布阶段,才能成为稳定版本。各预发布阶段的标准如下:

Alpha 版

  • Alpha 版功能稳定,但功能可能不完整。
  • 在版本处于 Alpha 版状态时,可以添加、移除或更改 API。

Beta 版

  • Beta 版功能稳定,并且具有功能完整的 API Surface。
    它们可以投入实际使用,但可能包含错误。
  • Beta 版无法使用实验性编译器功能(例如 @UseExperimental)。
  • 其他库的依赖项必须为 Beta 版、RC 版或稳定版。不允许使用 Alpha 版依赖项。

候选版本 (RC)

  • 候选版本是未来的稳定版。
  • 此版本可能包含在最后一刻提供的重要修复。
  • 此版本的 API Surface 无法更改。
  • 其他库的依赖项只能是 RC 版或稳定版。

一个库可以同时具有多个版本。每个版本都具有不同的发布阶段。例如,虽然 androidx.activity 的稳定版可以是 1.0.0,但也可能还有 1.1.0-beta02 版本以及 2.0.0-alpha01 版本。

kotlin 协程的使用

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 fragmentandroidx activity

  • 受限于appcompat 稳定版的更新速度,您可以选择单独使用androidx fragment/activity

  • AppCompatActivity 继承自 FragmentActivityAppCompatActivity可以直接使用 fragment

  • DataBinding ViewBinding 依赖于 android build gradle 插件 ,无需引入其他依赖

  • androidx fragment/activity下均依赖的ViewModelLiveData,您可以直接使用

  • 使用ViewModelLiveData完整功能需要单独引入,它们都是lifecycle大家族下的

  • lifecycle-extensions 已废弃,不要使用它了

  • 如果想使用实现LifecycleOwnerService ,需要引入 lifecycle-service

【Jetpack更新之Recyclerview】更优雅地恢复 recyclerview 的滚动位置

被我忽视的更新

androidx recyclerview 1.2.0-alpha02 版本添加了新功能 MergeAdapter,帮助开发者更容易地为 RecyclerView 添加 Header 和 Footer。详情参见 【译】MergeAdapter 的使用 使用官方 API 为 Recyclerview 添加 Header 和 Footer

该版本中还有一个改动:RecyclerView.Adapter lazy state restoration,帮助开发者恢复 RecyclerView 的状态

recyclerview update

我对这个功能并没有什么感觉。众所周知,Android 中的 View 内部是有着状态保存和恢复的方法的。RecyclerView 也是如此,它可以恢复自身已滚动的位置

View 内部恢复状态

有关状态保存的内容可以参见 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析

真实情况也是如此

RecyclerView 内部可以恢复滚动位置

意外发现

最近看到 Florina MuntenescuRestore 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 方法,该方法允许设置状态恢复策略,它有三个选项

ALLOW

这是 默认 的状态,它会立即恢复 RecyclerView 的状态,该种策略无法解决延迟加载的数据的问题,可以使用 PREVENT_WHEN_EMPTY

PREVENT_WHEN_EMPTY

仅当 Adapter 不为空(adapter.getItemCount() > 0)时,才恢复 RecyclerView 状态。 如果您的数据是异步加载的,那么 RecyclerView 会一直等到数据加载完毕,然后状态才能恢复。 如果您有默认 item(例如 Header 或 加载指示器)作为适配器的一部分,则应该使用PREVENT 选项,除非使用 MergeAdapter 添加了默认 item。 MergeAdapter 等待所有适配器准备就绪,然后才恢复状态

PREVENT

状态不会恢复,直到配置了 ALLOW 或者 PREVENT_WHEN_EMPTY

使用方式如下:

adapter.stateRestorationPolicy = PREVENT_WHEN_EMPTY

加入了上面的配置后即使是异步加载数据也能恢复 RecyclerView 的位置

设置 PREVENT_WHEN_EMPTY

追踪引入过程

老规矩,我们沿着官方的 commit log 来看看其实现原理

首先我们看看 IssueTracker 上提的 Feature

IssueTracker

表达的意思也很简单,就是当加载异步数据时 RecyclerView 的位置状态无法恢复,Adapter 应该提供相关的解决方案

有意思的是,实现该功能时还重新实现了前一个版本的逻辑,我在 git commit log 中看到了 revert 操作

revert操作

为了防止 LayoutManager#onRestore 执行多次,没有采用最开始的实现方式。但 Yigit Boyar (这个提交的开发者) 仍然希望使用最开始的实现方式,但是 LayoutManager#onRestoreInstance 的状态时 public ,因此只能选取一个折中的方案

新的实现方案

无奈之举

过去,开发者会无意间调用 onRestoreInstanceState(State) 方法。例如,一些开发者已使用它来手动设置自己更新的状态,这样即使在此状态之前已恢复,在此处传递状态也将导致 LayoutManager 接收它并相应地更新其内部状态。因此,即使看起来好像很奇怪,也必须始终调用 requestLayout 来保留功能

源码分析

接下来我们来分析这部分源码,内容很少,所以我们详细看下

首先是引入 StateRestorationPolicy 的枚举

然后需要提供 setStateRestorationPolicygetStateRestorationPolicy 方法,此时我们还需要一个方法来判断是否要将 SavedState 传递给 LayoutManager

前面的 setStateRestorationPolicy 方法中 调用了 notifyStateRestorationPolicyChanged,而 notifyStateRestorationPolicyChanged 为静态类 AdapterDataObservable 中的方法,该类中的其他方法我们也很熟悉,均是刷新 Adapter 中数据的方法。

notifyStateRestorationPolicyChanged

notifyStateRestorationPolicyChanged 中调用了 mObservers list 中元素的 onStateRestorationPolicyChanged 方法,通过源码我们得知该 list 中的元素类型为 AdapterDataObserver,因此还需要在 AdapterDataObserver 中加入 onStateRestorationPolicyChanged 方法

onStateRestorationPolicyChanged

该方法是个空实现,而 RecyclerViewDataObserver 重写了该方法

RecyclerViewDataObserver

配置恢复策略以及恢复策略变化时的监听都有了,接下来要做的就是如果之前有待恢复的装则恢复之前的状态

恢复状态

注意:发布之前 StateRestorationPolicy 叫做 StateRestorationStrategy,后来命名为 StateRestorationPolicy,alpha 版本的库可能随时更改 API 的命名和删除 API,因此查看这部分源码的同学请注意

至此,相关的源码都在这里了

总结

StateRestorationPolicy 提供了 RecyclerView 异步加载数据恢复滚动位置的解决方案。原理就是通过配置 StateRestorationPolicy 来改变恢复策略,同时在策略改变时调用 requestLayout 方法。在 dispatchLayoutStep2() (该方法会在 onLayout 和 measure 方法中调用) 方法中恢复状态(如果 canRestoreState() 返回 true)

demo 地址

一点思考:我们都知道 ViewPager2 是使用 RecyclerView 实现的,那么借助本文介绍的 API 可以做点什么吗?

欢迎各位小伙伴在评论区留言,说说你的想法

开源项目:使用 Activity Result API + Kotlin 扩展函数 封装权限请求库

前言

市面上权限请求的库很多,而前段时间官方刚刚将 requestPermissions() + onRequestPermissionsResult() API 弃用,那么官方的替代方案是什么呢?本文将介绍如何借助 Activity Result API 进行权限请求以及如何使用 Kotlin 扩展函数自己封装一个权限请求库

Activity Result API

在 Android Jetpack Activity 1.2.0-alpha02Fragment 1.3.0-alpha02 中,Google 提供了全新的 Activity Result API 来替换 startActivityForResult() + onActivityResult()requestPermissions() + onRequestPermissionsResult()。详情可移步 官方文档,中文可以参考 秉心说是时候丢掉 onActivityResult 了 !,有些 API 的名字发生了变化,请留意

requestPermissions() / onRequestPermissionsResult() 被弃用

紧接着在 Activity 1.2.0-alpha04Fragment 1.3.0-alpha04 版本中,

startActivityForResult()+onActivityResult()requestPermissions()+onRequestPermissionsResult()被标记为弃用,而在Fragment 1.3.0-alpha05 这些标记弃用的方法内部已改为使用 ActivityResultRegistry 实现

新 API 的使用

新的 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 扩展函数进行封装

配合 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 版本

配合 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 版本

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 上,引入姿势如下:

  1. 在项目根目录的 build.gradle 加入

    allprojects {
      repositories {
        //...
        maven { url 'https://jitpack.io' }
      }
    }
  2. 添加依赖

    dependencies {
      implementation 'com.github.Flywith24:Flywith24-Permission:$version'
    }

【背上Jetpack之Navigation】想去哪就去哪,Android世界的指南针

前言

androidx Navigation 组件是 Android 中应用内导航的官方库,当前最新的版本为 2.3.0-beta01(2020.05.20)

很多人不喜欢 Navigation 因为其设计不符合开发者的预期,它在 navigate 时会导致之前的 fragment 重建。网上针对这一问题有一个 重写 Navigator 的方案,大多数人会简单地认为 Navigation 无法保存 fragment 状态是因为使用了 replace(曾经的我也这样认为)

本文的内容为 Navigation 的职能边界,简单使用,高阶使用技巧(例如同一 activity 部分内部分 fragment 共享 ViewModel,模块化)以及关于 Navigation 所谓的「设计问题」的探讨

对 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 的扩展函数,界面间跳转的代码已足够简洁

但是

  • 如果在每个界面加上跳转动画呢?
  • 当你接手一个较大的项目,如何能快速理清界面间的跳转关系?
  • 在单 activity 的项目中,如何控制几个相关的 fragment 有着相同的 ViewModel 作用域,而不是整个 activity 共享?
  • 组件间的界面跳转?

Navigation 简介

摘自19I/0大会

Jetpack 导航组件是一套库,工具和指南,为应用内导航提供了强大的导航框架

它是一套库,封装着应用内导航的 API

引入依赖如下

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"
}

destination

它支持 fragment ,activity,或者是自定义的 destination 间的跳转

Navigation UI

Navigation UI 库 支持 Drawer,Toolbar 等 UI 组件

它是一套工具,在 Android Studio 中可以可视化管理界面的导航逻辑

Android Studio 提供可视化管理的工具

现在我们对 Navigation 有一个初步的认识,接下来我们看看 Navigation 的职能边界

Navigation 能做什么

  • 简化界面跳转的配置
  • 管理返回栈
  • 自动化 fragment transaction
  • 类型安全地传递参数
  • 管理转场动画
  • 简化 deep link
  • 集中并且可视化管理导航

Navigation 工作逻辑

Navigation 主要有三个部分

  • Navigation Graph
  • NavHost
  • NavController

Navigation Graph

Navigation Graph 是一种新的 resource type,它是一个集中管理 navigation 的 xml 文件

Navigation Graph

Navigation Graph 中的每一个界面叫:Destination,它可以使 fragment ,activity,或者自定义的 Destination

Navigation 管理的就是 Destination 间的跳转

点击 Destination,可以在屏幕右侧看见 deep link 等信息的配置

destination attitude

Navigation Graph 中的带箭头的线叫:Action,它代表着 Destination 间不同的路径

点击 Action,可以在屏幕右侧看到 Action 的详细配置,动画,Destination 间跳转传递的参数,操作返回栈,Launch Options

action attributes

不知道各位小伙伴大学是否学过 图论,个人感觉 Navigation Graph 就像 有向图,而其中的 Destination 和 Action 就像图论中的

NavHost

NavHost 是一个空容器,用于显示 navigation graph 中的 destination。 导航组件提供一个默认的 NavHost 实现 NavHostFragment,它显示 fragment destination

NavHostFragment 是 navigation-fragment 中的类

NavHostFragment

它提供了一个可独立导航的区域,使用时大概是这样

NavHostFragment 使用

所有的 fragment Destination 都是通过 NavHostFragment 管理,相当于一个容器

每个 NavHostFragment 都有一个 NavController,用于定义 navigation host 中的导航。 它还包括 navigation graph 以及 navigation 状态(例如当前位置和返回栈),它们将与 NavHostFragment 本身一起保存和恢复

NavController

NavController 帮助 NavHost 管理导航,其内部持有 NavGraphNavigator(通过持有 NavigatorProvider 间接持有)

其中 NavGraph 决定了界面间的跳转逻辑,它通常在 Android resource 下创建,同时也支持通过代码动态创建

Navigator 定义了一在应用内导航的机制。它的实现类有 ActivityNavigator, DialogFragmentNavigator, FragmentNavigator, NavGraphNavigator。当然,开发者也可以自定义 Navigator。每种 Navigator 都有自己的导航策略,例如 ActivityNavigator 使用 starActivity 来进行导航

总结

下面引用一张 KunMinX 的专栏 重学Android Navigation 一文中的配图,帮助大家理解这其中的依赖关系

来自 重学安卓:就算不用 Jetpack Navigation,也请务必领略的声明式编程之美!

我们在 res/navigatoin 创建的 xml 文件叫 Navigation Graph (类似图论中的图)

其内部每个节点叫 Destination(类似图论中的点) ,它对应着 activity/fragment/dialog,代表着屏幕上的界面

连接 DestinationDestination 之间的线叫 Action(类似图论中的边),它是从一个界面跳转另个一个界面的抽象,可以配置跳转动画,传递参数,以及返回栈等信息

NavHost 是显示 Navigation Graph 的容器,实现类为 NavHostFragment,每个 NavHostFragment 中都持有一个 NavController

NavController 是导航的大管家,封装着 navigate navigateUp popBackStack 等方法

Navigator 是对 Destination 之间跳转的封装。由于 Destination 可以是 activity 或 fragment,因此有了 ActivityNavigator FragmentNavigator 等实现类,用于实现具体的界面跳转

Navigation 的使用技巧

Dialog Destination

Navigation 2.1.0 引入,用于实现 navigate 到一个 DialogFragment

使用也很简单,使用 dialog 标签,其他处理的与 fragment 相同

dialog destination

同一 graph **享 ViewModel

我们都知道 fragment 可以使用 activity 级别共享 ViewModel,但是对于单 activity 项目,这就意味着所有的 fragment 都能拿到这个共享的 ViewModel。本着最少知道原则,这不是一个好的设计

想要在部分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

什么是 Safe Args

什么是 Safe Args?

它是一个 Gradle 插件,可以根据 navigation 文件生成代码,帮助开发者安全地在 Destination 之间传递数据

那么为什么要设计这样一个插件呢?

我们知道使用 bundle 或者 intent 传递数据时,如果出现类型不匹配或者其他异常,是在 Runtime 中才能发现的,而 Safe Args 把校验转移到了编译期

为什么设计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 创建以下类型安全的类和方法

  • 为拥有 action 的发送 destination 的创建一个类,该类的类名为 destination 名 + Directions。例如我们的发送 destination 为 SpecifyAmountFragment,那么将会生成 SpecifyAmountFragmentDirections 类。该类会为 destination 的每个 action 创建一个方法
  • 为每个传递参数的 action 创建一个内部类,如果 action 叫 confirmationAction 则会创建 ConfirmationAction 类。如果 action 的参数没有默认值,则要求开发者使用该类设置参数
  • 为接收 destination 创建一个类,该类的类名为 destination 名 + Args。例如我们的接收 destination 为 ConfirmationFragment,那么将会生成 ConfirmationFragmentArgs 类。使用该类的 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 + "")
}

嵌套 navigation graph

有一些 destination 通常是组合使用,并且在多个地方重复调用。例如独立的登录流程,后续的忘记密码,修改密码等 destination 可以看做一个整体来使用

这种情况我们使用嵌套 navigation graph,选中一组可以作为整体的 destination (按住 shift 鼠标点选),然后右击选中 Move to Nested Graph > New Graph 。这样就生成了 嵌套 navigation graph。在 嵌套 navigation graph 上双击即可查看内部的 destination

创建嵌套navigation graph

如果想要引用其他 module 中的 graph,可以使用 include 标签

include 使用

include 使用

全局 action

您可以为多个 destination 创建共用的 action,例如您可能想要在不同的 destination 中导航至相同的界面

对于这种情况您可以使用全局 action

选中一个 destination 并右击,选择 Add Action > Global ,一个箭头会 出现在 destination 的左边

全局 action

使用也很简单,向 navigate 方法传入全局 action 的资源 id 即可

viewTransactionButton.setOnClickListener { view ->
    view.findNavController().navigate(R.id.action_global_mainFragment)
}

条件导航

在开发过程中,我们可能会遇到一个 destination 根据条件跳转不同的 destination 的情况

例如一些 destination 需要用户处于登录状态才能进入,或者在游戏结束后,胜利和失败跳转不同的 destination

下面我们用一个示例来展示 Navigation 如何处理该种场景

该示例中,用户尝试跳转到资料页中,如果该用户处于未登录状态,则需要跳转到登录界面

graph

我们使用 LoginViewModel 来保存登录状态,从 ProfileFragment 点击按钮跳转到 ProfileDetailFragment,在该界面判断登录的状态,如果是未授权则跳转到 LoginFragment,如果已授权则提示欢迎

ProfileDetailFragment

在登录界面判断登录状态,如果授权成功则回到 ProfileDetailFragment,授权失败则显示登录失败提示

在登录界面点击返回视为未授权,应该直接返回 ProfileFragment 界面

LoginFragment

Deep Links

开发过程中我们可能会遇到这类的需求,我们需要让用户打开 app 时直接空降到某个特定页面(例如点开通知栏跳转到特定文章),亦或者我们需要从一个 destination 跳转到一个在其他流程中比较深的位置的 destination ,如下图,从 FriendList 跳转到 Chat 界面

上面的这种需求叫作 deep link ,Navigation 支持两种 deep link 的跳转。

显式 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

隐式 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

navigate to deep link

因此可直接调用

val userId = "1111"
findNavController().navigate("chat://convention/$userId".toUri())

模块化

参见 【奇技淫巧】使用 Navigation + Dynamic Feature Module 实现模块化

Navigation 设计探讨

fragment replace 你真的了解吗

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 的所谓 「设计问题」是怎么回事?

被重建的 fragment

我们使用navigation 从 HomeFragment 跳转到 DashboardFragment,日志如下

使用navigation切换fragment

可以看到 HomeFragment 被重建了,原实例(e6c266),新实例(c3e49cc)

fragment 被重建,这就是原因所在!

那么为什么出现这种现象?我们翻一下源码

navigate方法

通过反射创建新的fragment实例

从源码可以看到,其内部通过反射创建了新的 fragment 实例,这导致 fragment 内部的状态无法恢复

不过如果 navigation 导航的所有 destination 没有平级关系,换句话说在一个返回栈内,这样的设计是没有问题的

但是有些时候我们希望使用 navigation 管理一些平级界面,例如 BottomNavigation

不符合 Material Design 的 BottomNavigation

issuetracker 有这样一个 issue,注意提出的时间

issue

主要意思就是现阶段的 bottom tab navigation 不符合 Material Design 的规范

  • 标签间要保存滚动位置
  • 每个标签应该有自己独立的返回栈

相同类型的 issue 还有这些

相同类型的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 再次回复了开发者的疑问

官方补充1

官方补充2

多返回栈支持计划需要三步走

  • 为 fragment 提供相应的 API 以保证多返回栈的 fragment 状态能够被保存
  • NavController API 提供了通用框架,该框架允许任何 Navigator(括FragmentNavigator)支持多返回栈
  • NavigationUI 中提供的新API,可让您在使用 BottomNavigationView 或NavigationView 时 无论是单返回栈(当前模式)还是多返回栈都能控制 setupWithNavController()

之后他回复了 70 楼的开发者,明确了如果使用单个 NavHostFragment / navigation graph / FragmentManager 能够支持多返回栈,那么从 A 或 B 导航到 C 就不会有任何问题

接着时间来到了2020年

2020年的最新回复

今年 1月30日,Ian Lake 再次做了回复

大概意思就是原定 Fragment 1.3.0Navigation 2.3.0 提供多返回栈支持的计划跳水了,计划在 Fragment 1.4.0-alpah01Navigation 2.4.0-alpha01 提供

至此关于 Navigation 所谓的 「设计问题」就探讨结束了

【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇

这两天在准备写 fragment 返回栈的文章,但是发现必须先介绍一下 OnBackPressedDispatcher ,所以这是一篇介绍 what 的文章,喜欢一手资料的可以移步 官方文档

系列文章

【背上Jetpack】Jetpack 主要组件的依赖及传递关系

【背上Jetpack】AdroidX下使用Activity和Fragment的变化

【背上Jetpack之Fragment】你真的会用Fragment吗?Fragment常见问题以及androidx下Fragment的使用新姿势

【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析

When

OnBackPressedDispatcherandroidx activity 1.0.0 加入,旨在处理返回逻辑。您不仅可以获得在 Activity 之外处理返回键的便捷方式。 根据您的需要,您可以在任意位置定义 OnBackPressedCallback,使其可复用,或根据应用程序的架构进行任何操作。 您不再需要重写Activity 中的 onBackPressed 方法,也不必提供自己的抽象的来实现需求的代码。

OnBackPressedDispatcher

What

ComponentActivityFragmentActivityAppCompatActivity 的基类,它使您可以通过使用其 OnBackPressedDispatcher(可以通过调用 getOnBackPressedDispatcher() )来控制返回按钮的行为。

ComponentActivity-onBackPressed

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 时会自动删除其回调。

Activity onBackPressed()

如果您使用 onBackPressed() 处理返回按钮事件,建议您改用 OnBackPressedCallback 。 但是,如果您无法进行此更改,则适用以下规则:

  • 当您调用 super.onBackPressed() 时,将通过 addCallback 注册的所有回调。

  • 无论 OnBackPressedCallback 的任何注册实例,始终会调用 onBackPressed

Demo

关于 fragment 返回栈的 demo 已经写好了,感兴趣的小伙伴可以 在这 找到它。

我们下一篇再见。

【背上Jetpack之DataBinding】数据驱动魔法师 何时迎来翻身日?

前言

LiveData 篇 我们提到 Android 开发的主要工作内容是将数据转换为 UI ,同时我们也介绍了数据驱动 UI 的**,使用 ViewModel + LiveData,可以安全地在订阅者的生命周期内分发正确的数据。但是 activity 和 fragment 充斥着大量的模板代码,铺天盖地的 findViewById,以及各种 set (根据数据设置 UI)。如果能够消灭掉这些模板代码就好了

他来了他来了

他来了他来了,他欢快地走来了

DataBinding

然而,很多开发者对 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 基础

详细内容参见 官方文档 ,这里只简单介绍 DataBinding

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)
}

使用DataBinding 解决的问题及实现原理

不知道你是否有这些烦恼: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 如何解决上述问题的

我们可以查看 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 中的代码大大减少

Binding 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}" />

DadaBinding + LiveData

要将 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 ,如果只用这部分功能可以使用 ViewBinding
  • 进行 data 和 UI 的绑定,使用「数据驱动」的**解决了视图的一致性问题

各种 findViewById 替代方案对比

  • findViewById
  • Butterknife
  • Kotlin Synthetics
  • Data Binding
  • View Binding

findViewById

findViewById 有两个问题

  1. 当不能在 Activity/Fragment/ViewGroup 中定位到指定 id 的 View,会在运行期间崩溃,即非空安全
  2. 如果某个 view 为 TextView 类型,而在使用中将其指定为其他类型不会在编译器报错,即非类型安全

在 compileSdk 的 API 级别 26 中,对该方法的定义稍作更改以消除强制类型转换问题

现在,开发人员无需在代码中手动转换 view 类型。 如果您引用 id 指向类型 TextView 的 View 并将其指定为 Button,则 Android SDK 会尝试查找具有提供的 id 的 Button,并且它将返回 Null,因为它将无法找到它

但是在 Kotlin 中,您仍然需要提供诸如 findViewById(R.id.txtUsername) 之类的类型。 如果您不检查视图是否具有 null 安全,则可能出现 NullPointerException,但是此方法不会像以前那样抛出ClassCastException

Butterknife

ButterknifeJake Wharton 大神写的替代 findViewById 的库,该库使用注解处理并生成 findViewById 代码

它具有与 findViewById 几乎相似的问题。 但是,它在运行时添加了null 安全检查以避免 NullPointerException

由于 DataBinding 和 ViewBinding 的出现,沃神已经宣布弃用该库

Kotlin Synthetics

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

DataBinding 在功能上比其他方法优越得多,因为它不仅为您提供类型安全和空安全的 view 引用,而且还允许您直接在 xml 布局内使用数据驱动视图变化

ViewBinding

最近在 Android Studio 3.6 中引入的 ViewBinding 是 DataBinding 库的子集。 由于不需要注解处理,因此可以缩短构建时间。详细的使用可以参见 这篇文章

findViewById Butterknife Kotlin Synthetics DataBinding ViewBinding
一直空安全 部分 部分 ✔️ ✔️
类型安全 ✔️ ✔️ ✔️
样板代码 中等
构建时间 ✔️ ✔️ ✔️
支持语音 java/kotlin java/kotlin kotlin java/kotlin java/kotlin

【背上Jetpack】AdroidX下使用Activity和Fragment的变化

过去的一段时间,AndroidX 软件包下的 Activity/Fragmet 的 API 发生了很多变化。让我们看看它们是如何提升Android 的开发效率以及如何适应当下流行的编程规则和模式。

本文中描述的所有功能现在都可以在稳定的 AndroidX 软件包中使用,它们在去年均已发布或移至稳定版本。

在构造器中传入布局 ID

AndroidX AppCompat 1.1.0Fragment 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 中的方法。 这样,您现在可以在屏幕上组成几个独立的类,获得更高的灵活性,复用代码,并且通常在不引入自己的抽象的情况下,对代码结构具有更多控制。 让我们看看这在两个示例中如何工作。

1. OnBackPressedDispatcher

有时,您需要阻止用户返回上一级。 在这种情况下,您需要在 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 方法,也不必提供自己的抽象的来实现需求的代码。

2. SavedStateRegistry

如果您希望 Activity 在终止并重启后恢复之前的状态,则可能要使用 saved state 功能。 过去,您需要在 Activity 中重写两个方法:onSaveInstanceStateonRestoreInstanceState。 您还可以在 onCreate 方法中访问恢复的状态。 同样,在 Fragment 中,您可以使用 onSaveInstanceState 方法(并且可以在 onCreateonCreateViewonActivityCreated方法中恢复状态)。

AndroidX SavedState 1.0.0(它是 AndroidX ActivityAndroidX 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.0AndroidX Fragment 1.2.0 开始,启用 SavedStateSavedStateViewModelFactory 是在获取 ViewModel 的所有方式中使用的默认工厂:委托 ViewModelProvider 构造函数和 ViewModelProviders.of() 方法。

FragmentFactory

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 对象。

测试 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 ->
  ...
}

More Kotlin!

很高兴看到 -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)
}

FragmentContainerView

一件小而重要的事情。 如果您将 FrameLayout 用作 Fragment 的容器,则应改用 FragmentContainerView 。 它修复了一些动画 z轴索引顺序问题和窗口插入调度。 从 AndroidX Fragment 1.2.0 开始可以使用 FragmentContainerView

【奇技淫巧】巧用 kotlin 扩展函数和 typealias 封装 带网络状态和解决「粘性」事件的 LiveData

关于 LiveData 两个常用的姿势

使用包装类传递事件

我们在使用 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 的 扩展函数更优雅的使用

event 扩展函数

使用

demo 中封装了两种形式的 LiveData,一种为 LiveData<Boolean>,一种为 EventLiveData<Boolean>,当屏幕旋转时,前者会再次回调结果,而后者由于事件已被处理而不执行 onChanged,我们通过 Toast 可观察到这一现象

java 版的可参考

封装带网络状态的数据

很多时候我们在获取网络数据时要封装一层网络状态,例如:加载中,成功,失败

在使用时我们遇到了和上面一样的问题,多层泛型用起来很麻烦

我们依然可以使用 typealias + 扩展函数来优雅的处理该问题

typealias

扩展函数

使用

demo 截图

demo

Demo

demo 在这,如果感觉这个思路对你有帮助的话,点一颗小星星吧~ 😉

另外我还将它传到了 JitPack 上,引入姿势如下:

  1. 在项目根目录的 build.gradle 加入

    allprojects {
      repositories {
        //...
        maven { url 'https://jitpack.io' }
      }
    }
  2. 添加依赖

    dependencies {
      implementation 'com.github.Flywith24:WrapperLiveData:$version'
    }

【译】Android Styling 4: 主题实战

原文:Android Styling: Themes Overlay

作者:Nick Butcher

译者:Flywith24

题图来自 Virginia Poltrack

在 Android styling 系列文章中,我们研究了 style 与 theme 的区别 ,讨论了 使用主题和主题属性的优势,并且介绍了 常用的属性

今天,我们将集中讨论主题的实际使用,如何将其应用到您的应用程序中

范围

第一篇文章 中我们提到

Theme 可以作为 Context 的属性被获取,并且它可以从任何 Context 或 Context 的子类获得,例如 ActivityView,或者 ViewGroup。这些对象存在于一个「树」中,其中 Activity 包含 ViewGroup,ViewGroup 包含 View。在此树的任何级别上指定主题都会影响到其后代节点,例如在 ViewGroup 上设置 Theme 会作用域其所有子 View(这与只作用于单一 View 的 Style 相反)

在此「树」中的任何一层设置主题都不会 「替换 」当前有效的主题,而是将其 「覆盖」。下面的例子中有一个按钮,该按钮可以选择一个主题,但它的 parent 也可以指定一个主题:

<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<ViewGroupandroid:theme="@style/Theme.App.Foo">
  <Buttonandroid:theme="@style/Theme.App.Bar"/>
</ViewGroup>

如果在两个主题中都指定了属性,则最本地的 「获胜」,即 主题Bar 将应用于按钮。 在主题 Foo 中指定但 在主题 Bar 中指定的任何属性也将应用于按钮

Themes overlay each other

这可能看起来像是脱离实际的示例,但是有些场景特别有用。例如在浅色屏幕上的深色 Toolbar,或者这个界面(来自 Owl sample app),它大部分是粉色主题,但是底部是一个蓝色主题

A blue sub-section within a pink themed screen

这可以通过在蓝色部分的 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 作为命名前缀。MDCAppCompat 提供了很多方便的 theme overlays,您可以使用它们为程序的特定部分的颜色做从浅色到深色的转换

根据定义,theme overlays 不会指定很多内容,因此它不应被单独使用。例如,将其用作 activity 的主题。事实上,您可以在 app 中使用两种类型的主题:

  1. “Full” themes,它指定了屏幕所需要的一切,并继承自另一个 full theme,例如 Theme.MaterialComponents。它们应该在 activity 中使用
  2. Theme overlays,与 full theme 配合使用。它不应该独立使用,因为可能会缺失一些必要的内容

注意

总会有一个生效的主题,即使您未在应用中的任何地方指定一个主题,也会继承默认主题。 因此,上面的示例只是一种简化,因此您绝对不应在 View 中使用 full theme,而是应该使用 theme overlays

内存开销

使用主题会在运行期产生一定的开销。每当您声明一个 android:theme,您都在创建一个新的 ContextThemeWrapper 来分配新的 ThemeResource 实例。它还间接引入了更多层的 styling。请谨慎使用主题,尤其在 RecyclerView 这种重复使用的场景下

在 Context 中使用

我们在前文提到主题与 Context 关联——这意味着如果您正在使用 context 来检索代码中的资源,那么请注意使用正确的 context 。例如您在某个位置获取 Drawable

使用错误的 Context

如果 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

译文完

本系列完

系列译文

【奇技淫巧】子 module 的 build.gradle 中没有一行代码?多项目构建技巧

前言

之前写过两篇关于管理项目中依赖本的文章:

什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin

【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本?巧用 includeBuild

Android Detail 项目 目前使用的是第二篇提到的方式。

主工程(Android-Detail)与一个版本控制插件(version)通过组合构建(composing builds)进行管理。

今天我们来谈一谈在这种方式的基础上如何抽取公共配置,使得 build.gradle 文件的内容尽可能少,甚至内容可以为空。

阅读本文,你将了解:

  • 如何抽取 build.gradle 文件中的公共配置
  • 如何一键切换本地 module 与远程依赖

Demo 在这

抽取 android 闭包的配置

Android Detail 下除了 baselib 是 library module,其它都是 app module。app module 中有很多相同的配置,如下图:

我们可以为 BaseExtension 写一个扩展函数,名字叫 applyBaseCommons。在该方法中,我们配置 android 闭包下的公共配置,例如 compileSdkVersionversionCode 等。

接着,我们在 version plugin 中调用该方法,即可为所有 module 配置 android 闭包内的公共配置。

抽取公共依赖

每个 app module 都有着公共的依赖,如 test 相关的依赖,Kotlin 标准库的依赖,并且同时引用了 baselib module

按照上面的思路,我们可以再一个配置依赖的扩展函数

接着在在 version plugin 中调用该方法

抽取公共插件

每个 app module 都有 kotlin-androidkotlin-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

使用组合构建切换远程依赖/本地module

组合构建可以将多个 project 一起构建,例如我在 Android-Detail 的 settings.gradle 通过 includeBuild 关键字引入了我的 另一个项目,该项目已发布到 Jitpack,可以使用 com.github.Flywith24:Flywith24-Permission:1.0.1 引入

此处根据环境变量 useLocal 来判断是否 includeBuild permission 项目,如果为 false ,则使用远程依赖,如果为 true,则会使用本地 module :library

一键切换远程依赖/本地module 的其它姿势

在 project 使用前文类似的配置,也可实现一键切换远程依赖/本地module 的功能

这样调试阶段就不需要频繁的发快照包啦~

关于我

我是 Flywith24,我的博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

【玩转Test】Fragment 集成测试,FragmentScenario Espresso Mockito 介绍

前言

前三篇文章我们介绍了如何写单元测试,从这篇文章开始,我们介绍一下 集成测试

fragment 和 ViewModel 联系很紧密,我们需要确保 ViewModel 在适当时的时机更新 UI,那么该如何测试这部分内容呢?


本文内容来自 Udacity Advanced Android with Kotlin-Lesson 11-5.2 Testing: Intro to Test Doubles & Dependency Injection

Fragment 集成测试

为了在下面的架构上进行 集成测试 ,我们需要尽可能的屏蔽无关代码

例如我们可以使用 empty activity,它不包含 fragment 或 activity 的其他代码。对于数据层,可以使用 test doubles 来替代

这样就可以聚焦于 fragment 和 ViewModel 的代码

FragmentScenario

当你需要测试 activity 和 fragment 时,AndroidX test 中的 FragmentScenarioActivityScenario 的 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 中均可使用

Espresso

如果想要测试 Android 中的 UI 组件可以使用 Espresso 库,使用该库你可以使用 view 并检查它们的状态

引入

androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"

使用

一个 Espresso 有四个最基本的部分

Espresso 介绍

onView()Espresso 中一个常用的静态方法,它意味着接下来要对 view 进行操作

ViewMatches 的职责就是寻找 view ,上图中使用的是 withId() 方法,根据 id 匹配相应的 view。还有一些其他的匹配方法,例如 withText

注意:保证 ViewMatches 只能匹配到一个 view,否则会抛出 AmbiquousviewMatcher Exception

ViewAction 是 view 执行的动作,示例中是 click 方法

通过 ViewAssertion 我们可以判断 view 的状态是否符合我们的预期

tips:为了提高响应速度,您可以在开发者选项中将动画关闭

关闭动画

Mockito

Mock

我们在 【玩转Test】Test Doubles 的概念及如何测试 Repository 中介绍了 test doubles 的类型,其中我们主要介绍了 Fake。今天,我们来介绍 test doubles 的另一种类型:Mock

不同于 FakeMock 侧重于跟踪方法的调用,这么说可能比较抽象,让我们举个栗子

例子

如上图,有一个方法被调用并改变了 UI(更新 text )

如果使用 Mock ,则验证更新 text 的方法是否被正确地调用

如果不使用 Mock,我们通常会验证 TextView 中的 text 是否符合预期

引入 Mockito

为了进行 Mock 测试,我们需要引入 Mockito

androidTestImplementation "org.mockito:mockito-core:2.25.0"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.12.1"

有的小伙伴可能会问了,有什么场景需要使用 Mock 吗?

这里有一个例子比较合适,测试使用 navigation 进行 fragment 的跳转

Testing Navigation

我们有一个 HostFragment ,内部有一个 button,点击可以跳转到 RepoListFragment 并将用户名传递过去

navigation

接下来我们来测试这部分的跳转

首先提供第一个 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"))

【奇技淫巧】gradle依赖查找太麻烦?这个插件可能帮到你

作为 Android 开发者,项目中引入 gradle 依赖是家常便饭,但是 Android Studio 自带的依赖查询工具并不好用,mac 上使用 Alfred 搭配 workflow 可以方便地 copy gradle dependency。但 Alfred 是mac独占的,如果有一个跨平台的插件就好了。


「每当你在感叹,如果有这样一个东西就好了的时候,请注意,其实这是你的机会..」,于是这款插件诞生了。

uTools

uTools是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

当你熟悉它后,能够为你节约大量时间,让你可以更加专注地改变世界。

来自官网介绍

这是一款类似 WoxAlfred 的工具

支持三端

笔者使用 uTools 替代 Wox 半年多,完美切换,没有不适感。

插件介绍

插件介绍

使用

  1. 唤出 uTools
  2. 键入 google 并输入关键词,在 google maven repository 中查询
  3. 键入 maven 并输入关键词,在 maven center 中查询
  4. 选中结果自动 copy 至剪贴板

截图

google repository

maven center

剪切板

演示视频

前往查看B站 演示视频

项目地址

github 地址

笔者不擅长 js ,欢迎各位提 PR

todo

  • jitpack repository 查询
  • 依赖历史版本list
  • maven center 查询防抖优化

【Jetpack更新之Fragment】setRetainInstance 被弃用

我们都知道 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 2: 常用主题属性

原文:Android Styling: Common Theme Attributes

作者:Nick Butcher

译者:Flywith24

题图来自 Virginia Poltrack

B站官方视频


在 Android styling 系列文章的第一篇,我们研究了主题和样式之间的区别以及主题如何使开发者写出更灵活的样式和布局

具体来说,我们建议您使用主题属性来提供资源的间接访问点,以便您可以改变它们(例如,深色主题)。 也就是说,如果发现自己在布局或样式中编写了直接的资源引用(或更糟糕的是,一个硬编码值😱),请考虑是否应该使用主题属性

但是可以使用哪些主题属性? 本文重点介绍了您应该了解的常见知识; 来自 MaterialAppCompatplatform 的内容。 这不是一个完整的列表(为此,我建议您浏览定义在下面链接的 attrs 文件),但是这些都是我一直使用的属性(使用主题属性实现

Colors

这里的很多颜色来自于 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 次要颜色

Dimens

  • ?attr/listPreferredItemHeight list item 的标准(最小)高度
  • ?attr/actionBarSize toolbar 的高度

Drawables

  • ?attr/selectableItemBackground 当前交互项的水波纹/高亮(也为前景提供了便利)
  • ?attr/selectableItemBackgroundBorderless 无界的水波纹
  • ?attr/dividerVertical 一个可绘制对象,可用作元素之间的垂直分隔线
  • ?attr/dividerHorizontal 一个可绘制对象,可用作元素之间的水平分隔线

TextAppearances

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 文本

Shape

Material 采用了 shape system,该系统为小型,中型和大型组件 提供 了主题属性。请注意,如果要在自定义组件上设置 shape,则可能要使用 MaterialShapeDrawable 作为其背景,它可以理解并实现 shape

  • ?attr/shapeAppearanceSmallComponent 在 Button ,Chip,Text 的属性中使用,默认 4dp 的圆角
  • ?attr/shapeAppearanceMediumComponent 在 Card,Dialog,Date Picker 中使用,默认 4dp 的圆角
  • ?attr/shapeAppearanceLargeComponent 在 Bottom Sheet 中使用,默认 0dp 圆角

Button Styles

这看起来似乎很具体,但是 Material 定义了三种类型的 button:Contained, Text 以及 Outlined。MDC 提供了主题属性,可用于设置 MaterialButtonstyle

  • ?attr/materialButtonStyle 默认样式,可省略
  • ?attr/borderlessButtonStyle 文本样式的 button
  • ?attr/materialButtonOutlinedStyle outline 样式的 button

Floats

  • ?android:attr/disabledAlpha 为控件禁用透明度
  • ?android:attr/primaryContentAlpha 前景元素的透明度
  • ?android:attr/secondaryContentAlpha 次级元素的透明度

App vs Android namespace

你可能注意到,有些属性由 ?android:attr/foo 引用,而其他的则为 ?attr/bar 。这是因为它们中的部分是由 Android Platform 定义的,因此您需要 android前缀通过命名空间引用它们(就像 layout 中 view 的属性:android:id)。那些不是来自静态库(即 AppCompat 或 MDC ),它们已编译到您的应用程序中,因此不需要名称空间(类似于您在布局中使用 app:baz 的方式)。一些元素在 platform 和 library 均有定义(例如 colorPrimary)。在这种情况下,最好使用非平台版本,这样可以在所有 API 级别上使用。例如它们是在 library 中重复定义恰好目的是为了向后兼容。在这些情况下,我已在上面列出了非平台版本

首选可以在所有API级别上使用的非平台属性

More Resources

有关可用的主题属性的完整列表,可以直接访问以下链接

Material Design Components :

Do It Yourself

有时,没有主题属性可以抽象出您希望随主题变化的内容(同样的 attribute 在不同的主题下不同),不必担心,你可以自定义!这是 Google I / O 应用程序中的一个示例,该示例在两个屏幕中显示了会议列表

Two screens listing conference sessions

它们在很大程度上相似,但左屏幕必须为时间标题留出空间,而右屏幕则不能。 我们通过抽象在主题属性后面对齐 item 的位置来实现此目的,以便我们可以根据主题来改变它们并在两个不同的屏幕上使用相同的布局:

  1. attrs.xml 中定义主题属性
<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<attr name="sessionListKeyline" format="dimension" />
  1. 为不同的主题提供 different values
<!-- 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>
  1. 在同一个 layout 中 使用 主题属性,并配置在不同的界面(每个界面使用上面的两个主题之一)
<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<Guidelineapp:layout_constraintGuide_begin="?attr/sessionListKeyline" />

Question (mark) everything

了解可用的主题属性后,您便可以在编写布局,样式或可绘制对象时使用它们。 使用主题属性使支持主题(如深色主题)和编写更灵活,维护代码变得更加容易。 要对此进行深入研究,请看本系列的下一篇文章

感谢 Florina Muntenescu 和 Chris Banes

译文完

【背上Jetpack之Fragment】你真的会用Fragment吗?Fragment常见问题以及androidx下Fragment的使用新姿势

Android Jetpack 组件中,fragment作为视图控制器之一占有很重要的位置。但由于其bug众多,暗坑无数,以至于 Square 有这样一篇博客:Advocating Against Android Fragments。github上的 Fragmentation 有着 9.4k 的star。

而现在,androidx fragment 稳定版已来到 1.2.2,让我们总结一下fragment有哪些常见问题以及有哪些使用fragment的新姿势

Fragment 常见的问题

  • getSupportFragmentManager , getParentFragmentManager 和 getChildFragmentManager

  • FragmentStateAdapter 和 FragmentPagerAdapter

  • add 和 replace

  • observe LiveData时传入 this 还是 viewLifecycleOwner

  • 使用 simpleName 作为 fragment 的 tag 有何风险?

  • 在 BottomBarNavigation 和 drawer 中如何使用Fragment多次添加?

  • 返回栈

getSupportFragmentManager , getParentFragmentManager和getChildFragmentManager

FragmentManagerandroidx.fragment.app(已弃用的不考虑)下的抽象类,创建用于 添加,移除,替换 fragment 的事务(transaction

首先要确认一件事,getSupportFragmentManager()FragmentActivity下的方法

getParentFragmentManagergetChildFragmentManagerandroidx.fragment.app.Fragment 下的方法,其中 androidx.fragment 1.2.0getFragmentManagerrequireFragmentManager 已弃用

明确了这件事,接下来的就很清晰了

  • getSupportFragmentManageractivity关联,可以将其视为 activityFragmentManager
  • getChildFragmentManagerfragment关联,可以将其视为fragmentFragmentManager
  • getParentFragmentManager情况稍微复杂,正常情况返回的是该fragment 依附的activityFragmentManager。如果该fragment是另一个fragment 的子 fragment,则返回的是其父fragmentgetChildFragmentManager

如果这么说还不明白的话,我们可以做一个实践。

创建一个 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 中使用 ViewPagerBottomSheetFragmentDialogFragment 时,都应使用 getSupportFragmentManager

  • fragment 中使用 ViewPager 时应该使用getChildFragmentManager

错误的在 fragment 中使用 activityFragmentManager 会引发内存泄露。 为什么呢?假如您的fragment中有一些依靠 ViewPager 管理的子 fragment,并且所有这些 fragment 都在 activity 中,因为您使用的是activityFragmentManager 。 现在,如果关闭您的父fragment,它将被关闭,但不会被销毁,因为所有子fragment都处于活动状态,并且它们仍在内存中,从而导致泄漏。 它不仅会泄漏父fragment,还会泄漏所有子fragment,因为它们都无法从堆内存中清除。

FragmentStateAdapter 和 FragmentPagerAdapter

FragmentPagerAdapter将整个 fragment 存储在内存中,如果ViewPager中使用了大量 fragment,则可能导致内存开销增加。 FragmentStatePagerAdapter仅存储片段的savedInstanceState,并在失去焦点时销毁所有 fragment

让我们看看常见的两个问题

1. 刷新ViewPager不生效

ViewPager 中的 fragment 是通过 activity fragmentFragmentManager 管理的,FragmentManager 包含了viewpager的所有fragment的实例

因此,当ViewPager没有刷新时,它只是FragmentManager仍保留的旧 fragment 实例。 您需要找出为什么FragmentManger持有fragment实例的原因。

2. 在Viewpager中访问当前fragment

这也是我们遇到的一个非常普遍的问题。 如果遇到这种情况,我们一般在 adapter 内部创建 fragment 的数组列表,或者尝试使用某些标签访问fragment。 不过还有另一种选择。 FragmentStateAdapterFragmentPagerAdapter都提供方法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()

add 和 replace 如何选择?

在我们的activity中,我们有一个容器,其中装有fragment

add只会将一个fragment添加到容器中。 假设您将FragmentAFragmentB添加到容器中。 容器将具有FragmentAFragmentB,如果容器是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的情况下将调用onPauseonResumeonCreateView和其他生命周期事件,在add的情况下则不会。

如果不需要重新访问当前fragment并且不再需要当前fragment,请使用replace。 另外,如果您的应用有内存限制,请考虑使用replace

observe LiveData时传入 this 还是 viewLifecycleOwner

androidx fragment 1.2.0 起,添加了新的 Lint 检查,以确保您在从 onCreateView()onViewCreated()onActivityCreated() 观察 LiveData 时使用 getViewLifecycleOwner()

使用 simpleName 作为 fragment 的 tag 有何风险?

一般情况下我们会使用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 和 drawer 中如何使用Fragment多次添加?

当我们使用BottomBarNavigation NavigationDrawer时,通常会看到诸如fragment 重建或多次添加相同fragment之类的问题。

在这种情况下,您可以使用show / hide 而不是 addreplace

返回栈

如果您想在fragment的一系列跳转中按返回键返回上一个fragment,应该在commit transaction之前调用addToBackStack方法

//使用该扩展 androidx.fragment:fragment-ktx:1.2.0 以上
parentFragmentManager.commit {
	addToBackStack(null)
  	add<SecondFragment>(R.id.content)
}

Fragment的使用新姿势

  • fragment-ktx 有哪些好用的扩展函数

  • fragment 之间和与 activity 通信

  • 使用 FragmentContainerView 作为 fragment 容器

  • FragmentFactory 的使用

  • Fragment 返回键拦截

  • Fragment 使用 ViewBinding

  • Fragment 使用 ViewPager2

  • 不需要重写 onCreateView 了?

  • 使用require_()方法

fragment-ktx 有哪些好用的扩展函数

1. FragmentManagerKt

//before
supportFragmentManager
    .beginTransaction()
    .add(R.id.content,Fragment1())
    .commit()

//after
supportFragmentManager.commit {
	add<Fragment1>(R.id.content)
}

2. FragmentViewModelLazyKt

//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 之间和与 activity 通信

fragment 和 fragment之间,fragment 和 activity 之间的通信有很多方法,android jetpack 推荐我们使用 ViewModel + LiveData 处理

同一个activity 内的 fragment 之间通信,可以使用作用范围为activity的ViewModel,activity与 fragment通信同理。详情可移步 Android官方应用架构指南

使用 FragmentContainerView 作为 fragment 容器

过去我们使用 FrameLayout 作为 Fragment 的容器,在 AndroidX Fragment 1.2.0 后,可以使用 FragmentContainerView 代替 Fragment

它修复了一些动画 z轴索引顺序问题和窗口插入调度,这意味着两个fragment之间的退出和进入过渡不会互相重叠。使用FragmentContainerView将先开启退出动画然后才是进入动画。

FragmentContainerView 是专门为 fragment设计的自定义View,它继承自 FrameLayout

android:name 属性允许您添加fragmentandroid: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>

FragmentFactory 的使用

过去,我们只能使用其默认的空构造函数实例化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)
    }
}

fragmentFragmentManager 管理,因此很自然,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的实例。

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)
  }
}

Fragment 使用 ViewBinding

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
    }
}

Fragment 使用 ViewPager2

ViewPager使用了三个adapter的抽象类,而ViewPager2中只有两个

  • ViewPager 中使用 PagerAdaper,ViewPager2 中使用 Recyclerview.Adapter
  • ViewPager 中使用 FragmentPagerAdapter ,ViewPager2中使用 FragmentStateAdapter
  • ViewPager 中使用 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"

对于ViewPager2TabLayout布局应与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时,TabLayoutViewPager联动需要调用 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()
    }
    ...
}

不需要重写 onCreateView 了?

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)

使用require_()方法

androidx fragment 1.2.2 起,新增了一项lint检查,fragment 建议使用关联的require_()方法获取更多描述性错误消息,而不是使用checkNotNull(get_())requireNonNull(get_())get()! 适用于所有包含 get 和 require Fragment API

例如:使用 requireActivity() 替代 getActivity()

【奇技淫巧】Android组件化不使用 Router 如何实现组件间 activity 跳转

前言

越来越多的项目使用了组件化,组件之间的通信是一个比较重要的问题。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)

Demo

Demo 地址

各位有什么想法欢迎在评论区留言

【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本?巧用 includeBuild

buildSrc 的缺陷

Android 开发中统一不同 module 的依赖版本十分重要,传统的方式是使用 ext 的方式

ext

之前我发过关于使用 buildSrc 简化项目中 gradle 代码的译文:什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin

该种方式可以很好的管理 gradle 的公共配置,这其中当然包括依赖版本

配置依赖

如图,在使用依赖时有代码提示,而且可以点击进入查看

但是由于 buildSrc 是对全局的所有 module 的配置,因此在构建速度上会慢一些。那么有没有一个更纯净的方式来配置依赖版本呢?

今天我们来介绍一种新的方式

自定义 plugin + includeBuild

使用 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"
}

之后我们就可以使用了

Demo

demo代码截图

demo代码截图

demo 在这

【Jetpack更新之Fragment】终于动手了,onActivityCreated 被弃用

本系列文章介绍 Jetpack 组件库的更新

一直以来, fragment 的 api 都非常难用,官方也承认这一点。一个月前,fragment 中的 onActivityCreated() 被弃用了

Fragment

fragment 1.3.0-alpha02onActivityCreated() 方法被弃用了

让我们来看一下提交 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

那么 DialogFragment 怎么办?其 onActivityCreated 变为可选的

简单翻译一下

DialogFragment 使用 onActivityCreated() 帮助创建 dialog。onActivityCreated() 弃用后我们应当寻找一个更好的方式来执行这部分逻辑

关于 view 相关的代码已经转移至 DialogFragment

viewLifecycleOwnerLiveData ,其他初始化逻辑可以放在 onGetLayoutInflater

我们仍支持为自定义 dialog 在 onActivityCreated() 中配置 dialog

End

查看 Jetpack fragment 的变动,不难看出官方正致力于为 fragment 「减负」,将小的,独立的功能从 fragment 中抽离出去,降低耦合,后续文章我们介绍其他的改动

【译】Kotlin 协程,JVM 线程以及并发问题

原文:Bridging the gap between coroutines, JVM threads, and concurrency problems

作者:Manuel Vivo

译者:Flywith24

「协程是轻量级的线程」,是不是经常听到这样的描述?这个描述对你理解协程有实质性的帮助吗?可能没有。阅读本文,您会对 协程在 JVM 中实际的执行方式,协程与线程的关系以及使用 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,DispatchedContinuationresumeWith 方法负责分配给适合的协程!

此外,DispatchedContinuationDispatchedTask,在 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.DefaultDispatchers.IO 隐式地连接在一起,因为它们使用相同的线程池。下面我们来看看使用不同的 Dispatcher 调用 withContext 的运行时开销是怎样的?

线程与 withContext 性能

在 JVM 中,如果创建的线程多于可用的 CPU 核心数,则在线程之间进行切换会带来一些运行时开销。上下文切换 的成本并不低!操作系统需要保存和恢复执行上下文,CPU 需要花时间调度线程而不是运行实际的 app 工作。除此之外,如果线程正在运行的代码阻塞了,也可能会发生上下文切换。如果线程是这种情况,将 withContext 与不同的 Dispatchers 配合使用是否会对性能造成损失?

幸运的是,如您所料,线程池为我们管理了这些复杂的场景,并尝试尽可能优化被执行的工作(这就是在线程池上执行工作比手动在线程中执行工作更好的原因)。协程也从中受益(因为它们是在线程池中调度的)!最重要的是,协程不阻塞线程,而是 suspend 工作! 甚至更有效率!

默认情况下,CoroutineScheduler 是 JVM 实现中使用的线程池,它以最有效的方式将分派的协程分配给工作线程。由于 Dispatchers.DefaultDispatchers.IO 使用相同的线程池,因此优化了它们之间的切换,以尽可能避免线程切换。协程库可以优化这些调用,保留在相同的调度器(dispatcher)和线程上,并遵循一个快速路径(fast-path)。

由于 Dispatchers.Main 通常是 UI app 中不同的线程,因此在协程中 Dispatchers.DefaultDispatchers.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,可以使用 ConcurrentHashMapConcurrentHashMap 是一个线程安全的同步集合,可优化 Map 的读写吞吐量。

请注意,线程安全的数据结构不能防止调用方排序问题,它们只是确保内存访问是原子性的。当逻辑不太复杂时,它们有助于避免使用锁。例如,它们不能在上面显示的 transactionCache 示例中使用,因为操作顺序和它们之间的逻辑需要线程和访问保护。

同样,这些线程安全数据结构中的数据必须是不可变的或受保护的,以防止在修改已存储在其中的对象时出现竞争条件。

自定义解决方案

如果您有需要同步的复合操作,则 @Volatile 变量或线程安全的数据结构将无济于事!内置的 @Synchronized 注解可能不够精细,无法提高的效率。

在这种场景下,您可能需要使用并发工具(如 latch信号量 屏障)创建自己的同步机制。其它场景,您可以使用锁或互斥锁保护代码的多线程访问。

Kotlin 中的 Mutex 具有 lockunlock 的挂起函数以用来手动保护协程代码。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 线程模型中并受其所有约束。使用协程,仍会写出错误的多线程代码。因此,在代码中访问共享的可变状态要小心!

译文完。

译者总结

  • 基于 JVM 的 Kotlin 协程本质上是基于 JVM 线程池工作的
  • 协程是 Android 中异步编程的推荐方案
  • 协程也存在并发问题,开发者需要注意并解决
  • 并发问题的根源在于状态管理
  • 保护可变状态需要视具体情况而定,但有一些小技巧

推荐阅读

关于我

人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~

我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。

【译】Android Styling 1: Themes vs Styles

原文: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

Theme 和 Style 都使用 <style> 标签,但它们的用途截然不同。您可以将它们视为一个 key-value 模型,其中 key 是属性,而 value 代表资源。

Style 是什么?

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 -->
<Buttonandroid:gravity="center_horizontal"
  android:textAppearance="@style/TextAppearance.CommentAuthor"
  android:drawablePadding="@dimen/spacing_micro"/>

将它们抽取为 style 可以更方便地在多个 view 中复用和维护

Style 的使用

<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<Buttonstyle="@style/Widget.Plaid.Button.InlineAction"/>

View 只能使用一种 Style,这与其他的 styling systems 不同(例如 Web 上的 CSS,在该系统中,组件可以设置多个 CSS 样式)

Style 的作用范围

一个 Style 只作用于其应用的 view,不包含它的任何子 view。

例如,存在一个 ViewGroup,其内部有三个 button。为 ViewGroup 配置 style 不会作用于这些 button。Style 提供的值会与在布局中直接设置的值组合(使用 styling precedence order

Theme 是什么?

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)

Theme 的使用

您可以在具有 Context 的组件中使用 theme,例如 Activity ,View ,ViewGroup

<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->

<!-- AndroidManifest.xml -->
<applicationandroid:theme="@style/Theme.Plaid">
<activityandroid:theme="@style/Theme.Plaid.About"/>

<!-- layout/foo.xml -->
<ConstraintLayoutandroid:theme="@style/Theme.Plaid.Foo">

您还可以通过用 ContextThemeWrapper 包装现有的 Context 来在代码中设置 theme,然后将其用于 inflate 布局等

Theme 的作用范围

Theme 可以作为 Context 的属性被获取,并且它可以从任何 Context 或 Context 的子类获得,例如 ActivityView,或者 ViewGroup。这些对象存在于一个「树」中,其中 Activity 包含 ViewGroup,ViewGroup 包含 View。在此树的任何级别上指定主题都会影响到其后代节点,例如在 ViewGroup 上设置 Theme 会作用域其所有子 View(这与只作用于单一 View 的 Style 相反)

<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<ViewGroupandroid: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

译文完

【Jetpack 更新之Activity】ContextAware 是个啥?

前言

最近 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 的继承关系

如上图所示,Activity 的继承关系是:

android.app.Activityandroidx.core.app.ComponentActivityandroidx.activity.ComponentActivityandroidx.fragment.app.FragmentActivityandroidx.appcompat.app.AppCompatActivity → 自定义 Activity

  • android.app.Activity 是固化在 ROM 中的 framework 层的 Activity(代码逻辑随着 Android 版本发布而固定),最基础的 Activity,拥有 Activity 最核心的逻辑
  • androidx.core.app.ComponentActivityandroidx.core library (提供最新的平台功能并兼容旧设备)下的 Activity,其内部逻辑很少,主要作为兼容层存在
  • androidx.activity.ComponentActivityandroidx.activity library 下的 Activity,其内部封装者 activity 库最新的 API,例如新的 Activity Result API。主要使用多接口的组合实现,例如实现了 LifecycleOwnerViewModelStoreOwnerActivityResultRegistryOwner 等接口
  • androidx.fragment.app.FragmentActivityandroidx.fragment library 下的 Activity,用于支持 Fragment。其内部持有 FragmentController 以对 Fragment 进行操作。
  • androidx.appcompat.app.AppCompatActivityandroidx.appcompat library 下的 Activity,主要用于为旧设备提供高版本的功能,如使用 Toolbar 操作标题栏,将 TextView 等控件映射成 AppcompatTextView 等控件。

引入的代码

为了引入 ContextAware 功能,官方在 androidx.activity library 下引入了相关的代码:

代码十分简单,两个接口和一个 Helper 类

核心逻辑

androidx.activity library 的 ComponentActivity 实现 ContextAware 接口,并在调用 super.onCreate() 方法前调用 dispatchOnContextAvailable() 方法,代码如下,删减无关代码。

一些细节

  1. FragmentActivity 内部使用该 API 来为 Fragment 与宿主 Activity 建立关联

  1. androidx.activity-ktx 提供了一个挂起函数来在 Context 可用时处理数据

总结

  • ContextAware 提供了平台 Activity 执行 onCreate() 方法前的回调,可以在该回调做一些必须在 super.onCreate() 前执行的逻辑
  • 为了灵活性与兼容性,Activity 有很多「中间层 Activity」
  • activity 1.2.0 已发布正式版,可以在生产环境使用
  • OnContextAvailableListener 对 library 开发或封装 library 的场景很有用

关于我

人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~

我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。

【奇技淫巧】AndroidStudio Nexus3.x搭建Maven私服遇到问题及解决方案

之前写过 Android Studio 多个项目依赖同一个模块的用法

不过在使用中遇到了几个问题,编译速度慢,总是显示出关联项目。

所以决定将公共模块aar使用 maven 私服管理,在此记录之。

Nexus3 下载与安装

官网

下载后解压,这里以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搭建

网上文章很多,下面说一下搭建过程中出现的问题。

问题及解决方案

1 unable to resolve dependency for:xxx

正常配置并引入私服的依赖,但是提示无法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/'
        }
    }
}

2 aar中的class.jar为空

成功引入依赖后发现找不到aar中的类

详情参考 解决aar混淆后包里是空的问题,android混淆讲解

解决

打出的aar是release的,所以关闭release的混淆,或者想暴露出的类禁止混淆即可

3 错误: 编码GBK的不可映射字符

生成 java doc 时提示错误: 编码GBK的不可映射字符

在module的build.gradle中配置

tasks.withType(Javadoc) {
    options.addStringOption('Xdoclint:none', '-quiet')
    options.addStringOption('encoding', 'UTF-8')
}

4 javadoc: 错误 - 非法的程序包名称

在 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代码文档

5 deploy 时出现 500, ReasonPhrase: Internal Privoxy Error.

 > 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私有仓库持续交付与集成

Maven私服Nexus 3.x搭建

解决aar混淆后包里是空的问题,android混淆讲解

使用Gradle打包Kotlin项目代码、生成Kotlin代码文档

雕虫晓技(四) 搭建私有Maven仓库(带容灾备份)

Android 11 下 Toast 变化,不能自定义 Toast 了?

前言

Android 11(R)是2020年的下一代 Android,Google 于上周发布了 Android 11: Developer Preview 3

在 Android 11 Toast 的行为发生了变更

  1. 禁止后台自定义 Toast

  2. text toast 不允许自定义

  3. setView() 被弃用

  4. 新增 Toast.Callback 回调

Android 11 API 变更

禁止后台自定义 Toast

自定义 Toast 不能 在 app 处于后台时显示,取而代之会显示 "Background custom toast blocked for package [packageName] See g.co/dev/toast." 的文本 toast

禁止后台自定义 Toast

普通的 text toast 不受影响

普通的 text toast 不受影响

text toast 不允许自定义

默认的 toast 是 text toast,如果想使用自定义的 toast ,需要调用 setView() 方法

在 targetSdkVersion 为 R 或更高时,调用 setGravity 和 setMargin 方法将不进行任何操作

官方文档中所述的 Android R 仅影响 text toast ,而自定义的 toast 不受影响

调用无效,仅影响 text toast

调用无效,仅影响 test toast

如图,在 test toast 中调用 setGravity 和 setMargin 方法,但 toast 位置并未居中

在 test toast 中调用 setGravity 和 setMargin 方法

并未居中,方法不生效

setView() 被弃用

setView() 方法被标记弃用

Deprecated 表示该功能目前仍可以使用,但可能会在将来的 Android 版本中删除。 建议开发人员避免长期使用此功能

setView 被弃用

可以看到,官方在一步步禁止自定义 Toast

目前是 targetSdkVersion 为 R 或更高的 app 禁止后台弹出自定义 Toast

同时将 setView() 方法标记弃用,当该方法从源码中移除后,自定 Toast 的方式将被彻底消灭

当然,官方提供了相应的替代品,使用 Snackbar

新增 Toast.Callback 回调

添加了新的回调(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()

一些小 tips 及 demo

demo 在这 ,切换 Flavor 即可指定不同的 targetSdkVersion

切换 Flavor

在写 demo 时遇到一些小问题

tip1

Handler() 无参构造方法和 Handler(Handler.Callback) 构造方法 被弃用了

无参构造器被弃用

简单来讲就是在初始化 Handler 时要显示的配置 Looper

Handler 使用不当会有这样一种 bug,例如在子线程通过无参构造函数创建 Handler,您可能会看到这样的异常

错误日志

抛出异常源码

详细内容这里就不讲了,这是 Android 开发者的必备知识

官方通过强制使用传入 Looper 的 Handler 构造器来避免使用中的问题

tip2

过去使用 Toast 构造器创建 Toast 对象 并调用 setText 方法会崩溃,targetSdkVersion 为 R 时不会崩溃

相同的代码 targetSdkVersion 低版本会崩溃

崩溃,但设置位置生效

异常log

API 29 源码

API 29 中调用 setText() 方法时要保证 mNextView 不为空,而 mNextView 是调用 setView 赋值的

API 29 setView 源码

因此过去使用 Toast 构造器创建 toast 对象无法创建普通的 text toast,必须调用 setView 方法

至于 API 30 肯定在这里做了修改,由于现在看不到源码,我也猜测不出官方的用意

如果各位小伙伴有什么想法欢迎评论区留言

【玩转Test】Test Doubles 的概念及如何测试 Repository

前言

不会测试的开发不是好开发——鲁迅

一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,本篇是该系列的第三篇,前面我们介绍了如何测试 ViewModel 和 LiveData,今天我们介绍一下如何测试 Repository


本文内容来自 Udacity Advanced Android with Kotlin-Lesson 11-5.2 Testing: Intro to Test Doubles & Dependency Injection

测试 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 Doubles 的概念

Test Double 是为测试精心准备的类,它可以在测试中替换真实版本的数据。就像电影中替身演员会替代演员去完成一些危险动作一样。因此在 Repository 中,我们可以为数据源制作 Test Double

事实上,存在很多种类的 Test Double,本系列文章会介绍 FakeMock


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 意味着数据源不是从网络或者数据库中获取,因此它只适用于测试

测试 Repository

我们可以将 LocalDataSourceRemoteDataSource 替换为 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))
}

【奇技淫巧】新的图片加载库?基于 Kotlin 协程的图片加载库——Coil

新的图片加载库——Coil

CoilInstacart 团队研发的新的的图片加载库,它使用了很多高级功能,例如协程,Okhttpandroidx.lifecycleCoil 还包括一些高级功能,例如图像采样,有效的内存使用以及请求的自动取消/暂停

默认情况下 Coil 与 R8 完全兼容,开箱即用,不需要添加额外的规则。如果使用 Proguard ,您可能需要为 Coroutines, OkHttpOkio 添加规则

Coil 的优势

  • 快速:Coil 进行了很多优化,包括内存和磁盘缓存,对内存中的图像进行采样,重新使用位图,自动暂停/取消请求等等
  • 轻量:Coil 在您的APK中添加了约 2000 种方法(对于已经使用 OkHttpCoroutines 的应用程序),与 Picasso 相当,远少于 GlideFresco
  • 易用:Coil 的 API 利用 Kotlin 的特性简化了样板代码
  • 现代:CoilKotlin-first,使用现代化的库,例如 Coroutines, OkHttp, Okio, 以及 AndroidX Lifecycles

Coil 是以下名称的缩写:Coroutine Image Loader

Artifacts

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:引入一系列解码器以支持解码 gif
  • io.coil-kt:coil-svg:引入一系列解码器以支持 svg
  • io.coil-kt:coil-video:包括两个 fetchers ,以支持从 Android 支持的任何视频格式中提取和解码帧
// 普通使用引用
implementation "io.coil-kt:coil:0.11.0"
// 使用依赖注入时或者制作基于 coil 的库引用
implementation "io.coil-kt:coil-base:0.11.0"

Java 8

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"
    }
}

使用

ImageView 扩展函数

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())
}

Image Loaders

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 和网络监听

Requests

有两种 Request 类型

如果要加载到自定义 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 支持的数据类型为

  • String (mapped to a Uri)
  • HttpUrl
  • Uri (android.resource, content, file, http, and https schemes only)
  • File
  • @DrawableRes Int
  • Drawable
  • Bitmap

预加载

如果要预加载到内存中,执行一个不带 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

使用要求

  • AndroidX
  • Min SDK 14+
  • Compile SDK: 29+
  • Java 8+

详细内容移步 官方文档

【译】MergeAdapter 的使用-使用官方 API 为 Recyclerview 加 Header 和 Footer

原文:Merge adapters sequentially with MergeAdapter

作者:Florina Muntenescu

译者:Flywith24

MergeAdapterrecyclerview 1.2.0-alpha02 中提供的新类,它使您可以顺序组合多个 adapter,以在单个 RecyclerView 中显示。 这使您可以更好地封装 adapter,而不必将许多数据源组合到单个 adapter 中,从而使它们集中并复用。

一个用例是在 Header 或 Footer 中显示列表加载状态:当列表从网络中检索数据时,我们想显示一个进度条。 如果出现错误,我们要显示错误和重试按钮。

A RecyclerView with a footer displaying the loading state: progress or error

引入MergeAdapter

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 中

RecyclerView and Adapter data

在 Header 和 Footer 中显示加载状态

我们的 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,其中添加:

  • ViewModel 公开的 LoadState
  • Header 和 Footer 的加载状态
  • Header 和 Footer 的 ViewHolder 对象
  • 一个 ListAdapter,根据 LoadState 显示 0 或 1 个 item。 每次 LoadState 更改时,我们都会通知您需要更改,插入或删除该 item(请参见代码

有关MergeAdapter的更多信息

ViewHolders

默认情况下,每个 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

建议不要使用固定的 id 与 notifyDataSetChanged 一起使用,而是建议使用 adapter 的特定通知事件,该事件为 RecyclerView 提供有关数据集更改的更多信息。 这使 RecyclerView 可以更有效地更新 UI 并具有更好的动画。 如果您使用的是 ListAdapter,则在 DiffUtil 回调的帮助下,将在后台为您处理 notify 事件。 但是,如果确实需要使用 固定的 id,则 MergeAdapter.Config 为固定的 id 提供 3 种不同的配置:NO_STABLE_IDSISOLATED_STABLE_IDSSHARED_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 实现,例如 ListAdapterSortedList

查找 ViewHolder 位置

您过去可能曾经使用过 ViewHolder.getAdapterPosition 来获取 ViewHolder 在 adapter 中的位置。 现在,因为我们要合并多个 adapter,所以请使用 ViewHolder.getBindingAdapterPosition()。 如果要获取最后绑定 ViewHolder 的 adapter,则在共享 ViewHolder 的情况下,使用 ViewHolder.getBindingAdapter()

如果要顺序显示不同类型的数据,这些数据将从封装在其自己的 adapter 中受益,请开始使用 MergeAdapter。 要对 ViewHolder pool 和 固定的 id 进行高级控制,请使用 MergeAdapter.Config

【译】2020 年 Fragment 最新文档(上),该更新知识库啦

前言

很高兴见到你 👋,我是 Flywith24 。

最近 Android 官方针对 Fragment 文档进行了重新编写,使其适应 2020 年最佳实践的快速发展。

Fragment 的确是一个让开发者头疼的组件,它是一个很好的设计,但一直处于可改进的状态,随着 AndroidX Fragment 的快速更新,Fragment 已不同往日,虽然仍有改进的空间(单个 FragmentManager 不支持多返回栈,Fragment 自身和其 view 的生命周期不一致)。考虑到该文档的确有很多新知识以及官方文档的极慢的汉化速度,本文将 2020 版 Fragment 的官方文档翻译成中文,喜欢一手信息的小伙伴可直奔 官方原文。限于篇幅原因,该文档分上下两部分。

本文将介绍以下内容:

  • Fragment 的创建
  • Fragment manager
  • Fragment 事务
  • Fragment 动画
  • Fragment 生命周期

第二部分将介绍:

  • Fragment 的状态保存
  • Fragment 间通信
  • Fragment 于 AppBar 共同使用
  • 使用 DialogFragment 显示 Dialog
  • Fragment 测试
点击查看彩蛋😉 😝 欢迎来到彩蛋部分,您一定是个好奇心很强的小伙伴呢。

我是一个「强迫症晚期患者」,为了移动端更好阅读的体验,我经常将代码以图片的形式插入到文内。但随之而来出现一个问题:没办法 copy 代码(这对 cv 开发者很重要的 🤣)。

前些天,我在 github 某个项目的 README 文档中看到一个技巧,便是把较长且有些影响阅读的内容折叠,读者可以自由地选择展开。

这也是这个「彩蛋」的显示方式。后文中关于代码的部分我都会提供图片和可复制的源码两部分,其中后者处于折叠状态。您可以点击 「点击查看代码详情」以展开源码。

彩蛋结束。🥳

总览

一个 Fragment 代表了 开发者 app UI 可重用的部分。Fragment 定义和管理了自己的布局,拥有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独自存在——它们必须有一个 activity 或 fragment 作 宿主。Fragment 的视图树是其宿主视图树的一部分,或者附加到其宿主的视图树上。

🌟 注意:某些 Android Jetpack 库,如 NavigationBottomNavigationViewViewPager2 是被设计为与 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 负责显示正确布局的列表。

图1

上图展示了同一个界面的两个版本。左侧的大尺寸屏幕包含一个由 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 class

创建 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 类:

  • DialogFragment

    展示一个浮动 dialog。使用该类创建一个 dialog 是一种在 Activity 中使用 dialog 的很好替代方法,因为 fragment 会自动处理 dialog 的创建与清除。

  • PreferenceFragmentCompat

    Preference 对象的层次结构显示为列表。你可以使用 PreferenceFragmentCompat 来为您的 app 创建一个设置界面

在 activity 中添加 fragment

通常,您的 fragment 必须嵌入一个 AndroidX FragmentActivity 中才能作为 activity layout 的部分 UI。FragmentActivity 是 AppCompatActivity 的父类,因此如果您已经继承了 AppCompatActivity 则无需做任何更改。

将 fragment 添加到 activity 的视图树中有两种方式:

  • 在 activity layout 文件中定义 fragment
  • 在 activity layout 文件中定义 fragment container 并且稍后通过代码的方式在 activity 中添加 fragment。

无论哪种方式,您都需要添加一个 FragmentContainerView 来定义 fragment 在 activity 视图树中的位置。强烈建议始终使用 FragmentContainerView 作为 fragment 的容器,因为 FragmentContainerView 修复了一些 fragment 的 bug,其它 ViewGroup(如 FrameLayout)并没提供修复。(译者注:之前 fragment 在 Z 轴的顺序有些问题,FragmentContainerView 修复了该问题)

通过 XML 添加 fragment

请使用 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

若要以代码的方式将 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 事务一节。

在上一个示例中,仅当 saveInstanceStatenull 时才创建 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");
        ...
    }
}

Fragment manager

🌟 注意:我们强烈推荐使用 Navigation library 来管理您 app 的导航。该框架遵循有关处理 fragment,返回栈以及 fragment manager 的最佳实践。想要获取更多关于 Navigation 的信息,参见: Get started with the Navigation componentMigrate to the Navigation component

FragmentManager 是负责对 app 的 fragment 执行操作的类,如添加/移除/替换 fragment 并将这些操作加入到返回栈中。

如果您使用的是 Jetpack Navigation library,则可能永远不会直接与 FragmentManager 进行交互,因为它将 FragmentManager 使用的部分封装了起来。换句话说,任何 app 使用 fragment 都在某种层次上使用 FragmentManager,因此了解它的含义和工作方式非常重要。

本节内容介绍如何访问 FragmentManager,与 activity 和 fragment 相关的 FragmentManager 的角色,使用 FragmentManager 管理返回栈以及为 fragment 提供数据和依赖。

访问 FragmentManager

在 activity 中访问

每个 FragmentActivity 及其子类(如 AppCompatActivity)都可以通过 getSupportFragmentManager() 来访问 FragmentManager

在 fragment 中访问

Fragment 也能管理一个或多个子 fragment(译者注:嵌套 fragment,即一个 fragment 的直接宿主可能是 activity 或另一个 fragment)。在 fragment 中,您可以通过 getChildFragmentManager() 来获取管理子 fragment 的 FragmentManager 实例。如果需要访问该 fragment 宿主的 FragmentManager,可以使用 getParentFragmentManager()

我们来看几个示例,以展示 fragment 及其宿主和每个 fragment 相关联的 FragmentManager 实例之间的关系:

两个UI布局示例,显示了 fragment 及其宿主 activity 之间的关系

绿色代表宿主 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

使用合适的 FragmentManager 取决于调用者所在 fragment 层次结构中的位置以及您想要访问的 fragment manager。

一旦有了 FragmentManager 的引用,便可以使用它来操作显示给用户的 fragment。

子 fragment

一般而言,您的 app 应由一个或少量 activity 构成,每个 activity 代表一组相关的界面。Activity 可能提供 顶级导航,ViewModel 和 其它 fragment 间的 view-state。app 中每个独立的 目的地(destination)应该由一个 fragment 表示。

如果想要一次显示多个 fragment(如在一个拆分视图或仪表板中),则应使用由 destination fragment 及其 childFragmentManager 管理的 子 fragment。

其它使用子 fragment 的场景可能是:

  • Screen slides,父 fragment 使用 ViewPager2 管理着一系列子 fragment
  • 一组相关界面的子导航
  • Jetpack Navigation 使用子 fragment 作为独立目的地。但用户浏览您的 app 时,一个 activity 管理着一个单独的 parent NavHostFragment 并且使用不同的子 destination fragment 填充其 parent 的位置。

使用 FragmentManager

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() 有关更多信息,请参见生命周期一节。

寻找一个已存在的 fragment

您可以使用 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");

子 fragment 和兄弟 fragment 的注意事项

在任何给定时间,仅允许一个 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 时,可以手动实例化 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 工厂,除非在较低的级别被重写。

使用 FragmentFactory 测试

在单一 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。

您可以通过调用 FragmentManagerbeginTransaction() 方法获得一个 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();

允许 fragment 状态变化的重新排序

每个 FragmentTransaction 应该使用 setReorderingAllowed(true)

点击查看代码详情
// 👇 Kotlin
supportFragmentManager.commit {
    ...
    setReorderingAllowed(true)
}


// 👇 Java
FragmentManager fragmentManager = ...
fragmentManager.beginTransaction()
    ...
    .setReorderingAllowed(true)
    .commit();

为了兼容,默认不开启重排序。但是如果需要允许 FragmentManager 在返回栈上运行并运行动画和过渡时能够正确执行 FragmentTransaction,启用重排序可确保一起执行多个事务时,任何中间 fragment(如添加并立即替换的中间 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 事务。

请注意,commitNowaddToBackStack 不兼容。不过您可以通过调用 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()

限制 fragment 的生命周期

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 状态。

显示和隐藏 fragment 的 view

使用 FragmentTransaction 的 show() 和 hide() 方法来显示和隐藏已添加到容器的 fragment 的 view。这些方法设置 fragment view 的可见性而不影响 fragment 的生命周期。

尽管您不需要使用 fragment 事务来切换 fragment view 的可见性,但是这些方法对于改变返回栈上事务的可见性的场景很有用。

连接和分离 fragment

FragmentTransactiondetach() 方法将 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 框架,包括 AnimationAnimator。另一个是 Transition 框架,包含共享元素转换。

🌟 注意:在本节中,我们使用 animation 来描述 Animation 框架中的效果,使用 transition 来描述 Transition 框架的效果。这两个框架是互斥的,不应同时使用。

您可以将自定义效果看成进入和退出 fragment 以及 fragment 共享元素的过渡效果。

  • 进入效果定义了 fragment 如何进入屏幕。如您可以创建一种 fragment 进入时从屏幕边缘滑入的效果。
  • 退出效果定义了 fragment 如何退出屏幕。如您可以创建一种 fragment 离开时淡出的效果。
  • 共享元素过渡定义了两个 fragment 间共享的视图如何在它们之间移动。如一旦 B 变为可见,显示在 fragment A 的 ImageView 中的图像便会转换为 fragment B。

设置 animation

首先,您需要为进入和退出效果创建动画,这些动画将在导航到新 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 存在已知问题。

您还可以为弹出返回栈时运行的进入和退出效果指定动画。 这些被称为 popEnterpopExit 动画。 例如,当用户跳回到上一个屏幕时,您可能希望当前 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

您也可以使用 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 切换的步骤:

  1. 为每个共享元素视图分配一个唯一的 transition 名称
  2. 将共享元素视图和 transition 名称添加到 FragmentTransaction
  3. 设置共享元素过渡动画

首先,必须为每个共享元素视图分配唯一的 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)) 并传入持续时间和时间单位。 经过指定的时间后,过渡将自动开始。

在 RecyclerView 中使用共享元素过渡

在测量并布局了进入 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 和 fragment manager

当 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 生命周期状态和回调

在确定 fragment 的生命周期状态时,FragmentManager 考虑以下因素:

  • fragment 的最大状态由其 FragmentManager 决定。 fragment 无法超出其 FragmentManager 的状态
  • 作为 FragmentTransaction 的一部分,您可以使用 setMaxLifecycle() 在 fragment 上设置最大生命周期状态
  • fragment 的生命周期状态永远不能大于其宿主。 例如,父 fragment 或 activity 必须在其子 fragment 之前 start。 同样,子 fragment 必须在其父 fragment 或 activity 之前 stop

⚠️ 警告避免在 XML 中使用 <fragment> 标签添加 fragment因为 <fragment> 标签允许 fragment 移出其 FragmentManager 的状态。 请始终使用 FragmentContainerView 通过 XML 添加 fragment。

上图显示了每个 fragment 的生命周期状态,以及它们与 fragment 的生命周期回调和 fragment 的视图生命周期之间的关系。

随着 fragment 在其生命周期中的切换,它会在其状态之间上下移动。 例如,添加到返回栈的顶部的 fragment 从 CREATED 向上移动到 STARTED 再到 RESUMED。 相反,当一个 fragment 从返回栈中弹出时,它将在这些状态中向下移动,从 RESUMEDSTARTED 再到 CREATED,最后到 DESTROYED

状态的向上转换

当向上移动其生命周期状态时,fragment 首先为其新状态调用关联的生命周期回调。 回调完成后,相关的 Lifecycle.Event 发出给观察者,然后由 fragment 的 view Lifecycle(如果已实例化)跟随。

Fragment CREATED

当您的 fragment 达到 CREATED 状态时,已将其添加到 FragmentManager 中并且已经调用了 onAttach() 方法。

这将是通过 fragment 的 SavedStateRegistry 恢复与 fragment 本身关联的所有保存状态的合适位置。 请注意,此时尚未创建 fragment 的 view,并且只有在创建 view 之后,才应还原与 fragment 的 view 关联的任何状态。

这个过程将 调用 onCreate() 回调。 回调还将接收一个 saveInstanceStateStateBundle 参数,其中包含先前由 onSaveInstanceState() 保存的状态。 请注意,第一次创建该 fragment 时,savedInstanceState 的值为 null,但 对于之后的重新创建,即使未重写 onSaveInstanceState,它也始终为非 null。 有关更多详细信息,请参见使状态保存一节。

Fragment CREATED 和 View INITIALIZED

仅当您的 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 视图中的任何 RecyclerViewViewPager2 实例上设置 adapter 的适当位置。

Fragment 和 View CREATED

创建 fragment 的视图之后,将还原先前的视图状态(如果有),然后将视图的生命周期移至 CREATED 状态。 视图生命周期所有者还向其观察者发出 ON_CREATE 事件。 在这里,您应该还原与 fragment 视图关联的所有其他状态。

此过程还将 调用 onViewStateRestored() 回调

Fragment 和 View STARTED

强烈建议将支持生命周期的组件绑定到 fragment 的 STARTED 状态,因为这种状态可以确保该 fragment 的视图可用(如果已创建),并且可以安全地对该 fragment 的子 FragmentManager 执行 FragmentTransaction 。 如果 fragment 的视图为非 null,则在 fragment 的生命周期移至 STARTED 后立即将 fragment 的视图 Lifecycle 移至 STARTED

当 fragment 变为 STARTED 状态时,将 调用 onStart() 回调

🌟 注意:诸如 ViewPager2 之类的组件将屏幕外 fragment 的最大生命周期设置为 STARTED

Fragment 和 View RESUMED

当 fragment 可见时,所有 AnimatorTransition 效果均已完成,并且该 fragment 已准备就绪,可以与用户进行交互。 fragment 的生命周期移至 RESUMED 状态,并调用 onResume() 回调

切换到 RESUMED 状态是指示用户现在可以与您的 fragment 进行交互的状态。 未 RESUMED 的 fragment 不应手动设置 view 的焦点或尝试 处理输入法的可见性

状态的向下转换

当 fragment 向下移动到较低的生命周期状态时,相关的 Lifecycle.Event 将通过 fragment 的 view Lifecycle(如果已实例化)发送给观察者,然后是 fragment 的 Lifecycle。 发出 fragment 的生命周期事件后,fragment 将调用关联的生命周期回调。

Fragment 和 View STARTED

随着用户开始离开 fragment 并且在 fragment 仍可见的时候,fragment 及其 view 的生命周期将移回到 STARTED 状态,并向其观察者发出 ON_PAUSE 事件。 然后,该 fragment 调用其 onPause() 回调

Fragment 和 View CREATED

一旦该 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 CREATED 和 View DESTROYED

在所有退出动画和过渡都完成并且 fragment 的 view 已从窗口中 detach 出来之后,fragment 的 view Lifecycle 被移到 DESTROYED 状态,并向其观察者发出 ON_DESTROY 事件。 然后,该 fragment 调用其 onDestroyView()回调。 此时,fragment 的 view 已到达其生命周期的尽头,并且 getViewLifecycleOwnerLiveData() 返回 null。

此时,应该删除对 fragment view 的所有引用,从而可以垃圾回收 fragment 的 view。

Fragment DESTROYED

如果移除了 fragment,或者 FragmentManager 被销毁,则 fragment 的生命周期将进入 DESTROYED 状态,并将 ON_DESTROY 事件发送给其观察者。 然后,该 fragment 调用其 onDestroy() 回调。 至此,该 fragment 已达到其生命周期的尽头。

额外资源

有关 fragment 生命周期的更多信息,请参见以下其他资源。

关于我

我是 Flywith24,Android App/Rom 层开发者。目前专注于 Android 体系化文章的写作。

Android Detail 专栏 正在更新中,想要建立系统化知识体系的小伙伴可以去看看哦。我的所有博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

【背上Jetpack之ViewModel】即使您不使用MVVM也要了解ViewModel ——ViewModel 的职能边界

目录

前言

Android 开发时,我们使用 activity 和 fragment 作为视图控制器, 可能还会使用有一些类可以存储和提供 UI 数据(例如MVP中的 Presenter

但是 当配置更改时(如旋转屏幕),activity 会重建,但对于 UI 数据的持有者呢?

  • 开发者需要重新保存相关的信息并传递给重建的 activity ,否则开发者必须再次获取数据(通过网络请求或本地数据库)
  • 由于 UI 数据的持有者的生命周期可能比 activity 长,因此开发者还需要避免出现内存泄漏的问题

如何解决上述问题?ViewModel

本文重点介绍 ViewModel 的职责(what)以及重点功能的实现原理(how),即使您不使用 Jetpack MVVM 架构,也要了解一下 ViewModel

ViewModel 的原理部分要求您了解 activity 的启动流程,这部分内容网上文章很多,本文不再赘述

ViewModel 的职责

我先上个 视频 ,这个小姐姐表述的比文字更形象

ViewModel 主要用于存储 UI 数据以及生命周期感知的数据

图片来自 Android Architecture Components: ViewModel

ViewModel生命周期

ViewModel 的生命周期 ,图片来自 官方文档

作为数据持有者

ViewModel 能够实时进行配置更改。 这意味着即使在手机旋转后销毁并重新创建 activity 之后,您仍然拥有相同的 ViewModel 和相同的数据。 因此:

  • 您无需担心 UI 数据持有者的生命周期。 ViewModel 将由工厂自动创建,您无需自行创建和销毁
  • 数据将始终更新,旋转手机后,您将获得与以前相同的数据。 因此,您无需手动将数据传递给新的 activity 实例或再次调用网络或数据库来获取数据。

Fragment 间共享数据

一个 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 范围的 ViewModelViewModelProvider 构造器传入的 activity ),因此它们获得了相同的 ViewModel 实例,自然其持有的数据也是相同的,这也 保证了数据的一致性

这种方法具有以下优点:

  • 宿主 activity 无需执行任何操作,也无需了解此通信。

  • SharedViewModel 外,fragment 不需要彼此了解。 如果其中一个 fragment 消失了,则另一个继续照常工作。

  • 每个 fragment 都有其自己的生命周期,并且不受另一个 fragment 的生命周期影响。 如果一个 fragment 替换了另一个 fragment,则 UI 可以继续正常工作而不会出现任何问题。

代替 Loader

CursorLoader 这样的 Loader 类经常用于使应用程序 UI 中的数据与数据库保持同步。您可以使用 ViewModel 和其他一些类来替换 Loader。 使用 ViewModel 可将视图控制器与数据加载操作分开,这意味着您在类之间的强引用较少。

在使用 Loader 的一种常见方法中,应用程序可能会使用 CursorLoader 来观察数据库的内容。 当数据库中的值更改时,加载程序会自动触发数据的重新加载并更新 UI

图片来自 官方文档

ViewModelRoomLiveData 一起使用以替换 Loader。 ViewModel 确保数据在设备配置更改后仍然存在。 当数据库发生更改时,Room 会通知 LiveData ,然后 LiveData 会使用修改后的数据更新 UI

图片来自 官方文档

总结

  • ViewModel 可作为 UI 数据的持有者,在 activity/fragment 重建时 ViewModel 中的数据不受影响,同时可以避免内存泄漏
  • 可以通过 ViewModel 来进行 activity 和 fragment ,fragment 和 fragment 之间的通信,无需关心通信的对方是否存在,使用 application 范围的 ViewModel 可以进行全局通信
  • 可以代替 Loader

ViewModel 源码分析

分析源码时我们可以不计较细枝末节,只分析主要的逻辑即可。因此我们来思考几个问题,并从源码中寻找答案

  • 如何做到 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 的作用域,实现类为 ComponentActivityFragment,此外还有 FragmentActivity.HostCallbacks

  • ViewModelProvider:用于创建 ViewModel,其构造方法有两个参数,第一个参数传入 ViewModelStoreOwner ,确定了 ViewModelStore 的作用域,第二个参数为 ViewModelProvider.Factory,用于初始化 ViewModel 对象,默认为 getDefaultViewModelProviderFactory() 方法获取的 factory

简单来说 ViewModelStoreOwner 持有 ViewModelStore 持有 ViewModel

1. 如何做到 activity 重建后 ViewModel 仍然存在?

【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析 中我们提到了 androidx.core.app.ComponentActivity 的引入并探讨了其作为中间层的作用

我们已经讲过 SavedStateRegistryOwnerOnBackPressedDispatcherOwner 这两种角色,而今天我们来聊一下

ViewModelStoreOwnerHasDefaultViewModelProviderFactory 。其中前者代表着 ViewModelStore 的作用域,后者来标记 ViewModelStoreOwner 拥有默认的 ViewModelProvider.Factory

那么 ViewModel 的逻辑肯定就在该类了

ComponentActivity 实现了 ViewModelStoreOwner 接口,意味着需要重写 getViewModelStore() 方法,该方法为 ComponentActivitymViewModelStore 变量赋值。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 重建后 ViewModel 仍然存在?

对于问题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. 如何控制作用域?

对于问题3,我们知道 ViewModelStoreOwner 代表着作用域,其内部唯一的方法返回 ViewModelStore 对象,也即不同的作用域对应不同的 ViewModelStore ,而 ViewModelStore 内部维护着 ViewModel 的 HashMap ,因此只要保证相同作用域的 ViewModelStore 对象相同就能保证相同作用域获取到相同的 ViewModel 对象,而问题1我们已经解释了重建时如何保证 ViewModelStore 对象不变。

因此问题3也解决了。

4. 如何避免内存泄漏?

对于问题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

ViewModelonSaveInstanceState 的功能有些类似,但它们也有很多差异

从存储位置上来说,ViewModel 是在内存中,因此其读写速度更快,但当进程被系统杀死后,ViewModel 中的数据也不存在了。从数据存储的类型上来看,ViewModel 适合存储相对较重的数据,例如网络请求到的 list 数据,而 onSaveInstanceState 适合存储轻量可序列化的数据

那么我们该如何使用呢?可以使用 viewmodel-savedstate 库,详情参考 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析

【背上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 成为了 FragmentActivityAppCompatActivity 的基类。

androidx activity 1.0.0

俗话说「百因必有果」,带着强烈的好奇心,我查了一下 ComponentActivity 引入的原因。

可以看到 ComponentActivity 继承了 androidx.core.app.ComponentActivity(在fragment库中),并且最初仅实现了LifecycleOwner 接口

我们创建的 activity 的继承关系现在变成了这样:

那么回到最初的问题,为什么要引入 ComponentActivity ?其实看看现在 ComponentActivity 的类结构答案就很清楚了

ComponentActivity 实现了五个接口,代表着其除了 activity 还充当着五种角色。本着职能单一原则,官方通过建立一个中间层将部分功能分别交于专门的类来负责,OnBackPressedDispatcherOwner 就是我们讲 fragment 返回栈(【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇)时提到的结构,而其中的 SavedStateRegistryOwner 则是我们今天要讲的主角 SavedState 中的成员

SavedState

引入 SavedState

implementation "androidx.savedstate:savedstate:1.0.0"

其实您不需要显示地声明,因为 activity 库内部已经引入了。jetpack 组件依赖关系可参考 【背上Jetpack】Jetpack 主要组件的依赖及传递关系

这是一个很小的库

图片来自 Android ViewModels: State persistence — SavedState

SavedStateProvider

保存状态的组件,此状态将在以后恢复并使用

public interface SavedStateProvider {
    @NonNull
    Bundle saveState();
}

SavedStateRegistry

管理 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 。

SavedStateRegistryController

一个包装 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);
    }
}

SavedStateRegistryOwner

持有 SavedStateRegistry 的组件。 默认情况下,androidx 包中的ComponentActivityFragment 都实现此接口。

public interface SavedStateRegistryOwner extends LifecycleOwner {
    @NonNull
    SavedStateRegistry getSavedStateRegistry();
}

Activity 的状态保存

这里我们要明确一件事情,activity 保存的状态究竟都有什么?

这部分内容可以参见 官方文档

简单来说,activity 的状态保存分为 view 状态和成员状态

默认情况下,系统使用 Bundle 实例状态来保存有关 activity 布局中每个 View 对象的信息(例如,输入到 EditText 中的文本值或 recyclerview 的滚动位置)。 因此,如果 activity 实例被销毁并重新创建,则布局状态将恢复为之前的状态,而无需您执行任何代码。(注意,需要恢复状态的 view 需要配置 id

这部分逻辑在 activity 中的 onSaveInstanceState 方法内实现

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 的状态FragmentActivityonSaveInstanceState 方法有对其内部 fragment 的状态进行保存,并在 onCreate 方法中对已保存的 fragment 进行恢复。这解释了如果操作不当会导致 fragment 重叠的问题

Fragment 的状态保存

androidx fragment 使用 FragmentStateManager 来处理 fragment 的状态保存

其内部有四个保存相关的方法

  • saveState
  • saveBasicState
  • saveViewState
  • saveInstanceState

FragmentStateManager

其调用链为 activity 通过 FragmentController 间接 调用 FragmentManagersaveAllState,接着依次调用后面的save 方法

Fragment 的状态保存可分为 view 状态,成员状态,child fragment 状态

关于 view 状态 , FragmentStateManager 提供了 saveViewSate 方法,它的调用有两处:

  1. 在 activity 或父 fragment 触发状态保存时调用,即上述流程
  2. 在 fragment 即将进入 onDestroyView 生命周期时调用,其位置在 FragmentManager moveToState 方法内部,这解释了为什么加入返回栈的 replace 操作在返回时 view 状态可以自动恢复

关于成员状态,由 activity 中的状态机制处理,即上节内容

关于 child fragment 状态,fragment 的 onCreate 方法会调用 restoreChildFragmentState 来恢复 child fragment 的状态,并在 FragmentStateManager 中的 saveBasicState 方法中 调用 performSaveInstanceState 来保存 child fragment 的状态

Viewmodel-SavedState

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 对象(键值映射),可让您保存状态并查询已保存的状态。 这些值将在系统终止进程后继续存在,并可以通过同一对象使用。

ViewModel-SavedState

图片来自 Android ViewModels: State persistence — SavedState

SavedStateHandle

内部持有已保存状态 key-value 的 map,允许读取和写入状态,这些状态在应用进程被杀死后仍然存在

SavedStateHandle 通过 ViewModel 的构造器传入,下面是其主要的主要的几个方法

  • T get(String key)
  • MutableLiveData getLiveData(String key)
  • void set(String key, T value)

SavedStateHandle 还包含 SavedStateProvider 的实例,用于帮助 ViewModel 的 owner 保存状态

AbstractSavedStateViewModelFactory

一个实现 ViewModelFactory.KeyedFactoryViewModel 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;
    }
}

SavedStateViewModelFactory

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) 方法被调用,其 SavedStateRegistryperformSave(outState) 方法将被执行,其内部的所有 SavedStateProvidersaveState 方法均被执行,一旦执行完毕,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 方法

运行示意图

Demo 地址

SavedState 仅适合保存轻量级的数据,重量级操作请考虑持sp,数据库等持久化方案

【Jetpack更新之Fragment】setMaxLifecycle 上位,setUserVisibleHint 被弃用

很多情况下,fragment 的生命周期上限应该低于 FragmentManager/Activity。例如,ViewPager 屏幕外的界面不应被 resumed

理想状态下,可以通过以下 API 实现

supportFragmentManager
	  .beginTransaction()
      .setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
      .commit()

将最大生命周期设置为 Lifecycle.State.RESUMED 将有效地消除限制(因为这是最高生命周期状态)

这将允许废弃 setUserVisibleHint() API

setMaxLifecycle 出现始末

该功能应如何实现的?我们沿着 commit log 来理一下官方的思路

BackStackRecord 的部分逻辑转移至父类 FragmentTransaction

FragmentTransaction 中添加 setMaxLifecycle API

保存 fragment maxState

弃用 setUserVisibleHint

FragmentPagerAdapter 构造器新增参数,使用 setMaxLifecycle() API 确保 fragment resumed 时对用户可见

弃用 FragmentStatePagerAdapter 原来的单参构造器,推荐使用新的构造

随着 ViewPager2 1.0.0 正式版发布,与 ViewPager 交互的FragmentPagerAdapterFragmentStatePagerAdapter 被弃用了

至此我们捋顺了 setMaxLifecycle 的出现,setUserVisibleHint 的弃用以及与ViewPager 相关的 FragmentPagerAdapterFragmentStatePagerAdapter 的弃用

setMaxLifecycle 内部逻辑

接下来我们看看 setMaxLifecycle 是如何发挥作用的

首先我们要研究一下 fragment 的状态管理,为了更好的管理 fragment 的状态,官方添加了 FragmentStateManager 类来专门管理 fragment 的状态,职能单一原则哈

接着在该类中添加了计算 fragment 最大生命周期的方法 computeMaxState()

后来该方法改名为 computeExpectedState() 并加入了 moveToExpectedState() 方法

computeExpectedState() 方法会根据 fragment mMaxState 计算 fragment 应该所处的生命周期

而 fragment 的 mMaxState 是通过 FragmentManagersetMaxLifecycle() 方法设置的 ,而该方法是 BackStackRecord 执行 OP 时调用的,而 OP 值正是通过 FragmentTransactionsetMaxLifecycle() 设置的

至此,我们理清了 setMaxLifecycle() 的内部逻辑

总结

我们可以看到官方为了使 fragment 能够在正确的生命周期上,引入了 setMaxLifecycle() 方法,同时为了更好的管理 fragment 的状态,抽象出了 FragmentStateManager更少的代码,更少的职责,fragment 的内部逻辑会越来越清晰

开源项目:Motion 挑战,一场想象力的比拼与展示

前言

很高兴见到你!

基于搞一个多人协作项目的想法,我们首先找到一个切入点:提供了 开源项目:Jetpack 从 Java 到 Kotlin 无痛上车指南

而今天,我们的多人协作的想法又向前迈进一步,就是这—— Motion 挑战

Motion 挑战

Android Studio 4.0 为我们提供了全新的工具:MotionEditor,得益于它,我们创建动画变得十分简单

在此我们发起一个 Motion 挑战,欢迎小伙伴发挥自己的想象力,将原创作品与我们分享

参与方式

  • fork 该项目
  • 创建自己的动画并录制显示 gif 图
  • 联系我们(联系方式后文列出),我们将您的动画与项目链接展示在此项目的 README 中并在该文章中持续更新

联系方式

可以在微信公众号内给我们留言,或者通过邮箱联系我们

KunMinX

Flywith24

作品展示

我先来抛砖引玉

抛砖引玉

感觉如何?😉

其实该动画的编写十分简单。B 站有位声音好听的小哥哥录制了一份超详细的 教学视频

如果各位小伙伴对组织一次 「使用 Jetpack MVVM 多人协作开发」 有什么好的想法,也欢迎联系我们

投稿作品

按投稿先后顺序排列

反魂蝶五分

项目地址

【译】Android Styling 3: 使用主题和主题属性的优势

原文: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 -->
<Viewandroid:background="@color/white"/>

事实上,您应该引用主题属性,它允许您通过主题来控制颜色,例如,在暗黑主题下提供不同的颜色

<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<Viewandroid:background="?attr/colorSurface"/>

即使您当前不支持其他主题(什么?没有暗黑主题?),我还是建议您采用这种方法,因为这样会使采用主题更加容易

Qualified Color?

您可以在不同的配置下使用不同的色值(例如,@color/foores/values/colors.xmlres/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?

前文的 「始终* 通过主题属性来控制颜色」 中的(always)带有星号,因为在某些情况下,您很明确不想按主题更改颜色。例如 Material Design guidelines 中指出有些场景您可能希望在浅色和深色主题使用相同的「品牌色」

在这种特殊场景下,直接引用颜色资源也是可以的

<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<FloatingActionButtonapp:backgroundTint="@color/owl_pink_500"/>

State of the art

不使用主题属性的另一种情况是使用 ColorStateList

<!-- Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 -->
<Viewandroid: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 来处理。如果您的 主色 发生了变化,则只需要在一个地方进行更新,而无需跟踪对其进行了调整的所有实例

尽管有用,但要注意此技术的一些注意事项

  1. 如果指定的颜色也具有透明度,则会将 alpha 进行组合,例如 50% 的透明度应用与 50% 透明的白色会得到 25% 的白色

出于此原因,最好将主题颜色指定为完全不透明,并使用 ColorStateLists 修改其 Alpha

  1. 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 命名空间

  2. 将普通的颜色作为 drawable

View 的 background 属性 需要一个 drawable,我们使用普通的颜色设置 background 是可以的,其内部会把 color 转换为 ColorDrawable,然而 ColorStateList 是无法转换为 Drawable 的(直到 API 29 ColorStateListDrawable 的出现解决了这一问题)。我们可以曲线救国解决此限制。

Enforcement

因此,您应该使用主题属性和 ColorStateList,但是如何在整个代码库或团队中实施呢? 您可以在 code review 时关注,但这并不是个好办法。 更好的方法是依靠工具来解决此问题。这篇文章介绍了通过 lint 检查对不符合规范的用法给出更好的建议。文章在这

Be Indirect

使用主题属性和 ColorStateList 可以将颜色与主题分离,可以使布局和样式更加灵活,方便复用用并保持代码库精简和可维护性

感谢 Florina Muntenescu 和 Chris Banes

译文完

【译】深入研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用

原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities

作者:Somesh Kumar

译者:Fly_with24


译者注:2020.02.27更新,探讨了 `ViewBinding` 的空安全问题,见文章末尾

Image Source: Google I/O 2019

谷歌在2019 I/O 大会中的 What’s New in Architecture Components 介绍了 view binding

What’s New in Architecture Components 中,有一个简短的关于view binding 的演讲,演讲中将 view binding 与现有解决方案进行了比较,并进一步讨论了为什么view binding 比 data bindingKotlin 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

如何使用它?

activity 中使用

    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为 tvVersionNameTextView ,因此在使用view binding 时,我们要做的就是获取绑定类的引用,例如:

val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater) 

setContentView() 方法中使用 getRoot() ,该方法将返回布局的根布局。可以从我们创建的绑定类对象访问视图,并且可以在创建对象后立即使用它,如下所示:

binding.tvVersionName.text = getString(R.string.version)

在这里,绑定类知道 tvVersionNameTextView,因此我们不必担心类型转换。

fragment 中使用

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 有些不同。 我们需要传递 LayoutInflatorViewGroup和一个 attachToRoot 布尔变量,这些变量是通过覆盖 onCreateView 获得的。

我们可以通过调用 binding.root 返回 view。您还注意到,我们使用了两个不同的变量 binding_binding,并且 _binding 变量在 onDestroyView() 中设置为null。

这是因为该 fragment 的生命周期与 activity 的生命周期不同,并且该fragment 可以超出其视图的生命周期,因此如果不将其设置为null,则可能会发生内存泄漏。

另一个变量通过 !! 使一个变量为可空值而使另一个变量为非空值避免了空检查。 。

在 RecyclerView adapter 中使用

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()
    }
}

译者补充2(2020.02.27更新)关于ViewBinding的空安全

关于ViewBinding旋转屏幕等状态的空安全问题,译者进行了测试,步骤如下:

  1. 创建横竖屏两套布局文件,内容只有一个TextView,id 分别为 hello1 hello2
  2. 分别使用 kotlinjava 创建 activity ,使用 ViewBindinghello1 text 设置为 “你好”
  3. 运行项目并进行横竖屏切换

结论: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 提示开发者该位置可能出现空指针

【玩转Test】AndroidX Test 介绍,如何测试 ViewModel 与 LiveData

不会测试的开发不是好开发——鲁迅

一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,本篇是该系列的第二篇,我们来介绍一下 AndroidX Test 以及如何对 ViewModel 和 LiveData 进行测试

本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics

AndroidX Test

简介

再开始介绍 ViewModelLiveData 的 test 之前,我们先来介绍一下 AndroidX Test

  • 测试库的集合
  • 提供 Android 组件方法,例如 activity ,application
  • 在 local test 和 instrumented 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

在不使用 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 使用不同的依赖

Test ViewModel

我们来创建 ViewModel 的 test,在 ViewModel 类名上唤出 Generate 菜单,选择 test ,选择存储在 local test source 中

接下来我们开始编码,我们创建 getReposByUser_loadReposEvent 方法用于测试获取仓库数据

首先,我们要提供 ViewModel,不同于在 app 编码使用 ViewModelProvider ,test 中可以直接创建 ViewModel 实例

接着我们调用 ViewModel 中待测试的方法

最后我们需要检查结果,这里暂时不写

这里我们没使用 application context ,因此可以不加入 @RunWith(AndroidJUnit4::class) 注解标记

Test LiveData

Test LiveData 两个要素

对于 LiveData,我们主要注意两件事

  • InstantTaskExecutorRule()
  • Observe 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 扩展函数精简代码

这样的写法有些繁琐,我们每次测试 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
}

使用 @before 注解

前面我们提到 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))
    }
}

【背上Jetpack之Fragment】从源码的角度看Fragment 返回栈 附多返回栈demo

前言

上一篇 我们介绍了 OnBackPressedDispather ,那么今天我们来正式地从源码的角度看看 fragment 的返回栈吧。由于其主流程和生命周期差不多,因此本文将详细地分析返回栈相关的源码,并插入大量源码。建议将生命周期流程熟悉后阅读本文。文末提供单返回栈和多返回栈的 demo

如果您对 activity 对任务栈和返回栈不是很了解,可以移步 Tasks and the Back Stack

小问号你是否有很多朋友?

在分析源码之前,我们先来思考几个问题。

  • 返回栈中的元素是什么?
  • 谁来管理 fragment 的返回栈?
  • 如何返回?

返回栈中的元素是什么?

返回栈,顾名思义,是一个栈结构。所以我们要搞清楚,这个栈结构到底存的是什么。

我们都知道,使用 fragment 的返回栈需要调用 addToBackStack("") 方法

从源码角度看 Fragment 生命周期 一文中,我们提到了 FragmentTransaction ,它是一个「事务」的模型,事务可以回滚到之前的状态。所以当触发返回操作时,就是将之前提交的事务进行回滚。

FragmentTransaction 的实现类为 BackStackRecord ,所以 fragment 的返回栈其实存放的就是 BackStackRecord

作为返回栈的元素,BackStackRecord 实现了FragmentManager.BackStackEntry 接口

BackStackRecord

BackStackRecord 的定义我们可以发现 BackStackRecord 有三种身份

  • 继承了 FragmentTransaction,即是事务,保存了整个事务的全部操作
  • 实现了 FragmentManager.BackStackEntry ,作为回退栈的元素
  • 实现了OpGenerator ,可以生成 BackStackRecord 列表,后文详细介绍

谁来管理 fragment 的返回栈?

我们已经知道 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 操作

popBackStack 系列方法

FragmentManager 中提供了popBackStack 系列方法

popBackStack系列方法

是否觉得很眼熟?提交事务也有类似的api,commit 系列方法

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为0

//flag 为 POP_BACK_STACK_INCLUSIVE 弹出包括该元素及及以上的元素
childFragmentManager.popBackStack("",  androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE)

flag为1

走进源码

1. popBackStack() 逻辑

在分析返回栈源码之前我们回顾一下 FragmentManager 提交事务到 fragment 各个生命周期的流程

异步

commitNow

下面我们看看 popBackStack 的源码

popBackStack源码

等等,这个 enqueueAction 有些眼熟...

commit

commitInternal

看来提交事务和回滚事务的流程基本是相同的,只是传递的 action 不同

enqueueAction

OpGenerator

由源码可知,OpGenerator 是一个接口,其内只有一个 generateOps 方法,用于生成事务列表以及对应的该事务是否是弹出的。有两个实现类

OpGenerator实现类

由此可见 commit 调用的为 BackStackRecordgenerateOps 方法,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);
    }
}

后面的逻辑就完全一样了

popBackStack

2. fragment 是怎样拦截 activity 的返回逻辑的?

【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇 一文中我们介绍了 OnBackPressedDispatcher

ComponetActivity

activity 的 onBackPressed 的逻辑主要分为两部分,判断所有注册的 OnBackPressedCallback 是否有 enabled 的,如果有则拦截,不执行后续逻辑;

fragment 拦截返回逻辑

否则着执行 mFallbackOnBackPressed.run() ,其内部逻辑为调用 ComponentActivity 父类的 onBackPressed 方法

所以我们只需看 mOnBackPressedCallbacks(ArrayDeque<OnBackPressedCallback) 是怎样被添加的以及 isEnabled 何时赋值为 true

经过查找我们发现它是在 FragmentManager 的 attachController 调用 addCallback

 mOnBackPressedDispatcher.addCallback(owner,mOnBackPressedCallback)

进而执行了


mOnBackPressedCallback 在初始化时 enabled 赋值为 false

mOnBackPressedCallback

isEnadbled 会在返回栈数量大于 0 且其 mParent 为 PrimaryNavigation 时赋值为true

而返回栈(mBackStack)的赋值在 BackStackRecordgenerateOps 方法中,且是否添加到返回栈由 mAddToBackStack 这个布尔类型的属性控制

mAddToBackStack 的赋值在 addToBackStack 方法中,这也解释了为何调用 addToBackStack 方法就能将事务加入返回栈

我们来总结一下,fragment 拦截 activity 返回栈是通过 OnBackPressedDispatcher 实现的,如果开启事务调用了 addToBackStack 方法,则 mOnBackPressedCallbackisEnabled 属性会赋值为 true,进而起到拦截 activity 返回逻辑的作用。拦截后执行 popBackStackImmediate 方法

而 popBackStack系列方法会调用 popBackStackState 构造 recordsisRecordPop 列表,isRecordPop 的内部元素的值均为true 后续流程和提交事务是一样的,根据 isRecordPop 值的不同选择执行 executePopOpsexecuteOps 方法

单返回栈和多返回栈的实现

Ian LakeFragments: Past, Present, and Future (Android Dev Summit '19)

有提到未来会提供多返回栈的 api

那么以现有的 api 如何实现多返回栈呢?

首先我要弄清楚怎样才会有多返回栈,根据上文我们知道 FragmentManager 内部持有mBackStack list,这对应着一个返回栈,如果想要实现多返回栈,则需要多个 FragmentManager,而多 FragmentManager 则对应多个 fragment

因此我们可以创建多个宿主 frament 作为导航 fragment 这样就可以用不同的宿主 fragment 的 独立的FragmentManager 分别管理各自的返回栈,如果这样说比较抽象,可以参考下图

图中有四个返回栈,其中最外部有一个宿主 fragment ,内部有四个负责导航的 fragment 管理其内部的返回栈,外部的宿主负责协调各个返回栈为空后如何切换至其他返回栈

单返回栈就很容易了,我们只需在同一个 FragmentManager 上添加返回栈即可

详情参照 demo

【背上Jetpack之LiveData】ViewModel 的左膀右臂 数据驱动真的香

前言

之前我们讨论过 ViewModel 的职能边界 ,得益于 ViewModel 的生命周期更长,我们可以在 activity 重建后将数据传递给 activity ,也可以避免内存泄漏。但是如果不是每次需要就获取数据,而是当每次有新数据时通知我们,应该怎么办?

本文介绍 LiveData ,一个 生命周期感知的,可观察的,数据持有者。同时还会简单分析 LiveData 的源码实现

我们都是 Adapter

在谈 LiveData 前我们来思考一个问题

Android 开发(亦或者说前端开发)的本质工作内容是什么?

对于应用层 app 开发者,开发者的工作主要工作就是 Adapter

什么是 Adapter ,下图可能比较直观

Adapter

图片来自 google image

我们的工作本质是 将数据转换成 UI

数据可能来自网络,来自本地数据库,来自内存,而 UI 可能是 activity 或 fragment。

理想的数据模型

上面我们提到 Android 开发者的核心工作就是将数据转换为 UI 。这个过程比较理想的状态是:当数据发生变化时,UI 跟随变化。我们还可以进一步展开:当 UI 对用户可见时,数据发生变化时 UI 跟随变化;当 UI 对用户不可见时,我们希望数据变化时什么都不做,当 UI 再次对用户可见时根据最新的数据进行 UI 的处理。

LiveData 就是我们理想中的数据模型

LiveData

图片来自 Android Dev Summit '18-Fun with LiveData

LiveData 可以三个关键词概括

  • lifecycle-aware

  • observable

  • data holder

observable

Android 中不同的组件有着不同的生命周期,不同的存活时间

ViewModel

因此我们不会在 ViewModel 中持有 Activity 的引用,因为这会导致当 Activity 重建时内存泄漏,甚至出现空指针的情况

observable

通常我们会在 Activity 中持有 ViewModel 的引用,那么如何进行二者间的通信,如何向 Activity 发送 ViewModel 中的数据?

答案是让 Activity 观察 ViewModel

LiveDataobservable

lifecycle-aware

当观察者观察着某个数据时,该数据必须保留对观察者的引用才能调用它,为了解决这个问题,LiveData 被设计成可感知生命周期

当 activity / fragment 被销毁后,它会自动的取消订阅

data holder

LiveData 仅持有 单个且最新 的数据

data holder

上图中,最右侧是在 ViewModel 中的 LiveData,左侧为观察这个 LiveData 的 activity / fragment 。一旦我们为 LiveData 设值,该值会传递到 activity。简而言之,LiveData 值改变,activity 收到最新的值的变化。但是当观察者不再处于活动状态(STARTED 到 RESUMED ),数据 C 不会被发送到 activity 。当 activity 回到前台,它将收到最新的值,数据 D。LiveData 仅持有单个且最新的数据。当 activity 执行销毁流程时,此时的数据 E 也不会产生任何影响

Transformations

LiveData 提供 两种 transformation ,mapswitch map。开发者也可以创建自定义的 MediatorLiveData

我们都知道 LiveData 可以为 View 和 ViewModel 提供通信,但如果有一个第三方组件(例如 repository )也持有 LiveData。那么它应该如何在 ViewModel 中订阅?该组件并没有 lifecycle

一旦我们的应用愈发复杂,repository 可能会观察数据源

那么 view 如何获取 repository 中的 LiveData

一对一的静态转换(map)

one-to-one static transformation

在上面的示例中,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) }

一对一的动态转换(switchMap)

假如您正在观察一个提供用户的用户管理器,并且需要提供用户的 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)

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)
}

检查值是否准备好并发出结果(加载中,失败或成功)

LiveData 的错误用法

错误地使用 var LiveData

var lateinit randomNumber: LiveData<Int>

fun onGetNumber() {
   randomNumber = Transformations.map(numberGenerator.getNumber()) {
       it
   }
}

这里有一个重要的问题需要理解:转换会在调用时(mapswitchMap)会创建一个新的 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 粘性事件

一般来说我们使用 LiveData 持有 UI 数据和状态,但是如果通过它来发送事件,可能会出现一些问题。这些问题及解决方案 在这

fragment 中错误地传入 LifecycleOwner

androidx fragment 1.2.0 起,添加了新的 Lint 检查,以确保您在从 onCreateView()、onViewCreated() 或 onActivityCreated() 观察 LiveData 时使用 getViewLifecycleOwner()

bug

如图,我们有一个 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.0support library 28 了 viewLifecycle

因此,当需要观察 view 相关的 LiveData ,可以在 onCreateView()、onViewCreated() 或 onActivityCreated() 中 LiveData observe 方法中传入 viewLifecycleOwner 而不是传入 this

源码结构

首先来看 LiveData 主要的源码结构

  • LiveData
  • MutableLiveData
  • Observer

LiveData

LiveData 是可以在给定生命周期内观察到的数据持有者类。 这意味着可以将一个ObserverLifecycleOwner 成对添加,并且只有在配对的 LifecycleOwner 处于活动状态时,才会向该观察者通知有关包装数据的修改。 如果 LifecycleOwner 的状态为 Lifecycle.State.STARTEDLifecycle.State.RESUMED,则将其视为活动状态。 通过 observeForever(Observer)添加的观察者被视为始终处于活动状态,因此将始终收到有关修改的通知。 对于那些观察者,需要手动调用 removeObserver(Observer)

如果相应的生命周期移至 Lifecycle.State.DESTROYED 状态,则添加了生命周期的观察者将被自动删除。 这对于 activity 和 fragment 可以安全地观察 LiveData 而不用担心泄漏

此外,LiveData 具有 onActive() 和 onInactive() 方法,以便在活动观察者的数量在 0 到 1 之间变化时得到通知。这使 LiveData 在没有任何活动观察者的情况下可以释放大量资源。

主要方法有:

  • T getValue() 获取LiveData 包装的数据
  • observe(LifecycleOwner owner, Observer<? super T> observer) 设置观察者(主线程调用)
  • setValue(T value) 设值(主线程调用),可见性为 protected 无法直接使用
  • postValue(T value) 设置(其他线程调用),可见性为 protected 无法直接使用

MutableLiveData

LiveData 实现类,公开了 setValuepostValue 方法

Observer

接口,内部只有 onChanged(T t) 方法,在数据变化时该方法会被调用

源码分析

我们通过源码来看看 LiveData 如何实现它的特性的

    1. 如何控制在 activity 或 fragment 活动状态时接收回调,否则不接收?
    1. 如何在 activity 或 fragment 销毁时自动取消注册观察者?
    1. 如何保证 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 方法添加观察者的,因而当数据变化时,会调用 LifecycleBoundObserveronStateChanged 方法

// 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 比较的知识,详情在这

只有 STARTEDRESUMED 状态 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

mVersionsetValue 方法中 进行更改

@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 的单向依赖。

所谓架构,很多时候不是使用它能做什么,更多的是不要做什么,使用它时开发者能够得到约束,以便产出更健壮的代码

使用 AccountManager 实现系统内共享账号

前言

在开发过程中我们可能遇到自家应用间共享账号的场景。例如 APP1 登录成功后,启动 APP2 时自动完成登录并与 APP1 共享账号信息。

Android 为我们提供了AccountManager 来管理账号信息。

demo 地址

共享前提

  1. 两个 app 在一个用户组内
  2. 使用相同的签名(使用 debug 默认签名也可以共享)
  3. accountType 相同

原理

AccountManager是一个面向应用程序开发的组件,它提供了一套对应于 IAccountManager 协议的应用程序接口;这组接口通过Binder机制与系统服务AccountManagerService进行通信,协作完成帐号相关的操作。同时,AccountManager接收authenticators 提供的回调,以便在帐号操作完成之后向调用此帐号服务的业务返回对应的接口,同时触发这个业务对结果的处理。
- authenticators 即注册帐号服务的app;
- 业务调用方 即使用authenticators提供的帐号服务的第三方,也可以是authenticator自己

摘自:Android AccountManager帐号管理(一)

使用

该项目中有两个 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

创建账号

删除账号

第三方 app 获取 用户,token 等信息

【刀口漫谈】分享一次跨城市换工作的面试经历,入职快手(非面经)

前言

很高兴见到你 👋

本文不是面经

本文不是面经

本文不是面经

本文的目标读者是想要换城市/换工作的小伙伴(无论何种技术栈),喜欢「吃快餐」的小伙伴可以关闭窗口了。


分割线开始


分割线结束


很高兴您能读到这里,我想通过这样的方式过滤一部分非目标读者。我保证接下来的内容会干货满满~ 😉

本文将分享一下我跨城市换工作的经历以及心得(面试了快手和字节两家公司,拿到快手 offer)。

我将从 面试前准备 -> 面试过程 -> 面试后总结 三个阶段进行阐述。

这是我 2021 年的第一篇文章,让我们开始吧~

面试前准备

技术人的基本盘?

之前的文章 我提到了建立系统化知识体系的重要性。

一点建议:把知识看做一个语义树十分重要,在你进入到叶子/细节之前,确保自己清楚基本原理(即树干和大树枝),否则这些叶子将无处安放。—— 埃隆·马斯克

最近我想到了一个更好的词来描述,我把它称之为「技术人的基本盘」。

人的基本盘指个人长期的竞争力。

每个人由于拥有不同技术栈,有着不同的项目经历,因此会拥有不同的知识体系。「技术人的基本盘」大致是这样的:

  • 掌握扎实的计算机领域的通用知识(操作系统,网络,数据结构等)

  • 拥抱变化并拥有较强的学习能力(技术领域更新快)

  • 拥有良好的沟通协作能力(工作中大都是多人协作的场景)

  • 掌握扎实的自身技术栈相关的知识(吃饭的家伙)

面试是面试官给候选人贴标签的过程

贴标签的含义是:用最快的速度将人/事归类

生活中贴标签的场景比比皆是。

例如根据年代贴标签

  • **有「90后」
  • 美国有「Z世代」
  • 日本有「迷失世代」

游戏中也有贴标签:

上图是篮球游戏球员建模的画面,该建模被贴了「篮筐冲击者」的标签,玩家可快速了解该建模在球场上的位置以及技术长项。

图片摘自 B 站 2k 游戏 up 主 油管小王子Terry

贴标签是人类认识世界、进行社会交往最便捷的手段之一

企业面试候选人,希望能够最快速地将候选人分类,因此我认为 面试是面试官给候选人贴标签的过程。当然这个过程有一定的局限性,但它是筛选候选人最快速便捷的手段。有些人会认为「贴标签」是一个贬义词,而我认为正常情况下它是一个中性词,当它表示人们简单粗暴地对某个标签进行不全面的评价时,是一个贬义词。

既然面试过程是面试官给候选人贴标签的过程,那么作为候选人应努力向面试官展示自己积极意义的标签。

如果这么说比较抽象的话,举一个我自身的例子:

我没有「优秀学历」,「优秀项目经验」,「大厂经历」等标签,但我拥有其他的标签,并努力在面试过程中展示:

  • 「基本功扎实」
  • 「自驱动」
  • 「独立思考」
  • 「有潜力」
  • ...

知道自己想要什么很重要

我想说:知道自己想要什么很重要

人每个阶段都有自己的需求。上图是 马斯洛需求层次理论 的简单模型。

图片摘自网络。

明确了自己想要什么,便很容易去选择城市、公司。例如:

  • 如果处于安全需求的层次,你可能会去一个薪资水平高的城市,薪资高的公司,能够接受较大的工作强度;
  • 如果到达了社交需求或尊重需求的层次,你可能会考虑家庭等因素来选择城市,更在意工作与生活的平衡;
  • 如果已经到达自我实现的阶段,可能更多地做自己热爱的事情。

我是一个计划性很强的人,这次换城市工作是我在大学时期已经规划好的:

  • 大学自学 Android
  • 大四修够学分实习
  • 获得两年多的工作经验后去一线城市

我的 Android 学习经历已在 这篇文章 介绍,感兴趣的小伙伴可以移步查看。

明确了自己的需求,便明确了选择,接下来就能向心仪公司撒简历啦~🥳

不过有一个更高效的方式。

抓住每个可以「厚脸皮」的机会

我是一个敏感并十分在意他人对自己看法的人,东北话叫「脸儿小」。

有句俗语:脸皮厚吃个够,脸皮薄吃不着。

「脸皮薄」很可能错过很重要的机会。我举一个自身的例子。

我刚毕业时便听过 南尘 大佬在 HencoderPlus 中的课程,也知道他的联系方式,但直到去年 11 月份我才鼓起勇气加他的微信好友,请他帮忙内推字节,后来我又主动在 v2ex 上找了快手的内推(又结识了一个优秀的小伙伴)。

截图已得到 南尘 同意。

我发现 扔物线(朱凯)南尘 等大佬都很随和,甚至有些可爱。因此我十分后悔大学时期没有与他们多多交流,如果那样做的话我可能会进步得更快。

但是生活哪有如果呢?希望各位能做一个「厚脸皮」的人,抓住能够提升自己的机会。与优秀的人交流真的是如沐春风(丝毫没有夸张)。

在此郑重感谢 南尘 对我的帮助!(虽然最后没能入职字节🤣)

南尘 的主页有他的微信号哦😉

可能对你有用的细节

  • 快手和字节一般是三轮技术面+一轮 HR 面

  • 每轮技术面均有算法题

  • 算法题难度基本上是 leetcode 简单/中等(低概率是困难题,遇到过🙃)

  • 快手和字节下班晚,因此可以将面试时间约到晚上 8 点左右

  • 远程视频面试需要带有摄像头的电脑

  • 快手和字节远程面试使用牛客网,遇到问题可以找人工客服(很 kind)解决

  • 据说在面试前,女友的鼓励 kiss 有魔法加成哦🙈

面试过程

好记性不如录音机

之前一直疑惑为什么别人的面经记录得那么完整。后来释然了:可以使用录音/录像的方式记录每轮的面试。我没使用录屏软件,因为不清楚是否会被判定为作弊。我使用的是手机录音,面试过程中让电脑处于外放状态。可以结合自身情况选择合适的方式。

保持平和的心态

面试经验不足的候选人一般很难保持平和的心态。

我之前的面试经验很少,一只手都数得过来。但随着面试次数的不断增加(从去年 7 月开始一共与字节的 10 位面试官交流过🤣),心态便渐渐平和下来。

一点建议:在面试心仪公司前,多刷几次「高质量」的面试(不仅仅是数量上的增加,还要对每次面试进行复盘,进而提高自己),这真的很重要!

可能对你有用的细节

  • 视频面试时关闭 VPN 等程序,否则会产生回声!(否则会被吵到心态爆炸😠)

  • 如果未设备调试好可以约其他时间重新面试,千万不要忍受回声然后继续面试,这很影响状态(深受其苦😭)

  • 视频面试过程发现画面变卡,可以刷新界面重新连接(与面试官打好招呼)

  • 字节 HR 约面试使用座机(快手一般使用手机),注意不要被自己的手机拦截

  • 视频面试牛客网的链接会以邮箱的方式发出,注意简历上的邮箱的准确性

面试后总结

如何破除自我感觉良好的怪圈

有时我们在面试后会陷入自我感觉良好但实际却并非如此的怪圈,这导致我们很难通过复盘来提升自己。

在尝试解决这个问题前我们可以思考一下这个的问题:

在获取知识如此便捷的今天,我们为什么要通过上学来接受教育?

柏拉图给出了一个明确的答案,他认为,人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的。(摘自吴军博士的《硅谷来信》)

我们可以通过他人的帮助来验证自己面试过程中的表达是否准确,持有的观点是否正确(当然,最好找一个比自己水平高的小伙伴帮忙分析,什么?找不到?参考「脸皮厚吃个够」一节)。

在此郑重感谢 扔物线(朱凯)!感谢凯哥每轮面试后帮我复盘并指出存在的问题。

可能对你有用的细节

  • 可以抓住反问面试官的机会,让其帮忙复盘总结自己的面试表现(面试官没有义务这么做,做好遭到拒绝的准备)

  • 针对性学习面试过程中未回答好的内容,或者拓展已掌握内容的深度

  • 如果面试结果不理想调整好心态

总结

  • 换城市/换工作前想好自己想要什么,分析好自己目前的需求,并根据需求定制计划

  • 面试过程中实力才是硬道理,其他技巧只是辅助,因此要保证自己的「基本盘」牢固

  • 机会总是留给有准备的人,但很多时候机会是自己争取的,做一个「厚脸皮」的人

  • 每次面试都是一个提升自己的机会,做好记录和复盘

  • 面试要保持平和的心态,如何保持?多多练习/刷面试

祝愿各位小伙伴能够入职自己心仪的公司~

关于「刀口漫谈」系列

刀口漫谈 是一个非技术类的系列,主要分享一些我的思考。何为刀口?我的名字最后一个字是「召」。惊不惊喜,意不意外~🤪

关于我

人总是喜欢做能够获得正反馈(成就感)的事情,如果本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~

我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。

View 事件分发机制,大型职场 PUA 现场

前言

很高兴见到你 👋

我不是大佬,但我相信我在通往成为大佬的路上 | 掘金年度征文 中我结合自身经历阐述了自己对学习方法的思考与实践。简单来说,我认为学习需要建立一套系统的知识体系(知识树),在此基础上可将学习分为 通用学习需求学习。本文将简单讨论掌握知识的过程,并基于这一过程的各阶段进行内容创作实践。

掌握知识的三个阶段

个人认为,对知识的掌握,需要经历以下过程:

  • 感性认识阶段
  • 理性认识阶段
  • 实践阶段

感性认识 可以看作是一个 从 0 到 1 的过程,它是认识的初级阶段。在这一过程学习者通常会寻找已有的素材学习(如书籍,论坛博客,视频等)。而创作感性认识相关的内容看似简单,实则非常困难。这里列举一些优质的从 0 到 1 的内容的创作者:

点击查看详情

理性认识 建立在 感性认识 的基础上,在这一过程学习者通常会在头脑中建立起相应的知识体系并进行从 1 到 N 的扩展思考。具体些便是通过阅读源码等手段对原理进行分析并结合已有知识体系进行总结归纳。这部份内容的创作很容易陷入创作者「自说自话」的怪圈,而读者会觉得「不知所云」。

实践阶段 是整个过程的终点同时又是起点。之所以这么说是因为该过程并非简单的线性的结构,而是一个循环的过程。

通过具体的实践,不仅可以避免「一听就会,一做全废」的尴尬局面,还是对前面认识阶段的升华,是进入更高层级认识阶段的起点。

今天我将以 View 的事件分发机制 这一知识为切入点来进行内容创作。本文的定位为建立感性认识阶段。

前人栽树好乘凉

关于 View 的事件分发机制,个人认为写的最好的两篇文章分别是:

KunMinX 的文风简洁干净,清楚地表述出 View 事件分发机制 的本质以及这一过程中的「消费」这一概念的理解。

却把清梅嗅 则站在设计者的角度,阐述了事件分发的全貌,其中 View 的事件分发机制 只是 UI 层事件分发的一个环节。

基于前一节的理论,掌握 View 的事件分发这一内容的过程可以这样拆分:

感性认识阶段:

  • 确认该知识的在「知识树」的位置:是 Android 中 UI 层事件分发的一个环节

  • 在脑海中建立该内容的模型:

    1. Android 中的视图树是一个 N 叉树的模型
    2. N 叉树最底部的节点是最靠近用户的视图,响应用户操作的优先级较高
    3. 响应用户操作本质上是在 N 叉树上寻找特定节点
    4. N 叉树常用的遍历的方式是递归
    5. 为了提高性能,可以根据终止条件提前结束递归
    6. 为了提高性能,可以使用「拦截」的手段避免每次遍历全部节点
    7. ...

感性认识的深度与学习者所处知识层级有关,感性认识 → 理性认识 → 实践 是一个循环的过程,因此不同阶段的学习者感性认识的深度不同。

理性认识阶段:

脑中建立模型后可以进行一些细节的学习,该过程学习者会进行从 1 到 N 到扩展思考与学习:

例如在查看源码实现之前,如果不清楚 N 叉树的遍历方式或者不理解递归算法的流程,很可能会被「消费」,「返回 true」等描述搞得晕头转向。

  • 学习递归的概念以及写法
  • 学习实现拦截的常用手段:责任链模式
  • 查看具体的源码实现

同样,处于不同知识层级的学习者对理性认识的深度也不同。因此对于源码的阅读,开始可以过滤掉细节,理清核心逻辑(简单的分发 + 拦截),后续关注之前忽略掉的细节(如多指的处理)。

实践阶段:

对于前面的认识进行实践,例如自定义 ViewGroup 和 View,通过打日志等手段验证之前的学习内容,并在这一过程加深之前的认识并进入下一次循环。

本文的定位为感性认识阶段,适合对 View 事件分发机制理解不清晰,或者之前不知道该内容的读者阅读。

正文开始。

是大型职场 PUA 的现场啊

严格的等级制度

某公司有着严格的等级制度,模型类似 N 叉树:

该公司的员工可分为两种职位:View 和 ViewGroup。

  • View 职位的员工属于最底层的员工,权限最小
  • ViewGroup 职位的员工手下可以管理其他员工(有混得惨的,手下没有员工,如 ViewGroup4)
    • 从权限的角度讲:ViewGroup 拥有自己特殊的权限(管理手下)和 View 的权限
    • 从角色的角度讲:ViewGroup 作为上级时,充当 ViewGroup 角色,作为下级时,充当 View 的角色

除了上述通用的职位,还有一些特殊的「大佬级别」的职位:

  • ViewRootImpl:该公司对外业务的职位,唯一。当一个任务来临时,会先交给 ViewRootImpl,下属为 DecorView
  • DecorView:从 ViewGroup 晋升的职位,唯一。下属为 老大(Activity)安排的 ContentView
  • Activity:该公司老大,唯一。下属为 DecorView

该公司不仅有着严格的 等级 制度,还有着严格的 保密 制度。

严格的保密制度

每个员工只能和自己的上级/下级通信,并且上级并不了解下级的能力以及擅长的方向

因此,当一个任务来临时,上级不知道这项工作下级能不能处理。

Android 的应用世界有着若干个这样的公司。

处理任务的流程

由于前面的各种制度制约,该公司接到任务后的处理流程可以抽象为:

从 N 叉树的根节点开始,遍历整个 N 叉树,寻找一个能够处理该任务的节点

该公司处理任务的具体流程如下:

  • 一个任务被拆为多个指令,即任务是一个抽象概念,真正的载体是一个个的 任务指令
    • 「任务开始」指令意味着这是一个新任务的开始,后续的指令均是该任务的指令
    • 任务进行的过程会存在多个「任务进行」的指令
    • 最终任务以「任务结束」指令结束
  • 为了锻炼员工,任务优先交与下层员工处理
  • ViewGroup 的职责是收到上级的任务指令后,拥有两个选择:
    1. 自己直接处理该指令,不通知下级(即拦截),或者没有下级,只能自己处理,并将处理结果反馈给上级(处理结果 OK 或 不 OK)
    2. 通知下级处理该指令
      • 如果下级反馈处理结果 OK,则直接向上级交差(反馈结果 OK)
      • 如果下级反馈结果不 OK,则自己处理(充当了 View 的角色),并将处理结果反馈给上级(处理结果 OK 或 不 OK)
  • View 收到上级的指令后,会进行处理并将处理结果反馈给上级(处理结果 OK 或 不 OK)
  • 如果从 DecorView 遍历一遍后处理结果仍不 OK,则 DecorView 会将任务指令上报给老大 Activity

拦截流程

ViewGroup 拥有拦截任务指令的权限,拦截分为以下情况;

  • 当拦截的任务指令是「任务开始」时,下属根本不知道这个任务的存在,因此该任务的后续的指令不会交予下属处理
  • 当拦截的任务指令时 「任务进行」时,ViewGrroup 会接管该任务,并且使用「任务取消」的指令通知下属(别忙乎了,该任务我处理了)

由于 ViewGroup 拥有拦截任务的权限,这样会导致下级没有机会表现,因此公司赋予了下属可以「拒绝上级拦截」的权限

  • 当上级拦截的任务指令是「任务开始」时,下属根本不知道任务的存在,因此没机会使用「拒绝上级拦截」
  • 当上级拦截的任务指令是「任务进行」时,下属可以向上级「主动请缨」,使用「拒绝上级拦截」,这样上级的拦截无效了

优化

一个任务由多个任务指令组成,如果每个任务指令来临都要遍历一次整个公司的所有员工,处理时间太长。

因此可以进行一些优化,减少遍历的次数。

如上图所示,如果一个任务的「任务开始」ViewGroup4 的处理结果 OK,那么该任务的后续指令可以无需遍历完整的树,而直接将该任务的后续指令直接交予 ViewGroup1 → ViewGruop4 这一分支(红色标识)。

动画演示

上图演示了没有拦截并且遍历所有节点的情况。

DecorView -> Activity ->PhoneWindow 这一流程可参考 Android 事件分发机制的设计与实现 DecorView 的双重职责 一节。

总结

  • View 的事件分发机制 核心是 从 N 叉树的根节点开始,遍历整个 N 叉树,寻找一个能够处理该事件的节点
  • 任务-任务指令 模型对应着「事件序列」模型:
    • 任务开始ACTION_DOWN
    • 任务进行ACTION_MOVE
    • 任务结束ACTION_UP
    • 任务取消ACTION_CANCEL
  • 所谓的返回 true / false 代表着执行结果 OK / 不 OK
  • 上级 ViewGroup 拥有拦截能力,下级 ViewGroup/View 拥有让上级取消拦截的能力
  • 如果 ViewGroup 从 任务开始ACTION_DOWN 便拦截了 任务/事件,则 下级 ViewGroup/View 没机会让上级取消拦截
  • 为了提高性能,一个任务的后续指令会优先使用上次遍历结果 OK 的那条路径,对应模型为 TouchTarget(责任链模式的存储处理器模型)

下期内容将进入理性认识阶段,介绍 递归遍历 N 叉树的方式,责任链模式的常用实现,以及 View 事件分发的源码简析。

关于我

我是 Flywith24,我的博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

小专栏        
Github          
语雀

自建博客    
CSDN    
Bilibili

目前我正专注于建立系统化的知识体系,内容更新在语雀上。感兴趣的小伙伴可以在相关文档底部评论区补充优质的资源以及技术细节。点击查看

【背上Jetpack之Lifecycle】万物基于 Lifecycle 默默无闻大用处

前言

Android 中有一个比较重要的概念:「生命周期」。刚毕业去面试,总会被问到「四大组件的生命周期」这类的问题。17年的 IO 大会上,Google 推出了 Lifecycle-Aware Components(生命周期感知组件),帮助开发者组织更好,更轻量,易于维护的代码

本文介绍 Lifecycle 的职责以及简单分析 lifecycle 如何感知 activity 和 fragment ,帮助您对 Lifecycle 有一个感性的认识

万物基于 Lifecycle

手动管理生命周期的痛苦你不懂

lifecycles

鲁迅曾说过:万物基于 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 详解

因此在生命周期组件的生命周期发生变化时告诉观察者,内部组件即可感知外部的生命周期

引入 Lifecycle 后

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 ,可以处理多个观察者

LifecycleRegistry

其内部持有当前的状态 mState ,LifecycleOwner 以及观察者的自定义列表,同时重写了父类的添加/删除观察者的方法

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"

简单的源码分析

activity 生命周期处理

首先我们还是来看 androidx.activity.ComponentActivity ,这个类我们这个系列的文章里提到多次,第一次提及是在 【背上Jetpack】绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析 ,感兴趣的小伙伴可以看看。

ComponentActivity

其实现的接口大多数我们都已经探讨过了,今天我们来看看 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 生命周期处理

在 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 大小比较

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 篇会用到这个知识点

【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析

笔者看过不少源码分析类的文章,动辄贴上大段代码,这种方式很容易打断读者的思路,所以很多时候看过这类文章感叹好文好文,却感觉什么都没记住,亦或者默默加入收藏却不知何时能去细心地研读。

所以本文不会过多介绍源码的细节,更多地是抛砖引玉,如果您看过本文后能够跟着本文的思路自己翻一下源码相信您就不会有我上述的体验了。

本文默认您已对 fragment 的生命周期有所了解,并清楚fragment的缘起与职责。这部分基础内容可移步 fragment 官方文档

也即本文不会介绍 “what”,而是介绍 “how” 并且探讨一下 “why”

这里贴一下 androidx fragment 源码地址

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,让其执行相应的生命周期方法。

思路有了,下面进行一些细节的确认。

  1. activity 要能操作 fragment,fragment 亦可操作 fragment,所以需要抽象出一个管理 fragment 的模型
  2. activity 操作 fragment 的一系列动作,应该是互为可逆一组操作。例如添加 fragment 后,也应能移除 fragment
  3. activity 对 fragment 的每组操作不应是单一的,例如可以在一次操作中在 activity 不同位置添加两个 fragment,同时该操作还应满足 2 ,具有可逆性

对于第一条,我们抽象出一个可以管理 fragment 的模型,加入上下级的关系,即 activity 可管理其内部的 fragment,fragment 亦可管理其内部的 fragment。因此 fragment 同时充当着管理者与被管理者两种角色

对于后两条,相信在大学学过数据库的人会想到一种结构:事务(Transaction)

事务是指一组原子性的操作,这些操作是不可分割的整体,要么全完成,要么全不完成,完成后可以回滚到完成前的状态

因此,fragment 中两个最重要的概念出现了,FragmentManagerFragmentTransaction

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 ,日志如下:

单fragment启动日志

绿色部分为笔者手动添加的log,灰色和蓝色部分为 fragment 源码中的log

根据日志显示的流程,我们的猜测看似是正确的,“在 activity 每个生命周期的节点,去操作 fragment ,让其执行相应的生命周期方法”

其实这里是有干扰的,因为我们是在activity 的 onCreate 方法里 创建并提交 FragmentTransaction ,如果在 onResume 里调用呢?

单fragment启动日志2

WTF!

或许,我们的猜测有问题?看似调用 commitNow 后 fragment 的生命流程是自发进行的

那如果我们把调用挪到 onPause 呢?

打开 activity 并按下 home 键

单fragment启动日志-onPause

我知道好奇的读者会尝试在 onStop 中尝试一下,有惊喜。手动滑稽。

从这几段日志上来看,fragment 在提交事务后会自发进入自己的生命周期流程,而当其宿主 activity 生命周期发生变化时,fragment 的生命周期也跟随变化。

如果这么说比较抽象的话,我们可以看在 onPause 中显示fragment 的日志,当 Fragment 进入 onStart 生命周期后,如果是正常流程应该进入 onResume,但由于按下 home 键 activity进入onStop,fragment 也进入了 onStop 状态

因此,我们将之前的猜测进行扩展:

  1. 在activity每个生命周期的节点,去操作fragment,让其执行相应的生命周期方法
  2. FragmentTransaction 被提交后 fragment 会进入自己的生命周期流程,但受 1 约束

那么我们的源码解读就从两个方向入手

Activity 操作 Fragment 生命周期

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- 方法,基于这个想法我们可以看一下 FragmentManagerdispatch- 方法的调用

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 稍显复杂,

  1. 如果 fragment 的直接宿主是 activity ,则返回的是 activity 中的getSupportFragmentManager() 返回的 fragmentManager
  2. 如果 fragment 的直接宿主是 fragment,即该 fragment 是其他 fragment 的子 fragment,则返回的是其父 fragment 的 getChildFragmentManager

所以 嵌套fragment 的生命周期是父 fragment 在各个生命周期节点上通过 mChildFragmentManager 调用 dispatch- 以影响其子 fragment 的生命周期

这样我们第一部分的解读就告一段落了, 这里点到为止,一些细节需要您自己亲自看看源码

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 是没有调用的

dispatchOnDestroyView

在 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 LakeFragments: Past, Present, and Future (Android Dev Summit '19) 中提到未来官方会将二者合并,届时 fragment 的使用会更加简洁

这里引用 The Android Lifecycle cheat sheet — part III : Fragments 文中的图片 ,和我画的commit FragmentTransaction 的脑图(略简陋),帮您更好的理解

 The Android Lifecycle cheat sheet — part III : Fragments

fragment脑图

强烈建议您自己亲自看一看源码,不然就变为我文章开头时说的状态了。

【译】kotlin 协程 Flow:给 RxJava 使用者的介绍

原文: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

如果我们尝试获取列表并将其用作 flow ,并在流的末尾调用 collect {..},则会收到编译错误。 由于 flow 是基于协程构建的,因此默认情况下它具有异步功能,因此您可以在代码中使用协程时使用它

collect {…} 运算符,您可以将其想像为 Rxjava 中的 subscribe

流也是 cold stream ,这意味着,直到您调用操作符(如 collect)后,flow 才会被执行。 如果您重复调用 collect ,每次您将获得相同的结果

因此,Collections 扩展功能仅适用于小数据,sequence 可以节省您不必要的工作(不创建临时列表),而使用 flow,您可以用协程的强大功能来编写代码。 因此,让我们学习如何构建它

构建 flow

我们看到 asFlow 方法,它是 Collections 上的扩展函数,可将其转换为 flow,我们查看一下源码

public fun <T> Iterable<T>.asFlow(): Flow<T> = flow {
    forEach { value ->
        emit(value)
    }
}

如果我们要编写前面的示例在数据源中添加一些逻辑,则只需使用 flow{…} 或者 flowof()

转换操作符

flow 拥有一些列的用于转换的运算符,例如 mapfiltergroupByscan 等等

在由 Coroutines 提供支持的 flow 中,您可以自然地在您的操作符中使用异步代码,假设我们想要做一些耗时的操作,这里使用延迟一秒钟表示。 使用 RxJava 时,您可以使用 flatmap

这里想表达的是 flow 具有更简单的设计,并且与以其陡峭的学习曲线而闻名的 RxJava 相比易于学习,我在此使用 flow 将它简化一下

terminal 操作符

我已经提到 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-catchcatch {…}。 这是两种情况下的修改代码

// 使用 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

flowOn()

默认情况下,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()

App为了漂亮脸蛋也要美颜,Theme 与 Style 的使用,附一键变装 demo

前言

作为 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,效果如下

demo

关于 recreate 黑屏闪烁的问题,请见文章底部 20200705 更新

理解 Style 与 Theme 的区别

这部分内容在视频的 02.14 处

Style 是 View 属性的集合,可以将 Style 视为 Map<View Attribute, Resource>,其中 key 为 View 的属性,value 为资源

sytle key 为 View Attributes

style value 为 Resources

Resource 可以为以下类型

而 Theme 则不同,它的 key 是 「主题属性」,很显然下图中的 colorPrimary 不是任何 View 中的属性

主题的定义

主题属性有点像把配置抽象为语义化的命名的变量,并把它们塞到 map 中,以便未来使用,主题属性 与 View 属性很像,它们在 attr 中定义的方式以及对应的类型都是类似的,但二者仍有差异

主题属性的定义

在引用主题属性时,可以使用 ?.attr 语法,其中 ?代表在当前主题中搜索

主题属性带来的优势

如果我们 app 需要支持普通版本和 Pro 版本,它们的主色不同,我们只需定义两个主题,配置不同的 colorPrimary。接着我们需要适配深色主题,那么只需提供不同的数值即可

values文件夹下主题配置

values-night文件夹下主题配置

这就好比我们有一个 Theme 抽象类,而其中有一个抽象属性 colorPrimary,它有四个实现类,分别重写了 colorPrimary 属性,这样我们便得到了 四个变体,未来想加入新的变体,只需继承该抽象类并重写属性即可。看过 第一篇译文 的小伙伴知道,主题的作用范围是 「树」中的所有子节点。这样我们便很轻松地实现了更改程序主色的功能

如果要使用 Style 实现这一功能,首先,我们需要定义四种 Style。由于 Style 的作用范围是特定 View,因此我们要为每个 View 均定义四套 Style

两种方案对比

小结

简单来说,Style 与 Theme 的作用范围不同

Style 只会作用于单一的 View 中,使用时用 style 标签

Style 作用范围

Theme 会作用于「树」中的所有子节点,使用时用 theme 标签

theme 作用范围

在任意时刻,程序都是运行在某一特定主题下的,例如 activity 被设置了特定主题

我们在使用时应该注意 theme 与 style 各自的优势,灵活运用二者

Theme Overlay

这部分内容在视频的 08.55 处

主题是有继承关系的,当该继承关系链中有多个主题配置了同一属性,那么最继承链最底部的内容会生效,在下图中,如果多个主题都声明了 colorPrimary,那么 Theme.Owl.Pink 中的内容会生效(这有点像 Java 的继承关系)

利用这种继承关系我们可以实现在粉色主题下将部分界面使用蓝色主题

粉色主题下,某部分需要一个蓝色主题

我们看一下两种主题的继承关系,这两种主题的父级应该是比较相似的,这看起来比较浪费,因为很多属性是相同的

另外,当你把一个主题设置在另一个主题之上,你需要注意不能将自己想要保留的东西被覆盖掉

Theme Overlay 可以很好地解决这一问题,它并不是一项新技术,而是属于一种技巧

Theme Overlay

接下来我们只需关注想要更改的东西,应用下图的声明的主题,则会只改变 colorPrimary 和 colorSecondary 两项属性的值,而其它的所有属性均不变

技巧1:反转颜色

MaterialComponents 提供了 暗色的 Theme Overlay

使用该 Theme Overlay 可以将浅色主题中的某部分做成暗色主题

技巧2:使用正确的 Context

我们知道主题与 Context 相关,由于上文我们提到的主题的继承关系,使用正确的 Context 很重要

记住:使用距离最近 View 所在的 Context

当然更好的做法是使用主题属性

技巧3:在代码中使用 Theme Overlay

如果想要在代码中使用 Theme Overlay ,可以将其包裹为 ContextThemeWrapper,这也是 android:theme 标签内部做的事情

使用 Theme 和 Style

Color

这部分内容在视频的 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

技巧1:设置透明度

我们在开发中可能会遇到这种情况:对于同一个颜色,我们需要不同的透明度。因此我们可能会复制不同透明度的色值

你可能不想在更改一个颜色后然后再逐一更改其相应透明度的色值,此处我们可以使用 ColorStateList,我们可以使用默认颜色的功能,只配置一个 item,并在此处配置透明度(从 0 到 1)

技巧2:ColorStateList 与 Drawable 的转换

我们在 View 配置 background 等属性时可以直接传入 color,在内部系统会填充该颜色并将其包装为 ColorDrawable

但如果你将 ColorStateList 传入是不行的,在 API 28 及之前的设备会崩溃

这是因为 ColorDrawable 是无状态的,在 Android 10 中,官方加入了 ColorStateListDrawable 解决了这一问题

为了在所有 API 中获得相同的体验,我们可以使用一种变通的做法,使用 backgroundTint

此处使用纯色设置了一个矩形,接着使用 backgroundTint 指向了 ColorStateList

常用技巧

这部分内容在视频的 17.35 处

技巧1:正确命名资源

我们的项目中肯定有这样命名的资源,它们是按照主题属性命名的。Android Studio 新建项目默认的资源就是这样命名的

而你的主题大概是这样,主题属性指向同名的颜色资源

这样做是不推荐的

我们需要的不是一个语义的命名,而是需要一个文字的命名,我们可以用品牌颜色命名,也可以像 Material Color System,根据色调命名

根据品牌命名

根据色调命名

技巧2:使用统一的 style 名称

大家可能见过这样的样式,一个叫 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 来解决此问题,详情移步

技巧3:拆分多个文件

简单模式

将资源类型文件进行标准的分类

  • theme.xml :Theme 和 Theme Overlay
  • type.xml:字体,文本外观,文本尺寸,字体文件等
  • style.xml:只有 Widget style
  • dimens.xml colors.xml strings.xml:其它类型归类于实际的资源类型

简单模式

复杂模式

复杂模式是按照逻辑进行分类,例如形状相关的放入 shape.xml,如果想要实现全屏的UI,可以在 sys_ui.xml 中控制状态栏/导航栏颜色,以及是否显示等等

复杂模式

在 Android Studio 的 Android 视图下,这样做的效果是很好的。如下图,可以很清晰的看到 light 主题和 dark 主题的主题文件

Material

颜色系统

这部分内容在视频的 24:00 处

该系统构建基础是大量使用语义命名的变量,这些变量都属于「主题属性」。它的运作原理是 library 展示与这些使用语义命名的颜色相关的主题属性,而开发者负责为这些颜色提供数值。在 library 内,用这些颜色构建所有的 Widget

对于颜色系统,开发者需要了解一些常用的颜色

colorPrimarycolorSecondary 是 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,这样在打开/关闭 暗黑主题时相应的主题属性的色值都会跟随变化

技巧1 :抽取主题

很多时候只做上面的两步并不能很好地适配暗黑主题,例如我们的应用在浅色主题下是这样的,深色的内容在浅色的背景上

而使用了夜间主题,可能会变成这样

而我们想要的效果是这样的

这是由于设置颜色时硬编码导致的

实际上,这种情况使用主题属性会有更好的效果

如果想要 colorPrimary 在不同的主题下使用不同的颜色,我们应该如何设置?

或许你会在 values-night/colors.xml 为暗色主题定义色值,但不建议这样做!

不建议这样!

最好的做法是抽取公共部分到基础主题,然后在此基础上对浅色和深色主题分别配置差异化的属性

技巧2:ColorPrimary 的使用

有些时候我们的 colorPrimary 是一种亮色,例如下图中的蓝色,但在暗黑主题下我们想使用相对较暗的颜色,例如 ?attr/colorSurface,Material 组件内部为我们做好了转换,直接使用 ?attr/colorPrimarySurface 即可

Demo

demo地址在这里,如果感觉对你有帮助的话,点一颗小星星吧~ 😉

20200705 更新

评论区有不少小伙伴觉得重新创建 activity 时有一个黑屏的效果,切换非常生硬。为了解决这一问题,我找了相关的资料,在 issuetracker 有人提过 activity.create() 方法导致黑屏的问题,但官方只是标记了「已分配」,并没有提供解决的明确的时间

因此我们只能寻找其它的方式,考虑到 recreate 会使 activity 重建,因此我们可以考虑在动画上 做些文章,比如将透明度柔和地过渡

这是之前的效果

demo

虽然过渡效果不是特别完美,但比最开始的生硬切换要好一些

还有一种方式是不使用 recreate ,在重启 activity 时使用 finish + startActivity 并关闭动画来处理,但这样会导致原来的数据丢失,甚至 ViewModel 都非同一实例,因此我不是很喜欢这个方式

至于评论区提到的使用 view.getDrawingCache() 的方式,这篇文章有介绍 ,此处不再赘述

【奇技淫巧】什么?项目里gradle代码超过200行了!你可能需要 Kotlin+buildSrc Plugin

本文内容来自博文 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 文件就像是魔术师手中的魔法。

.

救世主:Kotlin & the buildSrc module?

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行代码即可重构为一段易于理解的代码

.

点击查看完整代码

那么这段新的脚本文件都做了什么?

  1. 依赖的项目的特有插件 com.quickbirdstudios.bluesqaure
  2. 使用了项目自定义的扩展 extension
  3. 定义了module 特有的依赖

.

下面我们来看看究竟是如何实现的吧

步骤1 配置自定义Gralde 插件

  • 在项目根目录创建名为buildSrc文件夹
  • 为该文件夹创建build脚本,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

.

详情参见官方文档,如何创建Gralde Plugin

.

这里使用 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 函数(方法)得到调用

步骤2 将公共代码抽取到自定义插件中

在我们配置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{}代码块再次配置即可覆盖插件中预置的配置

·

步骤3 配置默认依赖(dependencies)

一般来说,我们每个android 都需使用下列依赖

  • Kotlin Standard library
  • JUnit
  • Support Test Runner
  • Espresso

我们可以在插件中添加另一个函数 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")
}

.

步骤4 配置默认插件

上文的配置有一个缺陷:在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

我们也可以定义一些自定义的扩展

.

例如我们想处理两个配置

  1. 这个module发布了吗?
  2. 被发布时packageName是什么?

.

配置好后我们可以这样使用

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行代码便实现了以下工作

  • Configure our Android builds
  • Apply default plugins
  • Apply default dependencies
  • Run a custom linter
  • Run aggregated coverage reports
  • Run aggregated test result reports
  • Install default git hooks into the project
  • Setup Multiplatform builds
  • Setup publications for our library modules
  • Deploy libraries to the correct repository (Snapshot/Release)

.

以上为原作者表达的主要信息,原文请移步

.

demo代码

.

英文水平有限,如有纰漏请指教。

【Jetpack更新之Fragment】1.3.0-alpha04 来袭,Fragment 间通信的新姿势

前言

fragment 1.3.0-alpha04 发布了,其中有很多变动,其中提供了 fragment 间传递数据的新方式

1.3.0-alpha04 更新

API 更改

首先我们介绍一下 API 更改

  • startActivityForResult()/onActivityResult()requestPermissions()/onRequestPermissionsResult() 弃用
  • prepareCall() 重命名为 registerForActivityResult()
  • target fragment API 被弃用

Activity Result API 上位

由于官方提供了 Activity Result API 来替换 onActivityResult 机制,因此 fragment 的 startActivityForResult()/onActivityResult()requestPermissions()/onRequestPermissionsResult() 方法被标记弃用了

Activity Result API 详情可参考 秉心说是时候丢掉 onActivityResult 了 !

文章介绍的很详尽,这里不再赘述

prepareCall 重命名

值得注意的地方是 prepareCall() 被命名为 registerForActivityResult()

注意:在版本处于 Alpha 版状态时,可以添加、移除或更改 API。因此 Alpha 版本不适合在生产上使用

来自我的另一篇博客

target fragment API 被弃用

其实 target fragment API 早已被弃用

setTargetFragment 被弃用

target fragment 需要直接访问另一个 fragment 的实例,这是十分危险的,因为你不知道目标 fragment 处于什么状态。而且 target fragment 不支持 Navigation

弃用 target fragment API

那么,fragment 之间传递数据更干净的方式是什么呢?

fragment 之间传递数据的新方式

前文提到,在相同的 FragmentManager 中可以使用 target fragment API 来在 fragment 间传递数据,但这种方式需要直接访问目标fragment 的实例,这很危险,因为目标 fragment 的状态是未知的

因此官方提供了这样的 API,它允许在一个 fragment 上设置结果,并将该结果在 fragment 的适当的生命周期中使用。

这种传递数据的方式适用于 DialogFragment ,Navigation 中的 fragment

此更改还包括 -ktx 扩展功能以确保 kotlin 用户可以将 FragmentResultListener 作为 lambda 传递

FragmentA 源码

FragmentB 源码

demo

源码分析

老规矩,我们沿着官方的 commit log 来看看官方实现该功能的思路

首先,添加了 FragmentResultOwner 这样的的抽象,用于处理 fragment result,其内部有两个方法

  • setResult
  • setResultListener

前者用于发送数据,后者用于接收数据

FragmentResultOwner

而其实现类为 FragmentManager

FragmentManager implement FragmentResultOwner

我们来看看 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 LakeFragments: Past, Present, and Future (Android Dev Summit '19) 中提到了 fragment 间通信的问题,未来 fragment 会整合 fragment 自身和其内部 view 的生命周期,提供同一 FragmentManager 多返回栈的支持

看到 fragment result API ,我突然有个想法,如果将其应用到 Navigation 中是否是解决 Navigation 跳转返回后状态重置的一个方法呢?

各位小伙伴有什么想法欢迎评论区留言

【译】Fragment 的重大重构 —— 介绍 Fragment 新的状态管理器

原文: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 的职责:

  • 通过 Fragment 的生命周期方法来移动它们
  • 运行转场动画
  • 处理延迟事务

我们彻底回顾了这些系统过去的工作方式,发现 它们需要被从头重写 ,于是我们重写了它们。它们比以往的任何时候都更好,我们能够关闭至少 10 个长期存在的相关 issues,并且这个内部重构为单 FragmentManager 支持 多返回栈 扫清了道路(译者注:Bottom Navigation 管理平级界面的问题),并简化了 Fragment 的生命周期。

FragmentManager moveToState()

每个 FragmentManager 都与一个 host 关联,对于大多数 fragment,host 为 FragmentActivity(使用 FragmentControllerFragmentHostCallback 可以自定义 host,但不在本文的讨论范围)。当 activity 转移到 CREATEDSTARTED,以及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。

Postponed fragments

无论是好是坏,Fragment 都继承了许多与 Activity 相同的命名的 API。这种继承的一部分是围绕转换以及推迟转场直到目标准备好的能力。这对共享元素转场非常重要,与此同时还要确保在转场的同时不会产生更密集的数据加载(译者注:为了转场时先完成动画,再完成大数据的渲染)。

延迟 fragment 拥有两个重要的特性:

  1. 它的 view 已被创建,但不可见
  2. 它的生命周期上限为 STARTED

当您调用 startPostponedEnterTransition() 后,fragment 的转场便会执行,view 将变得可见,并且 fragment 将会移动到 RESUMED。实际上,这正是新的 state manager 做的,过去的 fragment 不是这样工作的,详情参考 Postponed Fragments leave the FragmentsFragmentManager 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 的主要特性。

在 container 层工作

FragmentManager 具有 container 这个不错的属性(读起来:很方便,但作为维护者却不那么有趣),在该属性中,您可以为要放置 Fragment 传入任何 container id 。甚至在一个 FragmentTransaction 中,您可以 add 一个 fragment 到一个 container,从一个不同的 container remove 另一个,replace 第三个container 最顶端的 fragment,等等。

fragment 动画进/出会产生接触,这仅发生在 container 层。

Fragment 支持一些列的动画系统:

  • 老旧破烂的 framework Animation API
  • framework Animator API
  • framework Transition API(仅支持 21+,同样很烂)
  • AndroidX 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 中的逻辑移动到了一个地方。

所以 'new state manager' 是个啥

它意味着要替代这种架构:

旧的 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 的未来。掘金官方文章在这

  • Fragment 的通信问题
  • 多返回栈
  • 简化 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 的其他内容,可参考

【译】2020 年 Fragment 最新文档(下),该更新知识库啦

前言

很高兴见到你 👋,我是 Flywith24 。

最近 Android 官方针对 Fragment 文档进行了重新编写,使其适应 2020 年最佳实践的快速发展。

Fragment 的确是一个让开发者头疼的组件,它是一个很好的设计,但一直处于可改进的状态,随着 AndroidX Fragment 的快速更新,Fragment 已不同往日,虽然仍有改进的空间(单个 FragmentManager 不支持多返回栈,Fragment 自身和其 view 的生命周期不一致)。考虑到该文档的确有很多新知识以及官方文档的极慢的汉化速度,本文将 2020 版 Fragment 的官方文档翻译成中文,喜欢一手信息的小伙伴可直奔 官方原文。如果只想关注新文档中的变化,可 点此直达。限于篇幅原因,该文档分上下两部分。

【译】2020 年 Fragment 最新文档(上),该更新知识库啦

【译】2020 年 Fragment 最新文档(下),该更新知识库啦

本文为下半部分,将介绍以下内容:

  • Fragment 的状态保存
  • Fragment 间通信
  • Fragment 于 AppBar 共同使用
  • 使用 DialogFragment 显示 Dialog
  • Fragment 测试

上半部分介绍:

  • Fragment 的创建
  • Fragment manager
  • Fragment 事务
  • Fragment 动画
  • Fragment 生命周期
点击查看彩蛋😉 😝 欢迎来到彩蛋部分,您一定是个好奇心很强的小伙伴呢。

我是一个「强迫症晚期患者」,为了移动端更好阅读的体验,我经常将代码以图片的形式插入到文内。但随之而来出现一个问题:没办法 copy 代码(这对 cv 开发者很重要的 🤣)。

前些天,我在 github 某个项目的 README 文档中看到一个技巧,便是把较长且有些影响阅读的内容折叠,读者可以自由地选择展开。

这也是这个「彩蛋」的显示方式。后文中关于代码的部分我都会提供图片和可复制的源码两部分,其中后者处于折叠状态。您可以点击 「点击查看代码详情」以展开源码。

彩蛋结束。🥳

状态保存

各种 Android 系统操作可能会影响 fragment 的状态。 为了确保用户状态得到保存,Android 会自动保存并还原 fragment 的返回栈。因此,您需要确保 fragment 中的所有数据也被保存和还原。

下表罗列了导致 fragment 丢失状态的操作,以及各种状态是否被保存。表中提到的状态类型如下:

  • Variables:fragment 的本地变量
  • View State:fragment 中 一个或多个 view 拥有 的所有数据
  • SavedState:该 fragment 实例固有的数据,应保存在 onSaveInstanceState()
  • NonConfig:从外部源(例如服务器或本地存储库)提取的数据,或由用户创建一旦提交就发送到服务器的数据。

通常,VariablesSavedState 的处理方式相同,但下表将两者进行了区分,以展示各种操作对它们的影响:

表1

*NonConfig state 在进程死亡时可以使用 Saved State module for ViewModel 保存状态。

让我们看一个具体的例子。我们生成一个随机字符串将其显示在 TextView 中,并提供一个发送给朋友之前编辑该字符串的选项:

用户按下编辑按钮后,将显示一个 EditText 视图,用户可以在其中编辑消息。如果用户点击CANCEL,则应清除 EditText 视图,并将其可见性设置为 View.GONE。为了保持良好的体验,该示例需要管理 4 个数据:

表2

以下各节介绍如何正确管理数据状态。

View State

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

SavedState

您的 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

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 库提供了两个通信选项:共享 ViewModelFragment Result API。如何选择应视场景而定: 要与任何自定义 API 共享持久数据,应使用 ViewModel。对于可以放入 Bundle 的一次性的结果类数据,应使用 Fragment Result API

下文介绍如何使用 ViewModelFragment Result API 在 fragment 和 activity 之间通信。

使用 ViewModel 共享数据

ViewModel 是多个 fragment 或 fragment 与其宿主之间共享数据的理想选择。ViewModel 对象存储并管理 UI 数据。关于 ViewModel 的更多信息,请参考 ViewModel overview(译者注:也可参考译者文章 即使您不使用 MVVM 也要了解 ViewModel)。

与宿主 activity 共享数据

在某些场景下,您可能需要在 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使用合适的作用域。在上面的示例中,MainActivityMainActivityListFragment 的作用域,因此它们能够获得相同的 ViewModel 对象。如果 ListFragment 改用自身的作用域,则将获得与 MainActivity 不同的 ViewModel 对象。

在 fragment 之间共享数据

同一 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 可能需要彼此共享数据。要在这些 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 Graph 范围内共享 ViewModel

如果您正在使用 Navigation library,还可以将 ViewModel 的作用域限定为目的地的 NavBackStackEntry 的生命周期。例如,可以将 ViewModel 的作用域限定为 ListFragmentNavBackStackEntry

点击查看代码详情
// 👇 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 Result API 获得结果

在某些情况下,您可能希望在两个 fragment 之间或 fragment 与其宿主 activity 之间传递一次性值。例如,您可能有一个读取二维码的 fragment,将数据传递回前一个 fragment。从 Fragment 1.3.0-alpha04 开始,每个 FragmentManager 都实现 FragmentResultOwner。这意味着 FragmentManager 可以充当 fragment 结果的**存储。此更改允许组件通过设置 fragment 结果并监听那些结果进而彼此通信,而无需那些组件彼此直接引用(译者注:Fragment Result API 引入的原因以及源码分析可参考 1.3.0-alpha04 来袭,Fragment 间通信的新姿势)。

在 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() 回调后,该结果将被清除。此行为有两个主要含义:

  • 返回栈上的 fragment 只有弹出并处于 STARTED 才能接收结果
  • 当 fragment 正在监听一个 STARTED 状态的结果,当结果被设置则立即触发 listener 的回调

🌟 注意:由于 fragment 结果存储在 FragmentManager 层级上,因此必须将 fragment attach 到父 FragmentManager 来调用 setFragmentResultListener()setFragmentResult()

测试 fragment 结果

使用 FragmentScenario 测试 setFragmentResult()setFragmentResultListener() 的调用。使用 launchFragmentInContainerlaunchFragment 为被测 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 间传递结果

要将结果从子 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 的结果

要在宿主 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");
                // 处理结果
            }
        });
    }
}

与 AppBar 共同使用

顶部 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>

该菜单包含两个选项:一个用于导航到配置文件界面,另一个用于保存对配置文件所做的所有更改。

Activity 拥有的 app bar

app bar 通常由宿主 activity 持有。当 activity 持有 app bar 时,fragment 可以通过重写在 fragment 创建期间调用的 framework 方法来与 app bar 进行交互。

🌟 注意:本节内容仅在 activity 持有 app bar 时才适用。 如果您的 app bar 是 fragment 布局中包含的 toolbar,请参见 Fragment 拥有的 app bar 一节。

注册 activity

您必须通知系统您的 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 的添加顺序。

加载 menu

要将菜单合并到 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();
    }
}

Fragment 拥有的 app bar

如果您的 app 中的大多数屏幕都不需要应用 app bar,或者一个屏幕可能需要一个截然不同的 app bar,则可以在 fragment 布局中添加 Toolbar。尽管您可以在 fragment 的视图树中的任何位置添加 Toolbar,但通常应将其放置在屏幕顶部。要在片段中使用 Toolbar,请提供一个 ID 并在 fragment 中获得对其的引用,就像在其他任何视图中一样。

使用 fragment 拥有的 app bar 时,强烈建议直接使用 Toolbar API。不要使用 setSupportActionBar() 和 Fragment menu API,它们仅适用于 activity 拥有的 app bar。

加载 menu

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

DialogFragment 是专门用于创建和托管 dialog 的特殊 fragment 子类。严格来说,您不需要在 fragment 中托管 dialog,但是这样做可以使 FragmentManager 管理 dialog 的状态并在配置发生变化时自动还原 dialog 。

🌟 注意:本节假定您熟悉创建 dialog 。有关更多信息,请参见 dialog 指南

创建 DialogFragment

要创建 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

显示 DialogFragment

无需手动创建 FragmentTransaction 即可显示 DialogFragment,使用 show() 方法显示 dialog 。您可以向该方法传递一个 FragmentManager 对象和 FragmentTransaction 的 tag(String 类型)。从 Fragment 中创建 DialogFragment 时,必须使用 Fragment 的子 FragmentManager 来确保在配置发生变化后正确恢复状态。非空标记允许您在以后使用 findFragmentByTag() 来获取 DialogFragment

为了更好地控制 FragmentTransaction,可以使用 show() 的重载方法传入一个 FragmentTransaction

🌟 注意:因为 DialogFragment 是在配置发生变化后自动恢复的,所以请考虑仅根据用户操作或 findFragmentByTag() 返回 null(代表 dialog 不存在)时才调用 show()

DialogFragment 生命周期

DialogFragment 遵循标准的 fragment 生命周期。此外,DialogFragment 还有一些其它的生命周期回调。常见的如下:

  • onCreateDialog() - 重写此回调,为 fragment 提供一个管理和显示的 dialog
  • onDismiss() - 如果在关闭 Dialog 时需要执行自定义逻辑(例如释放资源,取消订阅可观察的资源等),请重写此回调
  • onCancel() - 如果在取消 Dialog 时需要执行自定义逻辑,则重写该方法

DialogFragment 还包含用于关闭或设置 DialogFragment 可取消的方法:

  • dismiss() - 关闭 fragment 及其 dialog 。如果该 fragment 加入到了返回栈,则弹出该 fragment 及其顶部的所有 entry。否则,将提交一个新的事务 remove 该 fragment。
  • setCancellable() - 控制当前显示的 dialog 是否可以取消。应该使用 DialogFragment 的该方法而不是直接调用 Dialog.setCancelable(boolean)

请注意,在将 DialogFragmentDialog 一起使用时,您不要重写 onCreateView()onViewCreated()。dialog 不仅是 view,它还具有自己的 Windiow。因此,重写 onCreateView()是不行的。此外,除非您已重写 onCreateView() 并提供了非 null 的 view,否则永远不会在自定义 DialogFragment 上调用 onViewCreated()

🌟 注意:订阅支持生命周期的组件(如 LiveData)时,切勿在使用 DialogDialogFragment 中将 viewLifecycleOwner 用作 LifecycleOwner。相反,请使用 DialogFragment 本身,或者如果您使用的是 Jetpack Navigation,请使用 NavBackStackEntry

使用自定义 View

您可以通过 重写 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 的通用性,重要的是要验证它们是否提供了一致且资源高效的体验。请注意以下几点:

  • 你的 fragment 不应依赖特定的父 activity 或 fragment
  • 除非 fragment 对用户可见,否则不应创建 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"
}

本节示例使用的断言来自 EspressoTruth。关于测试和断言库的更多信息,请参考 Set up project for AndroidX Test

创建 Fragment

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 一节。

将 fragment 驱动到新状态

在 app 的 UI 测试中,通常需要 fragment 处于 RESUMED 状态时开始测试。但是,在更细粒度的单元测试中,当 fragment 从一种生命周期状态转换为另一种生命周期状态时,您可能也需要测试其行为。

要将 fragment 驱动到不同的生命周期状态,请调用 moveToState()。 此方法支持以下状态作为参数:CREATEDSTARTEDRESUMEDDESTROYED。 该方法模拟了该 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 状态。

重新创建 Fragment

如果您的 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 交互

要在被测 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。

测试 dialog 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())
    }
}

译者补充

新版文档的变化

  • 在 xml 使用 FragmentContainerView 作为 fragment 的容器,不要使用 <fragment> 标签或 FrameLayout
  • 建议使用 Navigation library 管理 app 内导航
  • activity 中使用 getSupportFragmentManager() 获取 FragmentManager
  • fragment 中使用 getChildFragmentManager() 获取管理子 fragment 的 FragmentManager
  • fragment 使用 getParentFragmentManager() 获取其宿主的 FragmentManager
  • commit 事务时建议调用 setReorderingAllowed(true)
  • Fragment 有一个带有 layoutId 参数的构造器,无需调用 onCreateView 设置布局
  • FragmentFactory 默认使用无参构造器创建 fragment,如果自定义了 fragment 构造器,为了保证重建时的一致性,需要自定义 FragmentFactory
  • 使用 setMaxLifecycle() 限制了 Fragment 的最大生命周期,因此 setUserVisibleHint 被弃用了,保证了 ViewPager 中 fragment 可见性判断与正常情况一致
  • 对于涉及多种动画效果的场景时建议使用 transition,嵌套 AnimationSet 存在已知问题。
  • 理解 Fragment Lifecycle,其 View Lifecycle 以及相应回调方法的关系
  • 理解 observe LiveData 时 lifecycleOwnerviewLifecycleOwner 的区别
  • 使用 ViewModel 配合 savestate 保存/恢复状态
  • 使用最新的 Fragment 通信机制:共享 ViewModelFragment Result API
  • 使用 DialogFragment 来显示 Dialog,能够更好的处理配置发生变化和系统资源回收的场景
  • fragment-ktx 库有很多方便的扩展函数和属性代理
  • fragment library 中包含了 activity library

Fragment 相关文章

关于我

我是 Flywith24,Android App/Rom 层开发者。目前专注于 Android 体系化文章的写作。

Android Detail 专栏 正在更新中,想要建立系统化知识体系的小伙伴可以去看看哦。我的所有博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.