diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cd8b5ce71eec405ff70ad76bd9eecb84353cd31e..a38f2ac74d457d72e09bc3254aefb31b74e090c5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -128,8 +128,8 @@ dependencies {
   withVersion("1.1.1") {
     implementation("android.arch.lifecycle", "extensions", version)
     implementation("android.arch.lifecycle", "reactivestreams", version)
-    kapt("android.arch.lifecycle", "compiler", version)
     testImplementation("android.arch.core", "core-testing", version)
+    implementation(project(":lifecycle-ktx"))
   }
 
   // App Arch Persistence
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
index 5f5fb4c831e45d0e62a7c3a6976ad1ab3d38a246..80b350d11b92c2c84c2dfb7aa80a328a99801365 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
@@ -19,7 +19,6 @@
 
 package de.kuschku.quasseldroid.ui.chat
 
-import android.annotation.TargetApi
 import android.app.Activity
 import android.arch.lifecycle.Observer
 import android.content.Context
@@ -27,7 +26,6 @@ import android.content.Intent
 import android.content.SharedPreferences
 import android.os.Build
 import android.os.Bundle
-import android.os.PersistableBundle
 import android.support.design.widget.BottomSheetBehavior
 import android.support.v4.widget.DrawerLayout
 import android.support.v7.app.ActionBarDrawerToggle
@@ -125,6 +123,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
   private var connectedAccount = -1L
 
+  private var restoredDrawerState = false
+
   override fun onNewIntent(intent: Intent?) {
     super.onNewIntent(intent)
     if (intent != null) {
@@ -157,8 +157,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
     setSupportActionBar(toolbar)
 
-    viewModel.buffer.toLiveData().observe(this, Observer {
-      if (it != null && drawerLayout.isDrawerOpen(Gravity.START)) {
+    viewModel.bufferOpened.toLiveData().observe(this, Observer {
+      actionMode?.finish()
+      if (drawerLayout.isDrawerOpen(Gravity.START)) {
         drawerLayout.closeDrawer(Gravity.START, true)
       }
     })
@@ -459,7 +460,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       .observe(this, Observer {
         if (connectedAccount != accountId) {
           if (resources.getBoolean(R.bool.buffer_drawer_exists) &&
-              viewModel.buffer.value == Int.MAX_VALUE) {
+              viewModel.buffer.value == Int.MAX_VALUE &&
+              !restoredDrawerState) {
             drawerLayout.openDrawer(Gravity.START)
           }
           connectedAccount = accountId
@@ -551,6 +553,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
   var bufferData: BufferData? = null
   var actionMode: ActionMode? = null
+  private var statusBarColor: Int? = null
 
   override fun onActionModeStarted(mode: ActionMode?) {
     when (mode?.tag) {
@@ -558,12 +561,24 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
       "MESSAGES" -> mode.menu?.retint(toolbar.context)
     }
     actionMode = mode
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      statusBarColor = window.statusBarColor
+      window.statusBarColor = theme.styledAttributes(R.attr.colorPrimaryDark) {
+        getColor(0, 0)
+      }
+    }
     super.onActionModeStarted(mode)
   }
 
   override fun onActionModeFinished(mode: ActionMode?) {
     actionMode = null
     super.onActionModeFinished(mode)
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      statusBarColor?.let {
+        window.statusBarColor = it
+        statusBarColor = null
+      }
+    }
   }
 
   override fun onStart() {
@@ -581,18 +596,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     outState?.putInt("OPEN_BUFFER", viewModel.buffer.value ?: -1)
     outState?.putInt("OPEN_BUFFERVIEWCONFIG", viewModel.bufferViewConfigId.value ?: -1)
     outState?.putLong("CONNECTED_ACCOUNT", connectedAccount)
-  }
-
-  override fun onSaveInstanceState(outState: Bundle?, outPersistentState: PersistableBundle?) {
-    super.onSaveInstanceState(outState, outPersistentState)
-    outState?.putInt("OPEN_BUFFER", viewModel.buffer.value ?: -1)
-    outState?.putInt("OPEN_BUFFERVIEWCONFIG", viewModel.bufferViewConfigId.value ?: -1)
-    outState?.putLong("CONNECTED_ACCOUNT", connectedAccount)
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-      outPersistentState?.putInt("OPEN_BUFFER", viewModel.buffer.value ?: -1)
-      outPersistentState?.putInt("OPEN_BUFFERVIEWCONFIG", viewModel.bufferViewConfigId.value ?: -1)
-      outPersistentState?.putLong("CONNECTED_ACCOUNT", connectedAccount)
-    }
+    outState?.putBoolean("OPEN_DRAWER_START", drawerLayout.isDrawerOpen(Gravity.START))
+    outState?.putBoolean("OPEN_DRAWER_END", drawerLayout.isDrawerOpen(Gravity.END))
   }
 
   override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
@@ -601,27 +606,16 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     viewModel.bufferViewConfigId.onNext(savedInstanceState?.getInt("OPEN_BUFFERVIEWCONFIG", -1)
                                         ?: -1)
     connectedAccount = savedInstanceState?.getLong("CONNECTED_ACCOUNT", -1L) ?: -1L
-  }
 
-  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-  override fun onRestoreInstanceState(savedInstanceState: Bundle?,
-                                      persistentState: PersistableBundle?) {
-    super.onRestoreInstanceState(savedInstanceState, persistentState)
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-      val fallbackBuffer = persistentState?.getInt("OPEN_BUFFER", -1) ?: -1
-      viewModel.buffer.onNext(
-        savedInstanceState?.getInt("OPEN_BUFFER", fallbackBuffer)
-        ?: fallbackBuffer
-      )
-      val fallbackBufferViewConfigId = persistentState?.getInt("OPEN_BUFFERVIEWCONFIG", -1) ?: -1
-      viewModel.bufferViewConfigId.onNext(
-        savedInstanceState?.getInt("OPEN_BUFFERVIEWCONFIG", fallbackBufferViewConfigId)
-        ?: fallbackBufferViewConfigId
-      )
-      val fallbackConnectedAccount = persistentState?.getLong("CONNECTED_ACCOUNT", -1L) ?: -1L
-      connectedAccount = savedInstanceState?.getLong(
-        "CONNECTED_ACCOUNT", fallbackConnectedAccount
-      ) ?: fallbackConnectedAccount
+    if (savedInstanceState?.getBoolean("OPEN_DRAWER_START") == true) {
+      drawerLayout.openDrawer(Gravity.START)
+    }
+    if (savedInstanceState?.getBoolean("OPEN_DRAWER_END") == true) {
+      drawerLayout.openDrawer(Gravity.END)
+    }
+    if (savedInstanceState?.getBoolean("OPEN_DRAWER_START") != null ||
+        savedInstanceState?.getBoolean("OPEN_DRAWER_END") != null) {
+      restoredDrawerState = true
     }
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt
index 8e20f0c83f4613aec818b6997f417387d7d93276..be60564e26134a37ed8ea98d1d6849ecdb75e199 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt
@@ -20,7 +20,6 @@
 package de.kuschku.quasseldroid.ui.chat.buffers
 
 import android.graphics.drawable.Drawable
-import android.support.v7.recyclerview.extensions.ListAdapter
 import android.support.v7.util.DiffUtil
 import android.support.v7.widget.RecyclerView
 import android.view.LayoutInflater
@@ -40,6 +39,7 @@ import de.kuschku.quasseldroid.util.helper.getVectorDrawableCompat
 import de.kuschku.quasseldroid.util.helper.styledAttributes
 import de.kuschku.quasseldroid.util.helper.tint
 import de.kuschku.quasseldroid.util.helper.visibleIf
+import de.kuschku.quasseldroid.util.lists.ListAdapter
 import de.kuschku.quasseldroid.viewmodel.data.BufferListItem
 import de.kuschku.quasseldroid.viewmodel.data.BufferProps
 import de.kuschku.quasseldroid.viewmodel.data.BufferState
@@ -60,6 +60,7 @@ class BufferListAdapter(
 ) {
   private var clickListener: ((BufferId) -> Unit)? = null
   private var longClickListener: ((BufferId) -> Unit)? = null
+  private var updateFinishedListener: ((List<BufferListItem>) -> Unit)? = null
   fun setOnClickListener(listener: ((BufferId) -> Unit)?) {
     this.clickListener = listener
   }
@@ -68,6 +69,14 @@ class BufferListAdapter(
     this.longClickListener = listener
   }
 
+  fun setOnUpdateFinishedListener(listener: ((List<BufferListItem>) -> Unit)?) {
+    this.updateFinishedListener = listener
+  }
+
+  override fun onUpdateFinished(list: List<BufferListItem>) {
+    this.updateFinishedListener?.invoke(list)
+  }
+
   fun expandListener(networkId: NetworkId) {
     if (collapsedNetworks.value.orEmpty().contains(networkId))
       collapsedNetworks.onNext(collapsedNetworks.value.orEmpty() - networkId)
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt
index c21c3b88408a4623d0fb86b6399e7aa48bfaae6b..c829eb24432218006632a379e31464a9ee639249 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt
@@ -37,9 +37,17 @@ class BufferViewConfigAdapter :
   ThemedSpinnerAdapter {
   var data = emptyList<BufferViewConfig>()
 
+
+  private var updateFinishedListener: ((List<BufferViewConfig>) -> Unit)? = null
+
+  fun setOnUpdateFinishedListener(listener: ((List<BufferViewConfig>) -> Unit)?) {
+    this.updateFinishedListener = listener
+  }
+
   fun submitList(list: List<BufferViewConfig>) {
     data = list
     notifyDataSetChanged()
+    updateFinishedListener?.invoke(list)
   }
 
   fun indexOf(id: Int) = data.indexOfFirst { it.bufferViewId() == id }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt
index c42ac4d621207d7e0ff39cc5bab66cede14955cf..814b968babe84c89511c65389c1ecd30d20dbdaf 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt
@@ -21,6 +21,7 @@ package de.kuschku.quasseldroid.ui.chat.buffers
 
 import android.arch.lifecycle.Observer
 import android.os.Bundle
+import android.os.Parcelable
 import android.support.v7.widget.*
 import android.view.*
 import android.widget.AdapterView
@@ -207,21 +208,28 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
     val view = inflater.inflate(R.layout.fragment_chat_list, container, false)
     ButterKnife.bind(this, view)
 
-    var hasSetBufferViewConfigId = false
     val adapter = BufferViewConfigAdapter()
     viewModel.bufferViewConfigs.switchMap {
       combineLatest(it.map(BufferViewConfig::liveUpdates))
     }.toLiveData().observe(this, Observer {
       if (it != null) {
         adapter.submitList(it)
-        if (!hasSetBufferViewConfigId) {
-          chatListSpinner.setSelection(adapter.indexOf(viewModel.bufferViewConfigId.value))
-          hasSetBufferViewConfigId = true
-        }
       }
     })
 
-    chatListSpinner.adapter = adapter
+    var hasSetBufferViewConfigId = false
+    var hasRestoredSpinnerState = false
+    adapter.setOnUpdateFinishedListener {
+      if (!hasRestoredSpinnerState) {
+        savedInstanceState?.getParcelable<Parcelable>(KEY_STATE_SPINNER)
+          ?.let(chatListSpinner::onRestoreInstanceState)
+        hasRestoredSpinnerState = true
+      }
+      if (!hasSetBufferViewConfigId) {
+        chatListSpinner.setSelection(adapter.indexOf(viewModel.bufferViewConfigId.value))
+        hasSetBufferViewConfigId = true
+      }
+    }
     chatListSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
       override fun onNothingSelected(adapter: AdapterView<*>?) {
         if (hasSetBufferViewConfigId)
@@ -234,6 +242,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
           viewModel.bufferViewConfigId.onNext(id.toInt())
       }
     }
+    chatListSpinner.adapter = adapter
 
     listAdapter = BufferListAdapter(
       viewModel.selectedBufferId,
@@ -291,6 +300,15 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
       })
     listAdapter.setOnClickListener(this@BufferViewConfigFragment::clickListener)
     listAdapter.setOnLongClickListener(this@BufferViewConfigFragment::longClickListener)
+
+    var hasRestoredChatListState = false
+    listAdapter.setOnUpdateFinishedListener {
+      if (!hasRestoredChatListState) {
+        savedInstanceState?.getParcelable<Parcelable>(KEY_STATE_LIST)
+          ?.let(chatList.layoutManager::onRestoreInstanceState)
+        hasRestoredChatListState = true
+      }
+    }
     chatList.adapter = listAdapter
 
     viewModel.selectedBuffer.toLiveData().observe(this, Observer { buffer ->
@@ -402,6 +420,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
       longClickListener(it)
     } else {
       viewModel.buffer.onNext(it)
+      viewModel.bufferOpened.onNext(Unit)
     }
   }
 
@@ -415,7 +434,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   }
 
   companion object {
-    private const val KEY_STATE_LIST = "KEY_STATE_LIST"
-    private const val KEY_STATE_SPINNER = "KEY_STATE_SPINNER"
+    private const val KEY_STATE_LIST = "STATE_LIST"
+    private const val KEY_STATE_SPINNER = "STATE_SPINNER"
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncDifferConfig.kt b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncDifferConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8861d3bb16b8d0fbcc34141ee8cc4bf3369c0fda
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncDifferConfig.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.kuschku.quasseldroid.util.lists
+
+import android.os.Handler
+import android.os.Looper
+import android.support.annotation.RestrictTo
+import android.support.v7.util.DiffUtil
+
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+/**
+ * Configuration object for [ListAdapter], [AsyncListDiffer], and similar
+ * background-thread list diffing adapter logic.
+ *
+ *
+ * At minimum, defines item diffing behavior with a [DiffUtil.ItemCallback], used to compute
+ * item differences to pass to a RecyclerView adapter.
+ *
+ * @param <T> Type of items in the lists, and being compared.
+</T> */
+class AsyncDifferConfig<T> private constructor(
+  @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+  /** @hide
+   */
+  val mainThreadExecutor: Executor,
+  val backgroundThreadExecutor: Executor,
+  val diffCallback: DiffUtil.ItemCallback<T>) {
+
+  /**
+   * Builder class for [AsyncDifferConfig].
+   *
+   * @param <T>
+  </T> */
+  class Builder<T>(private val mDiffCallback: DiffUtil.ItemCallback<T>) {
+    private var mMainThreadExecutor: Executor? = null
+    private var mBackgroundThreadExecutor: Executor? = null
+
+    /**
+     * If provided, defines the main thread executor used to dispatch adapter update
+     * notifications on the main thread.
+     *
+     *
+     * If not provided, it will default to the main thread.
+     *
+     * @param executor The executor which can run tasks in the UI thread.
+     * @return this
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    fun setMainThreadExecutor(executor: Executor): Builder<T> {
+      mMainThreadExecutor = executor
+      return this
+    }
+
+    /**
+     * If provided, defines the background executor used to calculate the diff between an old
+     * and a new list.
+     *
+     *
+     * If not provided, defaults to two thread pool executor, shared by all ListAdapterConfigs.
+     *
+     * @param executor The background executor to run list diffing.
+     * @return this
+     */
+    fun setBackgroundThreadExecutor(executor: Executor): Builder<T> {
+      mBackgroundThreadExecutor = executor
+      return this
+    }
+
+    private class MainThreadExecutor : Executor {
+      internal val mHandler = Handler(Looper.getMainLooper())
+      override fun execute(command: Runnable) {
+        mHandler.post(command)
+      }
+    }
+
+    /**
+     * Creates a [AsyncListDiffer] with the given parameters.
+     *
+     * @return A new AsyncDifferConfig.
+     */
+    fun build(): AsyncDifferConfig<T> {
+      if (mMainThreadExecutor == null) {
+        mMainThreadExecutor = sMainThreadExecutor
+      }
+      if (mBackgroundThreadExecutor == null) {
+        synchronized(sExecutorLock) {
+          if (sDiffExecutor == null) {
+            sDiffExecutor = Executors.newFixedThreadPool(2)
+          }
+        }
+        mBackgroundThreadExecutor = sDiffExecutor
+      }
+      return AsyncDifferConfig(
+        mMainThreadExecutor!!,
+        mBackgroundThreadExecutor!!,
+        mDiffCallback)
+    }
+
+    companion object {
+
+      // TODO: remove the below once supportlib has its own appropriate executors
+      private val sExecutorLock = Any()
+      private var sDiffExecutor: Executor? = null
+
+      // TODO: use MainThreadExecutor from supportlib once one exists
+      private val sMainThreadExecutor = MainThreadExecutor()
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncListDiffer.kt b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncListDiffer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..65ef18ec1c057f27a32da767f4411a0b4b16ddc6
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncListDiffer.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.kuschku.quasseldroid.util.lists
+
+import android.support.v7.util.AdapterListUpdateCallback
+import android.support.v7.util.DiffUtil
+import android.support.v7.util.ListUpdateCallback
+import android.support.v7.widget.RecyclerView
+import java.util.*
+
+/**
+ * Helper for computing the difference between two lists via [DiffUtil] on a background
+ * thread.
+ *
+ *
+ * It can be connected to a
+ * [RecyclerView.Adapter][android.support.v7.widget.RecyclerView.Adapter], and will signal the
+ * adapter of changes between sumbitted lists.
+ *
+ *
+ * For simplicity, the [ListAdapter] wrapper class can often be used instead of the
+ * AsyncListDiffer directly. This AsyncListDiffer can be used for complex cases, where overriding an
+ * adapter base class to support asynchronous List diffing isn't convenient.
+ *
+ *
+ * The AsyncListDiffer can consume the values from a LiveData of `List` and present the
+ * data simply for an adapter. It computes differences in list contents via [DiffUtil] on a
+ * background thread as new `List`s are received.
+ *
+ *
+ * Use [.getCurrentList] to access the current List, and present its data objects. Diff
+ * results will be dispatched to the ListUpdateCallback immediately before the current list is
+ * updated. If you're dispatching list updates directly to an Adapter, this means the Adapter can
+ * safely access list items and total size via [.getCurrentList].
+ *
+ *
+ * A complete usage pattern with Room would look like this:
+ * <pre>
+ * @Dao
+ * interface UserDao {
+ * @Query("SELECT * FROM user ORDER BY lastName ASC")
+ * public abstract LiveData&lt;List&lt;User>> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ * public final LiveData&lt;List&lt;User>> usersList;
+ * public MyViewModel(UserDao userDao) {
+ * usersList = userDao.usersByLastName();
+ * }
+ * }
+ *
+ * class MyActivity extends AppCompatActivity {
+ * @Override
+ * public void onCreate(Bundle savedState) {
+ * super.onCreate(savedState);
+ * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
+ * RecyclerView recyclerView = findViewById(R.id.user_list);
+ * UserAdapter adapter = new UserAdapter();
+ * viewModel.usersList.observe(this, list -> adapter.submitList(list));
+ * recyclerView.setAdapter(adapter);
+ * }
+ * }
+ *
+ * class UserAdapter extends RecyclerView.Adapter&lt;UserViewHolder> {
+ * private final AsyncListDiffer&lt;User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK);
+ * @Override
+ * public int getItemCount() {
+ * return mDiffer.getCurrentList().size();
+ * }
+ * public void submitList(List&lt;User> list) {
+ * mDiffer.submitList(list);
+ * }
+ * @Override
+ * public void onBindViewHolder(UserViewHolder holder, int position) {
+ * User user = mDiffer.getCurrentList().get(position);
+ * holder.bindTo(user);
+ * }
+ * public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK
+ * = new DiffUtil.ItemCallback&lt;User>() {
+ * @Override
+ * public boolean areItemsTheSame(
+ * @NonNull User oldUser, @NonNull User newUser) {
+ * // User properties may have changed if reloaded from the DB, but ID is fixed
+ * return oldUser.getId() == newUser.getId();
+ * }
+ * @Override
+ * public boolean areContentsTheSame(
+ * @NonNull User oldUser, @NonNull User newUser) {
+ * // NOTE: if you use equals, your object must properly override Object#equals()
+ * // Incorrectly returning false here will result in too many animations.
+ * return oldUser.equals(newUser);
+ * }
+ * }
+ * }</pre>
+ *
+ * @param <T> Type of the lists this AsyncListDiffer will receive.
+ *
+ * @see DiffUtil
+ *
+ * @see AdapterListUpdateCallback
+</T> */
+class AsyncListDiffer<T> {
+  private val mUpdateCallback: ListUpdateCallback
+  private val mUpdateFinishedCallback: ((List<T>) -> Unit)?
+  private val mConfig: AsyncDifferConfig<T>
+
+  private var mList: List<T>? = null
+
+  /**
+   * Non-null, unmodifiable version of mList.
+   *
+   *
+   * Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise
+   */
+  /**
+   * Get the current List - any diffing to present this list has already been computed and
+   * dispatched via the ListUpdateCallback.
+   *
+   *
+   * If a `null` List, or no List has been submitted, an empty list will be returned.
+   *
+   *
+   * The returned list may not be mutated - mutations to content must be done through
+   * [.submitList].
+   *
+   * @return current List.
+   */
+  var currentList = emptyList<T>()
+    private set
+
+  // Max generation of currently scheduled runnable
+  private var mMaxScheduledGeneration: Int = 0
+
+  /**
+   * Convenience for
+   * `AsyncListDiffer(new AdapterListUpdateCallback(adapter),
+   * new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());`
+   *
+   * @param adapter Adapter to dispatch position updates to.
+   * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when
+   *
+   * @see DiffUtil.DiffResult.dispatchUpdatesTo
+   */
+  constructor(adapter: RecyclerView.Adapter<*>,
+              updateFinishedCallback: ((List<T>) -> Unit)? = null,
+              diffCallback: DiffUtil.ItemCallback<T>) {
+    mUpdateCallback = AdapterListUpdateCallback(adapter)
+    mUpdateFinishedCallback = updateFinishedCallback
+    mConfig = AsyncDifferConfig.Builder(diffCallback).build()
+  }
+
+  /**
+   * Create a AsyncListDiffer with the provided config, and ListUpdateCallback to dispatch
+   * updates to.
+   *
+   * @param listUpdateCallback Callback to dispatch updates to.
+   * @param config Config to define background work Executor, and DiffUtil.ItemCallback for
+   * computing List diffs.
+   *
+   * @see DiffUtil.DiffResult.dispatchUpdatesTo
+   */
+  constructor(listUpdateCallback: ListUpdateCallback,
+              updateFinishedCallback: ((List<T>) -> Unit)? = null,
+              config: AsyncDifferConfig<T>) {
+    mUpdateCallback = listUpdateCallback
+    mUpdateFinishedCallback = updateFinishedCallback
+    mConfig = config
+  }
+
+  /**
+   * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background
+   * thread.
+   *
+   *
+   * If a List is already present, a diff will be computed asynchronously on a background thread.
+   * When the diff is computed, it will be applied (dispatched to the [ListUpdateCallback]),
+   * and the new List will be swapped in.
+   *
+   * @param newList The new List.
+   */
+  fun submitList(newList: List<T>?) {
+    val oldList = mList
+
+    if (newList === oldList) {
+      // nothing to do
+      return
+    }
+
+    // incrementing generation means any currently-running diffs are discarded when they finish
+    val runGeneration = ++mMaxScheduledGeneration
+
+    // fast simple remove all
+    if (newList == null) {
+      val countRemoved = oldList?.size ?: 0
+      mList = null
+      currentList = emptyList()
+      // notify last, after list is updated
+      mUpdateCallback.onRemoved(0, countRemoved)
+      return
+    }
+
+    // fast simple first insert
+    if (oldList == null) {
+      mList = newList
+      currentList = Collections.unmodifiableList(newList)
+      // notify last, after list is updated
+      mUpdateCallback.onInserted(0, newList.size)
+      return
+    }
+
+    mConfig.backgroundThreadExecutor.execute {
+      val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
+        override fun getOldListSize(): Int {
+          return oldList.size
+        }
+
+        override fun getNewListSize(): Int {
+          return newList.size
+        }
+
+        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+          return mConfig.diffCallback.areItemsTheSame(
+            oldList[oldItemPosition], newList[newItemPosition])
+        }
+
+        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+          return mConfig.diffCallback.areContentsTheSame(
+            oldList[oldItemPosition], newList[newItemPosition])
+        }
+
+        override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
+          return mConfig.diffCallback.getChangePayload(
+            oldList[oldItemPosition], newList[newItemPosition])
+        }
+      })
+
+      mConfig.mainThreadExecutor.execute {
+        if (mMaxScheduledGeneration == runGeneration) {
+          latchList(newList, result)
+        }
+      }
+    }
+  }
+
+  private fun latchList(newList: List<T>, diffResult: DiffUtil.DiffResult) {
+    mList = newList
+    // notify last, after list is updated
+    currentList = Collections.unmodifiableList(newList)
+    diffResult.dispatchUpdatesTo(mUpdateCallback)
+    mUpdateFinishedCallback?.invoke(newList)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/lists/ListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/lists/ListAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..55ffbf150f73d969e658c37c227fc031544573ea
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/lists/ListAdapter.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package de.kuschku.quasseldroid.util.lists
+
+import android.support.v7.util.AdapterListUpdateCallback
+import android.support.v7.util.DiffUtil
+import android.support.v7.widget.RecyclerView
+
+/**
+ * [RecyclerView.Adapter] base class for presenting List data in a
+ * [RecyclerView], including computing diffs between Lists on a background thread.
+ *
+ *
+ * This class is a convenience wrapper around [AsyncListDiffer] that implements Adapter common
+ * default behavior for item access and counting.
+ *
+ *
+ * While using a LiveData&lt;List> is an easy way to provide data to the adapter, it isn't required
+ * - you can use [.submitList] when new lists are available.
+ *
+ *
+ * A complete usage pattern with Room would look like this:
+ * <pre>
+ * @Dao
+ * interface UserDao {
+ * @Query("SELECT * FROM user ORDER BY lastName ASC")
+ * public abstract LiveData&lt;List&lt;User>> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ * public final LiveData&lt;List&lt;User>> usersList;
+ * public MyViewModel(UserDao userDao) {
+ * usersList = userDao.usersByLastName();
+ * }
+ * }
+ *
+ * class MyActivity extends AppCompatActivity {
+ * @Override
+ * public void onCreate(Bundle savedState) {
+ * super.onCreate(savedState);
+ * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
+ * RecyclerView recyclerView = findViewById(R.id.user_list);
+ * UserAdapter&lt;User> adapter = new UserAdapter();
+ * viewModel.usersList.observe(this, list -> adapter.submitList(list));
+ * recyclerView.setAdapter(adapter);
+ * }
+ * }
+ *
+ * class UserAdapter extends ListAdapter&lt;User, UserViewHolder> {
+ * public UserAdapter() {
+ * super(User.DIFF_CALLBACK);
+ * }
+ * @Override
+ * public void onBindViewHolder(UserViewHolder holder, int position) {
+ * holder.bindTo(getItem(position));
+ * }
+ * public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK =
+ * new DiffUtil.ItemCallback&lt;User>() {
+ * @Override
+ * public boolean areItemsTheSame(
+ * @NonNull User oldUser, @NonNull User newUser) {
+ * // User properties may have changed if reloaded from the DB, but ID is fixed
+ * return oldUser.getId() == newUser.getId();
+ * }
+ * @Override
+ * public boolean areContentsTheSame(
+ * @NonNull User oldUser, @NonNull User newUser) {
+ * // NOTE: if you use equals, your object must properly override Object#equals()
+ * // Incorrectly returning false here will result in too many animations.
+ * return oldUser.equals(newUser);
+ * }
+ * }
+ * }</pre>
+ *
+ * Advanced users that wish for more control over adapter behavior, or to provide a specific base
+ * class should refer to [AsyncListDiffer], which provides custom mapping from diff events
+ * to adapter positions.
+ *
+ * @param <T> Type of the Lists this Adapter will receive.
+ * @param <VH> A class that extends ViewHolder that will be used by the adapter.
+</VH></T> */
+abstract class ListAdapter<T, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH> {
+  private val mHelper: AsyncListDiffer<T>
+
+  protected constructor(diffCallback: DiffUtil.ItemCallback<T>) {
+    mHelper = AsyncListDiffer(AdapterListUpdateCallback(this),
+                              ::onUpdateFinished,
+                              AsyncDifferConfig.Builder(diffCallback).build())
+  }
+
+  protected constructor(config: AsyncDifferConfig<T>) {
+    mHelper = AsyncListDiffer(AdapterListUpdateCallback(this),
+                              ::onUpdateFinished,
+                              config)
+  }
+
+  /**
+   * Submits a new list to be diffed, and displayed.
+   *
+   *
+   * If a list is already being displayed, a diff will be computed on a background thread, which
+   * will dispatch Adapter.notifyItem events on the main thread.
+   *
+   * @param list The new list to be displayed.
+   */
+  fun submitList(list: List<T>) {
+    mHelper.submitList(list)
+  }
+
+  protected fun getItem(position: Int): T {
+    return mHelper.currentList[position]
+  }
+
+  override fun getItemCount(): Int {
+    return mHelper.currentList.size
+  }
+
+  open fun onUpdateFinished(list: List<T>) = Unit
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt
index bda94b13164351e16a160f11d26f74362e677d95..30526fc8559e1bfa7b0f91a5d6bf44871e5b8721 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt
@@ -19,10 +19,8 @@
 
 package de.kuschku.quasseldroid.util.service
 
-import android.arch.lifecycle.Lifecycle
-import android.arch.lifecycle.LifecycleObserver
+import android.arch.lifecycle.DefaultLifecycleObserver
 import android.arch.lifecycle.LifecycleOwner
-import android.arch.lifecycle.OnLifecycleEvent
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
@@ -34,7 +32,7 @@ import de.kuschku.quasseldroid.service.QuasselBinder
 import de.kuschku.quasseldroid.service.QuasselService
 import io.reactivex.subjects.BehaviorSubject
 
-class BackendServiceConnection : ServiceConnection, LifecycleObserver {
+class BackendServiceConnection : ServiceConnection, DefaultLifecycleObserver {
   val backend: BehaviorSubject<Optional<Backend>> = BehaviorSubject.createDefault(Optional.empty())
 
   var context: Context? = null
@@ -87,12 +85,9 @@ class BackendServiceConnection : ServiceConnection, LifecycleObserver {
     }
   }
 
-  @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
-  fun onCreate(lifecycleOwner: LifecycleOwner) = start()
+  override fun onCreate(owner: LifecycleOwner) = start()
 
-  @OnLifecycleEvent(Lifecycle.Event.ON_START)
-  fun onStart(lifecycleOwner: LifecycleOwner) = bind()
+  override fun onStart(owner: LifecycleOwner) = bind()
 
-  @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
-  fun onStop(lifecycleOwner: LifecycleOwner) = unbind()
+  override fun onStop(owner: LifecycleOwner) = unbind()
 }
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
index d0cc17b265288a8ba6f6c692542e6c6ee4ef0a4e..5902242d3240f2828c363c82bbcb23be91402141 100644
--- a/lib/build.gradle.kts
+++ b/lib/build.gradle.kts
@@ -17,25 +17,6 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/*
- * Quasseldroid - Quassel client for Android
- *
- * Copyright (c) 2018 Janne Koschinski
- * Copyright (c) 2018 The Quassel Project
- *
- * This program is free software: you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 3 as published
- * by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
 import org.gradle.api.Project
 import org.gradle.api.artifacts.ExternalModuleDependency
 import org.gradle.kotlin.dsl.*
diff --git a/lifecycle-ktx/build.gradle.kts b/lifecycle-ktx/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..42b0627f20ed1cf1b927435d940b460ac43f85ce
--- /dev/null
+++ b/lifecycle-ktx/build.gradle.kts
@@ -0,0 +1,41 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2018 Janne Koschinski
+ * Copyright (c) 2018 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ExternalModuleDependency
+import org.gradle.kotlin.dsl.*
+import org.jetbrains.kotlin.gradle.plugin.KaptExtension
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+  kotlin("jvm")
+  kotlin("kapt")
+}
+
+dependencies {
+  implementation(kotlin("stdlib", "1.2.41"))
+
+  withVersion("27.1.1") {
+    implementation("com.android.support", "support-annotations", version)
+  }
+
+  withVersion("1.1.1") {
+    implementation("android.arch.lifecycle", "common", version)
+  }
+}
diff --git a/lifecycle-ktx/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.kt b/lifecycle-ktx/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ddd156ec9e4b9fbd187e9fb38eea08d2f2db1ba
--- /dev/null
+++ b/lifecycle-ktx/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.arch.lifecycle
+
+/**
+ * Callback interface for listening to [LifecycleOwner] state changes.
+ *
+ *
+ * If you use Java 8 language, **always** prefer it over annotations.
+ */
+interface DefaultLifecycleObserver : FullLifecycleObserverProxy {
+  /**
+   * Notifies that `ON_CREATE` event occurred.
+   *
+   *
+   * This method will be called after the [LifecycleOwner]'s `onCreate`
+   * method returns.
+   *
+   * @param owner the component, whose state was changed
+   */
+  override fun onCreate(owner: LifecycleOwner) = Unit
+
+  /**
+   * Notifies that `ON_START` event occurred.
+   *
+   *
+   * This method will be called after the [LifecycleOwner]'s `onStart` method returns.
+   *
+   * @param owner the component, whose state was changed
+   */
+  override fun onStart(owner: LifecycleOwner) = Unit
+
+  /**
+   * Notifies that `ON_RESUME` event occurred.
+   *
+   *
+   * This method will be called after the [LifecycleOwner]'s `onResume`
+   * method returns.
+   *
+   * @param owner the component, whose state was changed
+   */
+  override fun onResume(owner: LifecycleOwner) = Unit
+
+  /**
+   * Notifies that `ON_PAUSE` event occurred.
+   *
+   *
+   * This method will be called before the [LifecycleOwner]'s `onPause` method
+   * is called.
+   *
+   * @param owner the component, whose state was changed
+   */
+  override fun onPause(owner: LifecycleOwner) = Unit
+
+  /**
+   * Notifies that `ON_STOP` event occurred.
+   *
+   *
+   * This method will be called before the [LifecycleOwner]'s `onStop` method
+   * is called.
+   *
+   * @param owner the component, whose state was changed
+   */
+  override fun onStop(owner: LifecycleOwner) = Unit
+
+  /**
+   * Notifies that `ON_DESTROY` event occurred.
+   *
+   *
+   * This method will be called before the [LifecycleOwner]'s `onStop` method
+   * is called.
+   *
+   * @param owner the component, whose state was changed
+   */
+  override fun onDestroy(owner: LifecycleOwner) = Unit
+}
diff --git a/lifecycle-ktx/src/main/java/android/arch/lifecycle/FullLifecycleObserverProxy.java b/lifecycle-ktx/src/main/java/android/arch/lifecycle/FullLifecycleObserverProxy.java
new file mode 100644
index 0000000000000000000000000000000000000000..82990c78449e47298563fc19d6387b5e701e3d34
--- /dev/null
+++ b/lifecycle-ktx/src/main/java/android/arch/lifecycle/FullLifecycleObserverProxy.java
@@ -0,0 +1,23 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2018 Janne Koschinski
+ * Copyright (c) 2018 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package android.arch.lifecycle;
+
+public interface FullLifecycleObserverProxy extends FullLifecycleObserver {
+}
diff --git a/settings.gradle b/settings.gradle
index eba2c7c83d29b370f8ef4e6730edf7ff248c253a..68b525eeb86ce580f54b645b2505c00848b0ced0 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,7 +17,7 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-include ':invokerannotations',
+include ':invokerannotations', ':lifecycle-ktx',
         ':invokergenerator',
         ':lib',
         ":viewmodel",
diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
index 3b802adc72bfefb7cee67f75863e171b79e3c8b3..a16314f81d0309c1cc6a9ee79b5305dd73d2cd8d 100644
--- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
+++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt
@@ -42,6 +42,7 @@ import io.reactivex.BackpressureStrategy
 import io.reactivex.Flowable
 import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
+import io.reactivex.subjects.PublishSubject
 import java.util.concurrent.TimeUnit
 
 class QuasselViewModel : ViewModel() {
@@ -58,6 +59,7 @@ class QuasselViewModel : ViewModel() {
   val expandedMessages = BehaviorSubject.createDefault(emptySet<MsgId>())
 
   val buffer = BehaviorSubject.createDefault(Int.MAX_VALUE)
+  val bufferOpened = PublishSubject.create<Unit>()
 
   val bufferViewConfigId = BehaviorSubject.createDefault(Int.MAX_VALUE)