Making sure ViewModels are deleted when switching accounts.

This commit is contained in:
Vitor Pamplona
2023-10-25 14:55:48 -04:00
parent 4a8c020c3d
commit 903384d02a
19 changed files with 102 additions and 43 deletions
@@ -81,7 +81,7 @@ import kotlinx.coroutines.withContext
@Composable
fun JoinUserOrChannelView(onClose: () -> Unit, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val searchBarViewModel: SearchBarViewModel = viewModel(
key = accountViewModel.account.userProfile().pubkeyHex + "SearchBarViewModel",
key = "SearchBarViewModel",
factory = SearchBarViewModel.Factory(
accountViewModel.account
)
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@@ -631,6 +632,7 @@ open class NewPostViewModel() : ViewModel() {
override fun onCleared() {
super.onCleared()
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
viewModelScope.launch(Dispatchers.IO) {
locUtil?.stop()
}
@@ -667,6 +667,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
override fun onCleared() {
collectorJob?.cancel()
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
super.onCleared()
}
@@ -81,9 +81,7 @@ fun PollNote(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val pollViewModel: PollNoteViewModel = viewModel(
key = baseNote.idHex + "PollNoteViewModel"
)
val pollViewModel: PollNoteViewModel = viewModel(key = "PollNoteViewModel")
pollViewModel.load(accountViewModel.account, baseNote)
@@ -127,7 +127,7 @@ fun UpdateReactionTypeDialog(
nav: (String) -> Unit
) {
val postViewModel: UpdateReactionTypeViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex,
key = "UpdateReactionTypeViewModel",
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account)
)
@@ -204,7 +204,7 @@ fun UpdateZapAmountDialog(
val scope = rememberCoroutineScope()
val postViewModel: UpdateZapAmountViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex,
key = "UpdateZapAmountViewModel",
factory = UpdateZapAmountViewModel.Factory(accountViewModel.account)
)
@@ -366,6 +366,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
override fun onCleared() {
collectorJob?.cancel()
bundlerInsert.cancel()
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
super.onCleared()
}
@@ -8,13 +8,18 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
@@ -40,26 +45,53 @@ fun AccountScreen(
LoginPage(accountStateViewModel, isFirstLogin = true)
}
is AccountState.LoggedIn -> {
val accountViewModel: AccountViewModel = viewModel(
key = state.account.hashCode().toString(),
factory = AccountViewModel.Factory(state.account, sharedPreferencesViewModel.sharedPrefs)
)
MainScreen(accountViewModel, accountStateViewModel, sharedPreferencesViewModel)
CompositionLocalProvider(
LocalViewModelStoreOwner provides state.currentViewModelStore
) {
LoggedInPage(
state.account,
accountStateViewModel,
sharedPreferencesViewModel
)
}
}
is AccountState.LoggedInViewOnly -> {
val accountViewModel: AccountViewModel = viewModel(
key = state.account.hashCode().toString(),
factory = AccountViewModel.Factory(state.account, sharedPreferencesViewModel.sharedPrefs)
)
MainScreen(accountViewModel, accountStateViewModel, sharedPreferencesViewModel)
CompositionLocalProvider(
LocalViewModelStoreOwner provides state.currentViewModelStore
) {
LoggedInPage(
state.account,
accountStateViewModel,
sharedPreferencesViewModel
)
}
}
}
}
}
}
@Composable
fun LoggedInPage(
account: Account,
accountStateViewModel: AccountStateViewModel,
sharedPreferencesViewModel: SharedPreferencesViewModel
) {
val accountViewModel: AccountViewModel = viewModel(
key = "AccountStateViewModel",
factory = AccountViewModel.Factory(
account,
sharedPreferencesViewModel.sharedPrefs
)
)
MainScreen(accountViewModel, accountStateViewModel, sharedPreferencesViewModel)
}
class AccountCentricViewModelStore(val account: Account) : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
}
@Composable
fun LoadingAccounts() {
Column(
@@ -5,6 +5,10 @@ import com.vitorpamplona.amethyst.model.Account
sealed class AccountState {
object Loading : AccountState()
object LoggedOff : AccountState()
class LoggedInViewOnly(val account: Account) : AccountState()
class LoggedIn(val account: Account) : AccountState()
class LoggedInViewOnly(val account: Account) : AccountState() {
val currentViewModelStore = AccountCentricViewModelStore(account)
}
class LoggedIn(val account: Account) : AccountState() {
val currentViewModelStore = AccountCentricViewModelStore(account)
}
}
@@ -103,9 +103,15 @@ class AccountStateViewModel() : ViewModel() {
when (val state = _accountContent.value) {
is AccountState.LoggedIn -> {
state.account.saveable.removeObserver(saveListener)
withContext(Dispatchers.IO) {
state.currentViewModelStore.viewModelStore.clear()
}
}
is AccountState.LoggedInViewOnly -> {
state.account.saveable.removeObserver(saveListener)
withContext(Dispatchers.IO) {
state.currentViewModelStore.viewModelStore.clear()
}
}
else -> {}
}
@@ -370,6 +370,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
}
override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
clear()
bundlerInsert.cancel()
bundler.cancel()
@@ -322,7 +322,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
private var collectorJob: Job? = null
init {
Log.d("Init", "${this.javaClass.simpleName}")
Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}")
collectorJob = viewModelScope.launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect { newNotes ->
checkNotInMainThread()
@@ -340,6 +340,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
}
override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
bundlerInsert.cancel()
bundler.cancel()
collectorJob?.cancel()
@@ -95,6 +95,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : View
}
override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -96,6 +97,7 @@ class RelayFeedViewModel : ViewModel() {
}
override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
bundler.cancel()
super.onCleared()
}
@@ -100,6 +100,7 @@ open class StringFeedViewModel(val dataSource: FeedFilter<String>) : ViewModel()
}
override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
@@ -126,6 +126,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
}
override fun onCleared() {
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
bundler.cancel()
collectorJob?.cancel()
super.onCleared()
@@ -792,7 +792,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
}
}
fun refreshMarkAsReadObservers() {
suspend fun refreshMarkAsReadObservers() {
updateNotificationDots()
accountMarkAsReadUpdates.value++
}
@@ -862,25 +862,26 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
private var collectorJob: Job? = null
val notificationDots = HasNotificationDot(bottomNavigationItems, account)
fun updateNotificationDots(newNotes: Set<Note> = emptySet()) {
viewModelScope.launch(Dispatchers.Default) {
val (value, elapsed) = measureTimedValue {
notificationDots.update(newNotes)
}
Log.d("Rendering Metrics", "Notification Dots Calculation in $elapsed for ${newNotes.size} new notes")
suspend fun updateNotificationDots(newNotes: Set<Note> = emptySet()) {
val (value, elapsed) = measureTimedValue {
notificationDots.update(newNotes)
}
Log.d("Rendering Metrics", "Notification Dots Calculation in $elapsed for ${newNotes.size} new notes")
}
init {
Log.d("Init", "AccountViewModel")
collectorJob = viewModelScope.launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect { newNotes ->
Log.d("Rendering Metrics", "Notification Dots Calculation refresh ${this@AccountViewModel}")
updateNotificationDots(newNotes)
}
}
}
override fun onCleared() {
Log.d("Init", "AccountViewModel onCleared")
collectorJob?.cancel()
super.onCleared()
}
@@ -920,11 +921,16 @@ class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>, val accoun
val hasNewItems = bottomNavigationItems.associateWith { MutableStateFlow(false) }
fun update(newNotes: Set<Note>) {
checkNotInMainThread()
hasNewItems.forEach {
val newResult = it.key.hasNewItems(account, newNotes)
if (newResult != it.value.value) {
it.value.value = newResult
val (value, elapsed) = measureTimedValue {
val newResult = it.key.hasNewItems(account, newNotes)
if (newResult != it.value.value) {
it.value.value = newResult
}
}
Log.d("Rendering Metrics", "Notification Dots Calculation for ${it.key.route} in $elapsed for ${newNotes.size} new notes")
}
}
}
@@ -141,58 +141,58 @@ fun MainScreen(
}
val followLists: FollowListViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel",
key = "FollowListViewModel",
factory = FollowListViewModel.Factory(accountViewModel.account)
)
// Avoids creating ViewModels for performance reasons (up to 1 second delays)
val homeFeedViewModel: NostrHomeFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrHomeFeedViewModel",
key = "NostrHomeFeedViewModel",
factory = NostrHomeFeedViewModel.Factory(accountViewModel.account)
)
val repliesFeedViewModel: NostrHomeRepliesFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrHomeRepliesFeedViewModel",
key = "NostrHomeRepliesFeedViewModel",
factory = NostrHomeRepliesFeedViewModel.Factory(accountViewModel.account)
)
val videoFeedViewModel: NostrVideoFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrVideoFeedViewModel",
key = "NostrVideoFeedViewModel",
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account)
)
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryLiveFeedViewModel",
key = "NostrDiscoveryLiveFeedViewModel",
factory = NostrDiscoverLiveFeedViewModel.Factory(accountViewModel.account)
)
val discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryCommunityFeedViewModel",
key = "NostrDiscoveryCommunityFeedViewModel",
factory = NostrDiscoverCommunityFeedViewModel.Factory(accountViewModel.account)
)
val discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrDiscoveryChatFeedViewModel",
key = "NostrDiscoveryChatFeedViewModel",
factory = NostrDiscoverChatFeedViewModel.Factory(accountViewModel.account)
)
val notifFeedViewModel: NotificationViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NotificationViewModel",
key = "NotificationViewModel",
factory = NotificationViewModel.Factory(accountViewModel.account)
)
val userReactionsStatsModel: UserReactionsViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "UserReactionsViewModel",
key = "UserReactionsViewModel",
factory = UserReactionsViewModel.Factory(accountViewModel.account)
)
val knownFeedViewModel: NostrChatroomListKnownFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrChatroomListKnownFeedViewModel",
key = "NostrChatroomListKnownFeedViewModel",
factory = NostrChatroomListKnownFeedViewModel.Factory(accountViewModel.account)
)
val newFeedViewModel: NostrChatroomListNewFeedViewModel = viewModel(
key = accountViewModel.userProfile().pubkeyHex + "NostrChatroomListNewFeedViewModel",
key = "NostrChatroomListNewFeedViewModel",
factory = NostrChatroomListNewFeedViewModel.Factory(accountViewModel.account)
)
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -86,7 +87,7 @@ fun SearchScreen(
nav: (String) -> Unit
) {
val searchBarViewModel: SearchBarViewModel = viewModel(
key = accountViewModel.account.userProfile().pubkeyHex + "SearchBarViewModel",
key = "SearchBarViewModel",
factory = SearchBarViewModel.Factory(
accountViewModel.account
)
@@ -198,6 +199,7 @@ class SearchBarViewModel(val account: Account) : ViewModel() {
override fun onCleared() {
bundler.cancel()
Log.d("Init", "OnCleared: ${this.javaClass.simpleName}")
super.onCleared()
}