From 4857a3d0ac9c2198dc58c60041aea814debf9a4e Mon Sep 17 00:00:00 2001 From: davidoskky Date: Thu, 25 Aug 2022 12:19:24 +0200 Subject: [PATCH] Move Home functionalities to the viewmodel --- .../android/HomeActivity.kt | 286 +++++++----------- .../apps/readerforselfossv2/android/MyApp.kt | 9 + .../android/viewmodel/AppViewModel.kt | 52 ++++ .../repository/RepositoryImpl.kt | 47 +-- 4 files changed, 198 insertions(+), 196 deletions(-) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index 87b5b67..243b87b 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.core.view.doOnNextLayout import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.* import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy @@ -34,10 +35,13 @@ import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper -import bou.amine.apps.readerforselfossv2.dao.ACTION -import bou.amine.apps.readerforselfossv2.repository.Repository +import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel import bou.amine.apps.readerforselfossv2.model.SelfossModel -import bou.amine.apps.readerforselfossv2.utils.* +import bou.amine.apps.readerforselfossv2.repository.Repository +import bou.amine.apps.readerforselfossv2.utils.ItemType +import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded +import bou.amine.apps.readerforselfossv2.utils.getIcon +import bou.amine.apps.readerforselfossv2.utils.longHash import com.ashokvarma.bottomnavigation.BottomNavigationBar import com.ashokvarma.bottomnavigation.BottomNavigationItem import com.ashokvarma.bottomnavigation.TextBadgeItem @@ -66,7 +70,6 @@ import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.instance import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware { @@ -118,6 +121,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar override val di by closestDI() private val repository : Repository by instance() + private val viewModel: AppViewModel by instance() data class DrawerData(val tags: List?, val sources: List?) @@ -142,6 +146,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } setContentView(view) + lifecycleScope.launch { + viewModel.refreshingIndicatorProvider.collect { showRefresh -> + binding.swipeRefreshLayout.isRefreshing = showRefresh + } + } handleThemeBinding() @@ -153,18 +162,25 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar mDrawerToggle.syncState() customTabActivityHelper = CustomTabActivityHelper() + handleSettings() + + lifecycleScope.launch { + viewModel.items.collect { fetchedItems -> + items = fetchedItems + handleListResult() + } + } handleBottomBar() handleDrawer() handleSwipeRefreshLayout() - handleSettings() - getElementsAccordingToTab() + handleBadgesContent() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { repository.tryToCacheItemsAndGetNewOnes() } @@ -180,10 +196,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar repository.offlineOverride = false lastFetchDone = false handleDrawerItems() - CoroutineScope(Dispatchers.Main).launch { - getElementsAccordingToTab() - binding.swipeRefreshLayout.isRefreshing = false - } + viewModel.getItems(false, elementsShown) } val simpleItemTouchCallback = @@ -219,8 +232,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar adapter.handleItemAtIndex(position) - reloadBadgeContent() - val tagHashes = i.tags.map { it.longHash() } tagsBadge = tagsBadge.map { if (tagHashes.contains(it.key)) { @@ -252,16 +263,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar tabNewBadge = TextBadgeItem() .setText("") - .setHideOnSelect(false).hide(false) + .setHideOnSelect(false) .setBackgroundColor(appColors.colorPrimary) + if (!displayUnreadCount) { + tabNewBadge.hide(false) + } tabArchiveBadge = TextBadgeItem() .setText("") - .setHideOnSelect(false).hide(false) + .setHideOnSelect(false) .setBackgroundColor(appColors.colorPrimary) tabStarredBadge = TextBadgeItem() .setText("") - .setHideOnSelect(false).hide(false) + .setHideOnSelect(false) .setBackgroundColor(appColors.colorPrimary) + if (!displayAllCount) { + tabArchiveBadge.hide(false) + tabStarredBadge.hide(false) + } val tabNew = BottomNavigationItem( @@ -453,7 +471,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar // TODO: refactor this. private fun handleDrawerItems() { tagsBadge = emptyMap() - fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) { + fun handleDrawerData(maybeDrawerData: DrawerData?) { fun createDrawerItem( it: SelfossModel.Tag ) { @@ -495,12 +513,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar fun handleTags(maybeTags: List?) { if (maybeTags == null) { - if (loadedFromCache) { - binding.mainDrawer.itemAdapter.add( - SecondaryDrawerItem() - .apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false } - ) - } + binding.mainDrawer.itemAdapter.add( + SecondaryDrawerItem() + .apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false } + ) } else { val filteredTags = maybeTags .filterNot { hiddenTags.contains(it.tag) } @@ -515,14 +531,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar fun handleHiddenTags(maybeTags: List?) { if (maybeTags == null) { - if (loadedFromCache) { - binding.mainDrawer.itemAdapter.add( - SecondaryDrawerItem().apply { - nameRes = R.string.drawer_error_loading_tags - isSelectable = false - } - ) - } + binding.mainDrawer.itemAdapter.add( + SecondaryDrawerItem().apply { + nameRes = R.string.drawer_error_loading_tags + isSelectable = false + } + ) } else { val filteredHiddenTags: List = maybeTags.filter { hiddenTags.contains(it.tag) } @@ -536,14 +550,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar fun handleSources(maybeSources: List?) { if (maybeSources == null) { - if (loadedFromCache) { - binding.mainDrawer.itemAdapter.add( - SecondaryDrawerItem().apply { - nameRes = R.string.drawer_error_loading_sources - isSelectable = false - } - ) - } + binding.mainDrawer.itemAdapter.add( + SecondaryDrawerItem().apply { + nameRes = R.string.drawer_error_loading_sources + isSelectable = false + } + ) } else { for (source in maybeSources) { val item = PrimaryDrawerItem().apply { @@ -632,61 +644,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } ) - if (!loadedFromCache) { - if (maybeDrawerData.tags != null) { - thread { - repository.resetDBTagsWithData(maybeDrawerData.tags) - } - } - if (maybeDrawerData.sources != null) { - thread { - repository.resetDBSourcesWithData(maybeDrawerData.sources) - } - } - } } else { - if (!loadedFromCache) { - binding.mainDrawer.itemAdapter.add( - PrimaryDrawerItem().apply { - nameRes = R.string.no_tags_loaded - identifier = DRAWER_ID_TAGS - isSelectable = false - }, - PrimaryDrawerItem().apply { - nameRes = R.string.no_sources_loaded - identifier = DRAWER_ID_SOURCES - isSelectable = false - } - ) - } - } - } - - fun drawerApiCalls(maybeDrawerData: DrawerData?) { - var tags: List? = null - var sources: List? - - fun sourcesApiCall() { - CoroutineScope(Dispatchers.Main).launch { - val response = repository.getSources() - if (response != null) { - sources = response - val apiDrawerData = DrawerData(tags, sources) - if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) { - handleDrawerData(apiDrawerData) - } - } else { - val apiDrawerData = DrawerData(tags, null) - if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) { - handleDrawerData(apiDrawerData) - } + binding.mainDrawer.itemAdapter.add( + PrimaryDrawerItem().apply { + nameRes = R.string.no_tags_loaded + identifier = DRAWER_ID_TAGS + isSelectable = false + }, + PrimaryDrawerItem().apply { + nameRes = R.string.no_sources_loaded + identifier = DRAWER_ID_SOURCES + isSelectable = false } - } - } - - CoroutineScope(Dispatchers.IO).launch { - tags = repository.getTags() - sourcesApiCall() + ) } } @@ -697,12 +667,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } ) - thread { - val drawerData = DrawerData(repository.getDBTags().map { it.toView() }, - repository.getDBSources().map { it.toView() }) + CoroutineScope(Dispatchers.IO).launch { + val drawerData = DrawerData(repository.getTags(), + repository.getSources()) runOnUiThread { - handleDrawerData(drawerData, loadedFromCache = true) - drawerApiCalls(drawerData) + handleDrawerData(drawerData) } } } @@ -839,21 +808,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar } firstVisible = if (appendResults) firstVisible else 0 - getItems(appendResults, elementsShown) - } - - private fun getItems(appendResults: Boolean, itemType: ItemType) { - CoroutineScope(Dispatchers.Main).launch { - binding.swipeRefreshLayout.isRefreshing = true - repository.displayedItems = itemType - items = if (appendResults) { - repository.getOlderItems() - } else { - repository.getNewerItems() - } - binding.swipeRefreshLayout.isRefreshing = false - handleListResult() - } + viewModel.getItems(appendResults, elementsShown) } private fun handleListResult(appendResults: Boolean = false) { @@ -915,26 +870,50 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar private fun reloadBadges() { if (displayUnreadCount || displayAllCount) { - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { repository.reloadBadges() - reloadBadgeContent() } } } - private fun reloadBadgeContent() { + private fun handleBadgesContent() { if (displayUnreadCount) { - tabNewBadge - .setText(repository.badgeUnread.toString()) - .maybeShow() + lifecycleScope.launch { + repository.badgeUnread.collect { unreadCount -> + if (unreadCount > 0) { + tabNewBadge + .setText(unreadCount.toString()) + .maybeShow() + } else { + tabNewBadge.removeBadge() + } + } + } } + if (displayAllCount) { - tabArchiveBadge - .setText(repository.badgeAll.toString()) - .maybeShow() - tabStarredBadge - .setText(repository.badgeStarred.toString()) - .maybeShow() + lifecycleScope.launch { + repository.badgeAll.collect { itemsCount -> + if (itemsCount > 0) { + tabArchiveBadge + .setText(itemsCount.toString()) + .maybeShow() + } else { + tabArchiveBadge.removeBadge() + } + } + } + lifecycleScope.launch { + repository.badgeStarred.collect { starredCount -> + if (starredCount > 0) { + tabStarredBadge + .setText(starredCount.toString()) + .maybeShow() + } else { + tabStarredBadge.removeBadge() + } + } + } } } @@ -992,56 +971,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar when (item.itemId) { R.id.refresh -> { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { - Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() - // TODO: Use Dispatchers.IO - CoroutineScope(Dispatchers.Main).launch { - val updatedRemote = repository.updateRemote() - if (updatedRemote) { - // TODO: Send toast messages from the repository - Toast.makeText( - this@HomeActivity, - R.string.refresh_success_response, Toast.LENGTH_LONG - ) - .show() - } else { - Toast.makeText( - this@HomeActivity, - R.string.refresh_failer_message, - Toast.LENGTH_SHORT - ).show() - } - } + viewModel.updateRemote() } return true } R.id.readAll -> { if (elementsShown == ItemType.UNREAD) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { - binding.swipeRefreshLayout.isRefreshing = true - - CoroutineScope(Dispatchers.Main).launch { - val success = repository.markAllAsRead(items) - if (success) { - Toast.makeText( - this@HomeActivity, - R.string.all_posts_read, - Toast.LENGTH_SHORT - ).show() - tabNewBadge.removeBadge() - - handleDrawerItems() - - getElementsAccordingToTab() - } else { - Toast.makeText( - this@HomeActivity, - R.string.all_posts_not_read, - Toast.LENGTH_SHORT - ).show() - } - handleListResult() - binding.swipeRefreshLayout.isRefreshing = false - } + viewModel.markAllAsRead() } } return true @@ -1055,10 +992,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar private fun maxItemNumber(): Int = when (elementsShown) { - ItemType.UNREAD -> repository.badgeUnread - ItemType.ALL -> repository.badgeAll - ItemType.STARRED -> repository.badgeStarred - else -> repository.badgeUnread // if !elementsShown then unread are fetched. + ItemType.UNREAD -> repository.badgeUnread.value + ItemType.ALL -> repository.badgeAll.value + ItemType.STARRED -> repository.badgeStarred.value + else -> repository.badgeUnread.value // if !elementsShown then unread are fetched. } private fun updateItems(adapterItems: ArrayList) { @@ -1082,9 +1019,4 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar WorkManager.getInstance(baseContext).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork) } } - - private fun handleOfflineActions() { - - } } - diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt index 3345124..3e891c5 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt @@ -84,6 +84,15 @@ class MyApp : MultiDexApplication(), DIAware { ).show() } } + CoroutineScope(Dispatchers.Main).launch { + viewModel.toastMessageProvider.collect { toastMessage -> + Toast.makeText( + applicationContext, + toastMessage, + Toast.LENGTH_SHORT + ).show() + } + } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/viewmodel/AppViewModel.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/viewmodel/AppViewModel.kt index 018900e..e76f6b4 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/viewmodel/AppViewModel.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/viewmodel/AppViewModel.kt @@ -3,16 +3,29 @@ package bou.amine.apps.readerforselfossv2.android.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import bou.amine.apps.readerforselfossv2.android.R +import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.repository.Repository +import bou.amine.apps.readerforselfossv2.utils.ItemType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class AppViewModel(private val repository: Repository) : ViewModel() { private val _networkAvailableProvider = MutableSharedFlow() val networkAvailableProvider = _networkAvailableProvider.asSharedFlow() + private val _refreshingIndicatorProvider = MutableSharedFlow() + val refreshingIndicatorProvider = _refreshingIndicatorProvider.asSharedFlow() + private val _toastMessageProvider = MutableSharedFlow() + val toastMessageProvider = _toastMessageProvider.asSharedFlow() private var wasConnected = true + private val _items = MutableStateFlow(ArrayList()) + val items = _items.asStateFlow() + init { viewModelScope.launch { repository.isConnectionAvailable.collect { isConnected -> @@ -28,4 +41,43 @@ class AppViewModel(private val repository: Repository) : ViewModel() { } } } + + fun updateRemote() { + CoroutineScope(Dispatchers.IO).launch { + _toastMessageProvider.emit(R.string.refresh_in_progress) + val updatedRemote = repository.updateRemote() + if (updatedRemote) { + _toastMessageProvider.emit(R.string.refresh_success_response) + } else { + _toastMessageProvider.emit(R.string.refresh_failer_message) + } + } + } + + fun getItems(appendResults: Boolean, itemType: ItemType) { + CoroutineScope(Dispatchers.Main).launch { + _refreshingIndicatorProvider.emit(true) + repository.displayedItems = itemType + val items = if (appendResults) { + repository.getOlderItems() + } else { + repository.getNewerItems() + } + _items.emit(items) + _refreshingIndicatorProvider.emit(false) + } + } + + fun markAllAsRead() { + CoroutineScope(Dispatchers.IO).launch { + _refreshingIndicatorProvider.emit(true) + val success = repository.markAllAsRead(items.value) + if (success) { + _toastMessageProvider.emit(R.string.all_posts_read) + } else { + _toastMessageProvider.emit(R.string.all_posts_not_read) + } + _refreshingIndicatorProvider.emit(false) + } + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt index cc8a878..bfca07a 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -11,6 +11,8 @@ import com.russhwolf.settings.Settings import io.github.aakira.napier.Napier import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService, private val connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) { @@ -33,12 +35,12 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails var offlineOverride = false var apiMajorVersion = 0 - var badgeUnread = 0 - set(value) {field = if (value < 0) { 0 } else { value } } - var badgeAll = 0 - set(value) {field = if (value < 0) { 0 } else { value } } - var badgeStarred = 0 - set(value) {field = if (value < 0) { 0 } else { value } } + private val _badgeUnread = MutableStateFlow(0) + val badgeUnread = _badgeUnread.asStateFlow() + private val _badgeAll = MutableStateFlow(0) + val badgeAll = _badgeAll.asStateFlow() + private val _badgeStarred = MutableStateFlow(0) + val badgeStarred = _badgeStarred.asStateFlow() init { // TODO: Dispatchers.IO not available in KMM, an alternative solution should be found @@ -125,27 +127,31 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails if (isNetworkAvailable()) { val response = api.stats() if (response != null) { - badgeUnread = response.unread - badgeAll = response.total - badgeStarred = response.starred + _badgeUnread.value = response.unread + _badgeAll.value = response.total + _badgeStarred.value = response.starred success = true } } else { // TODO: do this differently, because it's not efficient val dbItems = getDBItems() - badgeUnread = dbItems.filter { item -> item.unread }.size - badgeStarred = dbItems.filter { item -> item.starred }.size - badgeAll = items.size + _badgeUnread.value = dbItems.filter { item -> item.unread }.size + _badgeStarred.value = dbItems.filter { item -> item.starred }.size + _badgeAll.value = items.size } return success } suspend fun getTags(): List? { - return if (isNetworkAvailable()) { + val tags = if (isNetworkAvailable()) { api.tags() } else { getDBTags().map { it.toView() } } + if (tags != null) { + resetDBTagsWithData(tags) + } + return tags } suspend fun getSpouts(): Map? { @@ -157,12 +163,15 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails } suspend fun getSources(): ArrayList? { - - return if (isNetworkAvailable()) { + val sources = if (isNetworkAvailable()) { api.sources() } else { ArrayList(getDBSources().map { it.toView() }) } + if (sources != null) { + resetDBSourcesWithData(sources) + } + return sources } suspend fun markAsRead(item: SelfossModel.Item): Boolean { @@ -253,7 +262,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails private fun markAsReadLocally(item: SelfossModel.Item) { if (item.unread) { item.unread = false - badgeUnread -= 1 + _badgeUnread.value -= 1 } CoroutineScope(Dispatchers.Main).launch { @@ -264,7 +273,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails private fun unmarkAsReadLocally(item: SelfossModel.Item) { if (!item.unread) { item.unread = true - badgeUnread += 1 + _badgeUnread.value += 1 } CoroutineScope(Dispatchers.Main).launch { @@ -275,7 +284,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails private fun starrLocally(item: SelfossModel.Item) { if (!item.starred) { item.starred = true - badgeStarred += 1 + _badgeStarred.value += 1 } CoroutineScope(Dispatchers.Main).launch { @@ -286,7 +295,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails private fun unstarrLocally(item: SelfossModel.Item) { if (item.starred) { item.starred = false - badgeStarred -= 1 + _badgeStarred.value -= 1 } CoroutineScope(Dispatchers.Main).launch {