原文:Google Android Developers 文档贡献者,Testing Kotlin flows on Android。中文翻译与技术整理:未完纪。核对日期:2026-10-05;源页最后更新:2026-09-24。本篇连同原页链接的完整测试文件核查,示例没有在本环境执行。
测试依赖 Flow 通信的类,首先要确定 Flow 出现在被测对象的哪一侧。如果对象接收 Flow,就用可以控制的假实现替换生产者;如果对象向外暴露 Flow,就在测试里读取它发出的数据并断言。协程测试 API 的基础知识可先参阅官方协程测试指南。

用假生产者控制输入
生产环境中的 Repository 可能同时读取两个数据源。单元测试通常不需要让这些外部依赖一同工作,而是注入返回固定数据的假 Repository,使结果可重复。原文用 flow 构建器演示预先设定的发送序列:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
// 测试把假依赖注入被测对象。
val sut = MyUnitUnderTest(MyFakeRepository())
这里的 MyRepository、MyUnitUnderTest 和 ITEM_1 是教程语境中的类型和常量,不是可直接引用的 Android API。控制了输入以后,再触发被测对象的行为并断言其输出。这种替换方式也能用于 UI 等更大范围的测试;如何替换依赖取决于应用的注入方式,使用 Hilt 时应按其测试模块规则处理。
有限流:只收集测试真正需要的部分
first() 等待第一个元素,然后取消对上游的收集,适合只关心初始结果的测试。toList() 则要等上游结束才返回,因此只适用于会完成的流:
@Test
fun finiteFlow() = runTest {
val first = flowOf(1, 2, 3).first()
assertEquals(1, first)
val all = flowOf("Hello", "How are you?", "Bye").toList()
assertEquals(listOf("Hello", "How are you?", "Bye"), all)
}
上例为编辑整理的自足有限数据片段,用 flowOf 替换原文的业务假依赖,断言含义不变。对更复杂的流,可以组合操作符限定观察范围:
| 写法 | 断言意图与边界 |
|---|---|
outputFlow.drop(1).first() |
跳过首项,等待第二项;第二项永不到来就无法正常完成。 |
outputFlow.take(5).toList() |
最多收集前五项;若上游未结束且不足五项,仍会等待。 |
outputFlow.single() |
要求流完成且恰好只有一个元素,不等于只取首项。 |
outputFlow.count() 或 count(predicate) |
等待有限流结束后计数,后者只计满足条件的元素。 |
持续流:一边发送,一边断言
要测试长期存在的流,通常需要在测试执行期间持续收集,再交错进行“输入—断言”。下面保留原文的关键模型:假数据源用 MutableSharedFlow 接收测试发送的整数,Repository 把每项乘以十。
interface DataSource {
fun counts(): Flow<Int>
}
class FakeDataSource : DataSource {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun counts(): Flow<Int> = flow
}
class Repository(private val dataSource: DataSource) {
fun scores(): Flow<Int> = dataSource.counts().map { it * 10 }
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun continuouslyCollect() = runTest {
val source = FakeDataSource()
val repository = Repository(source)
val values = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
repository.scores().toList(values)
}
source.emit(1)
assertEquals(10, values[0])
source.emit(2)
source.emit(3)
assertEquals(30, values[2])
assertEquals(3, values.size)
}
这里 toList(values) 不会正常结束,因为 SharedFlow 本身不会完成。把它放在 backgroundScope 中,可以在 runTest 结束时自动取消。如果改为测试主体中的普通子协程而未取消,测试会一直等待它,最终超时。
UnconfinedTestDispatcher(testScheduler) 让收集协程立即进入运行,确保 launch 返回时已准备接收数据;同时显式共享测试调度器。默认 MutableSharedFlow() 没有 replay,发送之前若尚无订阅者,事件不会留给之后的订阅者。不能把切换到别的调度器、并发执行后仍观察到相同顺序当作当然结论。
用 Turbine 表达逐项预期
Turbine 提供收集协程和 awaitItem() 等辅助 API,适合把发送与预期写在同一个测试块内:
@Test
fun usingTurbine() = runTest {
val source = FakeDataSource()
val repository = Repository(source)
repository.scores().test {
source.emit(1)
assertEquals(10, awaitItem())
source.emit(2)
awaitItem() // 消费当前测试不关心的这一项
source.emit(3)
assertEquals(30, awaitItem())
}
}
触发发送的调用放在 test {} 内,才能在收集已经建立后发出事件。依赖版本和取消规则要按项目所使用的 Turbine 文档核查;本篇未添加未经核验的依赖版本号。
StateFlow:优先验证当前状态
StateFlow 是可观察的状态容器,会合并更新。快速连续赋值时,收集者不保证收到每一个中间值,只保证能看到最新状态。若业务目标是验证“现在的状态”,直接断言 value 通常更可靠。确实需要观察中间转换时,也可以收集,但测试必须明确其调度与合并假设。
interface MyRepository {
fun scores(): Flow<Int>
}
class HotFakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
class MyViewModel(private val repository: MyRepository) : ViewModel() {
private val _score = MutableStateFlow(0)
val score: StateFlow<Int> = _score.asStateFlow()
fun initialize() {
viewModelScope.launch {
repository.scores().collect { _score.value = it }
}
}
}
测试先断言 score.value == 0,再调用 initialize() 开始收集;发送 1 后断言状态为 1,连续发送 2、3 后断言最新状态为 3。不要据此宣称所有真实 UI 收集者必然看过 2。
stateIn:读取 value 不能替代订阅者
实际 ViewModel 往往用 stateIn 把流转为 StateFlow:
class MyViewModelWithStateIn(repository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = repository.scores()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
0
)
}
SharingStarted.Lazily 和 WhileSubscribed 都依赖订阅触发上游收集。在测试中,即使只断言 value,也必须建立至少一个收集者,否则上游不会启动。5000L 是停止共享前的等待时间,不是“每隔五秒更新一次”。
网页最后的片段依赖其链接源码中的 Dispatchers.Main 设置。下面补齐测试生命周期,并增加显式清理 ViewModel 协程作用域;相对于原文,这是编辑修正,尚未编译或执行:
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
@OptIn(ExperimentalCoroutinesApi::class)
class StateInTest {
private val mainDispatcher = UnconfinedTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(mainDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun stateInRequiresCollector() = runTest(mainDispatcher) {
val repository = HotFakeRepository()
val viewModel = MyViewModelWithStateIn(repository)
try {
backgroundScope.launch(
UnconfinedTestDispatcher(testScheduler)
) {
viewModel.score.collect {}
}
assertEquals(0, viewModel.score.value)
repository.emit(1)
assertEquals(1, viewModel.score.value)
repository.emit(2)
repository.emit(3)
assertEquals(3, viewModel.score.value)
} finally {
viewModel.viewModelScope.cancel()
}
}
}
片段仍需项目提供 Kotlin 协程、kotlinx-coroutines-test、Lifecycle ViewModel、JUnit4 及相关 import;Turbine 示例另需 Turbine。它不是完整 Gradle 工程。Dispatchers.setMain 改的是全局 Main 调度器,同进程并行测试要避免互相覆盖;无论断言成功与否,都要恢复。这里采用 eager 调度是为了教程中的顺序控制,不足以验证生产调度器下的所有竞争条件。
静态审查与适用范围
本篇已检查订阅建立顺序、无限收集的取消、Main 替换/恢复、StateFlow 合并以及 stateIn 的启动条件。没有执行 Gradle、连接设备或运行测试,不能把上述断言写法当成本次“测试通过”的记录。示例未见硬编码秘密或命令注入入口;这仅是所展示片段的静态结果,不等于对应用进行过安全审计。
参考资料:原文全文、完整测试源码(固定提交)、StateFlow 与 SharedFlow。
版权与许可:原示例 © 2026 The Android Open Source Project。按源页链接的内容许可,文档及示例代码在无另外声明时采用 Apache License 2.0。本稿对内容作中文翻译、结构整理,并对示例作上述标明的修改;按“AS IS”提供,无任何保证。
完整许可证副本:Apache License 2.0 完整文本。
完整官方测试源与许可
以下保留固定提交eca1606f475dc3b2c9743071cbfe4b4082947e42的FlowTest.kt完整源码、原版权和注释,便于核对原例中的上下文。正文编辑修订另列;原文件没有编译或执行。
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.basics.flow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
// Placeholder types
const val ITEM_1 = 1
val ALL_MESSAGES = listOf("Hello", "How are you?", "Bye")
class MyUnitUnderTest(repository: MyRepository)
interface MyRepository {
fun observeChatMessages(): Flow<String> = ALL_MESSAGES.asFlow()
fun scores(): Flow<Int> = flow {}
}
// [START android_kotlin_flow_test_fake]
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
// [END android_kotlin_flow_test_fake]
class FlowTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setup() {
Dispatchers.setMain(UnconfinedTestDispatcher())
}
@OptIn(ExperimentalCoroutinesApi::class)
@After
fun tearDown() {
Dispatchers.resetMain()
}
// [START android_kotlin_flow_test_verify]
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
// ...
}
// [END android_kotlin_flow_test_verify]
// [START android_kotlin_flow_test_assert]
@Test
fun myRepositoryTest() = runTest {
// [START_EXCLUDE silent]
val fakeSource1 = 1
val fakeSource2 = 2
class MyRepository(a: Int, b: Int) {
val counter: Flow<Int> = flow { emit(ITEM_1) }
}
// [END_EXCLUDE]
// Given a repository that combines values from two data sources:
val repository = MyRepository(fakeSource1, fakeSource2)
// When the repository emits a value
val firstItem = repository.counter.first() // Returns the first item in the flow
// Then check it's the expected item
assertEquals(ITEM_1, firstItem)
}
// [END android_kotlin_flow_test_assert]
// [START android_kotlin_flow_test_assert_list]
@Test
fun myRepositoryTestList() = runTest {
val repository = MyFakeRepository()
// Given a repository with a fake data source that emits ALL_MESSAGES
val messages = repository.observeChatMessages().toList()
// When all messages are emitted then they should be ALL_MESSAGES
assertEquals(ALL_MESSAGES, messages)
}
// [END android_kotlin_flow_test_assert_list]
@Suppress("UNUSED_VARIABLE")
suspend fun testTestingOperators(outputFlow: Flow<Int>, predicate: (Int) -> Boolean) {
// [START android_kotlin_flow_test_operators]
// Take the second item
outputFlow.drop(1).first()
// Take the first 5 items
outputFlow.take(5).toList()
// Takes the first item verifying that the flow is closed after that
outputFlow.single()
// Finite data streams
// Verify that the flow emits exactly N elements (optional predicate)
outputFlow.count()
outputFlow.count(predicate)
// [END android_kotlin_flow_test_operators]
}
// [START android_kotlin_flow_test_continuous]
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun continuouslyCollect() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
val values = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
repository.scores().toList(values)
}
dataSource.emit(1)
assertEquals(10, values[0]) // Assert on the list contents
dataSource.emit(2)
dataSource.emit(3)
assertEquals(30, values[2])
assertEquals(3, values.size) // Assert the number of items collected
}
// [END android_kotlin_flow_test_continuous]
// [START android_kotlin_flow_test_turbine]
@Test
fun usingTurbine() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
repository.scores().test {
// Make calls that will trigger value changes only within test{}
dataSource.emit(1)
assertEquals(10, awaitItem())
dataSource.emit(2)
awaitItem() // Ignore items if needed, can also use skip(n)
dataSource.emit(3)
assertEquals(30, awaitItem())
}
}
// [END android_kotlin_flow_test_turbine]
// [START android_kotlin_flow_test_hot_fake]
@Test
fun testHotFakeRepository() = runTest {
val fakeRepository = FakeRepository()
val viewModel = MyViewModel(fakeRepository)
assertEquals(0, viewModel.score.value) // Assert on the initial value
// Start collecting values from the Repository
viewModel.initialize()
// Then we can send in values one by one, which the ViewModel will collect
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value) // Assert on the latest value
}
// [END android_kotlin_flow_test_hot_fake]
// [START android_kotlin_flow_test_lazily]
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun testLazilySharingViewModel() = runTest {
val fakeRepository = HotFakeRepository()
val viewModel = MyViewModelWithStateIn(fakeRepository)
// Create an empty collector for the StateFlow
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.score.collect {}
}
assertEquals(0, viewModel.score.value) // Can assert initial value
// Trigger-assert like before
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value)
}
// [END android_kotlin_flow_test_lazily]
}
// [START android_kotlin_flow_test_continuous_source]
class Repository(private val dataSource: DataSource) {
fun scores(): Flow<Int> {
return dataSource.counts().map { it * 10 }
}
}
class FakeDataSource : DataSource {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun counts(): Flow<Int> = flow
}
// [END android_kotlin_flow_test_continuous_source]
interface DataSource {
fun counts(): Flow<Int>
}
// [START android_kotlin_flow_test_stateflow_vm]
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
private val _score = MutableStateFlow(0)
val score: StateFlow<Int> = _score.asStateFlow()
fun initialize() {
viewModelScope.launch {
myRepository.scores().collect { score ->
_score.value = score
}
}
}
}
// [END android_kotlin_flow_test_stateflow_vm]
// [START android_kotlin_flow_test_stateflow_fake]
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
// [END android_kotlin_flow_test_stateflow_fake]
class HotFakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
// [START android_kotlin_flow_test_statein]
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
// [END android_kotlin_flow_test_statein]
Apache License 2.0 全文
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
原文配图
以下是The Android Open Source Project官方原图,原图未改动,中文图注由未完纪补充;不是本次操作截图。















暂无评论内容