diff --git a/briar-android/res/layout/activity_blog.xml b/briar-android/res/layout/activity_blog.xml
index c0ac19fbe0eac0b76817a2c7bc9bb8566f6aa840..3ff8d409e0e60ee2b749b2e51726b8af8e5cab7c 100644
--- a/briar-android/res/layout/activity_blog.xml
+++ b/briar-android/res/layout/activity_blog.xml
@@ -1,10 +1,21 @@
 <?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.util.BriarRecyclerView
-	android:id="@+id/postList"
+<FrameLayout
 	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="match_parent"
-	android:layout_height="match_parent"
-	app:scrollToEnd="false"
-	tools:context=".android.blogs.BlogActivity"/>
+	android:layout_height="match_parent">
+
+	<android.support.v4.view.ViewPager
+		android:id="@+id/pager"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		tools:context=".android.blogs.BlogActivity"/>
+
+	<ProgressBar
+		android:id="@+id/progressBar"
+		style="?android:attr/progressBarStyleLarge"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="center"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/fragment_blog.xml b/briar-android/res/layout/fragment_blog.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c0ac19fbe0eac0b76817a2c7bc9bb8566f6aa840
--- /dev/null
+++ b/briar-android/res/layout/fragment_blog.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.briarproject.android.util.BriarRecyclerView
+	android:id="@+id/postList"
+	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="match_parent"
+	android:layout_height="match_parent"
+	app:scrollToEnd="false"
+	tools:context=".android.blogs.BlogActivity"/>
diff --git a/briar-android/res/layout/fragment_blog_post.xml b/briar-android/res/layout/fragment_blog_post.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ee02e64fbbf26756b845ebd18e3c0ce7d86ede78
--- /dev/null
+++ b/briar-android/res/layout/fragment_blog_post.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content">
+
+	<RelativeLayout
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:padding="@dimen/margin_activity_horizontal">
+
+		<de.hdodenhof.circleimageview.CircleImageView
+			android:id="@+id/avatar"
+			style="@style/BriarAvatar"
+			android:layout_width="30dp"
+			android:layout_height="30dp"
+			android:layout_marginRight="@dimen/margin_medium"
+			tools:src="@drawable/ic_launcher"/>
+
+		<TextView
+			android:id="@+id/authorName"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignTop="@+id/avatar"
+			android:layout_toEndOf="@+id/avatar"
+			android:layout_toRightOf="@+id/avatar"
+			android:textSize="@dimen/text_size_tiny"
+			tools:text="Author Name"/>
+
+		<TextView
+			android:id="@+id/date"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignBottom="@id/avatar"
+			android:layout_below="@+id/authorName"
+			android:layout_toEndOf="@+id/avatar"
+			android:layout_toRightOf="@+id/avatar"
+			android:gravity="bottom"
+			android:textSize="@dimen/text_size_tiny"
+			tools:text="yesterday"/>
+
+		<org.briarproject.android.util.TrustIndicatorView
+			android:id="@+id/trustIndicator"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_marginLeft="@dimen/margin_small"
+			android:layout_toRightOf="@+id/authorName"
+			tools:src="@drawable/trust_indicator_verified"/>
+
+		<TextView
+			android:id="@+id/title"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignParentLeft="true"
+			android:layout_alignParentStart="true"
+			android:layout_below="@+id/avatar"
+			android:layout_marginTop="@dimen/margin_medium"
+			android:textSize="@dimen/text_size_medium"
+			android:textStyle="bold"
+			tools:text="This Is A Blog Post Title"/>
+
+		<TextView
+			android:id="@+id/body"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignParentEnd="true"
+			android:layout_alignParentLeft="true"
+			android:layout_alignParentRight="true"
+			android:layout_alignParentStart="true"
+			android:layout_below="@+id/title"
+			android:layout_marginTop="@dimen/margin_medium"
+			tools:text="Body of Blog Post. This could be insanely large or just a short text as well."/>
+
+	</RelativeLayout>
+
+</ScrollView>
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index 793e97d7a50f08635359e75ab09ce45e1d6c5340..d283c5c978ccdc6b3803b3532c97a5e72297951c 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -267,6 +267,8 @@
 	<string name="blogs_write_blog_post_title_hint">Add a title (optional)</string>
 	<string name="blogs_write_blog_post_body_hint">Type your blog post here</string>
 	<string name="blogs_publish_blog_post">Publish</string>
+	<string name="blogs_blog_failed_to_load">Blog failed to load</string>
+	<string name="blogs_blog_post_failed_to_load">Blog Post failed to load</string>
 
 	<string name="blogs_blog_list">Blog List</string>
 	<string name="blogs_available_blogs">Available Blogs</string>
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index c55e56f831a719fb5f2a7878cad94600e0731637..464fddd6f64fc12a6791b4d3af9aafb24e5d1496 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -3,6 +3,8 @@ package org.briarproject.android;
 import android.app.Activity;
 
 import org.briarproject.android.blogs.BlogActivity;
+import org.briarproject.android.blogs.BlogFragment;
+import org.briarproject.android.blogs.BlogPostFragment;
 import org.briarproject.android.blogs.CreateBlogActivity;
 import org.briarproject.android.blogs.MyBlogsFragment;
 import org.briarproject.android.contact.ContactListFragment;
@@ -73,6 +75,10 @@ public interface ActivityComponent {
 
 	void inject(WriteBlogPostActivity activity);
 
+	void inject(BlogFragment fragment);
+
+	void inject(BlogPostFragment fragment);
+
 	void inject(SettingsActivity activity);
 
 	void inject(ChangePasswordActivity activity);
diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java
index ea984bca0bab42760c652869ea561f87c85e69ee..d9879e3420e8fa0aa44393ef7f5d10af4ecd8686 100644
--- a/briar-android/src/org/briarproject/android/ActivityModule.java
+++ b/briar-android/src/org/briarproject/android/ActivityModule.java
@@ -4,6 +4,8 @@ import android.app.Activity;
 import android.content.Context;
 import android.content.SharedPreferences;
 
+import org.briarproject.android.blogs.BlogController;
+import org.briarproject.android.blogs.BlogControllerImpl;
 import org.briarproject.android.controller.BriarController;
 import org.briarproject.android.controller.BriarControllerImpl;
 import org.briarproject.android.controller.ConfigController;
@@ -107,6 +109,13 @@ public class ActivityModule {
 		return forumController;
 	}
 
+	@ActivityScope
+	@Provides
+	BlogController provideBlogController(BlogControllerImpl blogController) {
+		activity.addLifecycleController(blogController);
+		return blogController;
+	}
+
 	@ActivityScope
 	@Provides
 	protected NavDrawerController provideNavDrawerController(
diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java
index d6a495eb0ff57af79c6f3774f2aec8422aadb370..3e59233be3dc1542bfa2b4867cb90197cc5c6e39 100644
--- a/briar-android/src/org/briarproject/android/AndroidComponent.java
+++ b/briar-android/src/org/briarproject/android/AndroidComponent.java
@@ -5,6 +5,7 @@ import org.briarproject.CoreModule;
 import org.briarproject.android.api.AndroidExecutor;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.api.ReferenceManager;
+import org.briarproject.android.blogs.BlogPersistentData;
 import org.briarproject.android.forum.ForumPersistentData;
 import org.briarproject.android.report.BriarReportSender;
 import org.briarproject.api.blogs.BlogManager;
@@ -118,6 +119,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
 
 	ForumPersistentData forumPersistentData();
 
+	BlogPersistentData blogPersistentData();
+
 	@IoExecutor
 	Executor ioExecutor();
 
diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java
index 36933a1184aec4c6df22703b391d15046b1c0750..825e639c6c69ab442b1d85d388b2769e68920733 100644
--- a/briar-android/src/org/briarproject/android/AppModule.java
+++ b/briar-android/src/org/briarproject/android/AppModule.java
@@ -4,6 +4,7 @@ import android.app.Application;
 
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.api.ReferenceManager;
+import org.briarproject.android.blogs.BlogPersistentData;
 import org.briarproject.android.forum.ForumPersistentData;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.PublicKey;
@@ -143,4 +144,10 @@ public class AppModule {
 	ForumPersistentData provideForumPersistence() {
 		return new ForumPersistentData();
 	}
+
+	@Provides
+	@Singleton
+	BlogPersistentData provideBlogPersistence() {
+		return new BlogPersistentData();
+	}
 }
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
index aed08eb1e2cdcada4284b84303f858a63988cc8d..35321f390a96ee09c356de2b294552cc812d9696 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogActivity.java
@@ -2,180 +2,260 @@ package org.briarproject.android.blogs;
 
 import android.content.Intent;
 import android.os.Bundle;
-import android.support.design.widget.Snackbar;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.app.ActivityOptionsCompat;
-import android.support.v7.widget.LinearLayoutManager;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.BriarActivity;
-import org.briarproject.android.util.BriarRecyclerView;
-import org.briarproject.api.blogs.BlogManager;
-import org.briarproject.api.blogs.BlogPostHeader;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.android.blogs.BlogController.BlogPostListener;
+import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener;
 import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
-import static android.support.design.widget.Snackbar.LENGTH_LONG;
-import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static android.widget.Toast.LENGTH_SHORT;
 
-public class BlogActivity extends BriarActivity {
+public class BlogActivity extends BriarActivity implements BlogPostListener,
+		OnBlogPostClickListener, BaseFragmentListener {
 
+	static final int REQUEST_WRITE_POST = 1;
 	static final String BLOG_NAME = "briar.BLOG_NAME";
 	static final String IS_MY_BLOG = "briar.IS_MY_BLOG";
 	static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG";
-	private static final int WRITE_POST = 1;
 
+	private static final String BLOG_PAGER_ADAPTER = "briar.BLOG_PAGER_ADAPTER";
 	private static final Logger LOG =
 			Logger.getLogger(BlogActivity.class.getName());
 
-	private BlogPostAdapter adapter;
-	private BriarRecyclerView list;
+	private ProgressBar progressBar;
+	private ViewPager pager;
+	private BlogPagerAdapter blogPagerAdapter;
+	private BlogPostPagerAdapter postPagerAdapter;
 	private String blogName;
-	private boolean myBlog;
+	private boolean myBlog, isNew;
 
 	// Fields that are accessed from background threads must be volatile
 	private volatile GroupId groupId = null;
-	private volatile boolean scrollToTop = false;
 	@Inject
-	volatile BlogManager blogManager;
+	BlogController blogController;
 
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
 
-		setContentView(R.layout.activity_blog);
-
+		// GroupId from Intent
 		Intent i = getIntent();
 		byte[] b = i.getByteArrayExtra(GROUP_ID);
 		if (b == null) throw new IllegalStateException("No Group in intent.");
 		groupId = new GroupId(b);
+
+		// Name of the Blog from Intent
 		blogName = i.getStringExtra(BLOG_NAME);
 		if (blogName != null) setTitle(blogName);
+
+		// Is this our blog and was it just created?
 		myBlog = i.getBooleanExtra(IS_MY_BLOG, false);
+		isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
 
-		adapter = new BlogPostAdapter(this, groupId, blogName);
-		list = (BriarRecyclerView) this.findViewById(R.id.postList);
-		list.setLayoutManager(new LinearLayoutManager(this));
-		list.setAdapter(adapter);
-		if (myBlog) {
-			list.setEmptyText(
-					getString(R.string.blogs_my_blogs_blog_empty_state));
+		setContentView(R.layout.activity_blog);
+
+		pager = (ViewPager) findViewById(R.id.pager);
+		progressBar = (ProgressBar) findViewById(R.id.progressBar);
+		hideLoadingScreen();
+
+		blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager());
+		if (state == null || state.getBoolean(BLOG_PAGER_ADAPTER, true)) {
+			pager.setAdapter(blogPagerAdapter);
 		} else {
-			list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
+			// this initializes and restores the postPagerAdapter
+			loadBlogPosts();
 		}
+	}
 
-		// show snackbar if this blog was just created
-		boolean isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
-		if (isNew) {
-			Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created,
-					LENGTH_LONG);
-			s.getView().setBackgroundResource(R.color.briar_primary);
-			s.show();
+	@Override
+	public void onSaveInstanceState(Bundle outState) {
+		super.onSaveInstanceState(outState);
+
+		// remember which adapter we had active
+		outState.putBoolean(BLOG_PAGER_ADAPTER,
+				pager.getAdapter() == blogPagerAdapter);
+	}
+
+	@Override
+	public void onBackPressed() {
+		if (pager.getAdapter() == postPagerAdapter) {
+			pager.setAdapter(blogPagerAdapter);
+		} else {
+			super.onBackPressed();
 		}
 	}
 
 	@Override
-	public void onResume() {
-		super.onResume();
-		loadBlogPosts();
+	public void injectActivity(ActivityComponent component) {
+		component.inject(this);
 	}
 
 	@Override
-	public boolean onCreateOptionsMenu(Menu menu) {
-		if (myBlog) {
-			MenuInflater inflater = getMenuInflater();
-			inflater.inflate(R.menu.blogs_my_blog_actions, menu);
-		}
-		return super.onCreateOptionsMenu(menu);
+	public void showLoadingScreen(boolean isBlocking, int stringId) {
+		progressBar.setVisibility(VISIBLE);
+	}
+
+	private void showLoadingScreen() {
+		showLoadingScreen(false, 0);
 	}
 
 	@Override
-	public boolean onOptionsItemSelected(final MenuItem item) {
-		switch (item.getItemId()) {
-			case R.id.action_write_blog_post:
-				Intent i = new Intent(this, WriteBlogPostActivity.class);
-				i.putExtra(GROUP_ID, groupId.getBytes());
-				i.putExtra(BLOG_NAME, blogName);
-				ActivityOptionsCompat options =
-						makeCustomAnimation(this, android.R.anim.slide_in_left,
-								android.R.anim.slide_out_right);
-				ActivityCompat.startActivityForResult(this, i, WRITE_POST,
-						options.toBundle());
-				return true;
-			default:
-				return super.onOptionsItemSelected(item);
-		}
+	public void hideLoadingScreen() {
+		progressBar.setVisibility(GONE);
 	}
 
 	@Override
-	public void onActivityResult(int requestCode, int resultCode, Intent data) {
-		if (requestCode == WRITE_POST && resultCode == RESULT_OK) {
-			scrollToTop = true;
-		}
+	public void onFragmentCreated(String tag) {
+
 	}
 
 	@Override
-	public void injectActivity(ActivityComponent component) {
-		component.inject(this);
+	public void onBlogPostClick(final int position) {
+		loadBlogPosts(position, true);
 	}
 
 	private void loadBlogPosts() {
-		runOnDbThread(new Runnable() {
-			@Override
-			public void run() {
-				try {
-					// load blog posts
-					long now = System.currentTimeMillis();
-					Collection<BlogPostItem> posts = new ArrayList<>();
-					try {
-						Collection<BlogPostHeader> header =
-								blogManager.getPostHeaders(groupId);
-						for (BlogPostHeader h : header) {
-							posts.add(new BlogPostItem(h));
+		loadBlogPosts(0, false);
+	}
+
+	private void loadBlogPosts(final int position, final boolean setItem) {
+		showLoadingScreen();
+		blogController
+				.loadBlog(groupId, false, new UiResultHandler<Boolean>(this) {
+					@Override
+					public void onResultUi(Boolean result) {
+						if (result) {
+							Collection<BlogPostItem> posts =
+									blogController.getBlogPosts();
+
+							if (postPagerAdapter == null) {
+								postPagerAdapter = new BlogPostPagerAdapter(
+										getSupportFragmentManager(),
+										posts.size());
+							} else {
+								postPagerAdapter.setSize(posts.size());
+							}
+							pager.setAdapter(postPagerAdapter);
+							if (setItem) pager.setCurrentItem(position);
+						} else {
+							Toast.makeText(BlogActivity.this,
+									R.string.blogs_blog_post_failed_to_load,
+									LENGTH_SHORT).show();
 						}
-					} catch (NoSuchGroupException e) {
-						// Continue
 					}
-					displayBlogPosts(posts);
-					long duration = System.currentTimeMillis() - now;
-					if (LOG.isLoggable(INFO))
-						LOG.info("Post header load took " + duration + " ms");
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-				}
-			}
-		});
+				});
 	}
 
-	private void displayBlogPosts(final Collection<BlogPostItem> items) {
+	@Override
+	public void onBlogPostAdded(final BlogPostItem post, final boolean local) {
 		runOnUiThread(new Runnable() {
 			@Override
 			public void run() {
-				if (items.size() == 0) {
-					list.showData();
-				} else {
-					adapter.addAll(items);
-					if (scrollToTop) list.scrollToPosition(0);
+				if (blogPagerAdapter != null) {
+					BlogFragment f = blogPagerAdapter.getFragment();
+					if (f != null && f.isVisible()) {
+						f.onBlogPostAdded(post, local);
+					}
+				}
+
+				if (postPagerAdapter != null) {
+					postPagerAdapter.onBlogPostAdded();
+					postPagerAdapter.notifyDataSetChanged();
 				}
-				scrollToTop = false;
 			}
 		});
 	}
 
-	// TODO listen to events and add new blog posts as they come in
+	@Override
+	protected void onActivityResult(int requestCode, int resultCode,
+			Intent data) {
+
+		// The BlogPostAddedEvent arrives when the controller is not listening,
+		// so we need to manually reload the blog posts :(
+		if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
+			BlogFragment f = blogPagerAdapter.getFragment();
+			if (f != null && f.isVisible()) {
+				f.reload();
+			}
+		}
+	}
+
+
+	private class BlogPagerAdapter extends FragmentStatePagerAdapter {
+		private BlogFragment fragment = null;
+
+		BlogPagerAdapter(FragmentManager fm) {
+			super(fm);
+		}
+
+		@Override
+		public int getCount() {
+			return 1;
+		}
+
+		@Override
+		public Fragment getItem(int position) {
+			return BlogFragment.newInstance(groupId, blogName, myBlog, isNew);
+		}
+
+		@Override
+		public Object instantiateItem(ViewGroup container, int position) {
+			// save a reference to the single fragment here for later
+			fragment =
+					(BlogFragment) super.instantiateItem(container, position);
+			return fragment;
+		}
+
+		BlogFragment getFragment() {
+			return fragment;
+		}
+	}
+
+	private class BlogPostPagerAdapter extends FragmentStatePagerAdapter {
+		private int size;
+
+		BlogPostPagerAdapter(FragmentManager fm, int size) {
+			super(fm);
+			this.size = size;
+		}
+
+		@Override
+		public int getCount() {
+			return size;
+		}
+
+		@Override
+		public Fragment getItem(int position) {
+			MessageId postIdOfPos = blogController.getBlogPostId(position);
+			return BlogPostFragment.newInstance(groupId, postIdOfPos);
+		}
+
+		void onBlogPostAdded() {
+			size++;
+		}
+
+		void setSize(int size) {
+			this.size = size;
+		}
+	}
 
 }
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogController.java b/briar-android/src/org/briarproject/android/blogs/BlogController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba9d085c2400d91ce520891bfc275e24f6060ca4
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogController.java
@@ -0,0 +1,29 @@
+package org.briarproject.android.blogs;
+
+import android.support.annotation.Nullable;
+
+import org.briarproject.android.controller.ActivityLifecycleController;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.TreeSet;
+
+public interface BlogController extends ActivityLifecycleController {
+
+	void loadBlog(final GroupId groupId, final boolean reload,
+			final UiResultHandler<Boolean> resultHandler);
+
+	TreeSet<BlogPostItem> getBlogPosts();
+
+	@Nullable
+	BlogPostItem getBlogPost(MessageId postId);
+
+	@Nullable
+	MessageId getBlogPostId(int position);
+
+	interface BlogPostListener {
+		void onBlogPostAdded(final BlogPostItem post, final boolean local);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f90a930953fe4e8f909bd39b1d79d1da8eb851a6
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogControllerImpl.java
@@ -0,0 +1,171 @@
+package org.briarproject.android.blogs;
+
+import android.app.Activity;
+import android.support.annotation.Nullable;
+
+import org.briarproject.android.controller.DbControllerImpl;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.api.blogs.BlogManager;
+import org.briarproject.api.blogs.BlogPostHeader;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.event.BlogPostAddedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public class BlogControllerImpl extends DbControllerImpl
+		implements BlogController, EventListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(BlogControllerImpl.class.getName());
+
+	@Inject
+	protected Activity activity;
+	@Inject
+	protected volatile BlogManager blogManager;
+	@Inject
+	protected volatile EventBus eventBus;
+	@Inject
+	protected BlogPersistentData data;
+
+	private volatile BlogPostListener listener;
+
+	@Inject
+	BlogControllerImpl() {
+	}
+
+	@Override
+	public void onActivityCreate() {
+		if (activity instanceof BlogPostListener) {
+			listener = (BlogPostListener) activity;
+		} else {
+			throw new IllegalStateException(
+					"An activity that injects the BlogController must " +
+							"implement the BlogPostListener");
+		}
+	}
+
+	@Override
+	public void onActivityResume() {
+		eventBus.addListener(this);
+	}
+
+	@Override
+	public void onActivityPause() {
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void onActivityDestroy() {
+		if (activity.isFinishing()) {
+			data.clearAll();
+		}
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof BlogPostAddedEvent) {
+			final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
+			if (m.getGroupId().equals(data.getGroupId())) {
+				LOG.info("New blog post added");
+				final BlogPostHeader header = m.getHeader();
+				try {
+					final byte[] body = blogManager.getPostBody(header.getId());
+					final BlogPostItem post = new BlogPostItem(header, body);
+					data.addPost(post);
+					listener.onBlogPostAdded(post, m.isLocal());
+				} catch (DbException ex) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, ex.toString(), ex);
+				}
+			}
+		} else if (e instanceof GroupRemovedEvent) {
+			GroupRemovedEvent s = (GroupRemovedEvent) e;
+			if (s.getGroup().getId().equals(data.getGroupId())) {
+				LOG.info("Blog removed");
+				activity.runOnUiThread(new Runnable() {
+					@Override
+					public void run() {
+						activity.finish();
+					}
+				});
+			}
+		}
+	}
+
+	@Override
+	public void loadBlog(final GroupId groupId, final boolean reload,
+			final UiResultHandler<Boolean> resultHandler) {
+
+		LOG.info("Loading blog...");
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					if (reload || data.getGroupId() == null ||
+							!data.getGroupId().equals(groupId)) {
+						data.setGroupId(groupId);
+						// load blog posts
+						long now = System.currentTimeMillis();
+						Collection<BlogPostItem> posts = new ArrayList<>();
+						Collection<BlogPostHeader> header =
+								blogManager.getPostHeaders(groupId);
+						for (BlogPostHeader h : header) {
+							byte[] body = blogManager.getPostBody(h.getId());
+							posts.add(new BlogPostItem(h, body));
+						}
+						data.setPosts(posts);
+						long duration = System.currentTimeMillis() - now;
+						if (LOG.isLoggable(INFO))
+							LOG.info("Post header load took " + duration +
+									" ms");
+					}
+					resultHandler.onResult(true);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					resultHandler.onResult(false);
+				}
+			}
+		});
+	}
+
+	@Override
+	public TreeSet<BlogPostItem> getBlogPosts() {
+		return data.getBlogPosts();
+	}
+
+	@Override
+	@Nullable
+	public BlogPostItem getBlogPost(MessageId id) {
+		for (BlogPostItem item : getBlogPosts()) {
+			if (item.getId().equals(id)) return item;
+		}
+		return null;
+	}
+
+	@Override
+	@Nullable
+	public MessageId getBlogPostId(int position) {
+		int i = 0;
+		for (BlogPostItem post : getBlogPosts()) {
+			if (i == position) return post.getId();
+			i++;
+		}
+		return null;
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..430cc611a1ef448f66b93d021427b1c5aebddd1a
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
@@ -0,0 +1,191 @@
+package org.briarproject.android.blogs;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.support.v7.widget.LinearLayoutManager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.blogs.BlogController.BlogPostListener;
+import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+import javax.inject.Inject;
+
+import static android.support.design.widget.Snackbar.LENGTH_LONG;
+import static android.support.v4.app.ActivityOptionsCompat.makeCustomAnimation;
+import static android.widget.Toast.LENGTH_SHORT;
+import static org.briarproject.android.BriarActivity.GROUP_ID;
+import static org.briarproject.android.blogs.BlogActivity.BLOG_NAME;
+import static org.briarproject.android.blogs.BlogActivity.IS_MY_BLOG;
+import static org.briarproject.android.blogs.BlogActivity.IS_NEW_BLOG;
+import static org.briarproject.android.blogs.BlogActivity.REQUEST_WRITE_POST;
+
+public class BlogFragment extends BaseFragment implements BlogPostListener {
+
+	public final static String TAG = BlogFragment.class.getName();
+
+	@Inject
+	BlogController blogController;
+
+	private GroupId groupId;
+	private String blogName;
+	private boolean myBlog;
+	private BlogPostAdapter adapter;
+	private BriarRecyclerView list;
+
+	static BlogFragment newInstance(GroupId groupId, String name,
+			boolean myBlog,	boolean isNew) {
+
+		BlogFragment f = new BlogFragment();
+
+		Bundle bundle = new Bundle();
+		bundle.putByteArray(GROUP_ID, groupId.getBytes());
+		bundle.putString(BLOG_NAME, name);
+		bundle.putBoolean(IS_MY_BLOG, myBlog);
+		bundle.putBoolean(IS_NEW_BLOG, isNew);
+
+		f.setArguments(bundle);
+		return f;
+	}
+
+	@Nullable
+	@Override
+	public View onCreateView(LayoutInflater inflater, ViewGroup container,
+			Bundle savedInstanceState) {
+
+		setHasOptionsMenu(true);
+
+		Bundle args = getArguments();
+		byte[] b = args.getByteArray(GROUP_ID);
+		if (b == null) throw new IllegalStateException("No Group found.");
+		groupId = new GroupId(b);
+		blogName = args.getString(BLOG_NAME);
+		myBlog = args.getBoolean(IS_MY_BLOG);
+		boolean isNew = args.getBoolean(IS_NEW_BLOG);
+
+		View v = inflater.inflate(R.layout.fragment_blog, container, false);
+
+		adapter = new BlogPostAdapter(getActivity(),
+				(OnBlogPostClickListener) getActivity());
+		list = (BriarRecyclerView) v.findViewById(R.id.postList);
+		list.setLayoutManager(new LinearLayoutManager(getActivity()));
+		list.setAdapter(adapter);
+		if (myBlog) {
+			list.setEmptyText(
+					getString(R.string.blogs_my_blogs_blog_empty_state));
+		} else {
+			list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
+		}
+
+		// show snackbar if this blog was just created
+		if (isNew) {
+			Snackbar s = Snackbar.make(list, R.string.blogs_my_blogs_created,
+					LENGTH_LONG);
+			s.getView().setBackgroundResource(R.color.briar_primary);
+			s.show();
+
+			// show only once
+			args.putBoolean(IS_NEW_BLOG, false);
+		}
+		return v;
+	}
+
+	@Override
+	public void injectFragment(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		loadData(false);
+	}
+
+	@Override
+	public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+		if (myBlog) {
+			inflater.inflate(R.menu.blogs_my_blog_actions, menu);
+		}
+		super.onCreateOptionsMenu(menu, inflater);
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		switch (item.getItemId()) {
+			case android.R.id.home:
+				getActivity().onBackPressed();
+				return true;
+			case R.id.action_write_blog_post:
+				Intent i =
+						new Intent(getActivity(), WriteBlogPostActivity.class);
+				i.putExtra(GROUP_ID, groupId.getBytes());
+				i.putExtra(BLOG_NAME, blogName);
+				ActivityOptionsCompat options =
+						makeCustomAnimation(getActivity(),
+								android.R.anim.slide_in_left,
+								android.R.anim.slide_out_right);
+				ActivityCompat.startActivityForResult(getActivity(), i,
+						REQUEST_WRITE_POST, options.toBundle());
+				return true;
+			default:
+				return super.onOptionsItemSelected(item);
+		}
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+	@Override
+	public void onBlogPostAdded(BlogPostItem post, boolean local) {
+		adapter.add(post);
+		if (local) list.scrollToPosition(0);
+	}
+
+	private void loadData(final boolean reload) {
+		blogController.loadBlog(groupId, reload,
+				new UiResultHandler<Boolean>(getActivity()) {
+					@Override
+					public void onResultUi(Boolean result) {
+						if (result) {
+							Collection<BlogPostItem> posts =
+									blogController.getBlogPosts();
+							if (posts.size() > 0) {
+								adapter.addAll(posts);
+								if (reload) list.scrollToPosition(0);
+							} else {
+								list.showData();
+							}
+						} else {
+							Toast.makeText(getActivity(),
+									R.string.blogs_blog_failed_to_load,
+									LENGTH_SHORT).show();
+							getActivity().supportFinishAfterTransition();
+						}
+					}
+				});
+	}
+
+	void reload() {
+		loadData(true);
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java b/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java
new file mode 100644
index 0000000000000000000000000000000000000000..a2834c809cd5afc0643be76ccdb7d6d5b3de1025
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPersistentData.java
@@ -0,0 +1,49 @@
+package org.briarproject.android.blogs;
+
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+import java.util.TreeSet;
+
+import javax.inject.Inject;
+
+/**
+ * This class is a singleton that defines the data that should persist, i.e.
+ * still be present in memory after activity restarts. This class is not thread
+ * safe.
+ */
+public class BlogPersistentData {
+
+	private volatile GroupId groupId;
+	private volatile TreeSet<BlogPostItem> posts = new TreeSet<>();
+
+	public BlogPersistentData() {
+
+	}
+
+	public void setGroupId(GroupId groupId) {
+		this.groupId = groupId;
+	}
+
+	public GroupId getGroupId() {
+		return groupId;
+	}
+
+	public void setPosts(Collection<BlogPostItem> posts) {
+		this.posts.clear();
+		this.posts.addAll(posts);
+	}
+
+	void addPost(BlogPostItem post) {
+		posts.add(post);
+	}
+
+	TreeSet<BlogPostItem> getBlogPosts() {
+		return posts;
+	}
+
+	void clearAll() {
+		groupId = null;
+		posts.clear();
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
index 728b33a5fdf25a12049bd0f72bb852e9c7616b98..c071dfe71161d54c5f747fa344a68d052388cd2b 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostAdapter.java
@@ -10,7 +10,6 @@ import android.view.ViewGroup;
 import android.widget.TextView;
 
 import org.briarproject.R;
-import org.briarproject.api.sync.GroupId;
 import org.briarproject.util.StringUtils;
 
 import java.util.Collection;
@@ -61,13 +60,11 @@ class BlogPostAdapter extends
 	});
 
 	private final Context ctx;
-	private final GroupId blogGroupId;
-	private final String blogTitle;
+	private final OnBlogPostClickListener listener;
 
-	BlogPostAdapter(Context ctx, GroupId blogGroupId, String blogTitle) {
+	BlogPostAdapter(Context ctx, OnBlogPostClickListener listener) {
 		this.ctx = ctx;
-		this.blogGroupId = blogGroupId;
-		this.blogTitle = blogTitle;
+		this.listener = listener;
 	}
 
 	@Override
@@ -78,7 +75,7 @@ class BlogPostAdapter extends
 	}
 
 	@Override
-	public void onBindViewHolder(BlogPostHolder ui, int position) {
+	public void onBindViewHolder(final BlogPostHolder ui, int position) {
 		final BlogPostItem item = getItem(position);
 
 		// title
@@ -95,7 +92,7 @@ class BlogPostAdapter extends
 		ui.layout.setOnClickListener(new View.OnClickListener() {
 			@Override
 			public void onClick(View v) {
-				// TODO #428
+				listener.onBlogPostClick(ui.getAdapterPosition());
 			}
 		});
 
@@ -154,4 +151,9 @@ class BlogPostAdapter extends
 			body = (TextView) v.findViewById(R.id.bodyView);
 		}
 	}
+
+	interface OnBlogPostClickListener {
+		void onBlogPostClick(int position);
+	}
+
 }
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..54ac2cdda54898ec9f60815fb3cbb63b2e9ef85c
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostFragment.java
@@ -0,0 +1,157 @@
+package org.briarproject.android.blogs;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.briarproject.R;
+import org.briarproject.android.ActivityComponent;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.fragment.BaseFragment;
+import org.briarproject.android.util.TrustIndicatorView;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.util.StringUtils;
+
+import javax.inject.Inject;
+
+import im.delight.android.identicons.IdenticonDrawable;
+
+import static android.view.View.GONE;
+import static android.widget.Toast.LENGTH_SHORT;
+import static org.briarproject.android.BriarActivity.GROUP_ID;
+
+public class BlogPostFragment extends BaseFragment {
+
+	public final static String TAG = BlogPostFragment.class.getName();
+
+	private final static String BLOG_POST_ID = "briar.BLOG_NAME";
+
+	private GroupId groupId;
+	private MessageId postId;
+	private BlogPostViewHolder ui;
+
+	@Inject
+	BlogController blogController;
+
+	static BlogPostFragment newInstance(GroupId groupId, MessageId postId) {
+		BlogPostFragment f = new BlogPostFragment();
+
+		Bundle bundle = new Bundle();
+		bundle.putByteArray(GROUP_ID, groupId.getBytes());
+		bundle.putByteArray(BLOG_POST_ID, postId.getBytes());
+
+		f.setArguments(bundle);
+		return f;
+	}
+
+	@Nullable
+	@Override
+	public View onCreateView(LayoutInflater inflater, ViewGroup container,
+			Bundle savedInstanceState) {
+
+		setHasOptionsMenu(true);
+
+		byte[] b = getArguments().getByteArray(GROUP_ID);
+		if (b == null) throw new IllegalStateException("No Group found.");
+		groupId = new GroupId(b);
+		byte[] p = getArguments().getByteArray(BLOG_POST_ID);
+		if (p == null) throw new IllegalStateException("No MessageId found.");
+		postId = new MessageId(p);
+
+		View v = inflater.inflate(R.layout.fragment_blog_post, container,
+				false);
+		ui = new BlogPostViewHolder(v);
+		return v;
+	}
+
+	@Override
+	public void injectFragment(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		blogController.loadBlog(groupId, false,
+				new UiResultHandler<Boolean>((Activity) listener) {
+					@Override
+					public void onResultUi(Boolean result) {
+						listener.hideLoadingScreen();
+						if (result) {
+							BlogPostItem post =
+									blogController.getBlogPost(postId);
+							if (post != null) {
+								bind(post);
+							}
+						} else {
+							Toast.makeText(getActivity(),
+									R.string.blogs_blog_post_failed_to_load,
+									LENGTH_SHORT).show();
+						}
+					}
+				});
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		switch (item.getItemId()) {
+			case android.R.id.home:
+				getActivity().onBackPressed();
+				return true;
+			default:
+				return super.onOptionsItemSelected(item);
+		}
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+	private void bind(BlogPostItem post) {
+		Author author = post.getAuthor();
+		IdenticonDrawable d = new IdenticonDrawable(author.getId().getBytes());
+		ui.avatar.setImageDrawable(d);
+		ui.authorName.setText(author.getName());
+		ui.trust.setTrustLevel(post.getAuthorStatus());
+		ui.date.setText(
+				DateUtils.getRelativeTimeSpanString(post.getTimestamp()));
+
+		if (post.getTitle() != null) {
+			ui.title.setText(post.getTitle());
+		} else {
+			ui.title.setVisibility(GONE);
+		}
+
+		ui.body.setText(StringUtils.fromUtf8(post.getBody()));
+	}
+
+	private static class BlogPostViewHolder {
+		private ImageView avatar;
+		private TextView authorName;
+		private TrustIndicatorView trust;
+		private TextView date;
+		private TextView title;
+		private TextView body;
+
+		BlogPostViewHolder(View v) {
+			avatar = (ImageView) v.findViewById(R.id.avatar);
+			authorName = (TextView) v.findViewById(R.id.authorName);
+			trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);
+			date = (TextView) v.findViewById(R.id.date);
+			title = (TextView) v.findViewById(R.id.title);
+			body = (TextView) v.findViewById(R.id.body);
+		}
+	}
+
+}