5 月的山景城,一年一度的谷歌 I/O 开发者大会如期而至,由于当地疫情管制的放开,今年大会重回线下举行,真心希望国内的疫情也尽早结束。
今年的 I/O 大会既是谷歌各种新产品发布会,同时也是谷歌开发者们的技术交流会。不少 Android 开发者希望通过本次 I/O 了解到有关 Jetpack 的最新动态。本文对这些内容进行了收集整理,一并分享给大家。
Android Jetpack 为我们日常开发提供了便利的工具集以及最佳实践,根据本次大会上发布的数据,目前 GooglePlay Top1000 的应用中,使用至少 2 个以上 Jetpack 库的占比从 79% 提升到 90%
接下来从 Architecture,UI,Performance 和 Compose 等四个方向为大家介绍和点评 Jetpack 的最新变化。
1. Architecture 1.1 Room 2.4/2.5Room 最新版本进入到 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 kapt
id 'kotlin-kapt'
//enable ksp
id("com.google.devtools.ksp")
}
dependencies {
//...
// use kapt
kapt "androidx.room:room-compiler:$room_version"
// use ksp
ksp "androidx.room:room-compiler:$room_version"
//...
}
Multi-map Relations:返回一对多数据
以前,Room 想要返回一对多的实体关系,需要额外增加类型定义,并通过 @Relatioin 进行关联,现在可以直接使用 Multi-map 返回,代码更加精简:
//before
data class ArtistAndSongs(
` @Embedded
val artist: Artist,
@Relation(...)
val songs: List
)
@Query("SELECt * FROM Artist")
fun getArtistAndSongs(): List
//now
@Query("SELECt * FROM Artist JOIN Song ON Artist.artistName = Song.songArtistName")
fun getAllArtistAndTheirSongsList(): Map>
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() { override suspend fun load(params: LoadParams ): LoadResult { try { val currentLoadingPageKey = params.key ?: 1 // 从 Repository 拉去数据 val response = repo.getListData(currentLoadingPageKey) val prevKey = if (currentLoadingPageKey == 1) null else currentLoadingPageKey - 1 // 返回分页结果,并填入前一页的 key 和后一页的 key return 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新增的事件监听Pager .observable: Observable > 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
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) abstract class LimitOffsetPagingSource( private val sourceQuery: RoomSQLiteQuery, private val db: RoomDatabase, vararg tables: String, ) : PagingSource ()
在构造时,基于 SQL 语句创建 RoomSQLiteQuery 并连同 db 实例一起传入即可。
1.3 Navigation 2.4 Multiple back stacks 多返回栈更多参考:https://proandroiddev.com/paging-3-easier-way-to-pagination-part-1-584cad1f4f61
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: NavController
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager.findFragmentById(
R.id.nav_host_container
) as NavHostFragment
//获取 navController
navController = navHostFragment.navController
// 底部导航栏设置 navController
val bottomNavigationView = findViewById(R.id.bottom_nav)
bottomNavigationView.setupWithNavController(navController)
// AppBar 设置 navController
appBarConfiguration = AppBarConfiguration(
setOf(R.id.titleScreen, R.id.leaderboard, R.id.register)
)
val toolbar = findViewById(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 即可,无需其他代码上的修改。
Two pane layout 双窗格布局更多参考:https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f
在平板等大屏设备下,为应用采用双窗格布局将极大提升用户的使用体验,比较典型的场景就是左屏列展示表页,右屏展示点击后的详情页。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)
}
//创建详情页区域的 NavHost
override 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 RecyclerView
recyclerView.adapter = TwoPaneAdapter(map.keys.toTypedArray()) {
map[it]?.let { destId -> openDetails(destId) }
}
}
private fun openDetails(destinationId: Int) {
//获取详情页区域的 NavController 实现详情页的内容切换
val detailNavController = detailPaneNavHostFragment.navController
detailNavController.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 NavHost
HomeScreen()
}
dialog("detail_dialog") {
// This content will be automatically added to a Dialog() composable
// and appear above the HomeScreen or other composable destinations
DetailDialogContent()
}
}
}
如上, 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: JankStats
override 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])
2.2 Baseline Profiles 基准配置更多参考:https://medium.com/androiddevelopers/jankstats-goes-alpha-8aff942255d5
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()
@Test
fun 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 代表方法的调用实际,比如是否是启动时调用等。
2.3 Benchmark 基准测试更多参考:https://android-developers.googleblog.com/2022/01/improving-app-performance-with-baseline.html
Jetpack 当前提供了两套 Benchmark 库,Microbenchmark 和 Macrobenchmark (微基准和宏基准),分别用于不同场景下的基准测试。
Mircobenchmark 的测试对象是代码块,它的依赖如下:
androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.1.0-beta03'
我们可以在 JUnit4 中应用 BenchmarkRule,示例如下:
@RunWith(AndroidJUnit4::class)
class SampleBenchmark {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Test
fun benchmarkSomeWork() {
benchmarkRule.measureRepeated {
doSomeWork() //执行待测试代码
}
}
}
Macrobenchmark 通常面向更大粒度的场景测试,例如一个 Activity 启动或者一个用户操作等。由于 Macrobenchmark 不进行代码级别测试,我们可以创建独立于业务代码的单独模块进行测试:
下面展示了使用 MacrobenchmarkRule 测试一个 Activity 的启动:
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "mypackage.myapp",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) { // this = MacrobenchmarkScope
pressHome()
val intent = Intent()
intent.setPackage("mypackage.myapp")
intent.setAction("mypackage.myapp.myaction")
startActivityAndWait(intent)
}
配合 2021.1.1 或更高版本的 Android Studio ,Benchmark 的测试结果会直接显示在 IDE 窗口中。
当然,测试结果也可以导出为 JSON 格式
2.4 Tracing 事件追踪更多参考:https://medium.com/androiddevelopers/measure-and-improve-performance-with-macrobenchmark-560abd0aa5bb
Tracing 用来在代码添加 trace 信息,trace 信息可以显示在 Systrace 和 Perfetto 等工具中。
implementation "androidx.tracing:tracing:1.1.0-beta01"
下面的例子汇总,我们通过 Trace 类的 benginSection/endSection 方法追踪 onCreateViewHolder 和 onBindViewHolder 方法执行的起始点
class MyAdapter : RecyclerView.Adapter() { 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()
3.2 DragAndDrop更多参考:https://medium.com/androiddevelopers/unbundling-the-windowmanager-fa060adb3ce9
Jetpack DragAndDrop 是专门处理拖放手势的库,它除了服务于普通手机设备上的开发,更重要的意义是可以实现折叠设备跨屏幕的拖放
implementation 'androidx.draganddrop:draganddrop:1.0.0-alpha02'
DragStartHelper 和 DropHelper 是其最核心的 API,可以配置拖防过程中的数据传递、显示效果等,还可以监听手势回调。
拖动 DragStartHelperDragStartHelper 负责监测拖动手势的开始时机,包括长按拖动、单击并用鼠标拖动等。我们可以将需要拖动的视图对象包装进来并开启监听,当监听到拖动手势触发时,完成一些简单配置即可。
// 使用 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 实现跨进程的拖动。
放置 DropHelperDropHelper 是另一个核心 API,关心拖动数据放下的时机和目标视图。
//针对可拖放视图调用 configureView
DropHelper.configureView(
this,// 当前Activity
outerDropTarget, //接收拖放的对象,会根据情况高亮显示
arrayOf(MIMETYPE_TEXT_PLAIN, "image
)
}
当然我们可以使用 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,它是更加真实和完善,非常值得大家研究和学习。
参考- What’s new in Android: https://www.youtube.com/watch?v=JhFRpxmWzEE
- What’s new in Jetpack: https://www.youtube.com/watch?v=jTd82lcuHTU



