iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 27

RecyclerView 進階實戰:從商品清單到飲料點餐系統

  • 分享至 

  • xImage
  •  

我會把 Day 27 和昨天的 Firebase 主題錯開,改用你今天上傳的 RecyclerView + Adapter + Activity Result 範例,整理成一篇偏「實戰整合」的文章。你的程式已經不是單純顯示 RecyclerView,而是做到圖片、品名、價格、數量加減、結帳,以及 Activity 之間傳值與回傳結果
參考:Android Developers-RecyclerView

Day 27|RecyclerView 進階實戰:從商品清單到飲料點餐系統

前面已經學過 ListViewRecyclerView、Activity 與 Intent,今天不再只做「把資料顯示出來」,而是把這些東西組合起來,做一個更接近真正 App 的 飲料點餐系統

今天要完成的流程是:

飲料資料
   ↓
RecyclerView
   ↓
Adapter
   ↓
顯示圖片 / 品名 / 價格
   ↓
使用者按 + / -
   ↓
修改購買數量
   ↓
取得最新購物資料
   ↓
計算小計
   ↓
計算總金額
   ↓
顯示訂單

這次真正重要的不是畫面,而是開始理解:

資料、Adapter、RecyclerView 與 Activity 到底怎麼合作?

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


一、RecyclerView 到底負責什麼?

RecyclerView 可以用來有效率地顯示大量、可捲動的資料,例如:

  • 商品列表
  • 聊天訊息
  • 新聞列表
  • 相簿
  • 聯絡人
  • 購物車
  • 飲料菜單

RecyclerView 並不是自己知道要顯示什麼。

它主要會搭配:

Data
 ↓
Adapter
 ↓
ViewHolder
 ↓
item_layout.xml
 ↓
RecyclerView

也就是:

資料負責內容,Adapter 負責把資料放進畫面,而 RecyclerView 負責顯示與回收 Item。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


二、第一步:準備飲料資料

今天的程式先從 resources 取得飲料名稱、價格與圖片:

val nameArray = resources.getStringArray(R.array.name)
val priceArray = resources.getIntArray(R.array.price)
val picArray = resources.obtainTypedArray(R.array.picture)

這樣就可以把資料與 Kotlin 程式分開。

例如概念上可以想成:

name
├── 紅茶
├── 綠茶
├── 奶茶
└── 珍珠奶茶

price
├── 30
├── 30
├── 45
└── 60

picture
├── blacktea
├── greentea
├── milktea
└── bubbletea

接下來再將每一筆商品組合成一個 MutableMap

datalist = mutableListOf<MutableMap<String, Any>>()

for (i in 0 .. nameArray.size - 1) {

    val data = mutableMapOf<String, Any>()

    data.put("name", nameArray[i])
    data.put("price", priceArray[i])
    data.put(
        "pic",
        picArray.getResourceId(i, R.drawable.blacktea)
    )

    // 一開始購買數量都是 0
    data.put("qty", 0)

    datalist.add(data)
}

picArray.recycle()

因此一筆商品資料就可以理解成:

{
    name = "紅茶",
    price = 30,
    pic = R.drawable.blacktea,
    qty = 0
}

這次多了一個很重要的欄位:

"qty"

它代表使用者目前購買幾杯。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


三、設定 RecyclerView

資料準備完成之後,建立 LinearLayoutManager

val layoutManager = LinearLayoutManager(
    this@MainActivity,
    LinearLayoutManager.VERTICAL,
    false
)

recyclerViewData.layoutManager = layoutManager

這裡使用:

LinearLayoutManager.VERTICAL

所以商品會由上往下排列:

┌──────────────────────┐
│ 🧋 紅茶    $30   - 0 + │
├──────────────────────┤
│ 🧋 綠茶    $30   - 0 + │
├──────────────────────┤
│ 🧋 奶茶    $45   - 0 + │
├──────────────────────┤
│ 🧋 珍奶    $60   - 0 + │
└──────────────────────┘

