Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement navigation drawer #81

Merged
merged 3 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import com.github.plplmax.notifications.data.Errors
import com.github.plplmax.notifications.data.user.Users
import com.github.plplmax.notifications.data.worker.ScheduleWorker
import com.github.plplmax.notifications.ui.navigation.Routes
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit

Expand Down Expand Up @@ -44,7 +48,7 @@ class MainViewModel(
state = UiState.Initial(login)
} else {
startDestination = Routes.Notifications
state = UiState.Success
state = UiState.Success(login)
}
}
}
Expand All @@ -61,22 +65,37 @@ class MainViewModel(
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

state = try {
try {
workManager.enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<ScheduleWorker>(
30, TimeUnit.MINUTES
).setConstraints(constraints).build()
).await()
UiState.Success
} catch (e: Exception) {
UiState.Failure(R.string.something_went_wrong)
initState()
} catch (_: Exception) {
currentCoroutineContext().ensureActive()
state = UiState.Failure(R.string.something_went_wrong)
}
}
}
}

fun signOut(): Deferred<Boolean> {
return viewModelScope.async {
try {
workManager.cancelUniqueWork(WORK_NAME).await()
users.signOut()
initState()
true
} catch (_: Exception) {
currentCoroutineContext().ensureActive()
false
}
}
}

