Adapter คือรูปแบบการออกแบบเชิงโครงสร้างที่แปลงอินเทอร์เฟซของคลาสหนึ่งไปเป็นอีกอินเทอร์เฟซหนึ่งที่ไคลเอ็นต์คาดหวัง ในการพัฒนาแอปพลิเคชันมือถือ Adapter มักถูกใช้ใน RecyclerView.Adapter สำหรับ Android และ UITableViewDataSource สำหรับ iOS ตามข้อมูลจาก Google I/O (2024) RecyclerView ถูกใช้ใน 95% ของแอปพลิเคชัน Android เพื่อแสดงรายการ และแต่ละแอปพลิเคชันต้องการการใช้งาน Adapter ของตัวเอง
ประเด็นสำคัญ
Adapter คือรูปแบบเชิงโครงสร้างที่แก้ปัญหาความไม่เข้ากันของอินเทอร์เฟซ มันห่อหุ้มออบเจ็กต์หนึ่ง (Adaptee) ไว้ในคลาส (Adapter) ที่มีอินเทอร์เฟซที่ไคลเอ็นต์คาดหวัง (Target) ไคลเอ็นต์ทำงานกับ Target โดยไม่รู้ถึงการมีอยู่ของ Adaptee นี่คือรูปแบบหนึ่งของ Wrapper
// คลาสที่มีอยู่กับอินเทอร์เฟซที่เข้ากันไม่ได้
class LegacyAuthApi {
fun loginWithToken(token: String): Map {
return mapOf("status" to "ok", "user_id" to 42)
}
}
// อินเทอร์เฟซเป้าหมาย (สิ่งที่ไคลเอ็นต์คาดหวัง)
interface AuthService {
suspend fun login(credentials: Credentials): Result
}
// Adapter — แปลง LegacyAuthApi เป็น AuthService
class LegacyAuthAdapter(
private val legacyApi: LegacyAuthApi
) : AuthService {
override suspend fun login(credentials: Credentials): Result {
val token = "${credentials.login}:${credentials.password}"
.encodeToByteArray().let { Base64.encodeToString(it) }
val response = legacyApi.loginWithToken(token)
return if (response["status"] == "ok") {
Result.success(User(response["user_id"] as Int))
} else {
Result.failure(AuthException("Login failed"))
}
}
}LegacyAuthAdapter แปลงการเรียก login(credentials) เป็น loginWithToken(token) สำหรับ API เก่า ไคลเอ็นต์ (ViewModel) ทำงานผ่านอินเทอร์เฟซ AuthService และไม่รู้ว่าภายใต้คือ Firebase Auth ล่าสุดหรือ API เก่าอายุสิบปี สิ่งนี้ช่วยให้เปลี่ยนการใช้งานได้โดยไม่ต้องแก้ไขโค้ดไคลเอ็นต์
RecyclerView.Adapter คือการใช้งานรูปแบบ Adapter ที่พบบ่อยที่สุดใน Android มันแปลงข้อมูล (รายการออบเจ็กต์) เป็น ViewHolder ที่ RecyclerView แสดงบนหน้าจอ ด้วยการเปิดตัว ListAdapter (Android Architecture Components) รูปแบบได้รับการสนับสนุนการคำนวณความแตกต่างในตัวสำหรับการเคลื่อนไหวของการเปลี่ยนแปลง
class UserAdapter(
private val onItemClick: (User) -> Unit
) : ListAdapter (DiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(
holder: UserViewHolder,
position: Int
) {
holder.bind(getItem(position), onItemClick)
}
class UserViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
private val tvName = itemView.findViewById(R.id.tvName)
fun bind(user: User, click: (User) -> Unit) {
tvName.text = user.name
itemView.setOnClickListener { click(user) }
}
}
private class DiffCallback : DiffUtil.ItemCallback () {
override fun areItemsTheSame(old: User, new: User) = old.id == new.id
override fun areContentsTheSame(old: User, new: User) = old == new
}
} UserAdapter ใช้ ListAdapter กับ DiffUtil เพื่อการวาดรายการใหม่ที่มีประสิทธิภาพ เมื่อข้อมูลถูกอัปเดต DiffCallback คำนวณความแตกต่างระหว่างรายการเก่าและใหม่ และ RecyclerView เคลื่อนไหวเฉพาะรายการที่เปลี่ยนไป — โดยไม่ต้องวาดรายการทั้งหมดใหม่ นี่คือข้อได้เปรียบหลักเหนือ ListView แบบเก่าที่ต้องการ notifyDataSetChanged() ด้วยตนเอง
UITableViewDataSource เป็นทั้งรูปแบบ Adapter และรูปแบบ DataSource ในเวลาเดียวกัน มันแปลงข้อมูลโมเดลเป็นเซลล์ตาราง ตั้งแต่ iOS 13 Apple ได้แนะนำ DiffableDataSource — ตัวแทนที่ทันสมัย ซึ่งเหมือนกับ ListAdapter ใน Android ที่คำนวณการเปลี่ยนแปลงโดยอัตโนมัติและเคลื่อนไหวการอัปเดต
class UserTableViewAdapter: NSObject, UITableViewDataSource {
var items: [User] = []
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "UserCell", for: indexPath)
let user = items[indexPath.row]
cell.textLabel?.text = user.name
return cell
}
}
// เวอร์ชันทันสมัย: DiffableDataSource
enum Section { case main }
class ModernUserAdapter {
var dataSource: UITableViewDiffableDataSource<Section, User>!
func configure(for tableView: UITableView) {
dataSource = UITableViewDiffableDataSource(
tableView: tableView
) { tableView, indexPath, user in
let cell = tableView.dequeueReusableCell(
withIdentifier: "UserCell", for: indexPath)
cell.textLabel?.text = user.name
return cell
}
}
func update(with users: [User]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, User>()
snapshot.appendSections([.main])
snapshot.appendItems(users)
dataSource.apply(snapshot, animatingDifferences: true)
}
}ModernUserAdapter กับ DiffableDataSource จะเคลื่อนไหวการเปลี่ยนแปลงโดยอัตโนมัติเมื่อข้อมูลถูกอัปเดต: รายการปรากฏ หายไป หรือเคลื่อนที่ด้วยแอนิเมชันที่ราบรื่น DiffableDataSource ของ iOS และ ListAdapter ของ Android แก้ปัญหาเดียวกันด้วย API ที่แตกต่างกันแต่ใช้ตรรกะเดียวกัน — การคำนวณความแตกต่าง + แอนิเมชันอัตโนมัติ
Object Adapter ใช้การประกอบ: อะแดปเตอร์เก็บการอ้างอิงถึงออบเจ็กต์ Adaptee และมอบหมายการเรียกไปยังมัน Class Adapter ใช้การสืบทอด: อะแดปเตอร์สืบทอดจาก Adaptee และใช้อินเทอร์เฟซเป้าหมายพร้อมกัน ใน Java, Swift และ Kotlin Class Adapter เป็นไปไม่ได้เนื่องจากขาดการสืบทอดหลายคลาส — เหลือเพียง Object Adapter
| คุณลักษณะ | Object Adapter | Class Adapter |
|---|---|---|
| กลไก | การประกอบ (ประกอบด้วย Adaptee) | การสืบทอด (ขยาย Adaptee) |
| ความยืดหยุ่น | สูงกว่า — Adaptee สามารถเปลี่ยนได้ที่รันไทม์ | ต่ำกว่า — Adaptee ถูกกำหนดที่เวลาคอมไพล์ |
| การเชื่อมโยง | อ่อน (ผ่านอินเทอร์เฟซ) | แข็งแกร่ง (ผ่านการสืบทอด) |
| การปรับคลาสย่อย | ได้ — Adaptee สามารถเป็นคลาสย่อยใดก็ได้ | ไม่ได้ — เฉพาะซูเปอร์คลาสที่กำหนด |
| ความพร้อมใช้งาน | Java, Kotlin, Swift, C++, C# | เฉพาะภาษาที่มีการสืบทอดหลายคลาส (C++) |
ในการพัฒนาแอปพลิเคชันมือถือ ใช้เฉพาะ Object Adapter เท่านั้น RecyclerView.Adapter เก็บการอ้างอิงถึงข้อมูลและ LayoutInflater, UITableViewDataSource เก็บอาร์เรย์ของรายการ การประกอบทำให้โค้ดยืดหยุ่นและทดสอบได้มากขึ้นเมื่อเทียบกับการสืบทอด
Adapter ไม่ได้ใช้เฉพาะสำหรับรายการเท่านั้น มาพิจารณาสามสถานการณ์จากการฝึกปฏิบัติพัฒนาแอปพลิเคชันมือถือที่รูปแบบแก้ปัญหาการรวมคอมโพเนนต์ที่เข้ากันไม่ได้
| สถานการณ์ | Adaptee | Target | Adapter |
|---|---|---|---|
| การรวมไลบรารีตรวจสอบสิทธิ์เก่า | LegacyAuthLib (แบบ callback) | AuthService (suspend) | LegacyAuthAdapter |
| การปรับ JSON ให้เข้ากับโมเดลใหม่ | GsonParser | JsonParser (Kotlinx Serialization) | GsonAdapter |
| อินเทอร์เฟซรวมสำหรับบริการ Push | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
แต่ละ Adapter ห่อหุ้มตรรกะการแปลง: GsonAdapter แปลงสตริง JSON เป็นออบเจ็กต์ผ่าน @SerializedName, PushServiceAdapter ทำให้การลงทะเบียนโทเค็นเป็นนามธรรมสำหรับแพลตฟอร์มต่างๆ ไคลเอ็นต์ (ตรรกะทางธุรกิจ) ทำงานกับอินเทอร์เฟซรวมและไม่ขึ้นอยู่กับการใช้งานเฉพาะ
ข้อผิดพลาดในการใช้งาน Adapter นำไปสู่ความช้าของรายการ หน่วยความจำรั่ว และบั๊กกับการอัปเดตข้อมูล มาดูปัญหาทั่วไปสามอย่าง
onBindViewHolder ถูกเรียกสำหรับแต่ละเซลล์ระหว่างการเลื่อน การสร้างออบเจ็กต์ การแยกวิเคราะห์ JSON หรือการเรียกฐานข้อมูลในเมธอดนี้นำไปสู่การกระตุกของเฟรม วิธีแก้: ดำเนินการคำนวณหนักทั้งหมดล่วงหน้า ส่งข้อมูลที่พร้อมแล้วให้อะแดปเตอร์ ใช้ไลบรารีแคชสำหรับรูปภาพ — Glide, Coil, Kingfisher
การเรียก notifyDataSetChanged() ทุกครั้งที่อัปเดตจะวาดรายการทั้งหมดใหม่ ทำให้เกิดการกะพริบและสูญเสียโฟกัสบนอินพุต วิธีแก้: ใช้ ListAdapter กับ DiffUtil ใน Android หรือ DiffableDataSource ใน iOS การคำนวณความแตกต่างใช้เวลา <1 มิลลิวินาทีสำหรับรายการสูงสุด 1,000 รายการและรับประกันแอนิเมชันที่ราบรื่น
อะแดปเตอร์มีอายุยืนเท่ากับ RecyclerView/UITableView การเก็บ Bitmap หรืออาร์เรย์ขนาดใหญ่ ในอะแดปเตอร์นำไปสู่การรั่วไหลของหน่วยความจำเมื่อเปลี่ยนการกำหนดค่า (หมุนหน้าจอ เปลี่ยนธีม) วิธีแก้: อะแดปเตอร์ควรเก็บเฉพาะโมเดลข้อมูลน้ำหนักเบา (data class / struct) และโหลดทรัพยากรหนักผ่าน ViewHolder เมื่อจำเป็น
คำถามที่พบบ่อย
Adapter แปลงอินเทอร์เฟซ Adaptee เป็นอินเทอร์เฟซ Target — ไคลเอ็นต์ได้รับวิธีการโต้ตอบใหม่ Proxy ให้อินเทอร์เฟซเดียวกันกับออบเจ็กต์ดั้งเดิม แต่เพิ่มการควบคุมการเข้าถึง แคช หรือการเริ่มต้นแบบขี้เกียจ Proxy ไม่เปลี่ยนอินเทอร์เฟซ Adapter เปลี่ยน
RecyclerView.Adapter แปลงข้อมูล (เช่น รายการ User) เป็น ViewHolder ที่ RecyclerView สามารถแสดงได้ RecyclerView คาดหวัง ViewHolder ข้อมูลอยู่ในรูปแบบ List<User> — Adapter ปรับให้เข้าคู่กัน นอกจากนี้ Adapter จัดการวงจรชีวิตของ ViewHolder ผ่านพูลรีไซเคิล เพิ่มประสิทธิภาพของรายการ
Object Adapter ใช้การประกอบ: เก็บการอ้างอิงถึง Adaptee และมอบหมายการเรียก Class Adapter ใช้การสืบทอดหลายคลาส: สืบทอดจาก Adaptee และใช้งาน Target ใน Java, Kotlin และ Swift มีเฉพาะ Object Adapter เท่านั้นเนื่องจากขาดการสืบทอดหลายคลาส Class Adapter เป็นไปได้เฉพาะใน C++
Adapter ใช้เมื่อคุณต้องการทำให้สองคลาสที่มีอยู่ทำงานร่วมกันโดยการแปลงอินเทอร์เฟซของหนึ่งในนั้น Facade ใช้เมื่อคุณต้องการทำให้การโต้ตอบกับระบบย่อยที่ซับซ้อนง่ายขึ้นโดยการให้อินเทอร์เฟซที่เรียบง่าย Adapter สำหรับความเข้ากันได้ Facade สำหรับการทำให้ง่ายขึ้น
ได้ โดยใช้ คลอเชอร์ แทนอินเทอร์เฟซที่มีหนึ่งเมธอด สามารถส่งฟังก์ชันได้ ตัวอย่างเช่น ใน Swift: let adapter = { (data: Data) -> [CellConfig] in /* การแปลง */ } วิธีการนี้เรียกว่าอะแดปเตอร์เชิงฟังก์ชัน อย่างไรก็ตาม สำหรับ 2+ เมธอด โปรโตคอล/อินเทอร์เฟซยังคงเป็นที่นิยม — ทำให้สัญญาชัดเจน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม