iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 27
0
Mobile Development

大一之 Android Kotlin 自習心路歷程系列 第 27

[Day 27] Android in Kotlin: RecyclerView 和 ViewPager2 實作分享

  • 分享至 

  • xImage
  •  

以下出現之畫面皆為參照 FieC 公司開發的 Ahorro 所仿造出來的,該創意全為該公司所有,且本文之全部內容都與其公司無任何直接關係。

Ahorro - 輕鬆記帳,簡單理財

※只顯示部份程式碼

在上述的 app 中,有一個部份是可以透過滑動,來選取其支出或收入的類別

~中間的早餐123~

筆者選用 view pager 2 加上擁有 recycler view 的 fragment 來達到這個效果。這篇文主要就是來秀實作的
在有 view pager 的 activity 中,先取得資料的筆數。

View Pager 的部份

Dao

@Dao
interface AccountingDao {
    @Query("SELECT * FROM category_table")
    fun getAllCategories(): LiveData<List<CategoryEntity>>
}

大同小異的 view model

class AddNewViewModel(
    application: Application
): AndroidViewModel(application) {
    private val repository: Repository
    var categories: LiveData<List<CategoryEntity>>


    init {
        val accountingDao = AccountingDatabase.getDatabase(application, viewModelScope).getAccountingDao()
        repository = Repository(accountingDao)
        
        categories= repository.getAllCategories()
    }

因為我的這支程式,是採用前面所說的 mvvm 架構,所以就有了 adapter 的資料就從 view model 中取得。

val pagerType: ViewPager2 = findViewById(R.id.pager_type)
val typePagerAdapter = TypePagerAdapter(viewModel, this)

viewModel.categories.observe(this, Observer {
    pagerType.adapter = typePagerAdapter
}

之所以要在 observe 裡面才設定 adpater 是為了避免資料為空

View Pager Adapter

因為是要顯示 fragment,所以繼承於 fragmentStateAdapter()

class TypePagerAdapter(
    val viewModel: AddNewViewModel, 
    activity: FragmentActivity
): FragmentStateAdapter(activity){
    override fun getItemCount(): Int {
        //類別總數除以10加1就會是總頁數
        return viewModel.categories.value!!.size / 10 + 1
    }

    override fun createFragment(position: Int): Fragment {
        //每一頁建立出 fragment,而且讓 fragment 本身也知道自己是第幾頁
        return AddNewTypeListFragment(position)
    }
}

Recycler View 的部分

AddNewTypeListFragment

這是每一個頁面的 fragment

class AddNewTypeListFragment(): Fragment() {

    private lateinit var rvType: RecyclerView
    private lateinit var recyclerAdapter: AddNewTypeRecyclerAdapter

    private lateinit var viewModel: AddNewTypeListViewModel

    private var page: Int= 0

    constructor(page: Int) : this() {
        this.page= page
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val root = inflater.inflate(R.layout.add_new_pager_type, container, false)

        //viewModel
        val factory = AddNewTypeListViewModelFactory(activity!!.application, page)
        viewModel = ViewModelProvider(this, factory).get(AddNewTypeListViewModel::class.java)

        //recyclerView
        rvType= root.findViewById(R.id.add_type_rv_)
        recyclerAdapter= AddNewTypeRecyclerAdapter(context!!, viewModel)
        val gridLayoutManager = GridLayoutManager(context!!, 5).apply {
            orientation= RecyclerView.VERTICAL
        }
        viewModel.pageCategories.observe(this, Observer {
            with(rvType) {
                adapter = recyclerAdapter
                layoutManager = gridLayoutManager
            }
        })

        return root
    }
}

比較特別的是,我這裡就沒有用 linear layout,而是使用 GridLayoutManager,讓我可以顯示兩行資料。


~當資料沒有滿的時候~

View Model

class AddNewTypeListViewModel(application: Application, val page: Int): AndroidViewModel(application) {
    private val repository: Repository

    var pageCategories: LiveData<List<CategoryEntity>>

    var selectedCategoryId: MutableLiveData<Int>

    init {
        val accountingDao = AccountingDatabase.getDatabase(application, viewModelScope).getAccountingDao()
        repository = Repository(accountingDao)

        selectedCategoryId= MutableLiveData(-1)

        //started id = page* 10+ 1
        //started id:  1 11 21
        //      page:  0  1  2
        pageCategories= repository.getPageCategories(page* 10+ 1)
    }
}

從本地資料庫分批取得資料給 recycler view

Recycler View Adapter

class TypeRecyclerAdapter(private val context: Context, private val viewModel: AddNewTypeListViewModel): RecyclerView.Adapter<TypeRecyclerAdapter.TypeViewHolder>() {
    inner class TypeViewHolder(item: View): RecyclerView.ViewHolder(item){
        var tvName: TextView = item.findViewById(R.id.add_type_item_tv_name)
        var layoutItem: LinearLayout = item.findViewById(R.id.add_type_item_layout)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TypeRecyclerAdapter.TypeViewHolder {
        return TypeViewHolder(
            LayoutInflater.from(context).inflate(R.layout.add_new_pager_type_item, parent, false)
        )
    }

    override fun getItemCount(): Int {
        //一頁共十個
        return viewModel.pageCategories.value!!.size
    }

    override fun onBindViewHolder(holder: TypeRecyclerAdapter.TypeViewHolder, position: Int) {
        //設定一個項目的寬度為螢幕的五分之一
        holder.layoutItem.layoutParams.width= context.resources.displayMetrics.widthPixels/ 5
        //一個 item 的點擊
        holder.layoutItem.setOnClickListener{
            viewModel.selectedCategoryId.value= position
            Log.v("position click", "$position")
        }
        // 這裡會發生 No adapter attached; skipping layout 的錯誤,但是可以成功顯示
        try {
            holder.tvName.text = "${viewModel.pageCategories.value!![position].name}${ viewModel.pageCategories.value!![position].id}"
        } catch (e: Exception){
            Log.e("EXCEPTION", "$e")
        }
    }
}

為了能夠讓每一的部分可以平均寬度,使用 displayMetrics.widthPixels/ 5 來設定每一個的寬度。


上一篇
[Day 26] Android in Kotlin: Data Binding 簡單示範
下一篇
[Day 28] Android in Kotlin: Splash Screen 簡單示範
系列文
大一之 Android Kotlin 自習心路歷程30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言