private fun stateForError(error: Throwable): UiState {
return when (val stringRes = stringResourceForError(error)) {
R.string.check_internet_connection -> UiState.Failure(stringRes, showSnackbar = true)
Expand Down Expand Up @@ -112,6 +131,6 @@ fun <T : ViewModel> T.createFactory(): ViewModelProvider.Factory {
sealed class UiState {
class Initial(val login: String = "") : UiState()
object Loading : UiState()
object Success : UiState()
class Success(val login: String = "") : UiState()
class Failure(@StringRes val id: Int, val showSnackbar: Boolean = false) : UiState()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package com.github.plplmax.notifications.data.user
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.github.plplmax.notifications.data.database.Database
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class LocalUsers(
context: Context,
private val origin: Users,
private val database: Database,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : Users by origin {
private val prefs: SharedPreferences =
Expand All @@ -35,6 +37,15 @@ class LocalUsers(
prefs.edit { putString(LOGIN_KEY, login) }
}

override suspend fun signOut() {
withContext(dispatcher) {
database.instance().use { realm ->
realm.executeTransaction { it.deleteAll() }
}
deleteId()
}
}

companion object {
private const val PREFS_NAME = "user"
private const val LOGIN_KEY = "login"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,16 @@ class LoggedUsers(
}
}
}

override suspend fun signOut() {
withContext(dispatcher) {
try {
origin.signOut()
} catch (e: Exception) {
currentCoroutineContext().ensureActive()
Timber.e(e)
throw e
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ class RemoteUsers(
override suspend fun saveLogin(login: String) {
// do nothing
}

override suspend fun signOut() {
// do nothing
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ interface Users {
suspend fun deleteId()
suspend fun login(): String
suspend fun saveLogin(login: String)
suspend fun signOut()
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Dependencies(context: Context) {
LocalUsers(
context,
RemoteUsers(httpClient),
database
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
package com.github.plplmax.notifications.ui

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
import com.github.plplmax.notifications.MainViewModel
import com.github.plplmax.notifications.ui.navigation.AppNavHost
import com.github.plplmax.notifications.ui.navigation.AppNavigationDrawer
import com.github.plplmax.notifications.ui.snackbar.LocalSnackbarState
import kotlinx.coroutines.launch

@Composable
fun MainScreen(viewModel: MainViewModel) {
val navController = rememberNavController()
Scaffold(
snackbarHost = { SnackbarHost(hostState = LocalSnackbarState.current) }
) { padding ->
AppNavHost(
viewModel = viewModel,
modifier = Modifier.padding(padding),
navController = navController
)
val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
AppNavigationDrawer(
viewModel = viewModel,
navController = navController,
drawerState = drawerState
) {
Scaffold(
snackbarHost = { SnackbarHost(hostState = LocalSnackbarState.current) }
) { padding ->
AppNavHost(
viewModel = viewModel,
modifier = Modifier.padding(padding),
navController = navController,
showNavigation = { scope.launch { drawerState.open() } }
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

@Composable
fun LoginScreen(viewModel: MainViewModel, onSuccess: () -> Unit = {}) {
fun LoginScreen(viewModel: MainViewModel) {
Box(
modifier = Modifier
.fillMaxSize()
Expand All @@ -72,7 +72,6 @@ fun LoginScreen(viewModel: MainViewModel, onSuccess: () -> Unit = {}) {
LoginContent(
state = viewModel.state,
signIn = viewModel::signIn,
onSuccess = onSuccess,
clearError = viewModel::clearError,
needPermission = viewModel::needRequestNotificationsPermission
)
Expand All @@ -98,7 +97,6 @@ private fun LoginImage() {
private fun LoginContent(
state: UiState = UiState.Initial(),
signIn: (String) -> Unit = {},
onSuccess: () -> Unit = {},
clearError: () -> Unit = {},
needPermission: () -> Boolean = { false }
) {
Expand All @@ -123,7 +121,6 @@ private fun LoginContent(
else -> login
}

if (state is UiState.Success) onSuccess()
if (needShowError(state, withSnackbar = true))
snackbarHostState.showSnackbar(
message = context.getString(state.id),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import com.github.plplmax.notifications.ui.welcome.WelcomeScreen
fun AppNavHost(
viewModel: MainViewModel,
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController()
navController: NavHostController = rememberNavController(),
showNavigation: () -> Unit = {}
) {
val startDestination = when (val destination = viewModel.startDestination) {
is Routes.Undefined -> return
Expand All @@ -30,18 +31,15 @@ fun AppNavHost(
}
}
}
composable(Routes.Login.route) {
LoginScreen(viewModel) {
navController.navigate(Routes.Notifications.route) {
popUpTo(Routes.Login.route) { inclusive = true }
}
}
}
composable(Routes.Login.route) { LoginScreen(viewModel) }
composable(Routes.Notifications.route) {
NotificationScreen {
val route = Routes.Diff.route.replace("{id}", it)
navController.navigate(route)
}
NotificationScreen(
onSelect = {
val route = Routes.Diff.route.replace("{id}", it)
navController.navigate(route)
},
showNavigation = showNavigation
)
}
composable(Routes.Diff.route) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")!!
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.github.plplmax.notifications.ui.navigation

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.github.plplmax.notifications.MainViewModel
import com.github.plplmax.notifications.R
import com.github.plplmax.notifications.UiState
import com.github.plplmax.notifications.ui.alert.ProblemWithNotificationsDialog
import com.github.plplmax.notifications.ui.snackbar.LocalSnackbarState
import kotlinx.coroutines.launch

@Composable
fun AppNavigationDrawer(
viewModel: MainViewModel,
navController: NavController,
drawerState: DrawerState,
content: @Composable () -> Unit
) {
val scope = rememberCoroutineScope()
val backStack by navController.currentBackStackEntryAsState()
val gesturesEnabled by remember {
derivedStateOf { backStack?.destination?.route == Routes.Notifications.route }
}
val snackbarState = LocalSnackbarState.current
val context = LocalContext.current
ModalNavigationDrawer(
drawerContent = {
DrawerContent(
login = (viewModel.state as? UiState.Success)?.login ?: "",
currentRoute = backStack?.destination?.route ?: "",
openNotifications = {
scope.launch {
navController.navigate(Routes.Notifications.route) {
launchSingleTop = true
}
drawerState.close()
}
},
onSignOut = {
scope.launch {
val signedOut = viewModel.signOut()
drawerState.close()
if (!signedOut.await()) {
val message = context.getString(R.string.something_went_wrong)
snackbarState.showSnackbar(message)
}
}
}
)
},
drawerState = drawerState,
gesturesEnabled = gesturesEnabled,
content = content
)
}

@Composable
private fun DrawerContent(
login: String,
currentRoute: String,
openNotifications: () -> Unit,
onSignOut: () -> Unit
) {
var showHelpDialog by remember { mutableStateOf(false) }
ModalDrawerSheet {
Text(
text = login,
modifier = Modifier.padding(horizontal = 32.dp, vertical = 24.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Divider(color = MaterialTheme.colorScheme.background, thickness = 2.dp)
Spacer(modifier = Modifier.height(12.dp))
NavigationDrawerItem(
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
label = { Text(text = stringResource(R.string.notifications)) },
selected = currentRoute == Routes.Notifications.route,
onClick = openNotifications,
icon = {
Icon(imageVector = Icons.Default.Notifications, contentDescription = null)
}
)
NavigationDrawerItem(
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding),
label = { Text(text = stringResource(R.string.sign_out)) },
selected = false,
onClick = onSignOut,
icon = {
Icon(imageVector = Icons.Default.ExitToApp, contentDescription = null)
}
)
Spacer(modifier = Modifier.height(6.dp))
Divider(color = MaterialTheme.colorScheme.background, thickness = 2.dp)
TextButton(
modifier = Modifier.padding(start = 18.dp),
onClick = { showHelpDialog = true }
) {
Text(text = stringResource(R.string.have_problems_getting_notifications))
}
ProblemWithNotificationsDialog(
visible = showHelpDialog,
onDismissRequest = { showHelpDialog = false },
onConfirm = { showHelpDialog = false }
)
}
}
Loading