5 月的山景城,一年一度的谷歌 I/O 开发者大会如期而至,由于当地疫情管制的放开,今年大会重回线下举行,真心希望国内的疫情也尽早结束。

今年的 I/O 大会既是谷歌各种新产品发布会,同时也是谷歌开发者们的技术交流会。不少 Android 开发者希望通过本次 I/O 了解到有关 Jetpack 的最新动态。本文对这些内容进行了收集整理,一并分享给大家。

Jetpack Overview

Android Jetpack 为我们日常开发提供了便利的工具集以及最佳实践,根据本次大会上发布的数据,目前 GooglePlay Top1000 的应用中,使用至少 2 个以上 Jetpack 库的占比从 79% 提升到 90%

接下来从 Architecture,UI,Performance 和 Compose 等四个方向为大家介绍和点评 Jetpack 的最新变化。

1. Architecture

1.1 Room 2.4/2.5

Room 最新版本进入到 2.5。 2.5 没有新功能的引入,最大变化就是使用 Kotlin 进行了重写,借助 Kotlin 空安全等特性,代码将更加稳定可靠。未来还会有更多 Jetpack 库逐渐迁移至 Kotlin。

在功能方面,Room 自 2.4 以来引入了不少新特性:

KSP:新的注解处理器

Room 将注解处理方式从 KAPT 升级为 KSP(Kotlin Symbol Processing)。 KSP 作为新一代 Kotlin 注解处理器,1.0 版目前已正式发布,功能更加稳定,可以帮助你极大缩短项目的构建时间。KSP 的启用非常简单,只要像 KAPT 一样地配置即可:

plugins {//enable kaptid 'kotlin-kapt'//enable kspid("com.google.devtools.ksp")
}dependencies {//...// use kaptkapt "androidx.room:room-compiler:$room_version"// use kspksp "androidx.room:room-compiler:$room_version"//...
}

Multi-map Relations:返回一对多数据

以前,Room 想要返回一对多的实体关系,需要额外增加类型定义,并通过 @Relatioin 进行关联,现在可以直接使用 Multi-map 返回,代码更加精简:

