Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
87 changes: 17 additions & 70 deletions app/src/main/java/com/washingtondcsquad/tudee/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -14,60 +15,42 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.BottomAppBarDefaults.windowInsets
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.washingtondcsquad.tudee.data.localSource.TudeeDataBase
import com.washingtondcsquad.tudee.domain.entity.Category
import com.washingtondcsquad.tudee.domain.services.AppPreferencesService
import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesScreen
import com.washingtondcsquad.tudee.presentation.components.SnackBarCard
import com.washingtondcsquad.tudee.presentation.components.bottom_nav_bar.TudeeNavigationBar
import com.washingtondcsquad.tudee.presentation.components.bottom_nav_bar.bottomNavBarRoutes
import com.washingtondcsquad.tudee.presentation.components.bottom_nav_bar.navBarItemsList
import com.washingtondcsquad.tudee.presentation.components.snack_bar.ObserveAsEvent
import com.washingtondcsquad.tudee.presentation.components.snack_bar.SnackbarController
import com.washingtondcsquad.tudee.presentation.design.AppTheme
import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesScreen
import com.washingtondcsquad.tudee.presentation.features.home.HomeScreen
import com.washingtondcsquad.tudee.presentation.features.onBoarding.OnBoardingScreen
import com.washingtondcsquad.tudee.presentation.features.tasks_screen.TasksScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.washingtondcsquad.tudee.presentation.utils.SnackBarHandler
import org.koin.android.ext.android.inject

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
class MainActivity : ComponentActivity() {
private val appPreferencesService: AppPreferencesService by inject()

private val mainViewModel: MainViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

enableEdgeToEdge()

setContent {
val isDarkMode by appPreferencesService.isDarkModeEnabled()
.collectAsState(initial = false)
val isOnboardingShownFlow = appPreferencesService.hasOnboardingBeenShown()
val isOnboardingShownState by isOnboardingShownFlow.collectAsState(initial = null)
val mainState by mainViewModel.state.collectAsState()

updateAppLocale(mainState)
AppTheme(
useDarkTheme = isDarkMode
useDarkTheme = mainState.isDarkTheme
) {
when (val isOnboardingShown = isOnboardingShownState) {
when (val isOnboardingShown = mainState.hasOnBoardingShown) {
Comment thread
kareem-01 marked this conversation as resolved.
Outdated
null -> {
Box(
modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center
Expand All @@ -81,7 +64,7 @@ class MainActivity : ComponentActivity() {
"home"
} else {
Log.d("sdasdsad", "onCreate: ")
/// createPreDefineCategories()
/// createPreDefineCategories()
"onboarding"
}
val navController = rememberNavController()
Expand Down Expand Up @@ -127,7 +110,7 @@ class MainActivity : ComponentActivity() {
}
}
}
SnackbarHandler()
SnackBarHandler()
}
}
//
Expand Down Expand Up @@ -160,49 +143,13 @@ class MainActivity : ComponentActivity() {
// }
// }
// }
}


@Composable
fun SnackbarHandler() {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
ObserveAsEvent(
flow = SnackbarController.event,
onEvent = { event ->
coroutineScope.launch {
snackbarHostState.currentSnackbarData?.dismiss()
snackbarHostState.showSnackbar(
message = event.message,
duration = SnackbarDuration.Short
)
}
}
)

SnackbarHost(
hostState = snackbarHostState,
snackbar = { snackbarData ->
val (iconRes, iconTint) = when {
snackbarData.visuals.message.contains("success", ignoreCase = true) ->
Pair(R.drawable.checkmark, AppTheme.colors.greenAccent)

snackbarData.visuals.message.contains("error", ignoreCase = true) ->
Pair(R.drawable.checkmark, AppTheme.colors.error)

else ->
Pair(R.drawable.information_diamond, AppTheme.colors.error)
}
private fun updateAppLocale(state: MainState) {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(state.currentAppLocale.toLanguageTag())
)
}
}

SnackBarCard(
message = snackbarData.visuals.message,
icon = painterResource(id = iconRes),
iconTint = iconTint,
iconBackgroundColor = AppTheme.colors.surface,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 48.dp)
)

}
)
}

