diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
deleted file mode 100644
index ed1a000c7b..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
+++ /dev/null
@@ -1,244 +0,0 @@
-package org.yuzu.yuzu_emu.adapters;
-
-import android.database.Cursor;
-import android.database.DataSetObserver;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.SystemClock;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.FragmentActivity;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.yuzu.yuzu_emu.YuzuApplication;
-import org.yuzu.yuzu_emu.R;
-import org.yuzu.yuzu_emu.activities.EmulationActivity;
-import org.yuzu.yuzu_emu.model.GameDatabase;
-import org.yuzu.yuzu_emu.ui.DividerItemDecoration;
-import org.yuzu.yuzu_emu.utils.FileUtil;
-import org.yuzu.yuzu_emu.utils.Log;
-import org.yuzu.yuzu_emu.utils.PicassoUtils;
-import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
-
-import java.util.stream.Stream;
-
-/**
- * This adapter gets its information from a database Cursor. This fact, paired with the usage of
- * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
- * large dataset.
- */
-public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
-        View.OnClickListener {
-    private Cursor mCursor;
-    private GameDataSetObserver mObserver;
-
-    private boolean mDatasetValid;
-    private long mLastClickTime = 0;
-
-    /**
-     * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
-     * display no data until a Cursor is supplied by a CursorLoader.
-     */
-    public GameAdapter() {
-        mDatasetValid = false;
-        mObserver = new GameDataSetObserver();
-    }
-
-    /**
-     * Called by the LayoutManager when it is necessary to create a new view.
-     *
-     * @param parent   The RecyclerView (I think?) the created view will be thrown into.
-     * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
-     * @return The created ViewHolder with references to all the child view's members.
-     */
-    @Override
-    public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        // Create a new view.
-        View gameCard = LayoutInflater.from(parent.getContext())
-                .inflate(R.layout.card_game, parent, false);
-
-        gameCard.setOnClickListener(this);
-
-        // Use that view to create a ViewHolder.
-        return new GameViewHolder(gameCard);
-    }
-
-    /**
-     * Called by the LayoutManager when a new view is not necessary because we can recycle
-     * an existing one (for example, if a view just scrolled onto the screen from the bottom, we
-     * can use the view that just scrolled off the top instead of inflating a new one.)
-     *
-     * @param holder   A ViewHolder representing the view we're recycling.
-     * @param position The position of the 'new' view in the dataset.
-     */
-    @RequiresApi(api = Build.VERSION_CODES.O)
-    @Override
-    public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
-        if (mDatasetValid) {
-            if (mCursor.moveToPosition(position)) {
-                PicassoUtils.loadGameIcon(holder.imageIcon,
-                        mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
-
-                holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
-                holder.textGameCaption.setText(mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION));
-
-                // TODO These shouldn't be necessary once the move to a DB-based model is complete.
-                holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
-                holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
-                holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
-                holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
-                holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
-                holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION);
-
-                final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled;
-                View itemView = holder.getItemView();
-                itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId));
-            } else {
-                Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
-            }
-        } else {
-            Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
-        }
-    }
-
-    /**
-     * Called by the LayoutManager to find out how much data we have.
-     *
-     * @return Size of the dataset.
-     */
-    @Override
-    public int getItemCount() {
-        if (mDatasetValid && mCursor != null) {
-            return mCursor.getCount();
-        }
-        Log.error("[GameAdapter] Dataset is not valid.");
-        return 0;
-    }
-
-    /**
-     * Return the contents of the _id column for a given row.
-     *
-     * @param position The row for which Android wants an ID.
-     * @return A valid ID from the database, or 0 if not available.
-     */
-    @Override
-    public long getItemId(int position) {
-        if (mDatasetValid && mCursor != null) {
-            if (mCursor.moveToPosition(position)) {
-                return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
-            }
-        }
-
-        Log.error("[GameAdapter] Dataset is not valid.");
-        return 0;
-    }
-
-    /**
-     * Tell Android whether or not each item in the dataset has a stable identifier.
-     * Which it does, because it's a database, so always tell Android 'true'.
-     *
-     * @param hasStableIds ignored.
-     */
-    @Override
-    public void setHasStableIds(boolean hasStableIds) {
-        super.setHasStableIds(true);
-    }
-
-    /**
-     * When a load is finished, call this to replace the existing data with the newly-loaded
-     * data.
-     *
-     * @param cursor The newly-loaded Cursor.
-     */
-    public void swapCursor(Cursor cursor) {
-        // Sanity check.
-        if (cursor == mCursor) {
-            return;
-        }
-
-        // Before getting rid of the old cursor, disassociate it from the Observer.
-        final Cursor oldCursor = mCursor;
-        if (oldCursor != null && mObserver != null) {
-            oldCursor.unregisterDataSetObserver(mObserver);
-        }
-
-        mCursor = cursor;
-        if (mCursor != null) {
-            // Attempt to associate the new Cursor with the Observer.
-            if (mObserver != null) {
-                mCursor.registerDataSetObserver(mObserver);
-            }
-
-            mDatasetValid = true;
-        } else {
-            mDatasetValid = false;
-        }
-
-        notifyDataSetChanged();
-    }
-
-    /**
-     * Launches the game that was clicked on.
-     *
-     * @param view The card representing the game the user wants to play.
-     */
-    @Override
-    public void onClick(View view) {
-        // Double-click prevention, using threshold of 1000 ms
-        if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
-            return;
-        }
-        mLastClickTime = SystemClock.elapsedRealtime();
-
-        GameViewHolder holder = (GameViewHolder) view.getTag();
-
-        EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
-    }
-
-    public static class SpacesItemDecoration extends DividerItemDecoration {
-        private int space;
-
-        public SpacesItemDecoration(Drawable divider, int space) {
-            super(divider);
-            this.space = space;
-        }
-
-        @Override
-        public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
-                                   @NonNull RecyclerView.State state) {
-            outRect.left = 0;
-            outRect.right = 0;
-            outRect.bottom = space;
-            outRect.top = 0;
-        }
-    }
-
-    private boolean isValidGame(String path) {
-        return Stream.of(
-                ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
-    }
-
-    private final class GameDataSetObserver extends DataSetObserver {
-        @Override
-        public void onChanged() {
-            super.onChanged();
-
-            mDatasetValid = true;
-            notifyDataSetChanged();
-        }
-
-        @Override
-        public void onInvalidated() {
-            super.onInvalidated();
-
-            mDatasetValid = false;
-            notifyDataSetChanged();
-        }
-    }
-}
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
new file mode 100644
index 0000000000..b3761166de
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -0,0 +1,178 @@
+package org.yuzu.yuzu_emu.adapters
+
+import android.database.Cursor
+import android.database.DataSetObserver
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.activities.EmulationActivity.Companion.launch
+import org.yuzu.yuzu_emu.model.GameDatabase
+import org.yuzu.yuzu_emu.utils.Log
+import org.yuzu.yuzu_emu.utils.PicassoUtils
+import org.yuzu.yuzu_emu.viewholders.GameViewHolder
+import java.util.*
+import java.util.stream.Stream
+
+/**
+ * This adapter gets its information from a database Cursor. This fact, paired with the usage of
+ * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
+ * large dataset.
+ */
+class GameAdapter : RecyclerView.Adapter<GameViewHolder>(), View.OnClickListener {
+    private var cursor: Cursor? = null
+    private val observer: GameDataSetObserver?
+    private var isDatasetValid = false
+
+    /**
+     * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
+     * display no data until a Cursor is supplied by a CursorLoader.
+     */
+    init {
+        observer = GameDataSetObserver()
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
+        // Create a new view.
+        val gameCard = LayoutInflater.from(parent.context)
+            .inflate(R.layout.card_game, parent, false)
+        gameCard.setOnClickListener(this)
+
+        // Use that view to create a ViewHolder.
+        return GameViewHolder(gameCard)
+    }
+
+    override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
+        if (isDatasetValid) {
+            if (cursor!!.moveToPosition(position)) {
+                PicassoUtils.loadGameIcon(
+                    holder.imageIcon,
+                    cursor!!.getString(GameDatabase.GAME_COLUMN_PATH)
+                )
+                holder.textGameTitle.text =
+                    cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE)
+                        .replace("[\\t\\n\\r]+".toRegex(), " ")
+                holder.textGameCaption.text = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
+
+                // TODO These shouldn't be necessary once the move to a DB-based model is complete.
+                holder.gameId = cursor!!.getString(GameDatabase.GAME_COLUMN_GAME_ID)
+                holder.path = cursor!!.getString(GameDatabase.GAME_COLUMN_PATH)
+                holder.title = cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE)
+                holder.description = cursor!!.getString(GameDatabase.GAME_COLUMN_DESCRIPTION)
+                holder.regions = cursor!!.getString(GameDatabase.GAME_COLUMN_REGIONS)
+                holder.company = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
+                val backgroundColorId =
+                    if (isValidGame(holder.path!!)) R.color.view_background else R.color.view_disabled
+                val itemView = holder.itemView
+                itemView.setBackgroundColor(
+                    ContextCompat.getColor(
+                        itemView.context,
+                        backgroundColorId
+                    )
+                )
+            } else {
+                Log.error("[GameAdapter] Can't bind view; Cursor is not valid.")
+            }
+        } else {
+            Log.error("[GameAdapter] Can't bind view; dataset is not valid.")
+        }
+    }
+
+    override fun getItemCount(): Int {
+        if (isDatasetValid && cursor != null) {
+            return cursor!!.count
+        }
+        Log.error("[GameAdapter] Dataset is not valid.")
+        return 0
+    }
+
+    /**
+     * Return the contents of the _id column for a given row.
+     *
+     * @param position The row for which Android wants an ID.
+     * @return A valid ID from the database, or 0 if not available.
+     */
+    override fun getItemId(position: Int): Long {
+        if (isDatasetValid && cursor != null) {
+            if (cursor!!.moveToPosition(position)) {
+                return cursor!!.getLong(GameDatabase.COLUMN_DB_ID)
+            }
+        }
+        Log.error("[GameAdapter] Dataset is not valid.")
+        return 0
+    }
+
+    /**
+     * Tell Android whether or not each item in the dataset has a stable identifier.
+     * Which it does, because it's a database, so always tell Android 'true'.
+     *
+     * @param hasStableIds ignored.
+     */
+    override fun setHasStableIds(hasStableIds: Boolean) {
+        super.setHasStableIds(true)
+    }
+
+    /**
+     * When a load is finished, call this to replace the existing data with the newly-loaded
+     * data.
+     *
+     * @param cursor The newly-loaded Cursor.
+     */
+    fun swapCursor(cursor: Cursor) {
+        // Sanity check.
+        if (cursor === this.cursor) {
+            return
+        }
+
+        // Before getting rid of the old cursor, disassociate it from the Observer.
+        val oldCursor = this.cursor
+        if (oldCursor != null && observer != null) {
+            oldCursor.unregisterDataSetObserver(observer)
+        }
+        this.cursor = cursor
+        isDatasetValid = if (this.cursor != null) {
+            // Attempt to associate the new Cursor with the Observer.
+            if (observer != null) {
+                this.cursor!!.registerDataSetObserver(observer)
+            }
+            true
+        } else {
+            false
+        }
+        notifyDataSetChanged()
+    }
+
+    /**
+     * 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
+        launch((view.context as FragmentActivity), holder.path, holder.title)
+    }
+
+    private fun isValidGame(path: String): Boolean {
+        return Stream.of(".rar", ".zip", ".7z", ".torrent", ".tar", ".gz")
+            .noneMatch { suffix: String? ->
+                path.lowercase(Locale.getDefault()).endsWith(suffix!!)
+            }
+    }
+
+    private inner class GameDataSetObserver : DataSetObserver() {
+        override fun onChanged() {
+            super.onChanged()
+            isDatasetValid = true
+            notifyDataSetChanged()
+        }
+
+        override fun onInvalidated() {
+            super.onInvalidated()
+            isDatasetValid = false
+            notifyDataSetChanged()
+        }
+    }
+}