Adapterは、あるクラスのインターフェースをクライアントが期待する別のインターフェースに変換する構造的なデザインパターンです。モバイル開発では、AdapterはAndroidのRecyclerView.AdapterやiOSのUITableViewDataSourceとして最も頻繁に使用されます。Google I/O(2024)によると、RecyclerViewはAndroidアプリの95%でリスト表示に使用されており、それぞれが独自のAdapter実装を必要とします。
重要なポイント
Adapterは、インターフェースの非互換性の問題を解決する構造的パターンです。オブジェクト(Adaptee)を、クライアントが期待するインターフェース(Target)を持つクラス(Adapter)でラップします。クライアントは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は、古いAPIのためにlogin(credentials)呼び出しをloginWithToken(token)に変換します。クライアント(ViewModel)はAuthServiceインターフェースを通じて動作し、その下にあるのが最新のFirebase Authか10年前のレガシーAPIかを知りません。これにより、クライアントコードを変更せずに実装を置き換えることができます。
RecyclerView.Adapterは、AndroidにおけるAdapterパターンの最も一般的な実装です。データ(オブジェクトのリスト)を、RecyclerViewが画面に表示するViewHolderに変換します。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は、効率的なリスト再描画のためにDiffUtilとともにListAdapterを使用します。データが更新されると、DiffCallbackが古いリストと新しいリストの差を計算し、RecyclerViewは変更されたアイテムのみをアニメーション化します — リスト全体を再描画することなく。これは、手動のnotifyDataSetChanged()が必要だった古いListViewに対する重要な利点です。
UITableViewDataSourceは、AdapterパターンとDataSourceパターンの両方を兼ねています。モデルデータをテーブルセルに変換します。iOS 13以降、AppleはDiffableDataSourceを導入しました — AndroidのListAdapterと同様に、変更を自動的に計算し、更新をアニメーション化する最新の代替品です。
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を使用して、データが更新されると変更を自動的にアニメーション化します:アイテムがスムーズなアニメーションで表示、非表示、または移動します。iOSのDiffableDataSourceとAndroidのListAdapterは、異なる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はリストだけでなく使用されます。互換性のないコンポーネントを統合する問題をパターンが解決する、モバイル開発の実践から3つのシナリオを考えてみましょう。
| シナリオ | Adaptee | Target | Adapter |
|---|---|---|---|
| レガシーauthライブラリの統合 | LegacyAuthLib(コールバックベース) | AuthService(suspend) | LegacyAuthAdapter |
| 新しいモデルへのJSON適応 | GsonParser | JsonParser(Kotlinx Serialization) | GsonAdapter |
| プッシュサービスの統一インターフェース | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
各Adapterは変換ロジックをカプセル化します:GsonAdapterは@SerializedNameを介してJSON文字列をオブジェクトに変換し、PushServiceAdapterは異なるプラットフォームのトークン登録を抽象化します。クライアント(ビジネスロジック)は統一されたインターフェースで動作し、特定の実装に依存しません。
Adapter実装の間違いは、リストの遅延、メモリリーク、データ更新のバグを引き起こします。3つの一般的な問題を見てみましょう。
onBindViewHolderはスクロール中に各セルに対して呼び出されます。このメソッドでのオブジェクト作成、JSON解析、データベース呼び出しはフレームドロップの原因になります。解決策:すべての重い計算を事前に行い、準備済みのデータをアダプターに渡します。画像にはキャッシュライブラリ(Glide、Coil、Kingfisher)を使用します。
更新のたびにnotifyDataSetChanged()を呼び出すと、リスト全体が再描画され、ちらつきや入力フォーカスの喪失を引き起こします。解決策:AndroidではDiffUtilとともにListAdapterを、iOSではDiffableDataSourceを使用します。差分計算は最大1000アイテムのリストで<1ミリ秒かかり、スムーズなアニメーションを保証します。
アダプターはRecyclerView/UITableViewと同じ期間存続します。アダプターにBitmapや大きな配列を保存すると、設定変更(画面回転、テーマ変更)時にメモリリークを引き起こします。解決策:アダプターは軽量なデータモデル(data class / struct)のみを保存し、重いリソースは必要に応じてViewHolderを介してロードする必要があります。
よくある質問
AdapterはAdapteeインターフェースをTargetインターフェースに変換します — クライアントは新しい相互作用の方法を得ます。Proxyは元のオブジェクトと同じインターフェースを提供しますが、アクセス制御、キャッシング、または遅延初期化を追加します。Proxyはインターフェースを変更しませんが、Adapterは変更します。
RecyclerView.Adapterはデータ(例:Userのリスト)を、RecyclerViewが表示できるViewHolderに変換します。RecyclerViewはViewHolderを期待し、データはList<User>形式です — Adapterが一方を他方に適応させます。さらに、Adapterはリサイクルプールを通じてViewHolderのライフサイクルを管理し、リストのパフォーマンスを向上させます。
Object Adapterはコンポジションを使用します:Adapteeへの参照を持ち、呼び出しを委譲します。Class Adapterは多重継承を使用します:Adapteeを継承し、Targetを実装します。Java、Kotlin、Swiftでは、多重クラス継承がないためObject Adapterのみが利用可能です。Class AdapterはC++でのみ可能です。
Adapterは、既存の2つのクラスを連携させる必要がある場合に、一方のインターフェースを変換して使用します。Facadeは、シンプルなインターフェースを提供して複雑なサブシステムとの相互作用を簡素化する必要がある場合に使用します。Adapterは互換性のため、Facadeは簡素化のためです。
はい、クロージャを使用して可能です。1つのメソッドを持つインターフェースの代わりに関数を渡すことができます。例:Swiftの場合:let adapter = { (data: Data) -> [CellConfig] in /* 変換 */ }。このアプローチは関数型アダプターと呼ばれます。ただし、2つ以上のメソッドの場合、プロトコル/インターフェースが依然として推奨されます — 契約を明示的にします。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。