如果改成:

LinearLayoutManager.HORIZONTAL

就可以變成左右滑動的商品清單。

今天上傳的另一個花卉 RecyclerView 範例,就是使用水平排列:

val layoutManager = LinearLayoutManager(
    this,
    LinearLayoutManager.HORIZONTAL,
    false
)

所以 RecyclerView 不一定只能做直向清單,也很適合做 App 首頁常見的水平商品列、照片列或分類選單。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


四、Adapter 是 RecyclerView 的核心

接著建立:

class MyAdapter(
    private val myContext: Context,
    private val datalist: MutableList<MutableMap<String, Any>>,
    private val onQtyChanged: () -> Unit
) : RecyclerView.Adapter<MyAdapter.ViewHolder>()

Adapter 接收三樣東西:

Context
   +
商品資料 datalist
   +
數量改變 Callback

其中:

private val onQtyChanged: () -> Unit

是一個 Lambda Callback。

也就是 Adapter 裡面的數量發生改變時,可以通知外面的 Activity。

這就是之前學過 Lambda 之後,開始真正看到它在 Android 開發中的實際用途。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


五、ViewHolder:記住每個 Item 裡有哪些元件

每一個商品 Item 裡面有:

  • 商品圖片
  • 商品名稱
  • 商品價格
  • 數量

因此 ViewHolder 寫成:

class ViewHolder(itemView: View) :
    RecyclerView.ViewHolder(itemView) {

    val imageViewPic: ImageView =
        itemView.findViewById(R.id.imageView_itemPic)

    val textViewName: TextView =
        itemView.findViewById(R.id.textView_itemName)

    val textViewPrice: TextView =
        itemView.findViewById(R.id.textView_itemPrice)

    val editTextQty: EditText =
        itemView.findViewById(R.id.editText_itemNumber)

    val imageViewMinus: ImageView =
        itemView.findViewById(R.id.imageView_itemMinus)

    val imageViewPlus: ImageView =
        itemView.findViewById(R.id.imageView_itemPlus)
}

可以把 ViewHolder 想成:

一個商品 Item
      │
      ├── imageViewPic
      ├── textViewName
      ├── textViewPrice
      ├── editTextQty
      ├── imageViewMinus
      └── imageViewPlus

RecyclerView 捲動時會重複利用這些 ViewHolder,而不是每次都重新建立所有 View,這也是 RecyclerView 名稱中「Recycle」的由來。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


六、onCreateViewHolder():建立 Item

接著:

override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
): MyAdapter.ViewHolder {

    val itemView = LayoutInflater
        .from(myContext)
        .inflate(
            R.layout.item_layout,
            parent,
            false
        )

    val viewHolder = ViewHolder(itemView)

    return viewHolder
}

這裡最重要的是:

R.layout.item_layout

代表:

RecyclerView 裡面的每一列,要長成 item_layout.xml 定義的樣子。

所以:

RecyclerView
      │
      ├── item_layout
      ├── item_layout
      ├── item_layout
      ├── item_layout
      └── item_layout

每個 Item 外觀相同,但是放進去的資料不同。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


七、onBindViewHolder():把資料塞進畫面

接著才是真正將商品資料顯示到畫面:

override fun onBindViewHolder(
    holder: MyAdapter.ViewHolder,
    position: Int
) {

    val data = datalist.get(position)

    val name = data["name"].toString()
    val price = data["price"] as Int
    val picId = data["pic"] as Int

    holder.textViewName.text = name
    holder.textViewPrice.text = price.toString()
    holder.imageViewPic.setImageResource(picId)

    holder.editTextQty.setText(
        data["qty"].toString()
    )
}

其中:

position

就是目前 RecyclerView 要顯示第幾筆資料。

例如:

position = 0 → 紅茶
position = 1 → 綠茶
position = 2 → 奶茶
position = 3 → 珍珠奶茶

所以 Adapter 最重要的工作其實就是:

