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