diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
index 15c7ca3c98..94c151325e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
@@ -5,48 +5,28 @@ package org.yuzu.yuzu_emu.adapters
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
-import androidx.recyclerview.widget.AsyncDifferConfig
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
 import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
 import org.yuzu.yuzu_emu.model.Addon
+import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
 
-class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>(
-    AsyncDifferConfig.Builder(DiffCallback()).build()
-) {
+class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() {
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
         ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
             .also { return AddonViewHolder(it) }
     }
 
-    override fun getItemCount(): Int = currentList.size
-
-    override fun onBindViewHolder(holder: AddonViewHolder, position: Int) =
-        holder.bind(currentList[position])
-
     inner class AddonViewHolder(val binding: ListItemAddonBinding) :
-        RecyclerView.ViewHolder(binding.root) {
-        fun bind(addon: Addon) {
+        AbstractViewHolder<Addon>(binding) {
+        override fun bind(model: Addon) {
             binding.root.setOnClickListener {
                 binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
             }
-            binding.title.text = addon.title
-            binding.version.text = addon.version
+            binding.title.text = model.title
+            binding.version.text = model.version
             binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
-                addon.enabled = checked
+                model.enabled = checked
             }
-            binding.addonSwitch.isChecked = addon.enabled
-        }
-    }
-
-    private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
-        override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
-            return oldItem == newItem
-        }
-
-        override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
-            return oldItem == newItem
+            binding.addonSwitch.isChecked = model.enabled
         }
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
index ab657a7b95..3d8f0bda84 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
@@ -8,19 +8,14 @@ import android.text.TextUtils
 import android.view.LayoutInflater
 import android.view.ViewGroup
 import androidx.fragment.app.FragmentActivity
-import androidx.recyclerview.widget.AsyncDifferConfig
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
 import org.yuzu.yuzu_emu.databinding.CardFolderBinding
 import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
 import org.yuzu.yuzu_emu.model.GameDir
 import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
 
 class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