datalist[position]
        ↓
onBindViewHolder()
        ↓
ViewHolder
        ↓
畫面

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


八、真正像購物 App 的地方:+ 按鈕

今天最有趣的地方就是數量控制。

按下

holder.imageViewPlus.setOnClickListener {

    var currentQty = data["qty"] as Int

    currentQty++

    data["qty"] = currentQty

    holder.editTextQty.setText(
        currentQty.toString()
    )

    onQtyChanged()
}

假設原本:

qty = 0

第一次按:

qty = 1

再按:

qty = 2

再按:

qty = 3

而且不是只有改畫面的 TextView / EditText。

真正重要的是:

data["qty"] = currentQty

也就是資料本身也一起修改

這個觀念非常重要。

錯誤的思考是:

按 +
 ↓
畫面變 1

正確的思考應該是:

按 +
 ↓
修改 Data
 ↓
qty = qty + 1
 ↓
更新畫面

Data 才是真正的狀態,UI 只是把狀態顯示出來。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


九、- 按鈕還要處理邊界

減少數量時:

holder.imageViewMinus.setOnClickListener {

    var currentQty = data["qty"] as Int

    if (currentQty > 0) {

        currentQty--

        data["qty"] = currentQty

        holder.editTextQty.setText(
            currentQty.toString()
        )

        onQtyChanged()
    }
}

這裡特別加:

if (currentQty > 0)

避免出現:

0
-1
-2
-3

因為買飲料不可能買 -3 杯。

所以 UI 程式除了「可以操作」,也必須考慮:

使用者可能做出什麼不合理操作?

這就是程式的邊界條件。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


十、Adapter 如何把最新資料交回 Activity?

Adapter 裡面另外提供:

fun returnData(): MutableList<MutableMap<String, Any>> {
    return datalist
}

Activity 結帳時就可以:

val latestDataList = adapter.returnData()

取得使用者操作之後的最新資料。

例如:

紅茶
price = 30
qty = 2

綠茶
price = 30
qty = 0

奶茶
price = 45
qty = 3

這時 Activity 就可以進行結帳。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


十一、Checkout:計算訂單總金額

按下 Checkout:

buttonCheckout.setOnClickListener {

    val latestDataList = adapter.returnData()

    textViewCheckout.text = "Your order is :\n"

    var sum = 0

    for (data in latestDataList) {

        val name = data["name"].toString()
        val price = data["price"] as Int
        val qty = data["qty"] as Int

        if (qty > 0) {

            sum += qty * price

            textViewCheckout.append(
                "$name : $price x $qty = $${qty * price}\n"
            )
        }
    }

    textViewCheckout.append(
        "\nThe total fee = $$sum"
    )
}

核心公式其實非常簡單:

商品小計 = 單價 × 數量

例如:

紅茶:30 × 2 = 60
奶茶:45 × 3 = 135

最後:

總金額
= 60 + 135
= 195

程式:

sum += qty * price

就是購物車最基本的計價邏輯。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


十二、Cancel:購物車全部清空

如果使用者按下 Cancel:

buttonCancel.setOnClickListener {

    for (data in datalist) {
        data["qty"] = 0
    }

    adapter.notifyDataSetChanged()

    textViewCheckout.text = ""
}

首先把所有商品:

data["qty"] = 0

然後呼叫:

adapter.notifyDataSetChanged()

告訴 RecyclerView:

我的資料已經改變,請重新更新畫面。

因此:

紅茶  2
綠茶  1
奶茶  3

按下 Cancel 後:

紅茶  0
綠茶  0
奶茶  0

這就是「資料改變 → 通知 Adapter → UI 更新」的流程。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


十三、RecyclerView 不只可以做購物車

今天另一份練習是 Flower RecyclerView。

先建立資料類別:

data class Flower(
    val name: String,
    val imageResourceId: Int
)

接著 Adapter 接收:

class FlowerAdapter(
    private val flowerList: List<Flower>,
    private val onItemClick: (Flower) -> Unit
) : RecyclerView.Adapter<FlowerAdapter.FlowerViewHolder>()

點擊某朵花:

holder.itemView.setOnClickListener {
    onItemClick(flower)
}

這個寫法比單純把資料塞進 RecyclerView 更重要,因為 Adapter 可以透過 Lambda 把「哪一筆被點擊」傳出去。

因此資料流會變成:

使用者點擊 Flower
        ↓
Adapter
        ↓
onItemClick(flower)
        ↓
MainActivity
        ↓
更新大圖與名稱

這就是 Android 很常見的 Adapter Callback 寫法。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


十四、Activity Result:從點餐頁把資料帶回來

除了 RecyclerView,今天的另一份程式也把之前的 Activity 傳值進一步整合。

MainActivity 先註冊:

val launcher =
    registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->

        if (result.resultCode == Activity.RESULT_OK) {

            val intent = result.data

            intent?.extras?.let {

                val drink = it.getString("drink")

                val quantityStr =
                    it.getString("quantity") ?: "0"

                val sugar =
                    it.getString("sugar")

                val ice =
                    it.getString("ice")

                val quantity =
                    quantityStr.toIntOrNull() ?: 0

                val total =
                    quantity * selectedDrinkPrice

                tvName.text =
                    "飲料名稱: $drink"

                tvMeal.text =
                    "數量: $quantity, 甜度: $sugar, 冰塊: $ice"

                tvTotal.text =
                    "總價: NT$ $total"
            }
        }
    }

這裡使用的是現在 AndroidX 建議使用的:

registerForActivityResult()

而不是以前常見的:

startActivityForResult()

參考資料:Android Developers
https://developer.android.com/training/basics/intents/result?hl=zh-TW


十五、MainActivity → SelectActivity

當使用者選好飲料:

btnChoice.setOnClickListener {

    if (selectedDrinkName.isEmpty())
        return@setOnClickListener

    val intent =
        Intent(this, SelectActivity::class.java)

    intent.putExtra(
        "selected_drink",
        selectedDrinkName
    )

    launcher.launch(intent)
}

流程:

MainActivity
     │
     │ selected_drink
     ▼
SelectActivity

SelectActivity 再取得:

val selectedDrink =
    intent.getStringExtra("selected_drink")

edDrink.setText(selectedDrink)

因此使用者前面選好的飲料名稱,可以直接出現在下一個 Activity。

參考資料:Android Developers
https://developer.android.com/training/basics/intents/result?hl=zh-TW


十六、SelectActivity → MainActivity

SelectActivity 裡讓使用者設定:

飲料
數量
甜度
冰塊

取得甜度:

val sugarId =
    rgSugar.checkedRadioButtonId

取得冰塊:

val iceId =
    rgIce.checkedRadioButtonId

如果沒有選:

if (sugarId == -1 || iceId == -1)
    return@setOnClickListener

接著取得 RadioButton 文字:

val sugar =
    findViewById<RadioButton>(sugarId)
        .text.toString()

val ice =
    findViewById<RadioButton>(iceId)
        .text.toString()

最後建立 Bundle:

val intent = Intent()
val bundle = Bundle()

bundle.putString("drink", drink)
bundle.putString("quantity", quantity)
bundle.putString("sugar", sugar)
bundle.putString("ice", ice)

intent.putExtras(bundle)

回傳:

setResult(
    Activity.RESULT_OK,
    intent
)

finish()

因此完整資料流就是:

MainActivity
     │
     │ selected_drink
     ▼
SelectActivity
     │
     ├── drink
     ├── quantity
     ├── sugar
     └── ice
     │
     ▼
MainActivity
     │
     ▼
計算總價

參考資料:Android Developers
https://developer.android.com/training/basics/intents/result?hl=zh-TW


十七、今天其實把之前學過的東西串起來了

