專案簡介
玩Android demo。用Jetpack MVVM開發架構、單Activity多Fragment專案設計,專案結構清晰,程式碼簡潔優雅,追求最官方的實現方式。用到以下知識點:
LiveData、ViewModel、DataBinding(包括雙向繫結、BindingAdapter的使用)、ViewBinding、coroutines(包含flow、suspend、livedata協程構造器、flow協程構造器的使用)、Hilt、Paging3(包含RemoteMediator、載入狀態)、Room、Navigation(通過ViewModel共享資料)、Banner(kotlin簡單實現)、TabLayout、BottomNavigationView、RecycleView(包含ListAdapter、ConcatAdapter、PagingDataAdapter的使用)、ViewPager2、Glide、Cookie、Retrofit2、啟動頁面、深色主題、沉浸式模式、Kotlin高階函式。
專案截圖(預設主題、深色主題)
專案參考demo
- Sunflower
- architecture-components-samples
- views-widgets-samples
- user-interface-samples
- architecture-samples
- compose-samples: 本demo中暫時不涉及Compose功能
專案知識點
LiveData
- LiveData文件
- LiveDataSample:
- 持有可被觀察的類類似於EventBus或者RxJava。LiveData是一種可感知生命週期的元件
- LiveData與MutableLiveData區別
- LiveData使用
- 理解協程、LiveData 和 Flow
- Google 推薦在 MVVM 架構中使用 Kotlin Flow
- 關於Retrofit和LiveData相關參考demo:GithubBrowserSample[]
ViewModel
- ViewModel文件
- ViewModel 四種整合方式,即:
- ViewModel 中的 Saved State —— 後臺程式重啟時,ViewModel 的資料恢復;
- 在 NavGraph 中使用 ViewModel —— ViewModel 與導航 (Navigation) 元件庫的整合;
- ViewModel 配合資料繫結 (data-binding) —— 通過使用 ViewModel 和 LiveData 簡化資料繫結;
- viewModelScope —— Kotlin 協程與 ViewModel 的整合。
- 在Activity或者Fragment中如何處理ViewModel的三種方式(沒太懂)
ViewBinding
DataBinding
- DataBinding文件
- 取代findviewbyId,類似於Butterknife。
coroutines
- 理解協程、LiveData 和 Flow
- liveData 協程構造方法提供了一個協程程式碼塊,這個塊就是 LiveData 的作用域,當 LiveData 被觀察的時候,裡面的操作就會被執行,當 LiveData 不再被使用時,裡面的操作就會取消。 而且該協程構造方法產生的是一個不可變的LiveData,可以直接暴露給對應的檢視使用。而 emit() 方法則用來更新 LiveData 的資料。
- 一個常見用例,比如當使用者在 UI 中選中一些元素,然後將這些選中的內容顯示出來。一個常見的做法是,把被選中的專案的 ID 儲存在一個 MutableLiveData 裡,然後執行 switchMap。現在在 switchMap 裡,您也可以使用協程構造方法:
private val itemId = MutableLiveData<String>() val result = itemId.switchMap { liveData { emit(fetchItem(it)) } }
- Google 推薦在 MVVM 架構中使用 Kotlin Flow
- 圖解協程原理
Hilt
- hilt 和 Koin
Paging
-
Paging 庫 3.0.0正式版已釋出,普天同慶!Paging 庫可幫助您載入和顯示來自本地儲存或網路中更大的資料集中的資料頁面。此方法可讓您的應用更高效地利用網路頻寬和系統資源。Paging 庫的元件旨在契合推薦的 Android 應用架構,流暢整合其他 Jetpack 元件,並提供一流的 Kotlin 支援。
-
官方demo:
- PagingSample : 本地資料庫的demo
- PagingWithNetworkSample : 網路資料的demo
-
Paging 庫包含以下功能:
- 分頁資料的記憶體中快取。該功能可確保您的應用在處理分頁資料時高效利用系統資源。
- 內建的請求重複資訊刪除功能,可確保您的應用高效利用網路頻寬和系統資源。
- 可配置的 RecyclerView 介面卡,會在使用者滾動到已載入資料的末尾時自動請求資料。
- 對 Kotlin 協程和 Flow 以及 LiveData 和 RxJava 的一流支援。
- 內建對錯誤處理功能的支援,包括重新整理和重試功能。
-
Paging 元件及其在應用架構的整合:
-
定義資料來源 : 資料來源的定義取決於您從哪裡載入資料。您僅需實現 PagingSource 或者 PagingSource 與 RemoteMediator 的組合:
- 如果您從單個源載入資料,例如網路、本地資料、檔案、記憶體快取等(不只是網路和資料庫,其他如檔案也可以使用Paging),實現 PagingSource 即可,如果您使用了 Room,從 2.3.0-alpha 開始,它將預設為您實現 PagingSource。
- 如果您從一個多層級資料來源載入資料,就像帶有本地資料庫快取的網路資料來源那樣。那麼您需要實現 RemoteMediator 來合併兩個資料來源到一個本地資料庫快取的 PagingSource 中。
-
PagingSource :
- PagingSource 可以定義一個分頁資料的資料來源,以及從該資料來源獲取資料的方式。
- LoadParams:PagingSource 的 密封類(sealed),包含有關要執行的載入操作的資訊,其中包括要載入的鍵和要載入的項數。作為load()函式的引數使用
- LoadResult:PagingSource 的 密封類(sealed),包含載入操作的結果。LoadResult 是一個密封的類,根據 load() 呼叫是否成功。作為load()函式的返回值
- getRefreshKey(): 該方法接受 PagingState 物件作為引數,並且當資料在初始載入後重新整理或失效時,該方法會返回要傳遞給 load() 方法的鍵。在後續重新整理資料時,Paging 庫會自動呼叫此方法。
- load(): 下圖說明了load() 函式如何接收每次載入的鍵併為後續載入提供鍵:
- 程式碼示例:
// 自定義PagingSource類 private const val ARTICLE_STARTING_PAGE_INDEX = 0 class HomeArticlePagingSource( private val api: WanJetpackApi ) : PagingSource<Int, ApiArticle>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ApiArticle> { val page = params.key ?: ARTICLE_STARTING_PAGE_INDEX return try { val response = api.getHomeArticle(page) val datas = response.data.datas LoadResult.Page( data = datas, prevKey = if (page == ARTICLE_STARTING_PAGE_INDEX) null else page - 1, nextKey = if (page == response.data.pageCount) null else page + 1, ) } catch (exception: Exception) { LoadResult.Error(exception) } } override fun getRefreshKey(state: PagingState<Int, ApiArticle>): Int? { return null } }
-
PagingData :
- 分頁資料的容器被稱為 PagingData,每次重新整理資料時,都會建立一個 PagingData 的例項。如果要建立 PagingData 資料流,您需要建立一個 Pager 例項,並提供一個 PagingConfig 配置物件和一個可以告訴 Pager 如何獲取您實現的 PagerSource 的例項的函式,以供 Pager 使用。
- Pager 類提供的方法可顯示來自 PagingSource 的 PagingData 物件的響應式流。Paging 庫支援使用多種流型別,包括 Flow、LiveData 以及 RxJava 中的 Flowable 和 Observable 型別。
- 通過 Pager().flow可以返回Flow<PagingData
>。然後在ViewModel中.cachedIn(viewModelScope), cachedIn()運算子使資料流可共享,並使用提供的 CoroutineScope 快取載入的資料 - 程式碼示例: (注:Pager 的 remoteMediator 引數可選項, RemoteMediator 是重點)
//Repository: fun getHomeArticle(): Flow<PagingData<ApiArticle>> { return Pager( config = PagingConfig(enablePlaceholders = false, pageSize = HOME_ARTICLE_PAGE_SIZE), pagingSourceFactory = { HomeArticlePagingSource(api) } ).flow }
//ViewModel: fun getHomeArticle(): Flow<PagingData<ApiArticle>> { val newResult: Flow<PagingData<ApiArticle>> = repository.getHomeArticle().cachedIn(viewModelScope) currentArticleResult = newResult return newResult }
-
PagingDataAdapter :
- 與定義 RecyclerView 列表 Adapter 時的通常做法相同:必須定義 onCreateViewHolder() 和 onBindViewHolder() 方法;指定 ViewHoler 和 DiffUtil.ItemCallback
- Adapter 及 UI ( Activity、Fragment )中的相關程式碼略。
-
LoadType : 是個 enum 類,包含三種狀態:REFRESH、PREPEND、APPEND。在 PagingSource 的 LoadParams 類中用到。
- 官方介紹:Type of load a [PagingData] can trigger a [PagingSource] to perform.
- REFRESH:[PagingData] content being refreshed, which can be a result of [PagingSource] invalidation, refresh that may contain content updates, or the initial load.
- PREPEND:Load at the start of a [PagingData].
- APPEND:Load at the end of a [PagingData].
-
LoadState : 是個 sealed(密封) 類。
- 官方介紹:LoadState of a PagedList load - associated with a [LoadType].
- [LoadState] of any [LoadType] may be observed for UI purposes by registering a listener via [androidx.paging.PagingDataAdapter.addLoadStateListener] or [androidx.paging.AsyncPagingDataDiffer.addLoadStateListener]
- Paging 庫通過 LoadState 物件公開可在介面中使用的載入狀態。LoadState 根據當前的載入狀態採用以下三種形式之一:
- 如果沒有正在執行的載入操作且沒有錯誤,則 LoadState 為 LoadState.NotLoading 物件。
- 如果有正在執行的載入操作,則 LoadState 為 LoadState.Loading 物件。
- 如果出現錯誤,則 LoadState 為 LoadState.Error 物件。
-
載入狀態的三個場景:下拉重新整理、上拉載入更多、首次進入頁面中間的滾動條(及載入失敗提醒)
-
顯示載入狀態 : 可通過兩種方法在介面中使用 LoadState:使用監聽器,以及使用特殊的列表介面卡在 RecyclerView 列表中直接顯示載入狀態。
- 方法一、 使用監聽器獲取載入狀態: 為了獲取載入狀態以用於介面中的一般用途,PagingDataAdapter 中提供了 addLoadStateListener()、loadStateFlow 兩種方式。來自 loadStateFlow 或 addLoadStateListener() 的更新可確保與介面的更新保持同步。這意味著,如果您收到 NotLoading.Incomplete 的 LoadState,則可以確定載入已完成,並且介面也已相應更新。
// addLoadStateListener 方式。 articleAdapter.addLoadStateListener { when (it.refresh) { is LoadState.NotLoading -> { progressBar.visibility = View.INVISIBLE recyclerView.visibility = View.VISIBLE } is LoadState.Loading -> { progressBar.visibility = View.VISIBLE recyclerView.visibility = View.INVISIBLE } is LoadState.Error -> { val state = it.refresh as LoadState.Error progressBar.visibility = View.INVISIBLE Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show() } } }
// loadStateFlow 方式 // collectLatest 是個 suspend 函式,所以要在協程或者另一個 suspend 中呼叫 lifecycleScope.launch { pagingAdapter.loadStateFlow.collectLatest { progressBar.isVisible = it.refresh is LoadState.Loading retry.isVisible = it.refresh !is LoadState.Loading errorMsg.isVisible = it.refresh is LoadState.Error } }
- 方法二、 使用介面卡呈現載入狀態: Paging 庫提供了另一個名為 LoadStateAdapter 的列表介面卡,用於直接在顯示的分頁資料列表中呈現載入狀態。其實該方法就是在PagingDataAdapter中把addLoadStateListener()和ConcatAdapter封裝了一下
- 首先,建立一個實現 LoadStateAdapter 的類,並定義 onCreateViewHolder() 和 onBindViewHolder() 方法:
class LoadStateViewHolder( parent: ViewGroup, retry: () -> Unit ) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.load_state_item, parent, false) ) { private val binding = LoadStateItemBinding.bind(itemView) private val progressBar: ProgressBar = binding.progressBar private val errorMsg: TextView = binding.errorMsg private val retry: Button = binding.retryButton .also { it.setOnClickListener { retry() } } fun bind(loadState: LoadState) { if (loadState is LoadState.Error) { errorMsg.text = loadState.error.localizedMessage } progressBar.isVisible = loadState is LoadState.Loading retry.isVisible = loadState is LoadState.Error errorMsg.isVisible = loadState is LoadState.Error } } // Adapter that displays a loading spinner when // state = LoadState.Loading, and an error message and retry // button when state is LoadState.Error. class ExampleLoadStateAdapter( private val retry: () -> Unit ) : LoadStateAdapter<LoadStateViewHolder>() { override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = LoadStateViewHolder(parent, retry) override fun onBindViewHolder( holder: LoadStateViewHolder, loadState: LoadState ) = holder.bind(loadState) }
- 然後,從 PagingDataAdapter 物件呼叫 withLoadStateHeaderAndFooter() 方法:
pagingAdapter .withLoadStateHeaderAndFooter( header = ExampleLoadStateAdapter(adapter::retry), footer = ExampleLoadStateAdapter(adapter::retry) )
- 如果您只想讓 RecyclerView 在頁首或頁尾中顯示載入狀態,則可以呼叫 withLoadStateHeader() 或 withLoadStateFooter()。 關於withLoadStateHeaderAndFooter()、withLoadStateHeader() 和 withLoadStateFooter()的實現,通過原始碼發現,其實就是用的PagingDataAdapter.addLoadStateListener()方案,只不過是通過ConcatAdapter封裝下。即:在PagingDataAdapter中把addLoadStateListener()和ConcatAdapter封裝了一下,且返回值是ConcatAdapter
- 注意:由於withLoadStateHeaderAndFooter()、withLoadStateHeader() 和 withLoadStateFooter()返回的是ConcatAdapter,所以如果已經用建構函式ConcatAdapter(firstAdapter, articleAdapter)的話,再用withLoadState···新增頁首頁尾會失敗,因為用withLoadState···返回的也是ConcatAdapter就有兩個ConcatAdapter了。這個時候正確的做法是用withLoadState···建立ConcatAdapter,然後再用concatAdapter.addAdapter(0,firstAdapter)新增其它的adapter,且呼叫concatAdapter.addAdapter的位置在binding.articleList.adapter = concatAdapter前後都可以。
- 首先,建立一個實現 LoadStateAdapter 的類,並定義 onCreateViewHolder() 和 onBindViewHolder() 方法:
- 方法一、 使用監聽器獲取載入狀態: 為了獲取載入狀態以用於介面中的一般用途,PagingDataAdapter 中提供了 addLoadStateListener()、loadStateFlow 兩種方式。來自 loadStateFlow 或 addLoadStateListener() 的更新可確保與介面的更新保持同步。這意味著,如果您收到 NotLoading.Incomplete 的 LoadState,則可以確定載入已完成,並且介面也已相應更新。
-
Pager : Pager().flow 把 PagingSource 轉換為 PagingData。在Repository中用到
-
RemoteMediator : 在Pager()中用到。
- 當您從一個多層級資料來源載入資料時,應當實現一個 RemoteMediator。
- 一般用法為從網路請求資料並存入資料庫。每當資料庫中沒有資料可以被展示時,就會觸發 load() 方法。基於 PagingState 和 LoadType,我們可以構造下一頁的資料請求。
-
PagingConfig : 在Pager()中用到
-
PagingState : 在自定義 PagingSource 的 getRefreshKey()方法中用到,在自定義RemoteMediator的load()方法中也用到了。
- 官方介紹:Snapshot state of Paging system including the loaded [pages], the last accessed [anchorPosition], and the [config] used.
-
參考部落格:目前Paging已經發布3.0正式版,下面這個部落格是alpha版本的,但可以參考:
Room
DataStore
App Startup
WorkManager
compose
Navigation
- Navigation文件
- NavigationAdvancedSample
- 和路由框架ARouter的區別:ARouter主要是用於Activity路由的框架,採用的是APT技術,可用於元件化改造。而Navigation主要是用於Fragment路由導航的框架。
- Jetpack 之 Navigation 全面剖析
- 底部導航欄1
- 底部導航欄2
- 使用者登入場景
- Safe Args 導航
- Fragment間轉場動畫:Android Material 元件 1.2.0 現已釋出
Preference
- Preference 庫
- 官方指南
- 官方demo:PreferencesKotlin
RecyclerView
- RecyclerView 庫
- 官方文件
- 官方demo
- RecyclerView: java版本普通demo
- RecyclerViewAnimations: 新增、刪除、更新 item的demo
- RecyclerViewKotlin: ConcatAdapter的demo
- 實現了ConcatAdapter,能規避巢狀滑動
- 用Kotlin高階函式處理 RecyclerView 中的點選事件
- 實現了新增、刪除item的方案及更新item動畫
- RecyclerViewSimple: kotlin版本普通demo
- 預設的adapter:RecyclerView.Adapter:認識 RecyclerView
- ListAdapter:繼承RecyclerView.Adapter:在 RecyclerView 中使用 ListAdapter
- ConcatAdapter:
- PagingDataAdapter:繼承RecyclerView.Adapter
滑動重新整理
- 滑動重新整理: 一般滑動重新整理用於RecyclerView中的下拉重新整理和上拉載入更多。
- 官方文件
- 官方demo:
- 滑動重新整理介面實現方案:
- 三方框架:SmartRefreshLayout
- 自己實現有三種方案:
- 方案一: 可以在RecyclerView外層自定義一個佈局,裡面放三個控制元件:HeaderView、RecyclerView、FooterView。 結合SwipeRefreshLayout的話,只需要寫個FooterView就行了。Android 簡單易上手的下拉重新整理控制元件、Android RecyclerView下拉重新整理 & 上拉載入更多
- 方案二: 可以作為RecyclerView的兩個item處理,通過不同的Type型別區分
- 方案三: 可以通過ConcatAdapter配置:使用 ConcatAdapter 順序連線其他 Adapter
- 方案四: 可以直接用 PagingDataAdapter.withLoadStateFooter()載入頁尾,但是下拉重新整理還要自己實現。查詢PagingDataAdapter中的實現方式發現,其實該方法也就是方案三,只是在PagingDataAdapter中已經封裝好了。
- 關於下拉重新整理,還可以利用左滑刪除的思想實現,但是體驗不是特別理想,暫時pass該方案
- 滑動重新整理功能實現方案:
- 如果是 ConcatAdapter、PagingDataAdapter : 即用了 Paging3 ,相關說明參考上面的Paging 庫說明。
- 如果是 ListAdapter 、 RecyclerView.Adapter:
- 下拉重新整理、左滑刪除參考demo:
載入狀態
- 載入狀態的幾個場景:下拉重新整理、上拉載入更多、底部的已載入全部內容、首次進入頁面的載入狀態(及載入失敗提醒)
- 下拉重新整理、上拉載入更多:略
- 首次進入頁面的載入狀態:
- 底部的已載入全部內容:方案比較多,個人比較傾向下面兩種方案
- 方案一:通過withLoadStateFooter實現,和上拉載入更多用同一套佈局,同一個adapter。【參考本demo】
- 方案二:通過ConcatAdapter.addAdapter實現,專門顯示載入更多
動畫
- Animation 動畫: 下拉重新整理場景通過屬性動畫實現
- 官方文件
- 屬性動畫:
- ValueAnimator: 屬性動畫的主計時引擎,它也可計算要新增動畫效果的屬性的值。它具有計算動畫值所需的所有核心功能,同時包含每個動畫的計時詳情、有關動畫是否重複播放的資訊、用於接收更新事件的監聽器以及設定待評估自定義型別的功能。為屬性新增動畫效果分為兩個步驟:計算新增動畫效果之後的值,以及對要新增動畫效果的物件和屬性設定這些值。ValueAnimator 不會執行第二個步驟,因此,您必須監聽由 ValueAnimator 計算的值的更新情況,並使用您自己的邏輯修改要新增動畫效果的物件。如需瞭解詳情,請參閱使用 ValueAnimator 新增動畫效果部分。
- ObjectAnimator: ValueAnimator 的子類,用於設定目標物件和物件屬性以新增動畫效果。此類會在計算出動畫的新值後相應地更新屬性。在大多數情況下,您不妨使用 ObjectAnimator,因為它可以極大地簡化對目標物件的值新增動畫效果這一過程。不過,有時您需要直接使用 ValueAnimator,因為 ObjectAnimator 存在其他一些限制,例如要求目標物件具有特定的訪問器方法。
- AnimationSet: 此類提供一種將動畫分組在一起的機制,以使它們彼此相對執行。您可以將動畫設定為一起播放、按順序播放或者在指定的延遲時間後播放。如需瞭解詳情,請參閱使用 AnimatorSet 編排多個動畫部分。
- LayoutTransition:
- LayoutAnimations:
ViewPager2
- ViewPager2 庫
- 官方文件
- 官方demo
- 官方demo中的ViewPager2 with a Preview of Next/Prev Page 相當於Banner中類似的場景
- 官方demo中的ViewPager2 with a Nested RecyclerViews 場景很好,提供瞭解決巢狀滑動的方案
- ViewPager2 底層使用 RecycleView 實現的,所以這裡不再使用 PagerAdapter 而是使用了 RecyclerView.Adapter
- 對應的fragment用的是 FragmentStateAdapter,而不是 FragmentStatePagerAdapter、FragmentPagerAdapter之類的
Banner
- Banner:其實就是 ViewPager 的應用
- 三方庫:
- 自己實現方案:
- 讓Banner和RecyclerView分開: 通過NestedScrollView裡包裹ViewPager2和RecyclerView的話,會有滑動卡頓的問題,即使加上android:nestedScrollingEnabled="false"屬性,除非再加上setHasFixedSize(true),但是還會有其他的問題:加上setHasFixedSize(true)後,介面的資料只顯示一頁了。故此方案暫時行不通了。本方案相關程式碼
binding.articleList.setHasFixedSize(true) binding.articleList.isNestedScrollingEnabled = false
- 讓Banner成為RecyclerView的一部分:
- 如果Banner在頂部:banner在頂部的話,就做header
- 如果Banner在中間:在中間的話,就type,或者對adapter做一個擴充套件,做一個可以在中間插入的類似header。畢竟type的話,寫起來也蠻麻煩的
- 通過 ConcatAdapter 實現:
- 本demo就是用的該方案,demo中通過HomeFirstAdapter新增RecyclerView的ConcatAdapter中,通過HomeBannerAdapter實現ViewPager2的adapter。
- 通過上述的方式加上ViewPager2之後,ViewPager2沒有影響RecyclerView的功能,RecyclerView上下滑動流暢;但是ViewPager2不能滑動,因為事件被RecyclerView攔截了。故需新增自定義佈局 NestedFrameLayout 巢狀在ViewPager2之上,在 NestedFrameLayout 去處理父類的事件分發,即當左右滑動 NestedFrameLayout 時,執行 NestedFrameLayout 的parent.requestDisallowInterceptTouchEvent(true)方法,讓ViewPager2消費事件。
- 通過 MultiTypeAdapter 實現:暫時沒有驗證
- 工行融e購實現方案:首頁除了viewpager功能都放在AppBarLayout裡面,但是這樣TabLayout可能就要和融e購一樣放在下面了,不是想要的。用ConcatAdapter也可以實現工行融e購的首頁效果。
- 京東首頁實現方案:自定義控制元件實現。用ConcatAdapter也能實現京東首頁效果
- 讓Banner和RecyclerView分開: 通過NestedScrollView裡包裹ViewPager2和RecyclerView的話,會有滑動卡頓的問題,即使加上android:nestedScrollingEnabled="false"屬性,除非再加上setHasFixedSize(true),但是還會有其他的問題:加上setHasFixedSize(true)後,介面的資料只顯示一頁了。故此方案暫時行不通了。本方案相關程式碼
NestedScrollView
- 直接在 NestedScrollView 中放入 ViewPager2 和 RecyclerView 時,會出現滑動卡頓。解決方案參考
- NestedScrollView
- 事件衝突的原因:Android 的事件分發機制中,只要有一個控制元件消費了事件,其他控制元件就沒辦法再接收到這個事件了。因此,當有巢狀滑動場景時,我們都需要自己手動解決事件衝突。而在 Android 5.0 Lollipop 之後,Google 官方通過 巢狀滑動機制 解決了傳統 Android 事件分發無法共享事件這個問題。
- 巢狀滑動機制:巢狀滑動機制 的基本原理可以認為是事件共享,即當子控制元件接收到滑動事件,準備要滑動時,會先通知父控制元件(startNestedScroll);然後在滑動之前,會先詢問父控制元件是否要滑動(dispatchNestedPreScroll);如果父控制元件響應該事件進行了滑動,那麼就會通知子控制元件它具體消耗了多少滑動距離;然後交由子控制元件處理剩餘的滑動距離;最後子控制元件滑動結束後,如果滑動距離還有剩餘,就會再問一下父控制元件是否需要在繼續滑動剩下的距離(dispatchNestedScroll)...
TabLayout
- TabLayout
- 和ViewPager2、Fragment應用
BottomNavigationView
- BottomNavigationView
- 和Navigation、Fragment、ViewPager2應用
Constraint Layout
-
CoordinatorLayout、NestedScrollView、CollapsingToolbarLayout、AppBarLayout、MaterialToolbart
-
待優化場景,搜尋場景:search在 Android 系統的協助下使用搜尋對話方塊或搜尋微件傳遞搜尋查詢
- 搜尋對話方塊
- 搜尋微件
Glide
- 和Coil對比,建議換成Coil載入圖片
Cookie
- CookieManager
- 本demo中,和收藏相關都需要登入操作,建議登入將返回的cookie(其中包含賬號、密碼)持久化到本地即可。
WebView
- WebView 庫
- 官方文件
- 官方demo
- 本demo中跳轉到WebFragment是通過 Bundle 傳遞引數,沒有用通過 Navigation 的 Safe Args 導航實現
- 本demo中的WebView適配了深色主題。
啟動介面
- 方案:通過windowSplashscreenContent屬性或者SplashActivity介面
- 注意 windowSplashscreenContent屬性是在Android8.0(v26)上才有的,如果在之前的版本上適配啟動介面,應該新增個Activity,即 SplashActivity。
- 冷啟動、熱啟動
- Splash Screen:展示品牌Logo或Slogan
- 如果只是單純的顯示個介面,只需要在themes裡設定
- @color/jetpack_green_500
即可。
- 如果只是單純的顯示個介面,只需要在themes裡設定
- Advertisement Screen:展示節日活動或日常廣告
- Guide Screen:演示重點功能,一般只展示一次
- 參考部落格:Android 12上全新的應用啟動API,適配一下?
樣式系統、沉浸式(在Android6.0、8.1、10、11上已經適配,詳見demo)
- 本demo沉侵式方案。關鍵屬性:windowTranslucentStatus、statusBarColor、fitsSystemWindows、mSemiTransparentBarColor、clipToPadding
- 在themes中設定(或者通過程式碼設定)
- true
。除錯發現不設定- @android:color/transparent
也行。 - 在佈局中設定(或者通過程式碼設定) AppBarLayout 的屬性 android:fitsSystemWindows="true",是為了防止AppBarLayout顯示在statusbar上。
- 通過反射設定 decorView 的 mSemiTransparentBarColor 為透明即可。程式碼如下:
try { val decorView = window.decorView::class.java val field = decorView.getDeclaredField("mSemiTransparentBarColor") field.isAccessible = true field.setInt(window.decorView, Color.TRANSPARENT) } catch (e: Exception) { }
- 在themes中設定(或者通過程式碼設定)
- 注意:statusbar 和 navigationbar 道理一樣
- 參考文章: