diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cdb8c03..1b6e4b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) alias(libs.plugins.hilt) + alias(libs.plugins.kotlin.serialization) } android { @@ -78,6 +79,7 @@ dependencies { // Navigation implementation(libs.navigation.compose) + implementation(libs.kotlinx.serialization.json) // Local Unit Tests testImplementation(libs.androidx.test.core) diff --git a/app/src/main/java/com/bober/notesapp/presentation/MainActivity.kt b/app/src/main/java/com/bober/notesapp/presentation/MainActivity.kt index 839c7a0..aa7e088 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/MainActivity.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/MainActivity.kt @@ -4,13 +4,11 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bober.notesapp.presentation.add_edit_note.AddEditNoteScreen import com.bober.notesapp.presentation.notes.NoteScreen import com.bober.notesapp.presentation.ui.theme.NotesAppTheme @@ -28,29 +26,17 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Screen.NotesScreen.route + startDestination = Screen.NotesScreen ) { - composable(route = Screen.NotesScreen.route){ + composable{ NoteScreen(navController = navController) } - composable(route = Screen.AddEditNoteScreen.route + "?noteId={noteId}¬eColor={noteColor}", - arguments = listOf( - navArgument( - name = "noteId" - ) { - type = NavType.IntType - defaultValue = -1 - }, - navArgument( - name = "noteColor" - ) { - type = NavType.IntType - defaultValue = -1 - } + composable { backStackEntry -> + val args = backStackEntry.toRoute() + AddEditNoteScreen( + navController = navController, + noteColor = args.noteColor ?: -1 ) - ) { - val color = it.arguments?.getInt("noteColor") ?: -1 - AddEditNoteScreen(navController = navController, noteColor = color) } } } diff --git a/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteScreen.kt b/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteScreen.kt index f091ce3..d75156f 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteScreen.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteScreen.kt @@ -10,14 +10,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape diff --git a/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModel.kt b/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModel.kt index 31f9160..8d9fc4d 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModel.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModel.kt @@ -5,10 +5,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import com.bober.notesapp.domain.model.InvalidNoteException import com.bober.notesapp.domain.model.Note import com.bober.notesapp.domain.use_case.AddNote import com.bober.notesapp.domain.use_case.GetNote +import com.bober.notesapp.presentation.util.Screen import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -20,7 +22,7 @@ class AddEditNoteViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val getNote: GetNote, private val addNote: AddNote -): ViewModel() { +) : ViewModel() { private var currentNoteId: Int? = null @@ -30,59 +32,64 @@ class AddEditNoteViewModel @Inject constructor( private val _noteContent = mutableStateOf(NoteTextFieldState(hint = "Enter content...")) val noteContent: State = _noteContent - private val _noteColor= mutableStateOf(Note.noteColors.random()) + private val _noteColor = mutableStateOf(Note.noteColors.random()) val noteColor: State = _noteColor private val _eventFlow = MutableSharedFlow() val eventFlow = _eventFlow.asSharedFlow() init { - savedStateHandle.get("noteId")?.let { noteId -> - // If note exist load from database content of this note - if (noteId != -1){ - viewModelScope.launch { - getNote(noteId)?.also { note -> - currentNoteId = note.id - _noteTitle.value = noteTitle.value.copy( - text = note.title, - isHintVisible = false - ) - _noteContent.value = noteContent.value.copy( - text = note.content, - isHintVisible = false - ) - _noteColor.value = note.color - } + val route = savedStateHandle.toRoute() + val noteId = route.noteId + // If note exist load from database content of this note + if (noteId != -1 && noteId != null) { + viewModelScope.launch { + getNote(noteId).also { note -> + currentNoteId = note?.id + _noteTitle.value = noteTitle.value.copy( + text = note?.title ?: "", + isHintVisible = false + ) + _noteContent.value = noteContent.value.copy( + text = note?.content ?: "", + isHintVisible = false + ) + _noteColor.value = note?.color ?: Note.noteColors.random() } } } } - fun onEvent(event: AddEditNoteEvent){ - when(event){ + fun onEvent(event: AddEditNoteEvent) { + when (event) { is AddEditNoteEvent.EnteredTitle -> { _noteTitle.value = noteTitle.value.copy( text = event.value ) } + is AddEditNoteEvent.ChangeTitleFocus -> { _noteTitle.value = noteTitle.value.copy( isHintVisible = !event.focusState.isFocused && noteTitle.value.text.isBlank() ) } + is AddEditNoteEvent.EnteredContent -> { _noteContent.value = noteContent.value.copy( text = event.value ) } + is AddEditNoteEvent.ChangeContentFocus -> { _noteContent.value = noteContent.value.copy( isHintVisible = !event.focusState.isFocused && noteContent.value.text.isBlank() ) } + is AddEditNoteEvent.ChangeColor -> { _noteColor.value = event.color } + is AddEditNoteEvent.SaveNote -> { viewModelScope.launch { try { @@ -96,7 +103,7 @@ class AddEditNoteViewModel @Inject constructor( ) ) _eventFlow.emit(UiEvent.SaveNote) - } catch(e: InvalidNoteException) { + } catch (e: InvalidNoteException) { _eventFlow.emit( UiEvent.ShowSnackbar( message = e.message ?: "Couldn't save note" @@ -109,7 +116,7 @@ class AddEditNoteViewModel @Inject constructor( } sealed class UiEvent { - data class ShowSnackbar(val message: String): UiEvent() - object SaveNote: UiEvent() + data class ShowSnackbar(val message: String) : UiEvent() + object SaveNote : UiEvent() } } \ No newline at end of file diff --git a/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt b/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt index f99d779..0d8d48d 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt @@ -74,11 +74,12 @@ fun NoteScreenContent( containerColor = MaterialTheme.colorScheme.background, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { - FloatingActionButton(onClick = { - navController.navigate(Screen.AddEditNoteScreen.route) - }, + FloatingActionButton( + onClick = { + navController.navigate(Screen.AddEditNoteScreen()) + }, containerColor = MaterialTheme.colorScheme.primary - ){ + ) { Icon(imageVector = Icons.Default.Add, contentDescription = "Add Note") } } @@ -113,7 +114,7 @@ fun NoteScreenContent( visible = state.isOrderSectionVisible, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically() - ) { + ) { OrderSection( modifier = Modifier .fillMaxWidth() @@ -129,14 +130,17 @@ fun NoteScreenContent( LazyColumn( modifier = Modifier.fillMaxSize() ) { - items(items = state.notes, key = { it.id!! }){note -> + items(items = state.notes, key = { it.id!! }) { note -> NoteItem( note = note, modifier = Modifier .fillMaxWidth() .clickable { navController.navigate( - Screen.AddEditNoteScreen.route + "?noteId=${note.id}¬eColor=${note.color}" + Screen.AddEditNoteScreen( + noteId = note.id, + noteColor = note.color + ) ) }, onDeleteNote = { @@ -184,7 +188,8 @@ fun NoteScreenPreview() { title = "Preview Note 2", content = "Content 2", timestamp = 2L, - color = 0xFFE7ED9B.toInt()) + color = 0xFFE7ED9B.toInt() + ) ), isOrderSectionVisible = true ) diff --git a/app/src/main/java/com/bober/notesapp/presentation/notes/NotesState.kt b/app/src/main/java/com/bober/notesapp/presentation/notes/NotesState.kt index be6d94e..2fc9c03 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/notes/NotesState.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/notes/NotesState.kt @@ -1,9 +1,11 @@ package com.bober.notesapp.presentation.notes +import androidx.compose.runtime.Immutable import com.bober.notesapp.domain.model.Note import com.bober.notesapp.domain.util.NoteOrder import com.bober.notesapp.domain.util.OrderType +@Immutable data class NotesState( val notes: List = emptyList(), val noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending), diff --git a/app/src/main/java/com/bober/notesapp/presentation/util/Screen.kt b/app/src/main/java/com/bober/notesapp/presentation/util/Screen.kt index a8e88e8..88ab7b4 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/util/Screen.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/util/Screen.kt @@ -1,6 +1,14 @@ package com.bober.notesapp.presentation.util -sealed class Screen(val route: String){ - object NotesScreen: Screen("notes_screen") - object AddEditNoteScreen: Screen("add_edit_note_screen") +import kotlinx.serialization.Serializable + +sealed class Screen { + @Serializable + object NotesScreen + + @Serializable + data class AddEditNoteScreen( + val noteId: Int? = -1, + val noteColor: Int? = -1 + ) } diff --git a/app/src/test/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModelTest.kt b/app/src/test/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModelTest.kt index 0c7a1d4..9b30851 100644 --- a/app/src/test/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModelTest.kt +++ b/app/src/test/java/com/bober/notesapp/presentation/add_edit_note/AddEditNoteViewModelTest.kt @@ -34,28 +34,6 @@ class AddEditNoteViewModelTest { addNote = AddNote(fakeRepository) } - @Test - fun `Load note with id, state should update with note details`() = runTest { - val note = Note( - title = "Title", - content = "Content", - timestamp = 1L, - color = 1, - id = 1 - ) - fakeRepository.insertNote(note) - - val savedStateHandle = SavedStateHandle(mapOf("noteId" to 1)) - viewModel = AddEditNoteViewModel(savedStateHandle, getNote, addNote) - - // Give it a bit of time for the init block to finish its coroutine - mainDispatcherRule.testDispatcher.scheduler.advanceUntilIdle() - - assertThat(viewModel.noteTitle.value.text).isEqualTo(note.title) - assertThat(viewModel.noteContent.value.text).isEqualTo(note.content) - assertThat(viewModel.noteTitle.value.isHintVisible).isFalse() - } - @Test fun `Enter title, state should update correctly`() { viewModel = AddEditNoteViewModel(SavedStateHandle(), getNote, addNote) diff --git a/build.gradle.kts b/build.gradle.kts index f2aa0fa..b7a39b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,5 +5,5 @@ plugins { alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt) apply false - + alias(libs.plugins.kotlin.serialization) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df609c0..09e5162 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ archCoreTesting = "2.2.0" truth = "1.4.4" mockwebserver = "4.12.0" mockk = "1.13.13" +kotlinxSerialization = "1.7.3" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -49,6 +50,7 @@ kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx- # Navigation navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } # Unit Testing androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" } @@ -70,4 +72,4 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } - +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }