四篇文章帶你快速入門Jetpck(終)之Navigation,Paging
Jetpack
Jetpack 是一個由多個庫組成的套件豌熄,可幫助開發(fā)者遵循最佳做法,減少樣板代碼并編寫可在各種 Android 版本和設備中一致運行的代碼物咳,讓開發(fā)者精力集中編寫重要的代碼锣险。
Android Architecture Component (AAC)蹄皱。
官方推薦架構
請注意,每個組件僅依賴于其下一級的組件芯肤。例如巷折,Activity 和 Fragment 僅依賴于視圖模型。存儲區(qū)是唯一依賴于其他多個類的類崖咨;在本例中锻拘,存儲區(qū)依賴于持久性數(shù)據(jù)模型和遠程后端數(shù)據(jù)源。
Navigation
Navigation的誕生
單個Activity嵌套多個Fragment的UI架構模式,已經被大多數(shù)Android工程師所接受和采用击蹲。但是署拟,對Fragment的管理一直是一件比較麻煩的事情。
需要在FragmentManager和FragmentTransaction來管理Fragment之間的切換歌豺。
頁面的切換通常還包括對應用程序App bar管理推穷,F(xiàn)ragment間的切換動畫,以及Fragment間的參數(shù)傳遞类咧,
Fragment和App bar管理和使用的過程顯得很混亂馒铃。
Navigation 的優(yōu)點
- 可視化的頁面導航圖。
- 通過destination和action完成頁面間的導航痕惋。
- 方便添加頁面切換動畫区宇。
- 頁面間類型安全的參數(shù)傳遞。
- 通過NavigationUI類值戳,對菜單议谷,底部導航,抽屜菜單導航進行統(tǒng)一的管理述寡。
添加依賴庫
implementation "androidx.navigation:navigation-fragment:2.3.1"
implementation "androidx.navigation:navigation-ui:2.3.1"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
創(chuàng)建Navigation
創(chuàng)建Navigation Graph柿隙,nav_graph.xml
-
添加NavHostFragment
<fragment android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph" />
- android:name="androidx.navigation.fragment.NavHostFragment" ,特殊的Fragment鲫凶。
- app:defaultNavHost="true"禀崖,系統(tǒng)自動處理返回鍵。
- app:navGraph="@navigation/nav_graph"螟炫,設置該Fragment對應的導航圖波附。
創(chuàng)建destination
完成Fragment之間的切換
-
使用NavController完成導航
btn_toSettingFragment.setOnClickListener { Navigation.findNavController(it).navigate(R.id.action_mainFragment_to_settingFragment) }
-
添加頁面切換東華效果,創(chuàng)建anim文件夾昼钻,添加常用東華
<action android:id="@+id/action_mainFragment_to_settingFragment" app:destination="@id/settingFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" />
Fragment傳遞參數(shù)
常見的傳遞參數(shù)的方式
val bundle = Bundle()
bundle.apply {
putString("user_name", "yaoxin")
putInt("age", 33)
}
Navigation.findNavController(it).navigate(R.id.action_mainFragment_to_settingFragment, bundle)
使用sate Arg傳遞參數(shù)
- 添加插件外層build.gradle
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.1"
}
- 添加依賴內層build.gradle
apply plugin: 'androidx.navigation.safeargs'
- 傳遞數(shù)據(jù)
val bundle = MainFragmentArgs.Builder().setUserName("yaoxin1").setAge(12).build().toBundle()
Navigation.findNavController(it).navigate(R.id.action_mainFragment_to_settingFragment, bundle)
- 獲取數(shù)據(jù)
Log.d("SettingFragment", " username : ${arguments?.let { MainFragmentArgs.fromBundle(it).userName }}")
Log.d("SettingFragment", " age : ${arguments?.let { MainFragmentArgs.fromBundle(it).age }}")
NacvigationUI的使用
actionBar
添加menu.xml掸屡。
在Acitivity中重寫onCreateOptionsMenu實例化菜單。
初始化然评,AppBarConfiguration仅财,NavController類并使用NacvigationUI完成配置。
重寫onOptionsItemSelected碗淌,onSupportNavigateUp方法盏求,實現(xiàn)點擊抖锥,頁面切換。
-
實現(xiàn)addOnDestinationChangedListener接口碎罚,監(jiān)聽頁面變化磅废。
navController.addOnDestinationChangedListener { _, _, _ -> Log.d("NavigationActivity", "頁面發(fā)生了改變") }
DrawLayout + NavigationView 抽屜菜單
-
添加DrawerLayout布局
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/dl_drawerLayout" android:layout_width="match_parent" android:layout_height="match_parent" />
-
添加NavigationView控件
<com.google.android.material.navigation.NavigationView android:id="@+id/nv_NavigationView" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" app:menu="@menu/menu_settings" />
-
使用NacvigationUI完成配置
appBarConfiguration = AppBarConfiguration.Builder(navController.graph).setDrawerLayout(dl_drawerLayout).build() NavigationUI.setupWithNavController(nv_navigationView, navController)
ButtomNavigationView 底部菜單
-
添加BottomNavigationView控件
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bnv_bottomNavigationView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="@color/design_default_color_primary" app:itemTextColor="#FFFFFF" app:menu="@menu/menu_setting" />
-
使用NacvigationUI完成配置
NavigationUI.setupWithNavController(bnv_bottomNavigationView, navController)
示例
NavigationActivity
class NavigationActivity : AppCompatActivity() {
lateinit var appBarConfiguration: AppBarConfiguration
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration.Builder(navController.graph).build()
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
navController.addOnDestinationChangedListener { _, _, _ ->
Log.d("NavigationActivity", "頁面發(fā)生了改變")
}
appBarConfiguration = AppBarConfiguration.Builder(navController.graph).setDrawerLayout(dl_drawerLayout).build()
NavigationUI.setupWithNavController(nv_navigationView, navController)
NavigationUI.setupWithNavController(bnv_bottomNavigationView, navController)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_setting, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return super.onOptionsItemSelected(item) || NavigationUI.onNavDestinationSelected(item, navController)
}
override fun onSupportNavigateUp(): Boolean {
return super.onSupportNavigateUp() || NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
activity_navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dl_drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".seventh.NavigationActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bnv_bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@color/design_default_color_primary"
app:itemTextColor="#FFFFFF"
app:menu="@menu/menu_setting" />
</RelativeLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nv_navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/menu_setting" />
</androidx.drawerlayout.widget.DrawerLayout>
MainFragment
class MainFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_toSettingFragment.setOnClickListener {
val bundle = Bundle()
bundle.apply {
putString("user_name", "yaoxin")
putInt("age", 33)
}
val bundle1 = MainFragmentArgs.Builder().setUserName("yaoxin1").setAge(12).build().toBundle()
Navigation.findNavController(it).navigate(R.id.action_mainFragment_to_settingFragment, bundle1)
}
}
}
SettingFragment
class SettingFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_setting, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bundle = arguments
if (bundle != null) {
Log.d("SettingFragment", " username : ${bundle.getString("user_name")}")
Log.d("SettingFragment", " age : ${bundle.getInt("age")}")
Log.d("SettingFragment", " username : ${arguments?.let { MainFragmentArgs.fromBundle(it).userName }}")
Log.d("SettingFragment", " age : ${arguments?.let { MainFragmentArgs.fromBundle(it).age }}")
}
}
}
menu_setting.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/mainFragment"
android:title="主頁" />
<item
android:id="@+id/settingFragment"
android:title="設置" />
</menu>
nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="com.yx.androidseniorprepare.seventh.MainFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_main">
<action
android:id="@+id/action_mainFragment_to_settingFragment"
app:destination="@id/settingFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<argument
android:name="user_name"
android:defaultValue="unknown"
app:argType="string" />
<argument
android:name="age"
android:defaultValue="0"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/settingFragment"
android:name="com.yx.androidseniorprepare.seventh.SettingFragment"
android:label="fragment_setting"
tools:layout="@layout/fragment_setting" />
</navigation>
Paging
Paging組件的意義
分頁加載是在應用程序開發(fā)過程中十分常見的需求,我們經常需要以列表的形式加載大量的數(shù)據(jù)荆烈,這些數(shù)據(jù)通常來自網絡或本地數(shù)據(jù)庫拯勉。若一次加載大量的數(shù)據(jù),這些數(shù)據(jù)消耗大量的時間和數(shù)據(jù)流量憔购。
[圖片上傳失敗...(image-19ef07-1610060544137)]
然而用戶實際需要的可能只是部分數(shù)據(jù)宫峦,因此有了分頁加載,分業(yè)加載按需加載倦始,在不影響用戶體驗的同時斗遏,還能節(jié)省數(shù)據(jù)流量,提升應用性能鞋邑。
數(shù)據(jù)來源
[圖片上傳失敗...(image-8f3c85-1610060544137)]
工作原理
- 在RecyclerView的滑動過程中,會觸發(fā)PagedListAdapter類中的onBindViewHolder()方法账蓉。數(shù)據(jù)與RecycleView中Item布局的UI控件正是在該方法中進行綁定的枚碗。
- 當RecyclerView滑動到底部時,在onBindViewHolder()方法中所調用的getItem()方法會通知PagedList,當前需要載入更多數(shù)據(jù)铸本。
- 接著肮雨,PagedList會根據(jù)PageList.Config中的配置通知DataSource執(zhí)行具體的數(shù)據(jù)獲取工作。
- DataSource從網絡/本地數(shù)據(jù)庫取得數(shù)據(jù)后箱玷,交給PagedList,PagedList將持有這些數(shù)據(jù)怨规。
- PagedList將數(shù)據(jù)交給PagedListAdapter中的DiffUtil進行比對和處理。
- 數(shù)據(jù)在經過處理后锡足,交由RecyclerView進行展示波丰。
Paging的3個核心類
- PagedListAdapter:
- 適配器,RecyclerView的適配器舶得,通過分析數(shù)據(jù)是否發(fā)生了變化掰烟,負責處理UI展示的邏輯(增加/刪除/替換等)。
- PageList:
- 負責通知DataSource何時獲取數(shù)據(jù)沐批,以及如何獲取數(shù)據(jù)纫骑。例如,何時加載第一頁/下一頁九孩、第一頁加載的數(shù)量先馆、提前多少條數(shù)據(jù)開始執(zhí)行預加載等。需要注意的是躺彬,從DataSource獲取的數(shù)據(jù)將存儲在PagedList中煤墙。
- DataSource
- 數(shù)據(jù)源缤底,執(zhí)行具體的數(shù)據(jù)載入工作。注意:數(shù)據(jù)的載入需要在工作線程中執(zhí)行番捂。數(shù)據(jù)可以來自網絡个唧,也可以來自本地數(shù)據(jù)庫,如Room.根據(jù)分頁機制的不同设预,Paging為我們提供了3種DataSource徙歼。
添加依賴權限
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation "android.arch.lifecycle:extensions:1.1.1"
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
<uses-permission android:name="android.permission.INTERNET" />
3種DataSource
PositionalDataSource
適用于可通過任意位置加載數(shù)據(jù),且目標數(shù)據(jù)源數(shù)量固定的情況鳖枕。例如魄梯,若請求時攜帶的參數(shù)為start=2&count=5,則表示向服務端請求從第2條數(shù)據(jù)開始往后的5條數(shù)據(jù)。
[圖片上傳失敗...(image-afa103-1610060544137)]接口:https://gank.io/api/v2/data/category/Girl/type/Girl/page/1/count/10
PageKeyedDataSource
- 適用于數(shù)據(jù)源以"頁"的方式進行請求的情況宾符。例如彬碱,若請求時攜帶的參數(shù)為page=2&pageSize=5,則表示數(shù)據(jù)源以5條數(shù)據(jù)為一頁或辖,當前返回第二頁的5條數(shù)據(jù)。
[圖片上傳失敗...(image-ab156b-1610060544137)] - 接口:https://api.stackexchange.com/2.2/users?page=1&pagesize=6&site=stackoverflow
ItemKeyedDataSource
適用于當目標數(shù)據(jù)的下一頁需要依賴于上一頁數(shù)據(jù)中最后一個對象中的某個字段作為key的情況。此類分頁形式常見于評論功能的實現(xiàn)遣妥。例如,若上一頁數(shù)據(jù)中最后一個對象的key為9527,那么在請求下一頁時兴猩,需要攜帶參數(shù)since=9527&pageSize=5,則服務器會返回key=9527之后的5條數(shù)據(jù)
[圖片上傳失敗...(image-9c9ff7-1610060544137)]
PositionalDataSource 使用
-
創(chuàng)建實體類
data class Users(var page: Int, var page_count: Int, var total_counts: Int, var data: List<User>) data class User(var _id: String, var author: String, var createdAt: String, var url: String)
-
創(chuàng)建API接口
interface Api { @GET("page/{page}/count/{count}") fun getUsers(@Path("page") page:Int,@Path("count") pageSize:Int):Call<Users> }
-
創(chuàng)建網絡請求框架
object RetrofitClient { private const val BASE_URL = "https://gank.io/api/v2/data/category/Girl/type/Girl/" private var retrofit: Retrofit? = null init { retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build() } fun getApi(): Api? { return retrofit?.create(Api::class.java) } }
-
創(chuàng)建UserDataSource繼承PositionalDataSource
class UserDataSource : PositionalDataSource<User>() { companion object { const val PER_PAGE = 10 var startPosition = 1 } override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<User>) { RetrofitClient .getApi()?.getUsers(startPosition, PER_PAGE) ?.enqueue(object : Callback<Users?> { override fun onResponse(call: Call<Users?>, response: Response<Users?>) { if (response.body() != null) { Log.e( "loadInitial()", "startPosition:" + params.requestedStartPosition + " response:" + response.body() ) callback.onResult( response.body()!!.data, response.body()!!.page, response.body()!!.total_counts ) } } override fun onFailure(call: Call<Users?>, t: Throwable) { Log.e("loadInitial()", call.toString()) } }) } override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<User>) { RetrofitClient .getApi()?.getUsers(startPosition + 1, PER_PAGE) ?.enqueue(object : Callback<Users?> { override fun onResponse(call: Call<Users?>, response: Response<Users?>) { if (response.body() != null) { startPosition += 1 Log.e( "loadRange()", "startPosition:" + params.startPosition + " response:" + response.body() ) callback.onResult(response.body()!!.data) } } override fun onFailure(call: Call<Users?>, t: Throwable) { Log.e("loadRange()", call.toString()) } }) } }
-
創(chuàng)建UserDataSourceFactory負責創(chuàng)建UserDataSource埂陆,并使用LiveData包裝UserDataSource講其暴露給UserViewModel
class UserDataSourceFactory : DataSource.Factory<Int, User>() { private val liveDataSource = MutableLiveData<UserDataSource>() override fun create(): DataSource<Int, User> { val dataSource = UserDataSource() liveDataSource.postValue(dataSource) return dataSource } }
-
創(chuàng)建ViewModel
class UserViewModel : ViewModel() { var userPagedList: LiveData<PagedList<User>>? = null init { val config = PagedList.Config.Builder() // 是否顯示占位。默認為true退客。當被設置為true時链嘀,要求在DataSource中提供數(shù)據(jù)源的總量,否則會報錯茫藏。 // 這是因為RecyclerView需要知道數(shù)據(jù)總量包个,為這些數(shù)據(jù)預留位置 .setEnablePlaceholders(true) //設置每頁加載的數(shù)量 .setPageSize(UserDataSource.PER_PAGE) //設置距離底部還有多少條數(shù)據(jù)時加載下一頁數(shù)據(jù) .setPrefetchDistance(0) //重點碧囊,這里需要設置預獲取的值>0树灶,否則getKey()和loadBefore()以及l(fā)oadAfter()不會被調用 //首次初始化加載的數(shù)量,需是分頁加載數(shù)量的倍數(shù)天通。若沒有設置,則默認是PER_PAGE的三倍 .setInitialLoadSizeHint(UserDataSource.PER_PAGE * 4) //好像沒有效果 //設置PagedList所能承受的最大數(shù)量熄驼,一般來說是PER_PAGE的許多許多倍像寒,超過可能會出現(xiàn)異常烘豹。 .setMaxSize(65536 * UserDataSource.PER_PAGE).build() // 通過LivePagedListBuilder創(chuàng)建PagedList userPagedList = LivePagedListBuilder(UserDataSourceFactory(), config).build() } }
-
創(chuàng)建PagedListAdapter
class UserAdapter(context: Context) : PagedListAdapter<User, UserAdapter.UserViewHolder>(DIFF_CALLBACK) { private var context: Context? = null init { this.context = context } companion object { /** * 用于計算列表中兩個非空項之間的差異的回調。 * * 之前數(shù)據(jù)更新了诺祸,需要通過notifyDataSetChanged()通知整個RecyclerView携悯,效率不高 * 使用DiffUtil只會更新需要更新的Item,不需要刷新整個RecyclerView筷笨,并且可以在Item刪除的時候加上動畫效果 */ private val DIFF_CALLBACK: DiffUtil.ItemCallback<User> = object : DiffUtil.ItemCallback<User>() { /** * 當DiffUtil想要檢測兩個對象是否代表同一個Item時憔鬼,調用該方法進行判斷 */ override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { return oldItem._id == newItem._id } /** * 當DiffUtil想要檢測兩個Item是否有一樣的數(shù)據(jù)時,調用該方法進行判斷 * * 內容如果更新了胃夏,展示給用戶看的東西可能也需要更新轴或,所以需要這個判斷 */ override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder { val view = LayoutInflater.from(context).inflate(R.layout.item_user, parent, false) return UserViewHolder(view) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val user = getItem(position) if (user != null) { Picasso.get().load(user.url).placeholder(R.drawable.ic_launcher_background).error(R.drawable.ic_launcher_background).into(holder.ivAvatar) holder.tvName.text = user.author + " " + user.createdAt } else { holder.ivAvatar.setImageResource(R.drawable.ic_launcher_background) holder.tvName.text = "" } } class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var ivAvatar: ImageView = itemView.findViewById(R.id.iv_Avatar) var tvName: TextView = itemView.findViewById(R.id.tv_Name) } }
xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="12dp"> <ImageView android:id="@+id/ivAvatar" android:layout_width="80dp" android:layout_height="80dp" android:layout_alignParentLeft="true" /> <TextView android:id="@+id/tvName" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" android:gravity="left" android:layout_marginLeft="12dp" android:layout_toRightOf="@+id/ivAvatar" android:textSize="24sp" /> </RelativeLayout>
-
配置Activity
class PositionalDataSourceActivity : AppCompatActivity() { val TAG = this.javaClass.simpleName override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_positional_data_source) rv_recycler.layoutManager = LinearLayoutManager(this) rv_recycler.setHasFixedSize(true) val userAdapter = UserAdapter(this) val movieViewModel = ViewModelProviders.of(this)[UserViewModel::class.java] movieViewModel.userPagedList!!.observe(this, { Log.d(TAG, "onChange()->$it") userAdapter.submitList(it) }) rv_recycler.adapter = userAdapter title = "PositionalDataSourceActivity" } }
效果圖
[圖片上傳失敗...(image-896f07-1610060544137)]
PageKeyedDataSource使用
-
創(chuàng)建實體類
data class User(var account_id: Int, var display_name: String?, var profile_image: String?) data class UserResponse(var items: List<User>?, var has_more: Boolean?)
-
創(chuàng)建API接口
interface Api { @GET("users") fun getUsers(@Query("page") page: Int, @Query("pagesize") pageSize: Int, @Query("site") site: String): Call<UserResponse> }
-
創(chuàng)建網絡請求框架
object RetrofitClient { private const val BASE_URL = "https://api.stackexchange.com/2.2/" private var retrofit: Retrofit? = null init { retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build() } fun getApi(): Api? { return retrofit?.create(Api::class.java) } }
-
創(chuàng)建UserDataSource繼承PageKeyedDataSource
class UserDataSource : PageKeyedDataSource<Int, User>() { companion object { const val FIRST_PAGE = 1 const val PER_PAGE = 10 const val SITE = "stackoverflow" } override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, User>) { RetrofitClient.getApi()?.getUsers(FIRST_PAGE, PER_PAGE, SITE)?.enqueue(object : Callback<UserResponse> { override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) { if (response.body() != null) { Log.e("loadInitial()", " response:" + response.body()) response.body()!!.items?.toMutableList()?.let { callback.onResult(it, null, FIRST_PAGE + 1) } } } override fun onFailure(call: Call<UserResponse>, t: Throwable) { TODO("Not yet implemented") } }) } override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, User>) { } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, User>) { Log.e("loadAfter()", "called"); RetrofitClient.getApi()?.getUsers(params.key, PER_PAGE, SITE)?.enqueue(object : Callback<UserResponse> { override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) { if (response.body() != null) { Log.e("loadAfter()", " response:" + response.body()) val nextKey = if (response.body()!!.has_more!!) params.key + 1 else null response.body()!!.items?.let { callback.onResult(it, nextKey) } } } override fun onFailure(call: Call<UserResponse>, t: Throwable) { TODO("Not yet implemented") } }) }
-
創(chuàng)建UserDataSourceFactory負責創(chuàng)建UserDataSource,并使用LiveData包裝UserDataSource講其暴露給UserViewModel
class UserDataSourceFactory : DataSource.Factory<Int, User>() { private val liveDataSource = MutableLiveData<UserDataSource>() override fun create(): DataSource<Int, User> { val dataSource = UserDataSource() liveDataSource.postValue(dataSource) return dataSource } }
-
創(chuàng)建ViewModel
class UserViewModel : ViewModel() { var userPagedList: LiveData<PagedList<User>>? = null init { val config = PagedList.Config.Builder() // 是否顯示占位仰禀。默認為true照雁。當被設置為true時,要求在DataSource中提供數(shù)據(jù)源的總量答恶,否則會報錯饺蚊。 // 這是因為RecyclerView需要知道數(shù)據(jù)總量亥宿,為這些數(shù)據(jù)預留位置 .setEnablePlaceholders(true) //設置每頁加載的數(shù)量 .setPageSize(UserDataSource.PER_PAGE) //設置距離底部還有多少條數(shù)據(jù)時加載下一頁數(shù)據(jù) .setPrefetchDistance(3) //重點,這里需要設置預獲取的值>0映企,否則getKey()和loadBefore()以及l(fā)oadAfter()不會被調用 //首次初始化加載的數(shù)量,需是分頁加載數(shù)量的倍數(shù)双絮。若沒有設置囤攀,則默認是PER_PAGE的三倍 .setInitialLoadSizeHint(UserDataSource.PER_PAGE * 3) //好像沒有效果 //設置PagedList所能承受的最大數(shù)量,一般來說是PER_PAGE的許多許多倍蝌衔,超過可能會出現(xiàn)異常噩斟。 .setMaxSize(65536 * UserDataSource.PER_PAGE).build() // 通過LivePagedListBuilder創(chuàng)建PagedList userPagedList = LivePagedListBuilder(UserDataSourceFactory(), config).build() } }
-
創(chuàng)建PagedListAdapter
class UserAdapter(context: Context) : PagedListAdapter<User, UserAdapter.UserViewHolder>(DIFF_CALLBACK) { private var context: Context? = null init { this.context = context } companion object { /** * 用于計算列表中兩個非空項之間的差異的回調沛简。 * * 之前數(shù)據(jù)更新了,需要通過notifyDataSetChanged()通知整個RecyclerView撒顿,效率不高 * 使用DiffUtil只會更新需要更新的Item凤壁,不需要刷新整個RecyclerView,并且可以在Item刪除的時候加上動畫效果 */ private val DIFF_CALLBACK: DiffUtil.ItemCallback<User> = object : DiffUtil.ItemCallback<User>() { /** * 當DiffUtil想要檢測兩個對象是否代表同一個Item時唧席,調用該方法進行判斷 */ override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { return oldItem.account_id == newItem.account_id } /** * 當DiffUtil想要檢測兩個Item是否有一樣的數(shù)據(jù)時,調用該方法進行判斷 * * 內容如果更新了徒仓,展示給用戶看的東西可能也需要更新掉弛,所以需要這個判斷 */ override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder { val view = LayoutInflater.from(context).inflate(R.layout.item_user, parent, false) return UserViewHolder(view) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val user = getItem(position) if (user != null) { Picasso.get().load(user.profile_image).placeholder(R.drawable.ic_launcher_background).error(R.drawable.ic_launcher_background).into(holder.ivAvatar) holder.tvName.text = user.display_name } else { holder.ivAvatar.setImageResource(R.drawable.ic_launcher_background) holder.tvName.text = "" } } class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var ivAvatar: ImageView = itemView.findViewById(R.id.iv_Avatar) var tvName: TextView = itemView.findViewById(R.id.tv_Name) } }
-
配置Activity
class PageKeyedDataSourceActivity : AppCompatActivity() { val TAG = this.javaClass.simpleName override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_page_keyed_data_source) rv_recycler.layoutManager = LinearLayoutManager(this) rv_recycler.setHasFixedSize(true) val userAdapter = UserAdapter(this) val movieViewModel = ViewModelProviders.of(this)[UserViewModel::class.java] movieViewModel.userPagedList!!.observe(this, { Log.e(TAG, "onChange()->$it") userAdapter.submitList(it) }) rv_recycler.adapter = userAdapter title = "PageKeyedDataSourceActivity" } }
效果
[圖片上傳失敗...(image-3f5622-1610060544137)]
ItemKeyedDataSource使用
-
創(chuàng)建實體類
data class User(var id: Int, @SerializedName("login") var name: String?, @SerializedName("avatar_url") var avatar: String?)
-
創(chuàng)建API接口
interface Api { @GET("users") fun getUsers(@Query("since") since: Int, @Query("per_page") perPage: Int): Call<List<User>> }
-
創(chuàng)建網絡請求框架
object RetrofitClient { private const val BASE_URL = "https://api.github.com/" private var retrofit: Retrofit? = null init { retrofit = Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build() } fun getApi(): Api? { return retrofit?.create(Api::class.java) } }
-
創(chuàng)建UserDataSource繼承ItemKeyedDataSource
class UserDataSource : ItemKeyedDataSource<Int, User>() { companion object { const val PER_PAGE = 12 } override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<User>) { Log.e("loadInitial()", "called") val since = 0 RetrofitClient.getApi()?.getUsers(since, PER_PAGE)?.enqueue(object : Callback<List<User>> { override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) { if (response.body() != null) { Log.e("loadInitial()", " response:" + response.body()) callback.onResult(response.body()!!) } } override fun onFailure(call: Call<List<User>>, t: Throwable) { TODO("Not yet implemented") } }) } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<User>) { Log.e("loadAfter()", "called"); RetrofitClient.getApi()?.getUsers(params.key, PER_PAGE)?.enqueue(object : Callback<List<User>> { override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) { if (response.body() != null) { Log.e("loadAfter()", " response:" + response.body()) callback.onResult(response.body()!!) } } override fun onFailure(call: Call<List<User>>, t: Throwable) { TODO("Not yet implemented") } }) } override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<User>) { Log.e("loadBefore()", "called") } override fun getKey(item: User): Int { Log.e("getkey()", "called") return item.id } }
-
創(chuàng)建UserDataSourceFactory負責創(chuàng)建UserDataSource,并使用LiveData包裝UserDataSource講其暴露給UserViewModel
class UserDataSourceFactory : DataSource.Factory<Int, User>() { private val liveDataSource = MutableLiveData<UserDataSource>() override fun create(): DataSource<Int, User> { val dataSource = UserDataSource() liveDataSource.postValue(dataSource) return dataSource } }
-
創(chuàng)建ViewModel
class UserViewModel : ViewModel() { var userPagedList: LiveData<PagedList<User>>? = null init { val config = PagedList.Config.Builder() // 是否顯示占位谬晕。默認為true。當被設置為true時不撑,要求在DataSource中提供數(shù)據(jù)源的總量焕檬,否則會報錯实愚。 // 這是因為RecyclerView需要知道數(shù)據(jù)總量腊敲,為這些數(shù)據(jù)預留位置 .setEnablePlaceholders(true) //設置每頁加載的數(shù)量 .setPageSize(UserDataSource.PER_PAGE) //設置距離底部還有多少條數(shù)據(jù)時加載下一頁數(shù)據(jù) .setPrefetchDistance(3) //重點,這里需要設置預獲取的值>0介时,否則getKey()和loadBefore()以及l(fā)oadAfter()不會被調用 //首次初始化加載的數(shù)量榕吼,需是分頁加載數(shù)量的倍數(shù)。若沒有設置乱凿,則默認是PER_PAGE的三倍 .setInitialLoadSizeHint(UserDataSource.PER_PAGE * 3) //好像沒有效果 //設置PagedList所能承受的最大數(shù)量徒蟆,一般來說是PER_PAGE的許多許多倍,超過可能會出現(xiàn)異常闹蒜。 .setMaxSize(65536 * UserDataSource.PER_PAGE).build() // 通過LivePagedListBuilder創(chuàng)建PagedList userPagedList = LivePagedListBuilder(UserDataSourceFactory(), config).build() } }
-
創(chuàng)建PagedListAdapter
class UserAdapter(context: Context) : PagedListAdapter<User, UserAdapter.UserViewHolder>(DIFF_CALLBACK) { private var context: Context? = null init { this.context = context } companion object { /** * 用于計算列表中兩個非空項之間的差異的回調姥闪。 * * 之前數(shù)據(jù)更新了催式,需要通過notifyDataSetChanged()通知整個RecyclerView荣月,效率不高 * 使用DiffUtil只會更新需要更新的Item,不需要刷新整個RecyclerView顿天,并且可以在Item刪除的時候加上動畫效果 */ private val DIFF_CALLBACK: DiffUtil.ItemCallback<User> = object : DiffUtil.ItemCallback<User>() { /** * 當DiffUtil想要檢測兩個對象是否代表同一個Item時咽白,調用該方法進行判斷 */ override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { return oldItem.id == newItem.id } /** * 當DiffUtil想要檢測兩個Item是否有一樣的數(shù)據(jù)時,調用該方法進行判斷 * * 內容如果更新了授段,展示給用戶看的東西可能也需要更新侵贵,所以需要這個判斷 */ override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder { val view = LayoutInflater.from(context).inflate(R.layout.item_user, parent, false) return UserViewHolder(view) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val user = getItem(position) if (user != null) { Picasso.get().load(user.avatar).placeholder(R.drawable.ic_launcher_background).error(R.drawable.ic_launcher_background).into(holder.ivAvatar) holder.tvName.text = user.name } else { holder.ivAvatar.setImageResource(R.drawable.ic_launcher_background) holder.tvName.text = "" } } class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var ivAvatar: ImageView = itemView.findViewById(R.id.iv_Avatar) var tvName: TextView = itemView.findViewById(R.id.tv_Name) } }
-
配置Activity
class ItemKeyedDataSourceActivity : AppCompatActivity() { val TAG = this.javaClass.simpleName override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_item_keyed_data_source) rv_recycler.layoutManager = LinearLayoutManager(this) rv_recycler.setHasFixedSize(true) val userAdapter = UserAdapter(this) val movieViewModel = ViewModelProviders.of(this)[UserViewModel::class.java] movieViewModel.userPagedList!!.observe(this, { Log.e(TAG, "onChange()->$it") userAdapter.submitList(it) }) rv_recycler.adapter = userAdapter title = "ItemKeyedDataSourceActivity" } }
效果
[圖片上傳失敗...(image-f7efec-1610060544137)]