WanJetpack項(xiàng)目:用Jetpack實(shí)現(xiàn)玩Android纱皆,追求最官方的實(shí)現(xiàn)方式

項(xiàng)目簡(jiǎn)介

玩Android demo。用Jetpack MVVM開發(fā)架構(gòu)芭商、單Activity多Fragment項(xiàng)目設(shè)計(jì)派草,項(xiàng)目結(jié)構(gòu)清晰,代碼簡(jiǎn)潔優(yōu)雅铛楣,追求最官方的實(shí)現(xiàn)方式近迁。用到以下知識(shí)點(diǎn):
LiveData、ViewModel簸州、DataBinding(包括雙向綁定鉴竭、BindingAdapter的使用)歧譬、ViewBinding、coroutines(包含flow搏存、suspend瑰步、livedata協(xié)程構(gòu)造器、flow協(xié)程構(gòu)造器的使用)璧眠、Hilt缩焦、Paging3(包含RemoteMediator、加載狀態(tài))责静、Room袁滥、Navigation(通過ViewModel共享數(shù)據(jù))、Banner(kotlin簡(jiǎn)單實(shí)現(xiàn))灾螃、TabLayout呻拌、BottomNavigationView、RecycleView(包含ListAdapter睦焕、ConcatAdapter藐握、PagingDataAdapter的使用)、ViewPager2垃喊、Glide猾普、Cookie、Retrofit2本谜、啟動(dòng)頁面初家、深色主題、沉浸式模式乌助、Kotlin高階函數(shù)溜在。

項(xiàng)目截圖(默認(rèn)主題、深色主題)

wanjetpack_night.png
jetpack.png

項(xiàng)目參考demo

項(xiàng)目知識(shí)點(diǎn)

LiveData

ViewModel

ViewBinding

DataBinding

coroutines

  • 理解協(xié)程韧掩、LiveData 和 Flow
    • liveData 協(xié)程構(gòu)造方法提供了一個(gè)協(xié)程代碼塊紊浩,這個(gè)塊就是 LiveData 的作用域,當(dāng) LiveData 被觀察的時(shí)候,里面的操作就會(huì)被執(zhí)行坊谁,當(dāng) LiveData 不再被使用時(shí)费彼,里面的操作就會(huì)取消。 而且該協(xié)程構(gòu)造方法產(chǎn)生的是一個(gè)不可變的LiveData呜袁,可以直接暴露給對(duì)應(yīng)的視圖使用敌买。而 emit() 方法則用來更新 LiveData 的數(shù)據(jù)简珠。
    • 一個(gè)常見用例阶界,比如當(dāng)用戶在 UI 中選中一些元素,然后將這些選中的內(nèi)容顯示出來聋庵。一個(gè)常見的做法是膘融,把被選中的項(xiàng)目的 ID 保存在一個(gè) MutableLiveData 里,然后運(yùn)行 switchMap〖烙瘢現(xiàn)在在 switchMap 里氧映,您也可以使用協(xié)程構(gòu)造方法:
        private val itemId = MutableLiveData<String>()
        val result = itemId.switchMap {
            liveData { emit(fetchItem(it)) }
        }
    
  • Google 推薦在 MVVM 架構(gòu)中使用 Kotlin Flow
  • 圖解協(xié)程原理

Hilt

  • hilt 和 Koin