-    ListAdapter<GameDir, FolderAdapter.FolderViewHolder>(
-        AsyncDifferConfig.Builder(DiffCallback()).build()
-    ) {
+    AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() {
     override fun onCreateViewHolder(
         parent: ViewGroup,
         viewType: Int
@@ -29,18 +24,11 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
             .also { return FolderViewHolder(it) }
     }
 
-    override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
-        holder.bind(currentList[position])
-
     inner class FolderViewHolder(val binding: CardFolderBinding) :
-        RecyclerView.ViewHolder(binding.root) {
-        private lateinit var gameDir: GameDir
-
-        fun bind(gameDir: GameDir) {
-            this.gameDir = gameDir
-
+        AbstractViewHolder<GameDir>(binding) {
+        override fun bind(model: GameDir) {
             binding.apply {
-                path.text = Uri.parse(gameDir.uriString).path
+                path.text = Uri.parse(model.uriString).path
                 path.postDelayed(
                     {
                         path.isSelected = true
@@ -50,7 +38,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
                 )
 
                 buttonEdit.setOnClickListener {
-                    GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir)
+                    GameFolderPropertiesDialogFragment.newInstance(model)
                         .show(
                             activity.supportFragmentManager,
                             GameFolderPropertiesDialogFragment.TAG
@@ -58,19 +46,9 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
                 }
 
                 buttonDelete.setOnClickListener {
-                    gamesViewModel.removeFolder(this@FolderViewHolder.gameDir)
+                    gamesViewModel.removeFolder(model)
                 }
             }
         }
     }
-
-    private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
-        override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
-            return oldItem == newItem
-        }
-
-        override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
-            return oldItem == newItem
-        }
-    }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index a578f0de82..e26c2e0ab2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -9,7 +9,6 @@ import android.graphics.drawable.LayerDrawable
 import android.net.Uri
 import android.text.TextUtils
 import android.view.LayoutInflater
-import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.Toast
@@ -25,10 +24,6 @@ import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.lifecycleScope
 import androidx.navigation.findNavController
 import androidx.preference.PreferenceManager
-import androidx.recyclerview.widget.AsyncDifferConfig
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -36,122 +31,26 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.activities.EmulationActivity
-import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
 import org.yuzu.yuzu_emu.databinding.CardGameBinding
 import org.yuzu.yuzu_emu.model.Game
 import org.yuzu.yuzu_emu.model.GamesViewModel
 import org.yuzu.yuzu_emu.utils.GameIconUtils
+import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
 
 class GameAdapter(private val activity: AppCompatActivity) :
-    ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
-    View.OnClickListener,
-    View.OnLongClickListener {
+    AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>() {
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
-        // Create a new view.
-        val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
-        binding.cardGame.setOnClickListener(this)
-        binding.cardGame.setOnLongClickListener(this)
-
-        // Use that view to create a ViewHolder.
-        return GameViewHolder(binding)
-    }
-
-    override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
-        holder.bind(currentList[position])
-
-    override fun getItemCount(): Int = currentList.size
-
-    /**
-     * Launches the game that was clicked on.
-     *
-     * @param view The card representing the game the user wants to play.
-     */
-    override fun onClick(view: View) {
-        val holder = view.tag as GameViewHolder
-
-        val gameExists = DocumentFile.fromSingleUri(
-            YuzuApplication.appContext,
-            Uri.parse(holder.game.path)
-        )?.exists() == true
-        if (!gameExists) {
-            Toast.makeText(
-                YuzuApplication.appContext,
-                R.string.loader_error_file_not_found,
-                Toast.LENGTH_LONG
-            ).show()
-
-            ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
-            return
-        }
-
-        val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
-        preferences.edit()
-            .putLong(
-                holder.game.keyLastPlayedTime,
-                System.currentTimeMillis()
-            )
-            .apply()
-
-        val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
-            action = Intent.ACTION_VIEW
-            data = Uri.parse(holder.game.path)
-        }
-
-        activity.lifecycleScope.launch {
-            withContext(Dispatchers.IO) {
-                val layerDrawable = ResourcesCompat.getDrawable(
-                    YuzuApplication.appContext.resources,
-                    R.drawable.shortcut,
-                    null
-                ) as LayerDrawable
-                layerDrawable.setDrawableByLayerId(
-                    R.id.shortcut_foreground,
-                    GameIconUtils.getGameIcon(activity, holder.game)
-                        .toDrawable(YuzuApplication.appContext.resources)
-                )
-                val inset = YuzuApplication.appContext.resources
-                    .getDimensionPixelSize(R.dimen.icon_inset)
-                layerDrawable.setLayerInset(1, inset, inset, inset, inset)
-                val shortcut =
-                    ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path)
-                        .setShortLabel(holder.game.title)
-                        .setIcon(
-                            IconCompat.createWithAdaptiveBitmap(
-                                layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
-                            )
-                        )
-                        .setIntent(openIntent)
-                        .build()
-                ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
-            }
-        }
-
-        val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
-        view.findNavController().navigate(action)
-    }
-
-    override fun onLongClick(view: View): Boolean {
-        val holder = view.tag as GameViewHolder
-        val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
-        view.findNavController().navigate(action)
-        return true
+        CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+            .also { return GameViewHolder(it) }
     }
 
     inner class GameViewHolder(val binding: CardGameBinding) :
-        RecyclerView.ViewHolder(binding.root) {
-        lateinit var game: Game
-
-        init {
-            binding.cardGame.tag = this
-        }
-
-        fun bind(game: Game) {
-            this.game = game
-
+        AbstractViewHolder<Game>(binding) {
+        override fun bind(model: Game) {
             binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
-            GameIconUtils.loadGameIcon(game, binding.imageGameScreen)
+            GameIconUtils.loadGameIcon(model, binding.imageGameScreen)
 
-            binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
+            binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
 
             binding.textGameTitle.postDelayed(
                 {
@@ -160,16 +59,79 @@ class GameAdapter(private val activity: AppCompatActivity) :
                 },
                 3000
             )
-        }
-    }
 
-    private class DiffCallback : DiffUtil.ItemCallback<Game>() {
-        override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
-            return oldItem == newItem
+            binding.cardGame.setOnClickListener { onClick(model) }
+            binding.cardGame.setOnLongClickListener { onLongClick(model) }
         }
 
-        override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
-            return oldItem == newItem
+        fun onClick(game: Game) {
+            val gameExists = DocumentFile.fromSingleUri(
+                YuzuApplication.appContext,
+                Uri.parse(game.path)
+            )?.exists() == true
+            if (!gameExists) {
+                Toast.makeText(
+                    YuzuApplication.appContext,
+                    R.string.loader_error_file_not_found,
+                    Toast.LENGTH_LONG
+                ).show()
+
+                ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
+                return
+            }
+
+            val preferences =
+                PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+            preferences.edit()
+                .putLong(
+                    game.keyLastPlayedTime,
+                    System.currentTimeMillis()
+                )
+                .apply()
+
+            val openIntent =
+                Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
+                    action = Intent.ACTION_VIEW
+                    data = Uri.parse(game.path)
+                }
+
+            activity.lifecycleScope.launch {
+                withContext(Dispatchers.IO) {
+                    val layerDrawable = ResourcesCompat.getDrawable(
+                        YuzuApplication.appContext.resources,
+                        R.drawable.shortcut,
+                        null
+                    ) as LayerDrawable
+                    layerDrawable.setDrawableByLayerId(
+                        R.id.shortcut_foreground,
+                        GameIconUtils.getGameIcon(activity, game)
+                            .toDrawable(YuzuApplication.appContext.resources)
+                    )
+                    val inset = YuzuApplication.appContext.resources
+                        .getDimensionPixelSize(R.dimen.icon_inset)
+                    layerDrawable.setLayerInset(1, inset, inset, inset, inset)
+                    val shortcut =
+                        ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path)
+                            .setShortLabel(game.title)
+                            .setIcon(
+                                IconCompat.createWithAdaptiveBitmap(
+                                    layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
+                                )
+                            )
+                            .setIntent(openIntent)
+                            .build()
+                    ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
+                }
+            }
+
+            val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true)
+            binding.root.findNavController().navigate(action)
+        }
+
+        fun onLongClick(game: Game): Boolean {
+            val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
+            binding.root.findNavController().navigate(action)
+            return true
         }
     }
 }