Skip to content

Commit

Permalink
Implement pull to refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
rumboalla committed Jul 14, 2023
1 parent 087cea3 commit 20d25f9
Show file tree
Hide file tree
Showing 16 changed files with 748 additions and 53 deletions.
10 changes: 5 additions & 5 deletions app/src/main/java/com/apkupdater/di/MainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.apkupdater.repository.ApkMirrorRepository
import com.apkupdater.repository.AppsRepository
import com.apkupdater.service.ApkMirrorService
import com.apkupdater.viewmodel.AppsViewModel
import com.apkupdater.viewmodel.BottomBarViewModel
import com.apkupdater.viewmodel.MainViewModel
import com.apkupdater.viewmodel.SearchViewModel
import com.apkupdater.viewmodel.SettingsViewModel
import com.apkupdater.viewmodel.UpdatesViewModel
Expand Down Expand Up @@ -69,14 +69,14 @@ val mainModule = module {

single { Prefs(get()) }

viewModel { AppsViewModel(get(), get()) }
viewModel { parameters -> AppsViewModel(parameters.get(), get(), get()) }

viewModel { BottomBarViewModel() }
viewModel { MainViewModel() }

viewModel { UpdatesViewModel(get(), get(), get()) }
viewModel { parameters -> UpdatesViewModel(parameters.get(), get(), get()) }

viewModel { SettingsViewModel(get()) }

viewModel { SearchViewModel(get(), get()) }
viewModel { parameters -> SearchViewModel(parameters.get(), get()) }

}
16 changes: 8 additions & 8 deletions app/src/main/java/com/apkupdater/prefs/Prefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import com.kryptoprefs.preferences.KryptoPrefs


