5 月的山景城,一年一度的谷歌 I/O 开发者大会如期而至,由于当地疫情管制的放开,今年大会重回线下举行,真心希望国内的疫情也尽早结束。
前言
今年的 I/O 大会既是谷歌各种新产品发布会,同时也是谷歌开发者们的技术交流会。不少开发者希望通过本次 I/O 了解有关 Jetpack 的最新动态。Jetpack 已经成为我们日常开发中比不可少的工具,根据本次大会上发布的数据,目前 GooglePlay Top1000 的应用中,使用至少 2 个以上 Jetpack 库的占比从 79% 提升到 90%
接下来,我会分四篇文章分别从 Architecture,UI,Performance 和 Compose 这四个方向带大家了解本次 I/O 上 Jetpack 的最新内容。
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 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<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 { ... }
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 和后一页的 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 <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 实例一起传入即可。
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: 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<BottomNavigationView>(R.id.bottom_nav) bottomNavigationView.setupWithNavController(navController) // AppBar 设置 navController appBarConfiguration = 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 即可,无需其他代码上的修改。
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) } //创建详情页区域的 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 项目中同样可以使用。
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 仍然是我们日常开发中的重要手段,但是它的角色正在发生变化。