Google I/O :Android Jetpack 最新變化(一) Architecture

語言: CN / TW / HK

theme: devui-blue highlight: atelier-sulphurpool-light


5 月的山景城,一年一度的谷歌 I/O 開發者大會如期而至,由於當地疫情管制的放開,今年大會重回線下舉行,真心希望國內的疫情也儘早結束。

前言

今年的 I/O 大會既是谷歌各種新產品釋出會,同時也是谷歌開發者們的技術交流會。不少開發者希望通過本次 I/O 瞭解有關 Jetpack 的最新動態。Jetpack 已經成為我們日常開發中比不可少的工具,根據本次大會上釋出的資料,目前 GooglePlay Top1000 的應用中,使用至少 2 個以上 Jetpack 庫的佔比從 79% 提升到 90%

接下來,我會分四篇文章分別從 Architecture,UI,Performance 和 Compose 這四個方向帶大家瞭解本次 I/O 上 Jetpack 的最新內容。

本文是第一篇:Architecture 篇。

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 一樣地配置即可: ```groovy 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 返回,程式碼更加精簡: ``kotlin //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 功能可以檢測出兩個表結構的區別,完成資料庫欄位的自動升級。 kotlin @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: ```kotlin 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 返回一個可訂閱結果 kotlin class MainViewModel(private val apiService: APIService) : ViewModel() { val listData = Pager(PagingConfig(pageSize = 6)) { PostDataSource(apiService) }.flow.cachedIn(viewModelScope) } 除了預設整合的 Flow 方式以外,通過擴充套件 Pager 也可返回 RxJava,Guava 等其他可訂閱型別 groovy implementation "androidx.paging:paging-rxjava2:$paging_version" implementation "androidx.paging:paging-guava:$paging_version" 例如,paging-rxjava2 中提供了將 Pager 轉成 Observable 的方法: kotlin 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 實現本地資料來源的分頁載入

groovy implementation "androidx.room:room-paging:2.5.0-alpha01" room-paging 提供了一個開箱即用的資料來源 LimitOffsetPagingSource kotlin /** * 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

3. Navigation 2.4

Multiple back stacks 多返回棧

Navigation 2.4.0 增加了對多返回棧的支援。當下大部分移動應用都帶有多 Tab 頁的設計。由於所有 Tab 頁共享同一個 NavHostFramgent 返回棧,因此 Tab 頁內的頁面跳轉狀態會因 Tab 頁的切換而丟失,想要避免此問題必須建立多個 NavHostFragment。

groovy implementation "androidx.navigation:navigation-ui:$nav_version" 在 2.4 中通過 navigation-ui 提供的 Tab 頁相關元件,可以實現單一 NavHostFragment 的多返回棧

```kotlin 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 即可,無需其他程式碼上的修改。

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

Two pane layout 雙窗格佈局

在平板等大屏裝置下,為應用採用雙窗格佈局將極大提升使用者的使用體驗,比較典型的場景就是左屏列展示表頁,右屏展示點選後的詳情頁。SlidingPaneLayout 可以為開發者提供這種水平的雙窗格佈局

Navigation 2.4.0 提供了AbstractListDetailFragment,內部通過繼承 SlidingPaneLayout ,實現兩側 Fragment 單獨顯示,而詳情頁部分更是可以實現獨立的頁面跳轉:

```kotlin 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 的專案非常重要。 groovy implementation "androidx.navigation:navigation-compose:$nav_version" navigation-compose 中,Composable 函式替代 Fragment 成為頁面導航的 Destination,我們使用 DSL 定義基於 Composable 的 NavGraph: kotlin 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 的通訊方式將更加簡單安全,它像普通回撥一般的使用方式極其簡單: ```kotlin // 在 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 仍然是我們日常開發中的重要手段,但是它的角色正在發生變化。