9 changes: 9 additions & 0 deletions app/src/main/java/com/washingtondcsquad/tudee/MainState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.washingtondcsquad.tudee

import java.util.Locale

data class MainState(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name of this data class shouble be MainActivityScreenState
or MainActivityUiState

but MainState is inConsistent and not the convention for naming screen state

val isDarkTheme: Boolean = false,
val hasOnBoardingShown: Boolean? = null,
Comment thread
kareem-01 marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't see any need of nullable value for hasOnBoardingShow, because it must be initialize when the app start

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree

val currentAppLocale: Locale = Locale.ENGLISH
)
72 changes: 72 additions & 0 deletions app/src/main/java/com/washingtondcsquad/tudee/MainViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.washingtondcsquad.tudee

import androidx.lifecycle.viewModelScope
import com.washingtondcsquad.tudee.domain.services.AppPreferencesService
import com.washingtondcsquad.tudee.presentation.base.BaseViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.Locale

class MainViewModel(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suggest to rename this to MainActivityViewModel

Comment thread
kareem-01 marked this conversation as resolved.
private val appPreferencesService: AppPreferencesService
) : BaseViewModel<MainState>(MainState()) {

init {
getIsDarkTheme()
getOnBoardingState()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as the above method

getAppLocale()
}

private fun getIsDarkTheme() {
Comment thread
kareem-01 marked this conversation as resolved.
tryToExecute(
request = {
appPreferencesService.isDarkModeEnabled()
Comment thread
kareem-01 marked this conversation as resolved.
},
onSuccess = ::onDarkThemeEnabled,
onError = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it always a good practice to handle error state, help in debugging and improve user experience

)
}

private fun onDarkThemeEnabled(isDarkThemeEnabled: Flow<Boolean>) = viewModelScope.launch {
isDarkThemeEnabled.collectLatest {
updateState {
copy(isDarkTheme = it)
}
}
}

private fun getOnBoardingState() {
tryToExecute(
request = {
appPreferencesService.hasOnboardingBeenShown()
},
onSuccess = ::onOnBoardingState,
onError = {}
)
}

private fun onOnBoardingState(hasOnboardingBeenShown: Flow<Boolean>) = viewModelScope.launch {
hasOnboardingBeenShown.collectLatest {
updateState {
copy(hasOnBoardingShown = it)
}
}
}

private fun getAppLocale() {
tryToCollect(
request = {
appPreferencesService.getCurrentLocale()
},
onChange = ::onAppLanguageChanged,
onError = {},
)
}

private fun onAppLanguageChanged(newLocale: Locale) {
updateState {
copy(currentAppLocale = newLocale)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
package com.washingtondcsquad.tudee.data.services

import androidx.appcompat.app.AppCompatDelegate
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import com.washingtondcsquad.tudee.domain.services.AppPreferencesService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.util.Locale

class AppPreferencesServiceImpl(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where isDarkModelEnabled function ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it exists in this file down below

private val dataStore: DataStore<Preferences>
) : AppPreferencesService {

private val _currentLocale = MutableStateFlow(getCurrentAppLocale())
val currentLocale: Flow<Locale> = _currentLocale.asStateFlow()

override suspend fun getCurrentLocale(): Flow<Locale> = currentLocale.distinctUntilChanged()


private fun getCurrentAppLocale(): Locale {
val androidLocale = AppCompatDelegate.getApplicationLocales()[0]
?: Locale.getDefault()
return Locale.Builder()
.setLocale(androidLocale)
.setLanguage(androidLocale.language)
.build()
}

companion object PreferencesKeys {
val HAS_ONBOARDING_BEEN_SHOWN = booleanPreferencesKey("has_onboarding_been_shown")
val DARK_MODE_ENABLED = booleanPreferencesKey("dark_mode_enabled")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
package com.washingtondcsquad.tudee.di

import com.washingtondcsquad.tudee.presentation.features.home.HomeViewModel
import com.washingtondcsquad.tudee.presentation.features.task_details.BottomSheetTaskViewModel
import com.washingtondcsquad.tudee.presentation.features.onBoarding.OnboardingViewModel
import com.washingtondcsquad.tudee.MainViewModel
import com.washingtondcsquad.tudee.presentation.features.add_task.AddTaskViewModel
import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesViewModel
import com.washingtondcsquad.tudee.presentation.features.edit_task.EditTaskViewModel
import com.washingtondcsquad.tudee.presentation.features.home.HomeViewModel
import com.washingtondcsquad.tudee.presentation.features.onBoarding.OnboardingViewModel
import com.washingtondcsquad.tudee.presentation.features.task_details.BottomSheetTaskViewModel
import com.washingtondcsquad.tudee.presentation.features.tasks_screen.TasksViewModel
import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import java.time.LocalDate
import com.washingtondcsquad.tudee.presentation.features.categories.CategoriesViewModel


val viewModelModule = module {
viewModelOf(::CategoriesViewModel)
viewModelOf(::OnboardingViewModel)
viewModelOf(::HomeViewModel)
viewModel{ TasksViewModel(
get(),
get()
) }
viewModel {
TasksViewModel(
get(),
get()
)
}
viewModelOf(::MainViewModel)
viewModel { BottomSheetTaskViewModel(get()) }
viewModel { (taskDate: LocalDate, onCancel: () -> Unit, onActionResult: (success: Boolean, message: String) -> Unit) ->
AddTaskViewModel(
Expand All @@ -32,7 +36,7 @@ val viewModelModule = module {
)
}

viewModel { (taskId:Int , onCancel: () -> Unit , onActionResult: (success: Boolean, message: String) -> Unit) ->
viewModel { (taskId: Int, onCancel: () -> Unit, onActionResult: (success: Boolean, message: String) -> Unit) ->

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we changed type of task id to be TaskId alise name not int

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check domain layer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this concerns the EditTask feature which is out of scope for this pr so I will not be modifying it, as to not affect other's work

EditTaskViewModel(
tasksService = get(),
categoryService = get(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.washingtondcsquad.tudee.domain.services

import kotlinx.coroutines.flow.Flow
import java.util.Locale

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try to search for kotlin version . it's not recommended to use java libraries in domain layer.


interface AppPreferencesService {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment on multiple lines

suspend fun getCurrentLocale(): Flow<Locale>
fun hasOnboardingBeenShown(): Flow<Boolean>
suspend fun setOnboardingShown()
fun isDarkModeEnabled(): Flow<Boolean>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.washingtondcsquad.tudee.presentation.utils

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.washingtondcsquad.tudee.R
import com.washingtondcsquad.tudee.presentation.components.SnackBarCard
import com.washingtondcsquad.tudee.presentation.components.snack_bar.ObserveAsEvent
import com.washingtondcsquad.tudee.presentation.components.snack_bar.SnackbarController
import com.washingtondcsquad.tudee.presentation.design.AppTheme
import kotlinx.coroutines.launch

@Composable
fun SnackBarHandler() {
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
ObserveAsEvent(
flow = SnackbarController.event,
onEvent = { event ->
coroutineScope.launch {
snackBarHostState.currentSnackbarData?.dismiss()
snackBarHostState.showSnackbar(
message = event.message,
duration = SnackbarDuration.Short
)
}
}
)

SnackbarHost(
hostState = snackBarHostState,
snackbar = { snackbarData ->
val (iconRes, iconTint) = when {
snackbarData.visuals.message.contains("success", ignoreCase = true) ->
Pair(R.drawable.checkmark, AppTheme.colors.greenAccent)

snackbarData.visuals.message.contains("error", ignoreCase = true) ->
Pair(R.drawable.checkmark, AppTheme.colors.error)

else ->
Pair(R.drawable.information_diamond, AppTheme.colors.error)
}

SnackBarCard(
message = snackbarData.visuals.message,
icon = painterResource(id = iconRes),
iconTint = iconTint,
iconBackgroundColor = AppTheme.colors.surface,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 48.dp)
)

}
)
}