Google I/O: The latest changes in Android Jetpack (1) Architecture

In Mountain View in May, the annual Google I/O developer conference came as scheduled. Due to the relaxation of local epidemic control, this year's conference will be held offline again. I sincerely hope that the domestic epidemic will end as soon as possible.

foreword

This year's I/O conference is not only Google's various new product launches, but also a technical exchange meeting for Google developers. Many developers hope to know the latest developments about Jetpack through this I/O. Jetpack has become an indispensable tool in our daily development. According to the data released at this conference, the proportion of GooglePlay Top 1000 applications using at least 2 or more Jetpack libraries has increased from 79% to 90%

Next, I will introduce you to the latest content of Jetpack on this I/O from the four directions of Architecture, UI, Performance and Compose in four articles.

This article is the first one: Architecture.

1. Room 2.4/2.5

The latest version of Room is 2.5. 2.5 There is no introduction of new functions. The biggest change is to use Kotlin to rewrite. With the help of Kotlin null safety and other features, the code will be more stable and reliable. More Jetpack libraries will be gradually migrated to Kotlin in the future.

In terms of functionality, Room has introduced many new features since 2.4:

KSP: New Annotation Processor

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 实例一起传入即可。

更多参考:proandroiddev.com/paging-3-ea…

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 即可,无需其他代码上的修改。

更多参考:medium.com/androiddeve…

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 仍然是我们日常开发中的重要手段,但是它的角色正在发生变化。

Guess you like

Origin juejin.im/post/7098142116664049678