//before
data class ArtistAndSongs(
`   @Embeddedval artist: Artist,@Relation(...)val songs: List<Song>
)@Query("SELECT * FROM Artist")
fun getArtistAndSongs(): List<ArtistAndSongs>//now
@Query("SELECT * FROM Artist JOIN Song ON Artist.artistName = Song.songArtistName")
fun getAllArtistAndTheirSongsList(): Map<Artist, List<Song>>

AutoMigrations:自动迁移

以前,当数据库表结构变化时,比如字段名之类的变化,需要手写 SQL 完成升级,而最近新增的 AutoMigrations 功能可以检测出两个表结构的区别,完成数据库字段的自动升级。

 @Database(version = MusicDatabase.LATEST_VERSION,entities = { Song.class,  Artist.class },autoMigrations = {@AutoMigration (from = 1,to = 2)},exportSchema = true)public abstract class MusicDatabase extends RoomDatabase {...}

1.2 Paging3

Paging3 相对于 Paging2 在使用方式上发生了较大变化。首先它提升了 Kotlin 协程的地位, 将 Flow 作为首选的分页数据的监听方案,其次它提升了 API 的医用型,降低了理解成本,同时它有着更丰富的能力,例如支持设置 Header 和 Footer等,建议大家尽可能地将项目中的 Paging2 升级到 Paging3。

简单易用的数据源

Paging2 的数据源有多种实现,PageKeyedDataSource, PositionalDataSource, ItemKeyedDataSource 等,需要我们根据场景做出不同选择 ,而 Paging3 在使用场景上进行了整合和简化,只提供一种数据源类型 PagingSource:

class MyPageDataSource(private val repo: DataRepository) : PagingSource<Int, Post>() {override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {try {val currentLoadingPageKey = params.key ?: 1  // 从 Repository 拉去数据val response = repo.getListData(currentLoadingPageKey)val prevKey = if (currentLoadingPageKey == 1) null else currentLoadingPageKey - 1// 返回分页结果,并填入前一页的 key 和后一页的 keyreturn LoadResult.Page(data = response.data,prevKey = prevKey,nextKey = currentLoadingPageKey.plus(1))} catch (e: Exception) {return LoadResult.Error(e)}
}

上面例子是一个自定义的数据源, Paging2 数据源中 load 相关的 API 有多个,但是 Paging3 中都统一成唯一的 load 方法,我们通过 LoadParams 获取分页请求的参数信息,并根据请求结果的成功与否,返回 LoadResult.Page() ,LoadResult.Invalid 或者 LoadResult.Error,方法的的输入输出都十分容理解。

支持 RxJava 等主流三方库

在 Paging3 中我们通过 Pager 类订阅分页请求的结果,Pager 内部请求 PagingSource 返回的数据,可以使用 Flow 返回一个可订阅结果

class MainViewModel(private val apiService: APIService) : ViewModel() {val listData = Pager(PagingConfig(pageSize = 6)) {PostDataSource(apiService)}.flow.cachedIn(viewModelScope)
}

除了默认集成的 Flow 方式以外,通过扩展 Pager 也可返回 RxJava,Guava 等其他可订阅类型

implementation "androidx.paging:paging-rxjava2:$paging_version"
implementation "androidx.paging:paging-guava:$paging_version"

例如,paging-rxjava2 中提供了将 Pager 转成 Observable 的方法:

val <Key : Any, Value : Any> Pager<Key, Value>.observable: Observable<PagingData<Value>>get() = flow.conflate().asObservable()

新增的事件监听

Paging3 通过 PagingDataDiffer 检查列表数据是否有变动,如果提交数据与并无变化则 PagingDataAdapter 并不会刷新视图。 因此 Paging3 为 PagingDataDiffer 中新增了 addOnPagesUpdatedListener 方法,通过它可以监听提交数据是否确实更新到了屏幕。

配合 Room 请求本地数据源

通过 room-paging ,Paging3 可以配合 Room 实现本地数据源的分页加载

implementation "androidx.room:room-paging:2.5.0-alpha01"

room-paging 提供了一个开箱即用的数据源 LimitOffsetPagingSource

/*** An implementation of [PagingSource] to perform a LIMIT OFFSET query** This class is used for Paging3 to perform Query and RawQuery in Room to return a PagingSource* for Pager's consumption. Registers observers on tables lazily and automatically invalidates* itself when data changes.*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
abstract class LimitOffsetPagingSource<Value : Any>(private val sourceQuery: RoomSQLiteQuery,private val db: RoomDatabase,vararg tables: String,
) : PagingSource<Int, Value>()

在构造时,基于 SQL 语句创建 RoomSQLiteQuery 并连同 db 实例一起传入即可。

更多参考:https://proandroiddev.com/paging-3-easier-way-to-pagination-part-1-584cad1f4f61

1.3 Navigation 2.4

Multiple back stacks 多返回栈

Navigation 2.4.0 增加了对多返回栈的支持。当下大部分移动应用都带有多 Tab 页的设计。由于所有 Tab 页共享同一个 NavHostFramgent 返回栈,因此 Tab 页内的页面跳转状态会因 Tab 页的切换而丢失,想要避免此问题必须创建多个 NavHostFragment。

implementation "androidx.navigation:navigation-ui:$nav_version"

在 2.4 中通过 navigation-ui 提供的 Tab 页相关组件,可以实现单一 NavHostFragment 的多返回栈

class MainActivity : AppCompatActivity() {private lateinit var navController: NavControllerprivate lateinit var appBarConfiguration: AppBarConfigurationoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_container) as NavHostFragment//获取 navControllernavController = navHostFragment.navController// 底部导航栏设置 navControllerval bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)bottomNavigationView.setupWithNavController(navController)// AppBar 设置 navControllerappBarConfiguration = AppBarConfiguration(setOf(R.id.titleScreen, R.id.leaderboard,  R.id.register))val toolbar = findViewById<Toolbar>(R.id.toolbar)setSupportActionBar(toolbar)toolbar.setupWithNavController(navController, appBarConfiguration)}override fun onSupportNavigateUp(): Boolean {return navController.navigateUp(appBarConfiguration)}
}

如上,通过 navigation-ui 的 setupWithNavController 为 BottomNavigationView 或者 AppBar 设置 NavController,当 Tab 页来回切换时依然可以保持 Tab 内部的返回栈状态。升级到 2.4.0 即可,无需其他代码上的修改。

更多参考:https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f

Two pane layout 双窗格布局

在平板等大屏设备下,为应用采用双窗格布局将极大提升用户的使用体验,比较典型的场景就是左屏列展示表页,右屏展示点击后的详情页。SlidingPaneLayout 可以为开发者提供这种水平的双窗格布局

Navigation 2.4.0 提供了AbstractListDetailFragment,内部通过继承 SlidingPaneLayout ,实现两侧 Fragment 单独显示,而详情页部分更是可以实现独立的页面跳转:

class TwoPaneFragment : AbstractListDetailFragment() {override fun onCreateListPaneView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View {return inflater.inflate(R.layout.list_pane, container, false)}//创建详情页区域的 NavHostoverride fun onCreateDetailPaneNavHostFragment(): NavHostFragment {return NavHostFragment.create(R.navigation.two_pane_navigation)}override fun onListPaneViewCreated(view: View, savedInstanceState: Bundle?) {super.onListPaneViewCreated(view, savedInstanceState)val recyclerView = view as RecyclerViewrecyclerView.adapter = TwoPaneAdapter(map.keys.toTypedArray()) {map[it]?.let { destId -> openDetails(destId) }}}private fun openDetails(destinationId: Int) {//获取详情页区域的 NavController 实现详情页的内容切换val detailNavController = detailPaneNavHostFragment.navControllerdetailNavController.navigate(destinationId,null,NavOptions.Builder().setPopUpTo(detailNavController.graph.startDestinationId, true).apply {if (slidingPaneLayout.isOpen) {setEnterAnim(R.anim.nav_default_enter_anim)setExitAnim(R.anim.nav_default_exit_anim)}}.build())slidingPaneLayout.open()}companion object {val map = mapOf("first" to R.id.first_fragment,"second" to R.id.second_fragment,"third" to R.id.third_fragment,"fourth" to R.id.fourth_fragment,"fifth" to R.id.fifth_fragment)}
}

支持 Compose

Navigation 通过 navigation-compose 支持了 Compose 的页面导航,这对于一个 Compose first 的项目非常重要。

implementation "androidx.navigation:navigation-compose:$nav_version"

navigation-compose 中,Composable 函数替代 Fragment 成为页面导航的 Destination,我们使用 DSL 定义基于 Composable 的 NavGraph:

val navController = rememberNavController()
Scaffold { innerPadding ->NavHost(navController, "home", Modifier.padding(innerPadding)) {composable("home") {// This content fills the area provided to the NavHostHomeScreen()}dialog("detail_dialog") {// This content will be automatically added to a Dialog() composable// and appear above the HomeScreen or other composable destinationsDetailDialogContent()}}
}

如上, composable 方法配置导航中的 Composable 页面,dialog 配置对话框,而 navigation-fragment 中各种常见功能,比如 Deeplinks,NavArgs,甚至对 ViewModel 的支持在 Compose 项目中同样可以使用。

1.4 Fragment

每次 I/O 大会几乎都有关于 Fragment 的分享,因为它是我们日常开发中重度使用的工具。本次大会没有带来 Fragment 的新功能,相反对 Framgent 的功能进行了大幅“削减”。不必惊慌,这并非是从代码上删减了功能,而是对 Fragment 使用方式的重定义。随着 Jetpack 组件库的丰富,Fragment 的很多职责已经被其他组件所分担,所以谷歌希望开发者能够重新认识这个老朋友,对使用场景的必要性进行更合理评估。

Fragmen 在最早的设计中作为 Activity 的代理者出现,因此它承担了很多来自 Activity 回调,例如 Lifecycle,SaveInstanceState,onActivityResult 等等

以前:各种职责 现在:职责外移

而如今这些功能已经有了更好的替代方案,生命周期可以提供 Lifecycle 组件感知,数据的保存恢复也可以通过 ViewModel 实现,因此 Fragment 只需要作为页面侧承载着持有 View 即可,而随着 Navigation 对 Compose 的支持,Fragment 作为页面载体的职责也变得不在必要。

尽管如此,我们也并不能彻底抛弃 Fragment,在很多场景中 Fragment 仍然是最佳选择,比如我们可以借助它的 ResultAPI 实现更简单的跨页面通信:

当我们需要通知一些一次性结果时,ResulAPI 比共享 ViewModel 的通信方式将更加简单安全,它像普通回调一般的使用方式极其简单:

// 在 FramgentA 中监听结果
setFragmentResultListener("requestKey") { requestKey, bundle ->// 通过约定的 key 获取结果val result = bundle.getString("bundleKey")// ...
}// FagmentB 中返回结果
button.setOnClickListener {val result = "result"// 使用约定的 key 发送结果setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

总结起来,Fragment 仍然是我们日常开发中的重要手段,但是它的角色正在发生变化。

2. Performance

2.1 JankStats 卡顿检测

JankStats 用来追踪和分析应用性能,发现 Jank 卡顿问题,它最低向下兼容到 API 16,可以在绝大多数机器设备上使用,有了它我们不必再求助 BlockCanery 等三方工具了。

implementation "androidx.metrics:metrics-performance:1.0.0-alpha01"

我们需要为每个 Window 创建一个 JankStats 实例,并通过 OnFrameListener 回调获取包含是否卡顿在内的帧信息,示例如下:

class JankLoggingActivity : AppCompatActivity() {private lateinit var jankStats: JankStatsoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// ...// metricsStateHolder可以收集环境信息,跟随帧信息返回val metricsStateHolder = PerformanceMetricsState.getForHierarchy(binding.root)// 基于当前 Window 创建 JankStats 实例jankStats = JankStats.createAndTrack(window,Dispatchers.Default.asExecutor(),jankFrameListener,)// 设置 Activity 名字到环境信息metricsStateHolder.state?.addState("Activity", javaClass.simpleName)// ...}private val jankFrameListener = JankStats.OnFrameListener { frameData ->// 监听到的帧信息Log.v("JankStatsSample", frameData.toString())}
}

PerformanceMetricsState 用来收集你希望跟随 frameData 一起返回的状态信息,比如上面例子中设置了当前 Activity 名称,下面是 frameData 的打印日志:

JankStats.OnFrameListener: FrameData(frameStartNanos=827233150542009, frameDurationUiNanos=27779985, frameDurationCpuNanos=31296985, isJank=false, states=[Activity: JankLoggingActivity])

更多参考:https://medium.com/androiddevelopers/jankstats-goes-alpha-8aff942255d5

2.2 Baseline Profiles 基准配置

Android 8.0 之后默认开启 ART 虚拟机。ART 最初版本在安装应用时会对全部代码进行 AOT 预编译,将字节码转换为机器码存在本地,这提升了运行时的速度,但是会导致安装过程变慢。因此后来 ART 改进为 JIT 和 AOT 相结合的方式,在应用安装时只将热点代码编译成机器码,缩短安装时间。

Baselin Profiles 基准配置文件允许我们配置哪些代码成为热点代码。基准配置文件将在 APK 的 assets/dexopt/baseline.prof 中编译为二进制形式,例如如果我们想提升首帧的性能,可以将应用启动或帧渲染期间使用的方法配置到 prof 文件中。

prof 文件可以通过自动或手动方式生成,我们可以编写 JUnit4 测试用例,通过执行 BaselineProfileRule 在测试中发现待优化的瓶颈代码,并生成对应的 prof 文件

@ExperimentalBaselineProfilesApi
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {@get:Rule val baselineProfileRule = BaselineProfileRule()@Testfun startup() =baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") {pressHome()startActivityAndWait()}
}

我们也可以手动创建 prof 文件,只需遵循一些简单的语法规则。例如下面展示了 Jetpack Compose 库中包含的一些 Prof 规则,

HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
Landroidx/compose/runtime/ComposerImpl;

上述配置遵循 [FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE] 格式,其中 FLAGS 中的 H/S/P 代表方法的调用实际,比如是否是启动时调用等。

更多参考:https://android-developers.googleblog.com/2022/01/improving-app-performance-with-baseline.html

2.3 Benchmark 基准测试

Jetpack 当前提供了两套 Benchmark 库,Microbenchmark 和 Macrobenchmark (微基准和宏基准),分别用于不同场景下的基准测试。

Mircobenchmark 的测试对象是代码块,它的依赖如下:

androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.1.0-beta03'

我们可以在 JUnit4 中应用 BenchmarkRule,示例如下:

@RunWith(AndroidJUnit4::class)
class SampleBenchmark {@get:Ruleval benchmarkRule = BenchmarkRule()@Testfun benchmarkSomeWork() {benchmarkRule.measureRepeated {doSomeWork() //执行待测试代码}}
}

Macrobenchmark 通常面向更大粒度的场景测试,例如一个 Activity 启动或者一个用户操作等。由于 Macrobenchmark 不进行代码级别测试,我们可以创建独立于业务代码的单独模块进行测试:

下面展示了使用 MacrobenchmarkRule 测试一个 Activity 的启动:

    @get:Ruleval benchmarkRule = MacrobenchmarkRule()@Testfun startup() = benchmarkRule.measureRepeated(packageName = "mypackage.myapp",metrics = listOf(StartupTimingMetric()),iterations = 5,startupMode = StartupMode.COLD) { // this = MacrobenchmarkScopepressHome()val intent = Intent()intent.setPackage("mypackage.myapp")intent.setAction("mypackage.myapp.myaction")startActivityAndWait(intent)}

配合 2021.1.1 或更高版本的 Android Studio ,Benchmark 的测试结果会直接显示在 IDE 窗口中。

当然,测试结果也可以导出为 JSON 格式

更多参考:https://medium.com/androiddevelopers/measure-and-improve-performance-with-macrobenchmark-560abd0aa5bb

2.4 Tracing 事件追踪

Tracing 用来在代码添加 trace 信息,trace 信息可以显示在 Systrace 和 Perfetto 等工具中。

implementation "androidx.tracing:tracing:1.1.0-beta01"

下面的例子汇总,我们通过 Trace 类的 benginSection/endSection 方法追踪 onCreateViewHolder 和 onBindViewHolder 方法执行的起始点

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {override fun onCreateViewHolder(parent: ViewGroup,viewType: Int): MyViewHolder {return try {Trace.beginSection("MyAdapter.onCreateViewHolder")MyViewHolder.newInstance(parent)} finally {//endSection 放到 finally 里,当出现异常时也会调用Trace.endSection()}}override fun onBindViewHolder(holder: MyViewHolder, position: Int) {Trace.beginSection("MyAdapter.onBindViewHolder")try {try {Trace.beginSection("MyAdapter.queryDatabase")val rowItem = queryDatabase(position)dataset.add(rowItem)} finally {Trace.endSection()}holder.bind(dataset[position])} finally {Trace.endSection()}}
}

需要注意 benginSection/endSection 必须成对出现,且必须在同一线程中。我们 Trace 的 section 会作为新增的自定义事件出现在 Perfetto 等工具视图中:

3. UI

3.1 WindowManager

这并非系统 WMS 获取的那个 WindowManager,它是 Jetpack 的新成员,当前刚刚迈入 1.1.0。

implementation "androidx.window:window:1.1.0-alpha02"

它可以帮助我们适配日益增多的可折叠设备,满足多窗口环境下的开发需求。
可折叠设备通常分为两类:单屏可折叠设备(一个整体的柔性屏幕)和双屏可折叠设备(两个屏幕由合页相连)。

目前单屏可折叠设备正逐渐成为主流,但无论哪种设备都可以通过 WindowManager 感知当前的屏幕显示特性,例如当前折叠的状态和姿势等。

获取折叠状态

多屏设备下,一个窗口可能会跨越物理屏幕显示,这样窗口中会出现铰链等不连续部分,FoldingFeature (DisplayFeature 的子类)对铰链这类的物理部件进行抽象,从中可以获取铰链在窗口中的准确位置,帮助我们避免将关键交互按钮布局在其中。另外 FoldingFeature 还提供了可以感知感知当前折叠状态的 API,我们可以根据这些状态改变应用的布局:

//铰链处于半开状态且位置水平,适合切换到平板模式
fun isTableTopMode(foldFeature: FoldingFeature) =foldFeature.isSeparating &&foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
//铰链处于半开状态且位置垂直,适合切换到阅读模式
fun isBookMode(foldFeature: FoldingFeature) =foldFeature.isSeparating &&foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
isTableTopMode isBookMode

WindowManager 允许我们通过 Flow 持续观察显示特性的变化。

lifecycleScope.launch(Dispatchers.Main) {lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {WindowInfoTracker.getOrCreate(this@SampleActivity).windowLayoutInfo(this@SampleActivity).collect { newLayoutInfo ->// Use newLayoutInfo to update the layout.}}
}

如上,当显示特性变化时,我们能获取 newLayoutInfo ,它是一个 WindowLayoutInfo 类型,内部持有了 FoldingFeature 信息。

感知窗口大小变化

应用窗口可能跟随设备配置变化时(例如折叠屏的展开、旋转,或窗口在多窗口模式下调整大小)发生变化,我们可以通过 WIndowManger 的 WindowMetrics 获取窗口大小,我们有两种获取当前 WindowMetrics 的方式,同步获取和异步监听:

//异步监听
lifecycleScope.launch(Dispatchers.Main) {windowInfoRepository().currentWindowMetrics.flowWithLifecycle(lifecycle).collect { windowMetrics: WindowMetrics ->val currentBounds = windowMetrics.bounds val width = currentBounds.width()val height = currentBounds.height()}
}//同步获取
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
val currentBounds = windowMetrics.bounds
val width = currentBounds.width()
val height = currentBounds.height()

更多参考:https://medium.com/androiddevelopers/unbundling-the-windowmanager-fa060adb3ce9

3.2 DragAndDrop

Jetpack DragAndDrop 是专门处理拖放手势的库,它除了服务于普通手机设备上的开发,更重要的意义是可以实现折叠设备跨屏幕的拖放

implementation 'androidx.draganddrop:draganddrop:1.0.0-alpha02'

DragStartHelper 和 DropHelper 是其最核心的 API,可以配置拖防过程中的数据传递、显示效果等,还可以监听手势回调。

拖动 DragStartHelper

DragStartHelper 负责监测拖动手势的开始时机,包括长按拖动、单击并用鼠标拖动等。我们可以将需要拖动的视图对象包装进来并开启监听,当监听到拖动手势触发时,完成一些简单配置即可。

// 使用 DragStartHelper 包装 draggableView 对象
DragStartHelper(draggableView) { view, _ ->// 将需要传递的数据封装到 ClipData 中val dragClipData = ClipData.newUri(contentResolver, "File", fileUri)// 创建目标拖动时的展示图片,可自定义也可以根据 draggableView 创建默认样式val dragShadow = View.DragShadowBuilder(view)// 基于数据、拖动效果启动拖动view.startDragAndDrop(dragClipData,dragShadow,null, // Optional extra local state information// 添加 flag 启动全局拖动DRAG_FLAG_GLOBAL or DRAG_FLAG_GLOBAL_URI_READ))
}.attach()

如上,准备好需要拖动数据和样式等,调用 View#startDragAndDrop 启动拖动。例子中拖动的目标是 content: 这类 URI,因此我们可以通过设置 DRAG_FLAG_GLOBAL 实现跨进程的拖动。

放置 DropHelper

DropHelper 是另一个核心 API,关心拖动数据放下的时机和目标视图。

//针对可拖放视图调用 configureView
DropHelper.configureView(this,// 当前ActivityouterDropTarget, //接收拖放的对象,会根据情况高亮显示arrayOf(MIMETYPE_TEXT_PLAIN, "image/*"), // 支持的 MIME 类型DropHelper.Options.Builder() //一些参数配置,例如放下时高亮的颜色,视图范围等.addInnerEditTexts(innerEditText).build()
) { _, payload ->// 监听到目标的放下,可以从 ClipData 中取得数据,// 执行上传、显示等处理,当然还可以处理非法拖放时的警告或视图提醒等...
}

构建 DropHelper.Options 实例的时候,需要调用 addInnerEditTexts(),这样可以确保嵌套的 EditText 控件不会抢夺视图焦点。

更多参考:https://medium.com/androiddevelopers/simplifying-drag-and-drop-3713d6ef526e

4. Compose

今年 I/O 大会上关于 Compose 的主题分享明显增多了,这也表明了谷歌对于 Compose 推广之重视。目前 GooglePlay Top1000 的应用中使用 Compose 的已经超过了 100 个,其中不乏一些领域头部应用,Compose 的稳定性和成熟度也借机得到了验证。

让我们看看 Compose 最新的 1.2 Beta 版本带来哪些新内容。

4.1 Material 3

新增的 Compose.M3 库,可以帮助我们开发符合 Material You 设计规范的的 UI 界面。

implementation "androidx.compose.material3:material3:1.0.0-alpha10"
implementation "androidx.compose.material3:material3-window-size-class:1.0.0-alpha10"

Material3 强调颜色的个性化和动态切换,Compose.M3 引入 ColorScheme 类自定义配色方案:

val AppLightColorScheme = lightColorScheme (primary = Color(...),// secondary、tertiary 等等// 具有浅色基准值的 ColorScheme 实例
)val AppDarkColorScheme = darkColorScheme(// primary、secondary、tertiary 等等// 具有深色基准值的 ColorScheme 实例val dark = isSystemInDarkTheme()
val colorScheme = if (dark) AppDarkColorScheme else AppLightColorScheme// 将 colorScheme 作为参数传递给 MaterialTheme。
MaterialTheme (colorScheme = colorScheme,// 字型
) {// 应用内容
}

上面是 MaterialTheme 通过 ColorScheme 配置不同主题颜色的例子,可以看到这与 Compose.M2 中 Colors 用法区别不大, 但是 ColorScheme 可定义的颜色槽(Primary,Secondary,Error 等MD颜色常量)种类更多,而且还可以支持 DynamicColor 动态配色。

DynamicColor 是 Material3 的重要特色,在 Android12 及以上设备中,可以实现应用的颜色跟随壁纸变化。如今 Compose 中也可以实现这个效果

// Dynamic color is available on Android 12+
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)    darkTheme -> DarkColorScheme    else -> LightColorScheme
}

如上,Compose 通过 dynamicXXXColorScheme 设置的颜色,无论是亮色还是暗色主题,都可以跟随用户设置的壁纸而变化:

更多参考:https://juejin.cn/post/7064410835422019615

4.2 Nested Scrolling Interop

Compose 支持与传统视图控件进行互操作,便于我们阶段性的引入 Compose 到项目中。但是在涉及到带有 Nested Scrolling 事件分发的场景中(例如 CoordinatorLayout ),会发生事件无法正常传递的兼容性问题,在 1.2 中对于此类问题进行了修复,无论是 CoordinatorLayout 内嵌 Composable , 或者在 Composable 中使用 Scrolling View 控件,事件传递都会更加平顺:

https://android-review.googlesource.com/c/platform/frameworks/support/+/2004590
https://android-review.googlesource.com/c/platform/frameworks/support/+/2038823

4.3 Downloadable Fonts

Android 8.0(API level 26)起支持了对可下载的谷歌字体的使用,允许通过代码动态请求一个非内置字体文件。在 Compose 1.2 对此功能也进行了支持,注意这个功能需要基于 GMS 服务。

implementation "androidx.compose.ui:ui-text-google-fonts:1.1.1"

使用时,首先使用 FontProvider 定义字体请求信息

@OptIn(ExperimentalTextApi::class)
val provider = GoogleFont.Provider(providerAuthority = "com.google.android.gms.fonts",providerPackage = "com.google.android.gms",certificates = R.array.com_google_android_gms_fonts_certs
)
然后,使用此 Provider 定义 FontFamily,接着在 Composable 应用即可,
val fontName = GoogleFont(“Lobster Two”)val fontFamily = FontFamily(Font(googleFont = GoogleFont(name), fontProvider = provider)
)Text(fontFamily = fontFamily,text = "Hello World!"
)

4.4 Lazy Grid

Compose 1.2 中进一步优化了 LazyRow 和 LazyColumn 的性能,并在此基础上新增了 LazyGrid 用来实现需求中常见的网格布局效果。Lazy Grid 在 1.0.2 就已经引入,如今 1.2 中对 API 进行调整并使之达到稳定。

以 LazyVerticalGrid 为例,我们可以通过 GridCells.Fixed 设置每行单元格的数量:

val data = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")LazyVerticalGrid(columns = GridCells.Fixed(3),contentPadding = PaddingValues(8.dp)
) {//this: LazyGridScopeitems(data.size) { index ->Card(modifier = Modifier.padding(4.dp),backgroundColor = Color.LightGray) {Text(text = data[index],textAlign = TextAlign.Center)}}
}

此外,也可以通过 GridCells.Adaptive() 通过制定单元格大小决定每行的数量。此时,所有单元格都会以 Adaptive 中的值设置统一的 width。

LazyGridScope 像 LazyListScope 一样也提供了 item, items, itemsIndexed 等方法布局子项。另外 LazyGridState 中的方法也基本上对齐了 LazyListState。

4.5 Tools

在工具方面,Android Studio 为 Compose 的开发调试提供了更多实用功能。

@Preview & Live Edit

1.2.0 中的 @Preview 可以作为元注解使用,修饰其他自定义注解

@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
annotation class MyDevices()@MyDevices
@Composable
fun Greeting() {...
}

如上,我们可以通过自定义注解可以复用 @Preview 中的各种配置,减少为了预览而写的模板代码。

说到预览,Android Studio 一直致力于提升预览效率,Android Studio Arctic Fox 曾引入 Live literals 功能,对于代码中 Int,String,Color,Dp,Boolean 等常见类型的字面值的修改,无需编译即可在预览画面中实时更新。本次大会上带来了升级版的 Live Edit,它需要使用最新的 Android Studio Electric Eel 中开启。不仅仅是字面值,它可以让任意代码的修改(函数签名变动之类的修改不行),在预览窗口或者你的设备上立即生效,几乎实现了前端一般的开发体验,是本次大会令我惊喜的功能,它将大幅提高 Compose 的开发和调试效率。

Layout Inspector & Recomposition Counts

我们在传统视图开发中经常使用 Layout Inspector 观察视图结构, Compose 虽然基于 Composable 函数构建 UI ,但同样也得到了 layout Inspector 的支持,它可以帮助我们查看 Composition 视图树的布局。

此外,本次 I/O 还介绍了 Layout Inspector 的一个新功能 Recomposition Counts,我们知道不必要的重组会拖慢 Compose UI 的刷新性能,借助这个新工具,我们可以在 IDE 中调试和观察 Composable 重组次数,帮助我们及时发现和优化不符合预期的多余重组。

Animation Preview

Android Studio 增加了对 Compose 动画效果实时预览。在动画预览窗口中,每个动画的状态值会以多轨道的形式呈现,我们可以查看特定时间点下的每个动画值的确切值,并且可以暂停、循环播放动画、快进或放慢动画,以便在动画过渡过程中调试动画。

Compose 的动画 API 数量众多,目前并非所有的 API 都支持预览,IDE 会自动检查代码中可以进行预览的动画,并添加 Start Animation Inspection 图标,便于开发者发现和使用

4.6 适应多种屏幕尺寸

Compose 正逐渐成为 Android 的首选 UI 开发方案,所以为了覆盖尽可能多的使用场景,Compose 第一时间对各种屏幕尺寸下(手机,平板,电脑,折叠屏)的 UI 开发进行了支持。
在具体开发中,我们需要先定义 WindowSizeClass 对各种屏幕类型分类,推荐分为三类:

当屏幕尺寸因为设备折叠等发生变化时,Compose 会自动响应 onConfigurationChanged 触发重组,重组中我们根据当前屏幕尺寸转换为对应的 WindowSizeClass

@Composable
fun Activity.rememberWindowSizeClass(): Pair<WindowWidthSizeClass, WindowHeightSizeClass> {val configuration = LocalConfiguration.currentval windowMetrics = remember(configuration) {WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)}val windowDpSize = with(LocalDensity.current) {windowMetrics.bounds.toComposeRect().size.toDpSize()}val widthWindowSizeClass = when {windowDpSize.width < 600.dp -> WindowWidthSizeClass.CompactwindowDpSize.width < 840.dp -> WindowWidthSizeClass.Mediumelse -> WindowWidthSizeClass.Expanded}val heightWindowSizeClass = when {windowDpSize.height < 480.dp -> WindowHeightSizeClass.CompactwindowDpSize.height < 900.dp -> WindowHeightSizeClass.Mediumelse -> WindowHeightSizeClass.Expanded}return widthWindowSizeClass to heightWindowSizeClass
}

接下来,我们就可以面向 WindowSizeClass 进行 Composable 布局了,这样做的好处是,无需关心具体的 width/height 数值,更不需要关心当前设备类型是平板还是手机,因为未来,硬件种类的界限将越来越模糊,所以最合理的分类方式是 WindowSizeClass。

@Composable
fun MyApp(widthSizeClass: WindowWidthSizeClass) {// 非 Compact 类型屏幕时,不显示 AppBarval showTopAppBar = widthSizeClass != WindowWidthSizeClass.Compact// MyScreen 不依赖 WindowSizeClass,只需要知道是否显示 showTopAppBar,关注点分离MyScreen(showTopAppBar = showTopAppBar,/* ... */)
}

当然我们可以使用 Android Studio 便利的预览功能,同时查看多种屏幕尺寸下的显示效果

最佳实践: Now In Android

最后推荐一个谷歌刚刚开源的新项目 Now In Android。
Now in Android 是 Android 官方的技术博客,分享技术文章和视频,如今这个博客有了自己的客户端,并在 Github 进行了开源,https://github.com/android/nowinandroid。

开发者通过 App 可以更好地追踪 Android 最新的技术动向,更重要的是它本身就是一个 Android Jetpack 的最佳实践,在技术上它具有以下特点:

  • 基于 Jetpack Compose 实现 UI
  • 基于 Material3 的视觉样式和主题
  • 对不同尺寸的屏幕进行了支持,能够自适应布局
  • 整体架构遵循官方文档 UDF 范式
  • 基于 Kotlin Flow 实现响应式编程模型
  • 遵循 Offline first 设计原则,基于 Room 以及 Proto DataSotre 实现本地数据源,
  • 基于 WorkManager 实现远程/本地数据源之间的同步

另外,GIthub 上还贴心了附上了架构设计文档,方便你了解它的开发思路,Now in Android 已经预定上架 GooglePlay, 相对于 Jetpack 的其他 Demo,它是更加真实和完善,非常值得大家研究和学习。

参考

  1. What’s new in Android: https://www.youtube.com/watch?v=JhFRpxmWzEE
  2. What’s new in Jetpack: https://www.youtube.com/watch?v=jTd82lcuHTU

谷歌 I/O 深度解析:Android Jetpack 最新变化相关推荐

  1. [NLP自然语言处理]谷歌BERT模型深度解析

    BERT模型代码已经发布,可以在我的github: NLP-BERT--Python3.6-pytorch 中下载,请记得start哦 目录 一.前言 二.如何理解BERT模型 三.BERT模型解析 ...

  2. 自然语言处理——谷歌BERT模型深度解析

    BERT模型代码已经发布,可以在我的github: NLP-BERT--Python3.6-pytorch 中下载,请记得start哦 目录 一.前言 二.如何理解BERT模型 三.BERT模型解析 ...

  3. 深度解析Android中字体设置

    1.在Android XML文件中设置字体 可以采用Android:typeface,例如android:typeface="monospace".在这里例子中我们在Activit ...

  4. 深度解析Android Q cutout刘海屏

    1.在 Settings/System/Developer options/DRAWING 里面看到有 "Display cutout" 菜单 2.根据这个字符串找到 packag ...

  5. Android Jetpack架构组件(一)带你了解Android Jetpack

    本文首发于微信公众号「后厂村码农」 前言 Android已经发展了11年,可以说是比较成熟的技术了,一开始时框架很少,也没有什么规范,所有的代码都是要自己写,比如网络请求,数据库请求,数据解析等等.后 ...

  6. Jetpack架构组件 (一)-- Android Jetpack 简介

    前言 Android 已经发展十多年了,可以说是比较成熟的技术了,一开始时框架很少,也没有什么规范,所有的代码都是要自己写,比如网络请求,数据库操作,数据解析等等.后来出现了一些框架来帮助开发者快速进 ...

  7. 预训练模型:BERT深度解析《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》

    目录 1. 背景 2. 什么是 Bert 及原理? 3. 论文内容<BERT: Pre-training of Deep Bidirectional Transformers for Langu ...

  8. 面试字节跳动Android工程师该怎么准备?深度解析,值得收藏

    前言 Android高级架构师需要学习哪些知识呢? 下面总结一下我认为作为一个资深开发者需要掌握的技能点. 1.Android开发的几个阶段 我的10年开发生涯中,有9年都是做Android相关开发, ...

  9. android nfc标签类型,Android NFC标签 开发深度解析 触碰的艺术

    原标题:Android NFC标签 开发深度解析 触碰的艺术 本文来自于CSDN博客,作者:郭朝,已获授权,版权归原作者所有,未经作者同意,请勿转载. 欢迎同有博客好文章的作者加微信(ID:tm_fo ...

最新文章

  1. 几个经典的TCP通信函数
  2. iOS开发UI篇--UIScrollView思维导图[不断更新]
  3. |9 其他(linux特定的), 用来存放内核例行程序的文档.,Linux下的帮助命令
  4. Linux-Rsync命令参数详解
  5. unity 下一帧执行_理解Unity中的优化(三):协程(Coroutines)
  6. K8s报错#!/bin/bash yum install -y yum-utils device-mapper-persistent-data lvm2 if [ $? = 0 ];then echo
  7. php转译html,使用php转义输出HTML到JavaScript
  8. Wordcounter,使用Lambdas和Fork / Join计算Java中的单词数
  9. Java笔记-使用Kaptcha验证码框架
  10. java并发之线程安全---java并发编程实践
  11. parrot linux 输入法,nur kirguzguq
  12. iFIX组态软件在某电厂辅控系统的应用
  13. PS4在Jetson nano下的配对使用,并用ROS接口来控制
  14. mos管h桥电机驱动电路与设计原理图-KIA
  15. 秀米排版 × ModStart,提升富文本排版效率
  16. sd和sem啥区别_生物统计学-标准差(SD)和标准误(SEM)有何区别.pdf
  17. C语言课设手机电话簿管理系统(大作业)
  18. maven阿里云仓库
  19. 90 后的他带出7 个“师弟”,爱说话的程序员脱单更快?
  20. 2022年武汉市创新型中小企业认定条件和评价指标

热门文章

  1. 计算机专业毕设评阅人评语,毕业论文评阅人评语模板
  2. 浮沙筑塔——protues仿真C51程序之LED灯闪烁
  3. 802.11标准及无线网运行模式
  4. 赛元科技EasyCodeCube的使用——Keil环境的配置
  5. 电脑窗口全半屏切换快捷键
  6. unity3D-learning:UI背包系统
  7. 家庭网络理解(家庭版光猫、路由器、交换机)
  8. 张同须全面介绍中国移动5G+AI新基建布局(附PPT)
  9. MATEBOOK E 2019 安装linux
  10. PPT文件不能编辑,什么情况?