Skip to content
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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.serialization)
}

android {
Expand Down Expand Up @@ -78,6 +79,7 @@ dependencies {

// Navigation
implementation(libs.navigation.compose)
implementation(libs.kotlinx.serialization.json)

// Local Unit Tests
testImplementation(libs.androidx.test.core)
Expand Down
30 changes: 8 additions & 22 deletions app/src/main/java/com/bober/notesapp/presentation/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Screen.NotesScreen>{
NoteScreen(navController = navController)
}
composable(route = Screen.AddEditNoteScreen.route + "?noteId={noteId}&noteColor={noteColor}",
arguments = listOf(
navArgument(
name = "noteId"
) {
type = NavType.IntType
defaultValue = -1
},
navArgument(
name = "noteColor"
) {
type = NavType.IntType
defaultValue = -1
}
composable<Screen.AddEditNoteScreen> { backStackEntry ->
val args = backStackEntry.toRoute<Screen.AddEditNoteScreen>()
AddEditNoteScreen(
navController = navController,
noteColor = args.noteColor ?: -1
)
) {
val color = it.arguments?.getInt("noteColor") ?: -1
AddEditNoteScreen(navController = navController, noteColor = color)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -30,59 +32,64 @@ class AddEditNoteViewModel @Inject constructor(
private val _noteContent = mutableStateOf(NoteTextFieldState(hint = "Enter content..."))
val noteContent: State<NoteTextFieldState> = _noteContent

private val _noteColor= mutableStateOf(Note.noteColors.random())
private val _noteColor = mutableStateOf(Note.noteColors.random())
val noteColor: State<Int> = _noteColor

private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()

init {
savedStateHandle.get<Int>("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<Screen.AddEditNoteScreen>()
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 {
Expand All @@ -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"
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down Expand Up @@ -113,7 +114,7 @@ fun NoteScreenContent(
visible = state.isOrderSectionVisible,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
) {
OrderSection(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -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}&noteColor=${note.color}"
Screen.AddEditNoteScreen(
noteId = note.id,
noteColor = note.color
)
)
},
onDeleteNote = {
Expand Down Expand Up @@ -184,7 +188,8 @@ fun NoteScreenPreview() {
title = "Preview Note 2",
content = "Content 2",
timestamp = 2L,
color = 0xFFE7ED9B.toInt())
color = 0xFFE7ED9B.toInt()
)
),
isOrderSectionVisible = true
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Note> = emptyList(),
val noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending),
Expand Down
14 changes: 11 additions & 3 deletions app/src/main/java/com/bober/notesapp/presentation/util/Screen.kt
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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" }
Expand All @@ -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" }
Loading