做到這裡,可以發現這個飲料點餐範例已經同時使用:

Array
 ↓
MutableList
 ↓
MutableMap
 ↓
RecyclerView
 ↓
Adapter
 ↓
ViewHolder
 ↓
Lambda Callback
 ↓
Intent
 ↓
Bundle
 ↓
Activity Result
 ↓
訂單計算

這也是為什麼前面 Kotlin 學的:

Array
List
Map
Lambda
Class

不是單純為了學語法。

進入 Android 之後,這些東西全部會開始組合在一起。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


十八、今天最重要的觀念:資料才是核心

今天最值得記住的並不是:

RecyclerView.Adapter

怎麼背。

而是開始建立:

Data
 ↓
Adapter
 ↓
RecyclerView
 ↓
User Action
 ↓
修改 Data
 ↓
更新 UI

這種思考方式。

例如按下:

真正發生的事情不是:

EditText 顯示 2

而應該理解成:

使用者按 +
      ↓
qty 由 1 改成 2
      ↓
Data 更新
      ↓
UI 顯示 2

這個觀念後面進入:

Room
Firebase
ViewModel
LiveData
StateFlow
Jetpack Compose

都會一直遇到。

參考資料:Android Developers
https://developer.android.com/develop/ui/views/layout/recyclerview


今日重點整理

今天完成的不只是 RecyclerView,而是一個小型的 App 資料流:

                ┌──────────────┐
                │  商品資料 Data │
                └──────┬───────┘
                       ↓
                ┌──────────────┐
                │   Adapter    │
                └──────┬───────┘
                       ↓
                ┌──────────────┐
                │ RecyclerView │
                └──────┬───────┘
                       ↓
              ┌──────────────────┐
              │ 圖片 / 名稱 / 價格 │
              │    -  數量  +     │
              └────────┬─────────┘
                       ↓
                  使用者操作
                       ↓
                 修改 qty
                       ↓
              ┌──────────────────┐
              │ adapter.returnData │
              └────────┬─────────┘
                       ↓
                  price × qty
                       ↓
                  計算總金額
                       ↓
                    Checkout

今天掌握五個重點:

  1. RecyclerView 負責有效率地顯示清單。
  2. Adapter 負責資料與 Item UI 的連接。
  3. ViewHolder 保存 Item 裡需要操作的 View。
  4. Lambda Callback 可以把 Adapter 的事件傳回 Activity。
  5. Activity Result API 可以完成 Activity 之間「送出去 → 再把結果帶回來」的雙向資料交換。

Day 27 小結

前面的 RecyclerView 如果只是把圖片和文字列出來,還比較像是在「練習元件」。

今天加入:

商品圖片
商品名稱
商品價格
數量 + / -
購物車資料
Checkout
Cancel
Activity 傳值
Activity Result

之後,就開始比較像真正的 App 了。

而 RecyclerView 最值得學習的地方,也不是單純會寫:

RecyclerView.Adapter

而是理解:

資料放在哪裡?誰負責顯示?誰修改資料?資料改完之後,畫面怎麼跟著改?

當這條資料流真正看懂之後,後面不論接 Room、Firebase,甚至進一步使用 ViewModel 與 Jetpack Compose,觀念都會清楚很多。

下一步,就可以思考一件更有趣的事情:

現在訂單只存在手機記憶體裡,如果把 RecyclerView 的訂單資料送到 Firebase,能不能做成真正的雲端點餐系統?

這就能把前一天的 Firebase 與今天的 RecyclerView 真正串在一起。

參考資料:
Android Developers-RecyclerView
https://developer.android.com/develop/ui/views/layout/recyclerview

Android Developers-取得 Activity 結果
https://developer.android.com/training/basics/intents/result?hl=zh-TW


上一篇
Android 雲端資料庫初體驗:Firebase Realtime Database 資料寫入與 App 串接
下一篇
Android Fragment 實戰:從生命週期、Fragment 切換到 Activity 資料傳遞
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言