Paging

  • Paging 庫 3.0.0正式版已發(fā)布,普天同慶脱货!Paging 庫可幫助您加載和顯示來自本地存儲(chǔ)或網(wǎng)絡(luò)中更大的數(shù)據(jù)集中的數(shù)據(jù)頁面岛都。此方法可讓您的應(yīng)用更高效地利用網(wǎng)絡(luò)帶寬和系統(tǒng)資源。Paging 庫的組件旨在契合推薦的 Android 應(yīng)用架構(gòu)振峻,流暢集成其他 Jetpack 組件臼疫,并提供一流的 Kotlin 支持。

  • 官方文檔

  • 官方demo:

  • Paging 庫包含以下功能:

    • 分頁數(shù)據(jù)的內(nèi)存中緩存扣孟。該功能可確保您的應(yīng)用在處理分頁數(shù)據(jù)時(shí)高效利用系統(tǒng)資源烫堤。
    • 內(nèi)置的請(qǐng)求重復(fù)信息刪除功能,可確保您的應(yīng)用高效利用網(wǎng)絡(luò)帶寬和系統(tǒng)資源凤价。
    • 可配置的 RecyclerView 適配器,會(huì)在用戶滾動(dòng)到已加載數(shù)據(jù)的末尾時(shí)自動(dòng)請(qǐng)求數(shù)據(jù)。
    • 對(duì) Kotlin 協(xié)程和 Flow 以及 LiveData 和 RxJava 的一流支持庶近。
    • 內(nèi)置對(duì)錯(cuò)誤處理功能的支持胖缤,包括刷新和重試功能。
  • Paging 組件及其在應(yīng)用架構(gòu)的集成:


    paging3-library-architecture.svg
  • 定義數(shù)據(jù)源 : 數(shù)據(jù)源的定義取決于您從哪里加載數(shù)據(jù)慢逾。您僅需實(shí)現(xiàn) PagingSource 或者 PagingSource 與 RemoteMediator 的組合:

    • 如果您從單個(gè)源加載數(shù)據(jù)格粪,例如網(wǎng)絡(luò)本地?cái)?shù)據(jù)氛改、文件帐萎、內(nèi)存緩存等(不只是網(wǎng)絡(luò)和數(shù)據(jù)庫,其他如文件也可以使用Paging)胜卤,實(shí)現(xiàn) PagingSource 即可疆导,如果您使用了 Room,從 2.3.0-alpha 開始葛躏,它將默認(rèn)為您實(shí)現(xiàn) PagingSource澈段。
    • 如果您從一個(gè)多層級(jí)數(shù)據(jù)源加載數(shù)據(jù)悠菜,就像帶有本地?cái)?shù)據(jù)庫緩存的網(wǎng)絡(luò)數(shù)據(jù)源那樣。那么您需要實(shí)現(xiàn) RemoteMediator 來合并兩個(gè)數(shù)據(jù)源到一個(gè)本地?cái)?shù)據(jù)庫緩存的 PagingSource 中败富。
  • PagingSource :

    • PagingSource 可以定義一個(gè)分頁數(shù)據(jù)的數(shù)據(jù)源悔醋,以及從該數(shù)據(jù)源獲取數(shù)據(jù)的方式。
    • LoadParams:PagingSource 的 密封類(sealed)兽叮,包含有關(guān)要執(zhí)行的加載操作的信息芬骄,其中包括要加載的鍵和要加載的項(xiàng)數(shù)。作為load()函數(shù)的參數(shù)使用
    • LoadResult:PagingSource 的 密封類(sealed)鹦聪,包含加載操作的結(jié)果账阻。LoadResult 是一個(gè)密封的類,根據(jù) load() 調(diào)用是否成功泽本。作為load()函數(shù)的返回值
    • getRefreshKey(): 該方法接受 PagingState 對(duì)象作為參數(shù)淘太,并且當(dāng)數(shù)據(jù)在初始加載后刷新或失效時(shí),該方法會(huì)返回要傳遞給 load() 方法的鍵规丽。在后續(xù)刷新數(shù)據(jù)時(shí)蒲牧,Paging 庫會(huì)自動(dòng)調(diào)用此方法。
    • load(): 下圖說明了load() 函數(shù)如何接收每次加載的鍵并為后續(xù)加載提供鍵:


      paging3-source-load.svg
    • 代碼示例:
          // 自定義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 :

    • 分頁數(shù)據(jù)的容器被稱為 PagingData赌莺,每次刷新數(shù)據(jù)時(shí)冰抢,都會(huì)創(chuàng)建一個(gè) PagingData 的實(shí)例。如果要?jiǎng)?chuàng)建 PagingData 數(shù)據(jù)流雄嚣,您需要?jiǎng)?chuàng)建一個(gè) Pager 實(shí)例晒屎,并提供一個(gè) PagingConfig 配置對(duì)象和一個(gè)可以告訴 Pager 如何獲取您實(shí)現(xiàn)的 PagerSource 的實(shí)例的函數(shù),以供 Pager 使用缓升。
    • Pager 類提供的方法可顯示來自 PagingSource 的 PagingData 對(duì)象的響應(yīng)式流鼓鲁。Paging 庫支持使用多種流類型,包括 Flow港谊、LiveData 以及 RxJava 中的 Flowable 和 Observable 類型骇吭。
    • 通過 Pager().flow可以返回Flow<PagingData<ApiArticle>>。然后在ViewModel中.cachedIn(viewModelScope)歧寺, cachedIn()運(yùn)算符使數(shù)據(jù)流可共享燥狰,并使用提供的 CoroutineScope 緩存加載的數(shù)據(jù)
    • 代碼示例: (注:Pager 的 remoteMediator 參數(shù)可選項(xiàng), RemoteMediator 是重點(diǎn))
          //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 時(shí)的通常做法相同:必須定義 onCreateViewHolder() 和 onBindViewHolder() 方法斜筐;指定 ViewHoler 和 DiffUtil.ItemCallback
    • Adapter 及 UI ( Activity龙致、Fragment )中的相關(guān)代碼略。
  • LoadType : 是個(gè) enum 類顷链,包含三種狀態(tài):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 : 是個(gè) 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 對(duì)象公開可在界面中使用的加載狀態(tài)。LoadState 根據(jù)當(dāng)前的加載狀態(tài)采用以下三種形式之一:
      • 如果沒有正在執(zhí)行的加載操作且沒有錯(cuò)誤霜大,則 LoadState 為 LoadState.NotLoading 對(duì)象构哺。
      • 如果有正在執(zhí)行的加載操作,則 LoadState 為 LoadState.Loading 對(duì)象战坤。
      • 如果出現(xiàn)錯(cuò)誤曙强,則 LoadState 為 LoadState.Error 對(duì)象。
  • 加載狀態(tài)的三個(gè)場(chǎng)景:下拉刷新湖笨、上拉加載更多旗扑、首次進(jìn)入頁面中間的滾動(dòng)條(及加載失敗提醒)

  • 顯示加載狀態(tài) : 可通過兩種方法在界面中使用 LoadState:使用監(jiān)聽器蹦骑,以及使用特殊的列表適配器在 RecyclerView 列表中直接顯示加載狀態(tài)慈省。

    • 方法一、 使用監(jiān)聽器獲取加載狀態(tài): 為了獲取加載狀態(tài)以用于界面中的一般用途眠菇,PagingDataAdapter 中提供了 addLoadStateListener()边败、loadStateFlow 兩種方式。來自 loadStateFlow 或 addLoadStateListener() 的更新可確保與界面的更新保持同步捎废。這意味著笑窜,如果您收到 NotLoading.Incomplete 的 LoadState,則可以確定加載已完成登疗,并且界面也已相應(yīng)更新排截。
          // 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 是個(gè) suspend 函數(shù)辐益,所以要在協(xié)程或者另一個(gè) suspend 中調(diào)用
          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
            }
          }
      
    • 方法二断傲、 使用適配器呈現(xiàn)加載狀態(tài): Paging 庫提供了另一個(gè)名為 LoadStateAdapter 的列表適配器,用于直接在顯示的分頁數(shù)據(jù)列表中呈現(xiàn)加載狀態(tài)智政。其實(shí)該方法就是在PagingDataAdapter中把a(bǔ)ddLoadStateListener()和ConcatAdapter封裝了一下
      • 首先认罩,創(chuàng)建一個(gè)實(shí)現(xiàn) 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 對(duì)象調(diào)用 withLoadStateHeaderAndFooter() 方法:
            pagingAdapter
              .withLoadStateHeaderAndFooter(
                header = ExampleLoadStateAdapter(adapter::retry),
                footer = ExampleLoadStateAdapter(adapter::retry)
              )
        
      • 如果您只想讓 RecyclerView 在頁眉或頁腳中顯示加載狀態(tài)垦垂,則可以調(diào)用 withLoadStateHeader() 或 withLoadStateFooter()。 關(guān)于withLoadStateHeaderAndFooter()牙瓢、withLoadStateHeader() 和 withLoadStateFooter()的實(shí)現(xiàn)劫拗,通過源碼發(fā)現(xiàn),其實(shí)就是用的PagingDataAdapter.addLoadStateListener()方案矾克,只不過是通過ConcatAdapter封裝下页慷。即:在PagingDataAdapter中把a(bǔ)ddLoadStateListener()和ConcatAdapter封裝了一下,且返回值是ConcatAdapter
      • 注意:由于withLoadStateHeaderAndFooter()、withLoadStateHeader() 和 withLoadStateFooter()返回的是ConcatAdapter差购,所以如果已經(jīng)用構(gòu)造函數(shù)ConcatAdapter(firstAdapter, articleAdapter)的話四瘫,再用withLoadState···添加頁眉頁腳會(huì)失敗,因?yàn)橛脀ithLoadState···返回的也是ConcatAdapter就有兩個(gè)ConcatAdapter了欲逃。這個(gè)時(shí)候正確的做法是用withLoadState···創(chuàng)建ConcatAdapter找蜜,然后再用concatAdapter.addAdapter(0,firstAdapter)添加其它的adapter,且調(diào)用concatAdapter.addAdapter的位置在binding.articleList.adapter = concatAdapter前后都可以稳析。
  • Pager : Pager().flow 把 PagingSource 轉(zhuǎn)換為 PagingData洗做。在Repository中用到

  • RemoteMediator : 在Pager()中用到。

    • 當(dāng)您從一個(gè)多層級(jí)數(shù)據(jù)源加載數(shù)據(jù)時(shí)彰居,應(yīng)當(dāng)實(shí)現(xiàn)一個(gè) RemoteMediator诚纸。
    • 一般用法為從網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)并存入數(shù)據(jù)庫。每當(dāng)數(shù)據(jù)庫中沒有數(shù)據(jù)可以被展示時(shí)陈惰,就會(huì)觸發(fā) load() 方法畦徘。基于 PagingStateLoadType抬闯,我們可以構(gòu)造下一頁的數(shù)據(jù)請(qǐng)求井辆。
  • 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已經(jīng)發(fā)布3.0正式版杯缺,下面這個(gè)博客是alpha版本的,但可以參考:

Room

DataStore

App Startup

WorkManager

compose

Navigation

Preference

RecyclerView

滑動(dòng)刷新

  • 滑動(dòng)刷新: 一般滑動(dòng)刷新用于RecyclerView中的下拉刷新和上拉加載更多铁孵。
  • 官方文檔
  • 官方demo:
  • 滑動(dòng)刷新界面實(shí)現(xiàn)方案:
    • 三方框架:SmartRefreshLayout
    • 自己實(shí)現(xiàn)有三種方案:
      • 方案一: 可以在RecyclerView外層自定義一個(gè)布局锭硼,里面放三個(gè)控件:HeaderView、RecyclerView蜕劝、FooterView檀头。 結(jié)合SwipeRefreshLayout的話轰异,只需要寫個(gè)FooterView就行了。Android 簡(jiǎn)單易上手的下拉刷新控件暑始、Android RecyclerView下拉刷新 & 上拉加載更多
      • 方案二: 可以作為RecyclerView的兩個(gè)item處理搭独,通過不同的Type類型區(qū)分
      • 方案三: 可以通過ConcatAdapter配置:使用 ConcatAdapter 順序連接其他 Adapter
      • 方案四: 可以直接用 PagingDataAdapter.withLoadStateFooter()加載頁腳,但是下拉刷新還要自己實(shí)現(xiàn)廊镜。查詢PagingDataAdapter中的實(shí)現(xiàn)方式發(fā)現(xiàn)牙肝,其實(shí)該方法也就是方案三,只是在PagingDataAdapter中已經(jīng)封裝好了嗤朴。
      • 關(guān)于下拉刷新配椭,還可以利用左滑刪除的思想實(shí)現(xiàn),但是體驗(yàn)不是特別理想雹姊,暫時(shí)pass該方案
  • 滑動(dòng)刷新功能實(shí)現(xiàn)方案:

加載狀態(tài)

  • 加載狀態(tài)的幾個(gè)場(chǎng)景:下拉刷新坎背、上拉加載更多替劈、底部的已加載全部?jī)?nèi)容寄雀、首次進(jìn)入頁面的加載狀態(tài)(及加載失敗提醒)
  • 下拉刷新得滤、上拉加載更多:略
  • 首次進(jìn)入頁面的加載狀態(tài):
  • 底部的已加載全部?jī)?nèi)容:方案比較多,個(gè)人比較傾向下面兩種方案
    • 方案一:通過withLoadStateFooter實(shí)現(xiàn)盒犹,和上拉加載更多用同一套布局懂更,同一個(gè)adapter〖卑颍【參考本demo】
    • 方案二:通過ConcatAdapter.addAdapter實(shí)現(xiàn)沮协,專門顯示加載更多

動(dòng)畫

  • Animation 動(dòng)畫: 下拉刷新場(chǎng)景通過屬性動(dòng)畫實(shí)現(xiàn)
  • 官方文檔
  • 屬性動(dòng)畫
    • ValueAnimator: 屬性動(dòng)畫的主計(jì)時(shí)引擎,它也可計(jì)算要添加動(dòng)畫效果的屬性的值卓嫂。它具有計(jì)算動(dòng)畫值所需的所有核心功能慷暂,同時(shí)包含每個(gè)動(dòng)畫的計(jì)時(shí)詳情、有關(guān)動(dòng)畫是否重復(fù)播放的信息晨雳、用于接收更新事件的監(jiān)聽器以及設(shè)置待評(píng)估自定義類型的功能行瑞。為屬性添加動(dòng)畫效果分為兩個(gè)步驟:計(jì)算添加動(dòng)畫效果之后的值,以及對(duì)要添加動(dòng)畫效果的對(duì)象和屬性設(shè)置這些值餐禁。ValueAnimator 不會(huì)執(zhí)行第二個(gè)步驟血久,因此,您必須監(jiān)聽由 ValueAnimator 計(jì)算的值的更新情況帮非,并使用您自己的邏輯修改要添加動(dòng)畫效果的對(duì)象氧吐。如需了解詳情讹蘑,請(qǐng)參閱使用 ValueAnimator 添加動(dòng)畫效果部分。
    • ObjectAnimator: ValueAnimator 的子類筑舅,用于設(shè)置目標(biāo)對(duì)象和對(duì)象屬性以添加動(dòng)畫效果座慰。此類會(huì)在計(jì)算出動(dòng)畫的新值后相應(yīng)地更新屬性。在大多數(shù)情況下翠拣,您不妨使用 ObjectAnimator角骤,因?yàn)樗梢詷O大地簡(jiǎn)化對(duì)目標(biāo)對(duì)象的值添加動(dòng)畫效果這一過程。不過心剥,有時(shí)您需要直接使用 ValueAnimator邦尊,因?yàn)?ObjectAnimator 存在其他一些限制,例如要求目標(biāo)對(duì)象具有特定的訪問器方法优烧。
    • AnimationSet: 此類提供一種將動(dòng)畫分組在一起的機(jī)制蝉揍,以使它們彼此相對(duì)運(yùn)行。您可以將動(dòng)畫設(shè)置為一起播放畦娄、按順序播放或者在指定的延遲時(shí)間后播放又沾。如需了解詳情,請(qǐng)參閱使用 AnimatorSet 編排多個(gè)動(dòng)畫部分熙卡。
    • LayoutTransition:
    • LayoutAnimations:

ViewPager2

  • ViewPager2 庫
  • 官方文檔
  • 官方demo
    • 官方demo中的ViewPager2 with a Preview of Next/Prev Page 相當(dāng)于Banner中類似的場(chǎng)景
    • 官方demo中的ViewPager2 with a Nested RecyclerViews 場(chǎng)景很好杖刷,提供了解決嵌套滑動(dòng)的方案
  • ViewPager2 底層使用 RecycleView 實(shí)現(xiàn)的,所以這里不再使用 PagerAdapter 而是使用了 RecyclerView.Adapter
  • 對(duì)應(yīng)的fragment用的是 FragmentStateAdapter驳癌,而不是 FragmentStatePagerAdapter滑燃、FragmentPagerAdapter之類的

Banner

  • Banner:其實(shí)就是 ViewPager 的應(yīng)用
  • 三方庫:
  • 自己實(shí)現(xiàn)方案:
    • 讓Banner和RecyclerView分開: 通過NestedScrollView里包裹ViewPager2和RecyclerView的話,會(huì)有滑動(dòng)卡頓的問題颓鲜,即使加上android:nestedScrollingEnabled="false"屬性表窘,除非再加上setHasFixedSize(true),但是還會(huì)有其他的問題:加上setHasFixedSize(true)后甜滨,界面的數(shù)據(jù)只顯示一頁了乐严。故此方案暫時(shí)行不通了。本方案相關(guān)代碼
          binding.articleList.setHasFixedSize(true)
          binding.articleList.isNestedScrollingEnabled = false
      
    • 讓Banner成為RecyclerView的一部分:
      • 如果Banner在頂部:banner在頂部的話衣摩,就做header
      • 如果Banner在中間:在中間的話昂验,就type,或者對(duì)adapter做一個(gè)擴(kuò)展艾扮,做一個(gè)可以在中間插入的類似header既琴。畢竟type的話,寫起來也蠻麻煩的
    • 通過 ConcatAdapter 實(shí)現(xiàn):
      • 本demo就是用的該方案栏渺,demo中通過HomeFirstAdapter添加RecyclerView的ConcatAdapter中呛梆,通過HomeBannerAdapter實(shí)現(xiàn)ViewPager2的adapter。
      • 通過上述的方式加上ViewPager2之后磕诊,ViewPager2沒有影響RecyclerView的功能填物,RecyclerView上下滑動(dòng)流暢纹腌;但是ViewPager2不能滑動(dòng),因?yàn)槭录籖ecyclerView攔截了滞磺。故需新增自定義布局 NestedFrameLayout 嵌套在ViewPager2之上升薯,在 NestedFrameLayout 去處理父類的事件分發(fā),即當(dāng)左右滑動(dòng) NestedFrameLayout 時(shí)击困,執(zhí)行 NestedFrameLayout 的parent.requestDisallowInterceptTouchEvent(true)方法涎劈,讓ViewPager2消費(fèi)事件。
    • 通過 MultiTypeAdapter 實(shí)現(xiàn):暫時(shí)沒有驗(yàn)證
    • 工行融e購實(shí)現(xiàn)方案:首頁除了viewpager功能都放在AppBarLayout里面阅茶,但是這樣TabLayout可能就要和融e購一樣放在下面了蛛枚,不是想要的。用ConcatAdapter也可以實(shí)現(xiàn)工行融e購的首頁效果脸哀。
    • 京東首頁實(shí)現(xiàn)方案:自定義控件實(shí)現(xiàn)蹦浦。用ConcatAdapter也能實(shí)現(xiàn)京東首頁效果

NestedScrollView

  • 直接在 NestedScrollView 中放入 ViewPager2 和 RecyclerView 時(shí),會(huì)出現(xiàn)滑動(dòng)卡頓撞蜂。解決方案參考
  • NestedScrollView
  • 事件沖突的原因:Android 的事件分發(fā)機(jī)制中盲镶,只要有一個(gè)控件消費(fèi)了事件,其他控件就沒辦法再接收到這個(gè)事件了蝌诡。因此溉贿,當(dāng)有嵌套滑動(dòng)場(chǎng)景時(shí),我們都需要自己手動(dòng)解決事件沖突浦旱。而在 Android 5.0 Lollipop 之后宇色,Google 官方通過 嵌套滑動(dòng)機(jī)制 解決了傳統(tǒng) Android 事件分發(fā)無法共享事件這個(gè)問題。
  • 嵌套滑動(dòng)機(jī)制:嵌套滑動(dòng)機(jī)制 的基本原理可以認(rèn)為是事件共享闽寡,即當(dāng)子控件接收到滑動(dòng)事件代兵,準(zhǔn)備要滑動(dòng)時(shí),會(huì)先通知父控件(startNestedScroll)爷狈;然后在滑動(dòng)之前,會(huì)先詢問父控件是否要滑動(dòng)(dispatchNestedPreScroll)裳擎;如果父控件響應(yīng)該事件進(jìn)行了滑動(dòng)涎永,那么就會(huì)通知子控件它具體消耗了多少滑動(dòng)距離;然后交由子控件處理剩余的滑動(dòng)距離鹿响;最后子控件滑動(dòng)結(jié)束后羡微,如果滑動(dòng)距離還有剩余,就會(huì)再問一下父控件是否需要在繼續(xù)滑動(dòng)剩下的距離(dispatchNestedScroll)...

