diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
index a09a4d1ced8f13d8a4129b239ad7cb1b89fe71de..bd5e773b4ff4f47395c732417e232dc34b5ec0e6 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
@@ -165,7 +165,7 @@ private fun PreviewUtils.PreviewScope.mapErrors(name: String?): AddContactError?
 
 @Composable
 fun AddContactDialog(
-    viewModel: AddContactViewModel = viewModel(),
+    viewModel: AddContactViewModel,
 ) = AddContactDialog(
     viewModel::dismissDialog,
     viewModel.visible.value,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
index 2df8d312002f14035387dff51e6b5c464d8861d1..5e36f631adb9dec2c029de6d2bb47f6de448aca5 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -73,6 +73,7 @@ import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
 import org.briarproject.briar.desktop.viewmodel.SingleStateEvent
 import org.briarproject.briar.desktop.viewmodel.asList
 import org.briarproject.briar.desktop.viewmodel.asState
+import org.briarproject.briar.desktop.viewmodel.update
 import javax.inject.Inject
 import kotlin.concurrent.thread
 
@@ -373,13 +374,13 @@ constructor(
             is ContactConnectedEvent -> {
                 if (e.contactId == _contactId.value) {
                     LOG.i { "Contact connected" }
-                    _contactItem.value = _contactItem.value!!.updateIsConnected(true)
+                    _contactItem.update { this?.updateIsConnected(true) }
                 }
             }
             is ContactDisconnectedEvent -> {
                 if (e.contactId == _contactId.value) {
                     LOG.i { "Contact disconnected" }
-                    _contactItem.value = _contactItem.value!!.updateIsConnected(false)
+                    _contactItem.update { this?.updateIsConnected(false) }
                 }
             }
             is ClientVersionUpdatedEvent -> {
@@ -459,7 +460,7 @@ constructor(
         val newAlias = _newAlias.value.ifBlank { null }
         if (_contactId.value != null && contactItem.value != null) {
             contactManager.setContactAlias(_contactId.value!!, newAlias)
-            _contactItem.value = contactItem.value!!.updateAlias(newAlias)
+            _contactItem.update { this?.updateAlias(newAlias) }
         }
     }
 
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
index 67511a08d261cf4248df9a67f210356554ce04b8..42065c51fbebd05694032f75d8096e7f85b9de06 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
@@ -55,7 +55,7 @@ fun PrivateMessageScreen(
     viewModel: ContactListViewModel = viewModel(),
     addContactViewModel: AddContactViewModel = viewModel(),
 ) {
-    AddContactDialog()
+    AddContactDialog(addContactViewModel)
 
     ConfirmRemovePendingContactDialog(
         viewModel.removePendingContactDialogVisible.value,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt
index b06a03988c938f318e393f05de5887b6c2f9f89a..14ea3742af0a748ea43145049802c15b80d8614d 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingDrawerContent.kt
@@ -52,7 +52,7 @@ import org.briarproject.briar.desktop.viewmodel.viewModel
 fun ForumSharingDrawerContent(
     groupId: GroupId,
     close: () -> Unit,
-    viewModel: ForumSharingViewModel = viewModel(),
+    viewModel: ForumSharingViewModel,
 ) = Column {
     LaunchedEffect(groupId) {
         viewModel.setGroupId(groupId)
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt
index da70d650622e05877a6284800eb0c548f823fc3c..3ddff4f582030405bb029102e83e41c2b893f428 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumSharingViewModel.kt
@@ -44,7 +44,9 @@ import org.briarproject.briar.desktop.utils.clearAndAddAll
 import org.briarproject.briar.desktop.utils.removeFirst
 import org.briarproject.briar.desktop.utils.replaceFirst
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
+import org.briarproject.briar.desktop.viewmodel.asList
 import org.briarproject.briar.desktop.viewmodel.asState
+import org.briarproject.briar.desktop.viewmodel.update
 import javax.inject.Inject
 
 class ForumSharingViewModel @Inject constructor(
@@ -67,7 +69,7 @@ class ForumSharingViewModel @Inject constructor(
     private lateinit var groupId: GroupId
 
     private val _currentlySharedWith = mutableStateListOf<ContactItem>()
-    val currentlySharedWith: List<ContactItem> = _currentlySharedWith
+    val currentlySharedWith = _currentlySharedWith.asList()
 
     private val _sharingInfo = mutableStateOf(SharingInfo(0, 0))
     val sharingInfo = _sharingInfo.asState()
@@ -96,28 +98,28 @@ class ForumSharingViewModel @Inject constructor(
                     )
                     txn.attach {
                         _currentlySharedWith.add(item)
-                        _sharingInfo.value = _sharingInfo.value.addContact(connected)
+                        _sharingInfo.update { addContact(connected) }
                     }
                 }
 
             e is ContactLeftShareableEvent && e.groupId == groupId -> {
                 _currentlySharedWith.removeFirst { it.idWrapper.contactId == e.contactId }
                 val connected = connectionRegistry.isConnected(e.contactId)
-                _sharingInfo.value = _sharingInfo.value.removeContact(connected)
+                _sharingInfo.update { removeContact(connected) }
             }
 
             e is ContactConnectedEvent -> {
                 val isMember = _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) {
                     it.updateIsConnected(true)
                 }
-                if (isMember) _sharingInfo.value = _sharingInfo.value.updateContactConnected(true)
+                if (isMember) _sharingInfo.update { updateContactConnected(true) }
             }
 
             e is ContactDisconnectedEvent -> {
                 val isMember = _currentlySharedWith.replaceFirst({ it.idWrapper.contactId == e.contactId }) {
                     it.updateIsConnected(false)
                 }
-                if (isMember) _sharingInfo.value = _sharingInfo.value.updateContactConnected(false)
+                if (isMember) _sharingInfo.update { updateContactConnected(false) }
             }
         }
     }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
index 629bb298f4bf429f8a27059a98bff3d7b709fa47..507d80a609dffa08a3cfe1bc43d8638b43cea030 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
@@ -83,13 +83,13 @@ class ForumViewModel @Inject constructor(
         // since the threadViewModel is tightly coupled to the ForumViewModel
         // and not injected using the usual `viewModel()` approach,
         // we have to manually call the functions for (de)initialization
-        threadViewModel.onInit()
+        threadViewModel.onEnterComposition()
         loadGroups()
     }
 
     override fun onCleared() {
         super.onCleared()
-        threadViewModel.onCleared()
+        threadViewModel.onExitComposition()
     }
 
     override fun eventOccurred(e: Event) {
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt
index 160a9c7be613bc436d4d6b95bd1af07f4a0a371b..b8a8adbb4c768c3c7027e6845c905c5f96ed877b 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt
@@ -43,7 +43,6 @@ import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment.Companion.BottomCenter
@@ -59,7 +58,6 @@ import org.briarproject.briar.desktop.ui.HorizontalDivider
 import org.briarproject.briar.desktop.ui.getInfoDrawerHandler
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
-import org.briarproject.briar.desktop.viewmodel.viewModel
 
 @Composable
 fun GroupConversationScreen(
@@ -67,7 +65,7 @@ fun GroupConversationScreen(
 ) {
     Scaffold(
         topBar = {
-            GroupConversationHeader(viewModel.groupItem) {
+            GroupConversationHeader(viewModel.groupItem, viewModel.forumSharingViewModel) {
                 viewModel.deleteGroup(viewModel.groupItem)
             }
         },
@@ -92,14 +90,9 @@ fun GroupConversationScreen(
 @Composable
 private fun GroupConversationHeader(
     groupItem: GroupItem,
-    viewModel: ForumSharingViewModel = viewModel(),
-    // todo: using the same viewModel twice could lead to problems when one of the occurrences goes out of scope
-    //  -> viewModel.onCleared() will be called and it will, e.g., stop listening to events
+    forumSharingViewModel: ForumSharingViewModel,
     onGroupDelete: () -> Unit,
 ) {
-    LaunchedEffect(groupItem) {
-        viewModel.setGroupId(groupItem.id)
-    }
     val deleteGroupDialogVisible = remember { mutableStateOf(false) }
     val menuState = remember { mutableStateOf(CLOSED) }
     val close = { menuState.value = CLOSED }
@@ -126,7 +119,7 @@ private fun GroupConversationHeader(
                         overflow = Ellipsis,
                         style = MaterialTheme.typography.h2,
                     )
-                    val sharingInfo = viewModel.sharingInfo.value
+                    val sharingInfo = forumSharingViewModel.sharingInfo.value
                     Text(
                         text = i18nF("forum.sharing.status.with", sharingInfo.total, sharingInfo.online)
                     )
@@ -149,6 +142,7 @@ private fun GroupConversationHeader(
                                 ForumSharingDrawerContent(
                                     groupId = groupItem.id,
                                     close = infoDrawerHandler::close,
+                                    viewModel = forumSharingViewModel,
                                 )
                             }
                         }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt
index 4b62526064823cbe621aa0977e878089fefb69b1..3351fa255a49ae5333b45d0bd201e27b4a1ccf98 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt
@@ -48,6 +48,7 @@ import java.lang.Long.max
 import javax.inject.Inject
 
 class ThreadedConversationViewModel @Inject constructor(
+    val forumSharingViewModel: ForumSharingViewModel,
     private val forumManager: ForumManager,
     private val identityManager: IdentityManager,
     private val clock: Clock,
@@ -78,6 +79,7 @@ class ThreadedConversationViewModel @Inject constructor(
         this.groupItem = groupItem
         this.onPostAdded = onPostAdded
         _selectedPost.value = null
+        forumSharingViewModel.setGroupId(groupItem.id)
         loadPosts(groupItem.id)
     }
 
@@ -91,6 +93,16 @@ class ThreadedConversationViewModel @Inject constructor(
         }
     }
 
+    override fun onInit() {
+        super.onInit()
+        forumSharingViewModel.onEnterComposition()
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        forumSharingViewModel.onExitComposition()
+    }
+
     private fun loadPosts(groupId: GroupId) {
         _posts.value = Loading
         runOnDbThreadWithTransaction(true) { txn ->
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt
index 0c10d49c97fac3e6d99d67c100b3571c67b54e0c..223e98e9ab737db652fe8dd3a5f615267411fa33 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt
@@ -30,7 +30,7 @@ class SidebarViewModel
 @Inject
 constructor(
     private val identityManager: IdentityManager,
-) : ViewModel {
+) : ViewModel() {
 
     override fun onInit() {
         loadAccountInfo()
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
index f6ebda2998632b87a9e2af78db6134dd2569526c..96d87757715f31a317772cb630c82ea567462cfb 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
@@ -72,13 +72,14 @@ inline fun <T, reified U : T> MutableList<T>.replaceFirst(predicate: (U) -> Bool
     return false
 }
 
-inline fun <T, reified U : T> MutableList<T>.removeFirst(predicate: (U) -> Boolean) {
+inline fun <T, reified U : T> MutableList<T>.removeFirst(predicate: (U) -> Boolean): Boolean {
     val li = listIterator()
     while (li.hasNext()) {
         val n = li.next()
         if (n is U && predicate(n)) {
             li.remove()
-            break
+            return true
         }
     }
+    return false
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt
index aab516b5f773b7fc1f2b4604be5fc17044ced4ea..be5cb5a81a0d32671ea33dca15d53b57001e2cfd 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ComposeUtils.kt
@@ -59,6 +59,8 @@ inline fun <reified VM : ViewModel> viewModel(
  * It will be automatically initialized as soon as the calling screen is composed
  * for the first time, and cleared when it goes out of scope.
  *
+ * **Therefore, it is not allowed to call [viewModel] with the same type from within multiple Composables.**
+ *
  * @param modelClass The class of the [ViewModel] to create an instance of it if it is not
  * present.
  * @param viewModelProvider The scope that the created [ViewModel] should be associated with.
@@ -76,10 +78,10 @@ fun <VM : ViewModel> viewModel(
     val viewModel = viewModelProvider.get(modelClass, key)
 
     DisposableEffect(key) {
-        viewModel.onInit()
+        viewModel.onEnterComposition()
 
         onDispose {
-            viewModel.onCleared()
+            viewModel.onExitComposition()
         }
     }
 
@@ -95,3 +97,10 @@ fun <T> MutableState<T>.asState(): State<T> = this
  * Returns this [SnapshotStateList] as an immutable [List].
  */
 fun <T> SnapshotStateList<T>.asList(): List<T> = this
+
+/**
+ * Update the [MutableState] with the given [transformation] on its value.
+ */
+inline fun <T> MutableState<T>.update(transformation: T.() -> T) {
+    value = value.run(transformation)
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
index e3b97b9c3178eee70b8657db20de4559d8a4e907..a67eb885d68cfab93e5ca4edd636a8fc5420f901 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
@@ -28,7 +28,7 @@ abstract class DbViewModel(
     private val briarExecutors: BriarExecutors,
     private val lifecycleManager: LifecycleManager,
     private val db: TransactionManager,
-) : ViewModel {
+) : ViewModel() {
 
     /**
      * Waits for the DB to open and runs the given [task] on the [DatabaseExecutor].
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt
index c367cc24fe5d88aa717ee4ea1260ef4a5736e903..17af4811fa33cb65d8d45bafb8ded7f9a063bf69 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModel.kt
@@ -18,17 +18,55 @@
 
 package org.briarproject.briar.desktop.viewmodel
 
-interface ViewModel {
+abstract class ViewModel {
+
+    private var inComposition = false
+
+    /**
+     * Called to initialize the [ViewModel] as soon as it is first used
+     * inside a Composable function.
+     *
+     * If you want to specify initialization code, overwrite [onInit] instead.
+     */
+    fun onEnterComposition() {
+        if (inComposition)
+            throw RuntimeException("Injecting the same instance of ${this::class.simpleName} in different Composables is not permitted.")
+        inComposition = true
+        onInit()
+    }
+
+    /**
+     * Called to clear the [ViewModel] as soon as the calling
+     * Composable function goes out of scope.
+     *
+     * If you want to specify de-initialization code, overwrite [onCleared] instead.
+     */
+    fun onExitComposition() {
+        if (!inComposition)
+            throw RuntimeException("Wrong use of ViewModel.")
+        inComposition = false
+        onCleared()
+    }
 
     /**
      * Called to initialize the [ViewModel] as soon as it is first used
      * inside a Composable function.
+     *
+     * This function can be overridden in child classes,
+     * but implementations should always call `super.onInit()` first.
+     *
+     * Apart from that, **do not call this function manually anywhere.**
      */
-    fun onInit() {}
+    open fun onInit() {}
 
     /**
      * Called to clear the [ViewModel] as soon as the calling
      * Composable function goes out of scope.
+     *
+     * This function can be overridden in child classes,
+     * but implementations should always call `super.onInit()` first.
+     *
+     * Apart from that, **do not call this function manually anywhere.**
      */
-    fun onCleared() {}
+    open fun onCleared() {}
 }