diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppPreviewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppPreviewAdapter.java
new file mode 100644
index 000000000..a37e80117
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppPreviewAdapter.java
@@ -0,0 +1,40 @@
+package org.fdroid.fdroid.views.categories;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.data.App;
+
+class AppPreviewAdapter extends RecyclerView.Adapter<AppCardController> {
+
+    private Cursor cursor;
+    private final Activity activity;
+
+    AppPreviewAdapter(Activity activity) {
+        this.activity = activity;
+    }
+
+    @Override
+    public AppCardController onCreateViewHolder(ViewGroup parent, int viewType) {
+        return new AppCardController(activity, activity.getLayoutInflater().inflate(R.layout.app_card_normal, parent, false));
+    }
+
+    @Override
+    public void onBindViewHolder(AppCardController holder, int position) {
+        cursor.moveToPosition(position);
+        holder.bindApp(new App(cursor));
+    }
+
+    @Override
+    public int getItemCount() {
+        return cursor == null ? 0 : cursor.getCount();
+    }
+
+    public void setAppCursor(Cursor cursor) {
+        this.cursor = cursor;
+        notifyDataSetChanged();
+    }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java
new file mode 100644
index 000000000..cf7f4b017
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryAdapter.java
@@ -0,0 +1,44 @@
+package org.fdroid.fdroid.views.categories;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.support.v4.app.LoaderManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.data.Schema;
+
+public class CategoryAdapter extends RecyclerView.Adapter<CategoryController> {
+
+    private Cursor cursor;
+    private final Activity activity;
+    private final LoaderManager loaderManager;
+
+    public CategoryAdapter(Activity activity, LoaderManager loaderManager) {
+        this.activity = activity;
+        this.loaderManager = loaderManager;
+    }
+
+    @Override
+    public CategoryController onCreateViewHolder(ViewGroup parent, int viewType) {
+        return new CategoryController(activity, loaderManager, activity.getLayoutInflater().inflate(R.layout.category_item, parent, false));
+    }
+
+    @Override
+    public void onBindViewHolder(CategoryController holder, int position) {
+        cursor.moveToPosition(position);
+        holder.bindModel(cursor.getString(cursor.getColumnIndex(Schema.CategoryTable.Cols.NAME)));
+    }
+
+    @Override
+    public int getItemCount() {
+        return cursor == null ? 0 : cursor.getCount();
+    }
+
+    public void setCategoriesCursor(Cursor cursor) {
+        this.cursor = cursor;
+        notifyDataSetChanged();
+    }
+
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java
new file mode 100644
index 000000000..85dcc9aa0
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java
@@ -0,0 +1,168 @@
+package org.fdroid.fdroid.views.categories;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.data.AppProvider;
+import org.fdroid.fdroid.data.Schema;
+
+import java.util.Random;
+
+public class CategoryController extends RecyclerView.ViewHolder implements LoaderManager.LoaderCallbacks<Cursor> {
+    private final Button viewAll;
+    private final TextView heading;
+    private final AppPreviewAdapter appCardsAdapter;
+    private final FrameLayout background;
+
+    private final Activity activity;
+    private final LoaderManager loaderManager;
+
+    private String currentCategory;
+
+    CategoryController(final Activity activity, LoaderManager loaderManager, View itemView) {
+        super(itemView);
+
+        this.activity = activity;
+        this.loaderManager = loaderManager;
+
+        appCardsAdapter = new AppPreviewAdapter(activity);
+
+        viewAll = (Button) itemView.findViewById(R.id.button);
+        viewAll.setOnClickListener(onViewAll);
+
+        heading = (TextView) itemView.findViewById(R.id.name);
+
+        background = (FrameLayout) itemView.findViewById(R.id.category_background);
+
+        RecyclerView appCards = (RecyclerView) itemView.findViewById(R.id.app_cards);
+        appCards.setAdapter(appCardsAdapter);
+        appCards.addItemDecoration(new ItemDecorator(activity));
+    }
+
+    void bindModel(@NonNull String categoryName) {
+        currentCategory = categoryName;
+        heading.setText(categoryName);
+        viewAll.setVisibility(View.INVISIBLE);
+        loaderManager.initLoader(currentCategory.hashCode(), null, this);
+        loaderManager.initLoader(currentCategory.hashCode() + 1, null, this);
+
+        background.setBackgroundColor(getBackgroundColour(categoryName));
+    }
+
+    public static int getBackgroundColour(@NonNull String categoryName) {
+        // Seed based on the categoryName, so that each time we try to choose a colour for the same
+        // category it will look the same for each different user, and each different session.
+        Random random = new Random(categoryName.toLowerCase().hashCode());
+
+        float[] hsv = new float[3];
+        hsv[0] = random.nextFloat() * 360;
+        hsv[1] = 0.4f;
+        hsv[2] = 0.5f;
+        return Color.HSVToColor(hsv);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        if (id == currentCategory.hashCode() + 1) {
+            return new CursorLoader(
+                    activity,
+                    AppProvider.getCategoryUri(currentCategory),
+                    new String[]{Schema.AppMetadataTable.Cols._COUNT},
+                    null,
+                    null,
+                    null
+            );
+        } else {
+            return new CursorLoader(
+                    activity,
+                    AppProvider.getTopFromCategoryUri(currentCategory, 20),
+                    new String[]{
+                            Schema.AppMetadataTable.Cols.NAME,
+                            Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
+                            Schema.AppMetadataTable.Cols.SUMMARY,
+                            Schema.AppMetadataTable.Cols.ICON_URL,
+                    },
+                    null,
+                    null,
+                    null
+            );
+        }
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        int topAppsId = currentCategory.hashCode();
+        int countAllAppsId = topAppsId + 1;
+
+        // Anything other than these IDs indicates that the loader which just finished finished
+        // is no longer the one this view holder is interested in, due to the user having
+        // scrolled away already during the asynchronous query being run.
+        if (loader.getId() == topAppsId) {
+            appCardsAdapter.setAppCursor(cursor);
+        } else if (loader.getId() == countAllAppsId) {
+            cursor.moveToFirst();
+            int numAppsInCategory = cursor.getInt(0);
+            viewAll.setVisibility(View.VISIBLE);
+            viewAll.setText(activity.getResources().getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory, numAppsInCategory));
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        appCardsAdapter.setAppCursor(null);
+    }
+
+    private final View.OnClickListener onViewAll = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+        }
+    };
+
+    /**
+     * Applies excessive padding to the start of the first item. This is so that the category artwork
+     * can peek out and make itself visible. This is RTL friendly.
+     * @see org.fdroid.fdroid.R.dimen#category_preview__app_list__padding__horizontal
+     * @see org.fdroid.fdroid.R.dimen#category_preview__app_list__padding__horizontal__first
+     */
+    private static class ItemDecorator extends RecyclerView.ItemDecoration {
+        private final Context context;
+
+        ItemDecorator(Context context) {
+            this.context = context.getApplicationContext();
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+            int horizontalPadding = (int) context.getResources().getDimension(R.dimen.category_preview__app_list__padding__horizontal);
+            int horizontalPaddingFirst = (int) context.getResources().getDimension(R.dimen.category_preview__app_list__padding__horizontal__first);
+            boolean isLtr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR;
+            int itemPosition = parent.getChildLayoutPosition(view);
+            boolean first = itemPosition == 0;
+
+            // Leave this "paddingEnd" local variable here for clarity when converting from
+            // left/right to start/end for RTL friendly layout.
+            // noinspection UnnecessaryLocalVariable
+            int paddingEnd = horizontalPadding;
+            int paddingStart = first ? horizontalPaddingFirst : horizontalPadding;
+
+            int paddingLeft = isLtr ? paddingStart : paddingEnd;
+            int paddingRight = isLtr ? paddingEnd : paddingStart;
+            outRect.set(paddingLeft, 0, paddingRight, 0);
+        }
+    }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java
new file mode 100644
index 000000000..3bcf6567c
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java
@@ -0,0 +1,80 @@
+package org.fdroid.fdroid.views.main;
+
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.data.CategoryProvider;
+import org.fdroid.fdroid.data.Schema;
+import org.fdroid.fdroid.views.categories.CategoryAdapter;
+
+/**
+ * Responsible for ensuring that the categories view is inflated and then populated correctly.
+ * Will start a loader to get the list of categories from the database and populate a recycler
+ * view with relevant info about each.
+ */
+class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
+
+    private static final int LOADER_ID = 429820532;
+
+    private final CategoryAdapter categoryAdapter;
+    private final AppCompatActivity activity;
+
+    CategoriesViewBinder(AppCompatActivity activity, FrameLayout parent) {
+        this.activity = activity;
+
+        View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true);
+
+        categoryAdapter = new CategoryAdapter(activity, activity.getSupportLoaderManager());
+
+        RecyclerView categoriesList = (RecyclerView) categoriesView.findViewById(R.id.category_list);
+        categoriesList.setHasFixedSize(true);
+        categoriesList.setLayoutManager(new LinearLayoutManager(activity));
+        categoriesList.setAdapter(categoryAdapter);
+
+        activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        if (id != LOADER_ID) {
+            return null;
+        }
+
+        return new CursorLoader(
+                activity,
+                CategoryProvider.getAllCategories(),
+                Schema.CategoryTable.Cols.ALL,
+                null,
+                null,
+                null
+        );
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        if (loader.getId() != LOADER_ID) {
+            return;
+        }
+
+        categoryAdapter.setCategoriesCursor(cursor);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        if (loader.getId() != LOADER_ID) {
+            return;
+        }
+
+        categoryAdapter.setCategoriesCursor(null);
+    }
+
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java
index 668a44f9a..234a6ebe4 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/main/MainViewController.java
@@ -46,7 +46,11 @@ class MainViewController extends RecyclerView.ViewHolder {
         new MyAppsViewBinder(activity, frame);
     }
 
+    /**
+     * @see CategoriesViewBinder
+     */
     public void bindCategoriesView() {
+        new CategoriesViewBinder(activity, frame);
     }
 
     /**
diff --git a/app/src/main/res/drawable/category_preview_app_card_background.xml b/app/src/main/res/drawable/category_preview_app_card_background.xml
new file mode 100644
index 000000000..b2eb1a1fa
--- /dev/null
+++ b/app/src/main/res/drawable/category_preview_app_card_background.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Visually different from other app cards because it doesn't have a drop shadow, and has a larger
+  corner radius.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="4dp" />
+    <solid android:color="#faf8ef" />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_card_normal.xml b/app/src/main/res/layout/app_card_normal.xml
new file mode 100644
index 000000000..e303d6196
--- /dev/null
+++ b/app/src/main/res/layout/app_card_normal.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="100dp"
+    android:layout_height="130dp"
+    android:background="@drawable/category_preview_app_card_background"
+    android:padding="8dp">
+
+    <!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
+         when it will inevitably read out the name of the app straight after. -->
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="48dip"
+        android:layout_height="48dip"
+        tools:src="@drawable/ic_launcher"
+        android:scaleType="fitCenter"
+        android:layout_marginEnd="16dp"
+        android:layout_marginRight="16dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginLeft="16dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginBottom="8dp"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="ContentDescription" />
+
+    <TextView
+        tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/summary"
+        android:maxLines="3"
+        android:ellipsize="end"
+        app:layout_constraintTop_toBottomOf="@+id/icon"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        android:layout_marginTop="8dp" />
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/category_item.xml b/app/src/main/res/layout/category_item.xml
new file mode 100644
index 000000000..392ee0889
--- /dev/null
+++ b/app/src/main/res/layout/category_item.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:orientation="vertical" android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <TextView
+        android:id="@+id/name"
+        tools:text="Business"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBaseline_toBaselineOf="@+id/button"
+        android:textAppearance="@style/TextAppearance.AppCompat.Headline"
+        android:textSize="18sp"
+        android:textColor="#4a4a4a"
+        android:paddingLeft="18dp"
+        android:paddingStart="18dp"
+        android:paddingRight="18dp"
+        android:paddingEnd="18dp"
+        tools:layout_editor_absoluteX="0dp" />
+
+    <Button
+        android:id="@+id/button"
+        tools:text="View all 10"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:background="@android:color/transparent"
+        android:paddingLeft="18dp"
+        android:paddingStart="18dp"
+        android:paddingRight="18dp"
+        android:paddingEnd="18dp"
+        android:paddingTop="24dp"
+        android:paddingBottom="12dp"
+        android:textSize="14sp"
+        android:textAllCaps="true"
+        android:textColor="@color/fdroid_blue"
+
+        tools:layout_editor_absoluteX="268dp" />
+
+    <FrameLayout
+        android:id="@+id/category_background"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="@+id/app_cards"
+        app:layout_constraintBottom_toBottomOf="@+id/app_cards"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        tools:background="#ffffbbbb"/>
+
+    <ImageView
+        android:id="@+id/category_image"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/button"
+        android:layout_width="100dp"
+        android:layout_height="100dp"
+        tools:ignore="ContentDescription" />
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/app_cards"
+        tools:listitem="@layout/app_card_normal"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/button"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layoutManager="LinearLayoutManager"
+        android:orientation="horizontal"
+        android:paddingTop="@dimen/category_preview__app_list__padding__vertical"
+        android:paddingBottom="@dimen/category_preview__app_list__padding__vertical"
+        android:clipToPadding="false"
+        android:layout_marginLeft="8dp"
+        android:layout_marginRight="8dp"/>
+
+</android.support.constraint.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/main_tab_categories.xml b/app/src/main/res/layout/main_tab_categories.xml
new file mode 100644
index 000000000..2b4676585
--- /dev/null
+++ b/app/src/main/res/layout/main_tab_categories.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/category_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:listitem="@layout/category_item"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        android:scrollbars="vertical" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index bf3461874..2e70e8ac4 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -20,4 +20,11 @@
 
     <dimen name="whats_new__padding__app_card__horizontal">12dp</dimen>
     <dimen name="whats_new__padding__app_card__vertical">10dp</dimen>
+
+    <dimen name="category_preview__app_list__padding__horizontal">4dp</dimen>
+    <dimen name="category_preview__app_list__padding__horizontal__first">72dp</dimen>
+    <dimen name="category_preview__app_list__padding__vertical">18dp</dimen>
+    <dimen name="category_preview__padding__recycler_view__top">12dp</dimen>
+    <dimen name="category_preview__padding__app_card__horizontal">3dp</dimen>
+    <dimen name="category_preview__padding__app_card__vertical">4dp</dimen>
 </resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4ee0d76f1..7052fc58c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -277,6 +277,13 @@
     <string name="category_Time">Time</string>
     <string name="category_Writing">Writing</string>
 
+    <plurals name="button_view_all_apps_in_category">
+        <!-- Even though these are the same as eachother, Android docs suggest always specifying at
+             least "one" and "other": https://developer.android.com/guide/topics/resources/string-resource.html#Plurals -->
+        <item quantity="one">View all %d</item>
+        <item quantity="other">View all %d</item>
+    </plurals>
+
     <string name="empty_installed_app_list">No apps installed.\n\nThere are apps on your device, but they are not available from F-Droid. This could be because you need to update your repositories, or the repositories genuinely don\'t have your apps available.</string>
     <string name="empty_available_app_list">No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps.</string>
     <string name="empty_can_update_app_list">All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date).</string>