TabLayout

  • TabLayout
  • 和ViewPager2惶我、Fragment應(yīng)用

BottomNavigationView

Constraint Layout

Glide

  • 和Coil對(duì)比虑绵,建議換成Coil加載圖片

Cookie

  • CookieManager
  • 本demo中,和收藏相關(guān)都需要登錄操作闽烙,建議登錄將返回的cookie(其中包含賬號(hào)翅睛、密碼)持久化到本地即可。

WebView

  • WebView 庫
  • 官方文檔
  • 官方demo
  • 本demo中跳轉(zhuǎn)到WebFragment是通過 Bundle 傳遞參數(shù)黑竞,沒有用通過 Navigation 的 Safe Args 導(dǎo)航實(shí)現(xiàn)
  • 本demo中的WebView適配了深色主題捕发。

啟動(dòng)界面

  • 方案:通過windowSplashscreenContent屬性或者SplashActivity界面
    • 注意 windowSplashscreenContent屬性是在Android8.0(v26)上才有的,如果在之前的版本上適配啟動(dòng)界面很魂,應(yīng)該新增個(gè)Activity爬骤,即 SplashActivity。
  • 冷啟動(dòng)莫换、熱啟動(dòng)
  • Splash Screen:展示品牌Logo或Slogan
    • 如果只是單純的顯示個(gè)界面霞玄,只需要在themes里設(shè)置<item name="android:windowSplashscreenContent">@color/jetpack_green_500</item>即可。
  • Advertisement Screen:展示節(jié)日活動(dòng)或日常廣告
  • Guide Screen:演示重點(diǎn)功能拉岁,一般只展示一次
  • 參考博客:Android 12上全新的應(yīng)用啟動(dòng)API坷剧,適配一下?

樣式系統(tǒng)喊暖、沉浸式(在Android6.0惫企、8.1、10陵叽、11上已經(jīng)適配狞尔,詳見demo

觸摸手勢(shì)

項(xiàng)目地址:https://github.com/lelelongwang/WanJetpack

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末招狸,一起剝皮案震驚了整個(gè)濱河市敬拓,隨后出現(xiàn)的幾起案子邻薯,更是在濱河造成了極大的恐慌,老刑警劉巖恩尾,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弛说,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡翰意,警方通過查閱死者的電腦和手機(jī)木人,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冀偶,“玉大人醒第,你說我怎么就攤上這事〗” “怎么了稠曼?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)客年。 經(jīng)常有香客問我霞幅,道長(zhǎng),這世上最難降的妖魔是什么量瓜? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任司恳,我火速辦了婚禮,結(jié)果婚禮上绍傲,老公的妹妹穿的比我還像新娘扔傅。我一直安慰自己,他們只是感情好烫饼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布猎塞。 她就那樣靜靜地躺著,像睡著了一般杠纵。 火紅的嫁衣襯著肌膚如雪荠耽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天淡诗,我揣著相機(jī)與錄音骇塘,去河邊找鬼。 笑死韩容,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的唐瀑。 我是一名探鬼主播群凶,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼哄辣!你這毒婦竟也來了请梢?” 一聲冷哼從身側(cè)響起赠尾,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毅弧,沒想到半個(gè)月后气嫁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡够坐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年寸宵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片元咙。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梯影,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出庶香,到底是詐尸還是另有隱情甲棍,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布赶掖,位于F島的核電站感猛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏奢赂。R本人自食惡果不足惜陪白,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呈驶。 院中可真熱鬧拷泽,春花似錦、人聲如沸袖瞻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽聋迎。三九已至脂矫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霉晕,已是汗流浹背庭再。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牺堰,地道東北人拄轻。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像伟葫,于是被迫代替她去往敵國(guó)和親恨搓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容