class Prefs(prefs: KryptoPrefs): KryptoContext(prefs) {
val ignoredApps = json("ignoredApps", emptyList<String>())
val excludeSystem = boolean("excludeSystem", true)
val excludeDisabled = boolean("excludeDisabled", true)
val excludeStore = boolean("excludeStore", false)
val portraitColumns = int("portraitColumns", 2)
val landscapeColumns = int("landscapeColumns", 4)
val ignoreAlpha = boolean("ignoreAlpha", true)
val ignoreBeta = boolean("ignoreBeta", true)
val ignoredApps = json("ignoredApps", emptyList<String>(), true)
val excludeSystem = boolean("excludeSystem", defValue = true, backed = true)
val excludeDisabled = boolean("excludeDisabled", defValue = true, backed = true)
val excludeStore = boolean("excludeStore", defValue = false, backed = true)
val portraitColumns = int("portraitColumns", 2, true)
val landscapeColumns = int("landscapeColumns", 4, true)
val ignoreAlpha = boolean("ignoreAlpha", defValue = true, backed = true)
val ignoreBeta = boolean("ignoreBeta", defValue = true, backed = true)
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/apkupdater/repository/AppsRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import com.apkupdater.prefs.Prefs
import com.apkupdater.transform.toAppInstalled
import com.apkupdater.util.orFalse
Expand All @@ -30,6 +31,7 @@ class AppsRepository(
.toList()
emit(Result.success(apps))
}.catch {
Log.e("AppsRepository", "Error getting apps.", it)
emit(Result.failure(it))
}

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/apkupdater/ui/component/UiComponents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ fun AppImage(app: AppInstalled, onIgnore: (String) -> Unit = {}) = Box {
TextBubble(app.versionCode.toString(), Modifier.align(Alignment.BottomStart))
IgnoreIcon(
app.ignored,
{ onIgnore(app.packageName) },
{ onIgnore(app.packageName) },
Modifier.align(Alignment.TopEnd).padding(4.dp)
)
}
Expand All @@ -239,7 +239,7 @@ fun UpdateImage(app: AppUpdate, onInstall: (String) -> Unit = {}) = Box {
LoadingImageApp(app.packageName)
TextBubble(app.versionCode.toString(), Modifier.align(Alignment.BottomStart))
InstallIcon(
{ onInstall(app.link) },
{ onInstall(app.link) },
Modifier.align(Alignment.TopEnd).padding(4.dp)
)
}
Expand Down
7 changes: 1 addition & 6 deletions app/src/main/java/com/apkupdater/ui/screen/AppsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,25 @@ import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.apkupdater.R
import com.apkupdater.data.ui.AppsUiState
import com.apkupdater.ui.component.InstalledItem
import com.apkupdater.ui.component.DefaultErrorScreen
import com.apkupdater.ui.component.DefaultLoadingScreen
import com.apkupdater.ui.component.ExcludeSystemIcon
import com.apkupdater.ui.component.InstalledGrid
import com.apkupdater.ui.component.InstalledItem
import com.apkupdater.ui.component.RefreshIcon
import com.apkupdater.viewmodel.AppsViewModel
import com.apkupdater.viewmodel.BottomBarViewModel
import org.koin.androidx.compose.koinViewModel


@Composable
fun AppsScreen(
barViewModel: BottomBarViewModel,
viewModel: AppsViewModel = koinViewModel()
) {
viewModel.state().collectAsStateWithLifecycle().value.onLoading {
barViewModel.changeAppsBadge("")
AppsScreenLoading()
}.onError {
barViewModel.changeAppsBadge("!")
AppsScreenError()
}.onSuccess {
barViewModel.changeAppsBadge(it.apps.count().toString())
AppsScreenSuccess(viewModel, it)
}
}
Expand Down
56 changes: 45 additions & 11 deletions app/src/main/java/com/apkupdater/ui/screen/MainScreen.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.apkupdater.ui.screen

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
Expand All @@ -10,10 +11,16 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
Expand All @@ -23,20 +30,44 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.apkupdater.data.ui.Screen
import com.apkupdater.ui.component.BadgeText
import com.apkupdater.viewmodel.BottomBarViewModel
import com.apkupdater.viewmodel.AppsViewModel
import com.apkupdater.viewmodel.MainViewModel
import com.apkupdater.viewmodel.SearchViewModel
import com.apkupdater.viewmodel.SettingsViewModel
import com.apkupdater.viewmodel.UpdatesViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf


@Composable
fun MainScreen(viewModel: BottomBarViewModel = koinViewModel()) {
fun MainScreen(mainViewModel: MainViewModel = koinViewModel()) {
// ViewModels
val appsViewModel: AppsViewModel = koinViewModel(parameters = { parametersOf(mainViewModel) })
val updatesViewModel: UpdatesViewModel = koinViewModel(parameters = { parametersOf(mainViewModel) })
val searchViewModel: SearchViewModel = koinViewModel(parameters = { parametersOf(mainViewModel) })

// Navigation
val navController = rememberNavController()
Scaffold(bottomBar = { BottomBar(navController, viewModel) }) { padding ->
NavHost(navController, padding, viewModel)

// Pull to refresh
val isRefreshing = mainViewModel.isRefreshing.collectAsStateWithLifecycle()
val pullToRefresh = rememberPullRefreshState(isRefreshing.value, {
mainViewModel.refresh(appsViewModel, updatesViewModel)
})
LaunchedEffect(pullToRefresh) {
mainViewModel.refresh(appsViewModel, updatesViewModel)
}

Scaffold(bottomBar = { BottomBar(navController, mainViewModel) }) { padding ->
Box(modifier = Modifier.pullRefresh(pullToRefresh)) {
NavHost(navController, padding, appsViewModel, updatesViewModel, searchViewModel)
PullRefreshIndicator(isRefreshing.value, pullToRefresh, Modifier.align(Alignment.TopCenter))
}
}
}

@Composable
fun BottomBar(navController: NavController, viewModel: BottomBarViewModel) = BottomAppBar {
fun BottomBar(navController: NavController, viewModel: MainViewModel) = BottomAppBar {
val badges = viewModel.badges.collectAsState().value
viewModel.screens.forEach { screen ->
val state = navController.currentBackStackEntryAsState().value
Expand All @@ -57,7 +88,7 @@ fun RowScope.BottomBarItem(
BadgedBox({ BadgeText(badge) }) {
Icon(if (selected) screen.iconSelected else screen.icon, contentDescription = null)
}
},
},
label = { Text(stringResource(screen.resourceId)) },
selected = selected,
onClick = { navigateTo(navController, screen.route) }
Expand All @@ -67,16 +98,19 @@ fun RowScope.BottomBarItem(
fun NavHost(
navController: NavHostController,
padding: PaddingValues,
viewModel: BottomBarViewModel
appsViewModel: AppsViewModel,
updatesViewModel: UpdatesViewModel,
searchViewModel: SearchViewModel,
settingsViewModel: SettingsViewModel = koinViewModel()
) = NavHost(
navController = navController,
startDestination = Screen.Apps.route,
modifier = Modifier.padding(padding)
) {
composable(Screen.Apps.route) { AppsScreen(viewModel) }
composable(Screen.Search.route) { SearchScreen(viewModel) }
composable(Screen.Updates.route) { UpdatesScreen(viewModel) }
composable(Screen.Settings.route) { SettingsScreen() }
composable(Screen.Apps.route) { AppsScreen(appsViewModel) }
composable(Screen.Search.route) { SearchScreen(searchViewModel) }
composable(Screen.Updates.route) { UpdatesScreen(updatesViewModel) }
composable(Screen.Settings.route) { SettingsScreen(settingsViewModel) }
}

fun navigateTo(navController: NavController, route: String) = navController.navigate(route) {
Expand Down
5 changes: 0 additions & 5 deletions app/src/main/java/com/apkupdater/ui/screen/SearchScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,19 @@ import com.apkupdater.ui.component.DefaultErrorScreen
import com.apkupdater.ui.component.DefaultLoadingScreen
import com.apkupdater.ui.component.InstalledGrid
import com.apkupdater.ui.component.SearchItem
import com.apkupdater.viewmodel.BottomBarViewModel
import com.apkupdater.viewmodel.SearchViewModel
import org.koin.androidx.compose.koinViewModel

@Composable
fun SearchScreen(
barViewModel: BottomBarViewModel,
viewModel: SearchViewModel = koinViewModel()
) = Column {
SearchTopBar(viewModel)
viewModel.state().collectAsStateWithLifecycle().value.onError {
barViewModel.changeSearchBadge("!")
DefaultErrorScreen()
}.onSuccess {
barViewModel.changeSearchBadge(it.updates.count().toString())
SearchScreenSuccess(it)
}.onLoading {
barViewModel.changeSearchBadge("")
DefaultLoadingScreen()
}
}
Expand Down
7 changes: 1 addition & 6 deletions app/src/main/java/com/apkupdater/ui/screen/UpdatesScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,19 @@ import com.apkupdater.ui.component.DefaultLoadingScreen
import com.apkupdater.ui.component.InstalledGrid
import com.apkupdater.ui.component.RefreshIcon
import com.apkupdater.ui.component.UpdateItem
import com.apkupdater.viewmodel.BottomBarViewModel
import com.apkupdater.viewmodel.UpdatesViewModel
import org.koin.androidx.compose.koinViewModel


@Composable
fun UpdatesScreen(
barViewModel: BottomBarViewModel,
viewModel: UpdatesViewModel = koinViewModel()
viewModel: UpdatesViewModel = koinViewModel()
) {
viewModel.state().collectAsStateWithLifecycle().value.onLoading {
barViewModel.changeUpdatesBadge("")
UpdatesScreenLoading()
}.onError {
barViewModel.changeUpdatesBadge("!")
UpdatesScreenError()
}.onSuccess {
barViewModel.changeUpdatesBadge(it.updates.count().toString())
UpdatesScreenSuccess(viewModel, it.updates)
}
}
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/com/apkupdater/viewmodel/AppsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,26 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.sync.Mutex

class AppsViewModel(
private val mainViewModel: MainViewModel,
private val repository: AppsRepository,
private val prefs: Prefs
) : ViewModel() {

private val mutex = Mutex()
private val state = MutableStateFlow<AppsUiState>(AppsUiState.Loading)

init { refresh() }

fun state(): StateFlow<AppsUiState> = state

fun refresh(load: Boolean = true) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) {
if (load) state.value = AppsUiState.Loading
mainViewModel.changeAppsBadge("")
repository.getApps().collect {
it.onSuccess { apps ->
state.value = AppsUiState.Success(apps, prefs.excludeSystem.get())
mainViewModel.changeAppsBadge(apps.size.toString())
}.onFailure { ex ->
state.value = AppsUiState.Error
mainViewModel.changeAppsBadge("!")
Log.e("InstalledViewModel", "Error getting apps.", ex)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.apkupdater.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.apkupdater.data.ui.Screen
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

class BottomBarViewModel : ViewModel() {
class MainViewModel : ViewModel() {

val screens = listOf(Screen.Apps, Screen.Search, Screen.Updates, Screen.Settings)

Expand All @@ -15,6 +17,19 @@ class BottomBarViewModel : ViewModel() {
Screen.Settings.route to ""
))

val isRefreshing = MutableStateFlow(false)

fun refresh(
appsViewModel: AppsViewModel,
updatesViewModel: UpdatesViewModel
) = viewModelScope.launch {
isRefreshing.value = true
appsViewModel.refresh(false)
updatesViewModel.refresh(false).invokeOnCompletion {
isRefreshing.value = false
}
}

fun changeSearchBadge(number: String) = changeBadge(Screen.Search.route, number)

fun changeAppsBadge(number: String) = changeBadge(Screen.Apps.route, number)
Expand Down
8 changes: 5 additions & 3 deletions app/src/main/java/com/apkupdater/viewmodel/SearchViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.apkupdater.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.apkupdater.data.ui.SearchUiState
import com.apkupdater.prefs.Prefs
import com.apkupdater.repository.ApkMirrorRepository
import com.apkupdater.util.launchWithMutex
import kotlinx.coroutines.Dispatchers
Expand All @@ -12,8 +11,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.sync.Mutex

class SearchViewModel(
private val apkMirrorRepository: ApkMirrorRepository,
private val prefs: Prefs
private val mainViewModel: MainViewModel,
private val apkMirrorRepository: ApkMirrorRepository
) : ViewModel() {

private val mutex = Mutex()
Expand All @@ -23,10 +22,13 @@ class SearchViewModel(

fun search(text: String) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) {
state.value = SearchUiState.Loading
mainViewModel.changeSearchBadge("")
apkMirrorRepository.search(text).collect {
it.onSuccess { apps ->
state.value = SearchUiState.Success(apps)
mainViewModel.changeSearchBadge(apps.size.toString())
}.onFailure {
mainViewModel.changeSearchBadge("!")
state.value = SearchUiState.Error
}
}
Expand Down
Loading

0 comments on commit 20d25f9

Please sign in to comment.