Skip to content
Open
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
3 changes: 3 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ kotlin {
implementation(libs.voyager.navigator)
implementation(libs.voyager.navigator.transitions)
implementation(libs.voyager.screenmodel)
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.kermit)
// sql-delight runtime
implementation(libs.runtime.v200)
// flows support for sql-delight
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package app.academy

import android.app.Application
import app.academy.di.AppModule
import app.academy.di.getKoinModule
import org.koin.core.context.startKoin

class NotesAppApplication : Application() {
val appModule by lazy { AppModule(this) }

override fun onCreate() {
super.onCreate()
startKoin {
modules(getKoinModule(appModule = appModule))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import app.academy.model.Note
import app.academy.model.toNote
import app.academy.model.toSavedNoteEntity
import app.academy.notes.database.SavedNoteEntity
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

Expand All @@ -14,6 +15,7 @@ class DefaultNotesRepository(

override val savedNotesStream: Flow<List<Note>> =
localNotesDataSource.savedNotesStream.map { savedNoteEntities ->
Logger.d("NotesCRUD") {"savednotes are ::$savedNoteEntities"}
savedNoteEntities.map { savedNoteEntity: SavedNoteEntity -> savedNoteEntity.toNote() }
}

Expand Down
8 changes: 8 additions & 0 deletions composeApp/src/commonMain/kotlin/app/academy/di/KoinModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.academy.di

import org.koin.dsl.module

fun getKoinModule(appModule: AppModule) = module {
single { appModule.provideNotesRepository() }
single { appModule.provideDispatchersProvider() }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package app.academy.domain

import app.academy.model.Note

/**
* The UI state for the home screen.
*/
data class HomeScreenUiState(
val isLoadingSavedNotes: Boolean = false,
val isLoadingSearchResults: Boolean = false,
val savedNotes: List<Note> = emptyList(),
val searchResults: List<Note> = emptyList()
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,99 @@
package app.academy.domain

import app.academy.data.NotesRepository
import app.academy.di.AppModule
import app.academy.model.Note
import app.academy.utils.asNativeStateFlow
import cafe.adriel.voyager.core.model.ScreenModel
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

class HomeViewModel(
coroutineScope: CoroutineScope?,
appModule: AppModule
) : ScreenModel {

private val viewModelScope = coroutineScope ?: MainScope()
private val notesRepository: NotesRepository = appModule.provideNotesRepository()
private val defaultDispatcher: CoroutineDispatcher =
appModule.provideDispatchersProvider().defaultDispatcher

/**
* The current [HomeScreenUiState]
*/
private val _uiState = MutableStateFlow(HomeScreenUiState(isLoadingSavedNotes = true))
val uiState = _uiState.asNativeStateFlow()

private val currentSearchText = MutableStateFlow("")
private var recentlyDeletedNote: Note? = null

init {
notesRepository.savedNotesStream.onEach { savedNotesList ->
Logger.d("NotesCRUD") { "Update: $savedNotesList" }
_uiState.update { it.copy(isLoadingSavedNotes = false, savedNotes = savedNotesList) }
}.launchIn(viewModelScope)

combine(
uiState,
currentSearchText.debounce(200)
) { updatedUiState, searchText ->
val savedNotes = updatedUiState.savedNotes
// filtering notes with titles containing the search text
_uiState.update { it.copy(isLoadingSearchResults = true) }
val notesWithTitleContainingSearchText = savedNotes.filter {
it.title.contains(searchText, ignoreCase = true)
}
_uiState.update { it.copy(searchResults = notesWithTitleContainingSearchText) }

// filtering notes with the content containing the search text.
// Since this is slower than the previous filtering operation above,
// update ui state independently
val notesWithContentContainingSearchText = savedNotes.filter {
it.content.contains(searchText, ignoreCase = true)
}
_uiState.update {
it.copy(searchResults = (it.searchResults + notesWithContentContainingSearchText).distinct())
}
_uiState.update {
it.copy(isLoadingSearchResults = false)
}

}.flowOn(defaultDispatcher).launchIn(viewModelScope)
}

/**
* Searches for notes with titles or contents containing the given [searchText].
*/
fun search(searchText: String) {
currentSearchText.value = searchText
}

fun fetchNotes() {
notesRepository.savedNotesStream.onEach { savedNotesList ->
Logger.d("NotesCRUD") { "Update: $savedNotesList" }
_uiState.update { it.copy(isLoadingSavedNotes = false, savedNotes = savedNotesList) }
}.launchIn(viewModelScope)
}

fun deleteNote(note: Note) {
viewModelScope.launch {
notesRepository.deleteNote(note)
recentlyDeletedNote = note
}
}

fun restoreRecentlyDeletedNote() {
viewModelScope.launch { recentlyDeletedNote?.let { notesRepository.saveNote(it) } }
}

class HomeViewModel : ScreenModel {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package app.academy.domain

import app.academy.data.NotesRepository
import app.academy.model.Note
import app.academy.utils.UUID
import app.academy.utils.asNativeStateFlow
import cafe.adriel.voyager.core.model.ScreenModel
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock

@OptIn(FlowPreview::class)
class NoteDetailViewModel(
private val currentNoteId: String?,
private val notesRepository: NotesRepository,
coroutineScope: CoroutineScope?
) : ScreenModel {

private val viewModelScope =
coroutineScope ?: CoroutineScope(Dispatchers.Main + SupervisorJob())

private val _titleText = MutableStateFlow("")
val titleTextStream = _titleText.asNativeStateFlow()

private val _contentText = MutableStateFlow("")
val contentTextStream = _contentText.asNativeStateFlow()

private lateinit var currentNote: Note


init {
viewModelScope.launch {
currentNote = getOrCreateNoteWithId(currentNoteId ?: UUID.randomUUIDString())
_titleText.update { currentNote.title }
_contentText.update { currentNote.content }
val debounceTimeout = 200L
combine(
_titleText.debounce(timeoutMillis = debounceTimeout).distinctUntilChanged(),
_contentText.debounce(timeoutMillis = debounceTimeout).distinctUntilChanged()
) { updatedTitleText, updatedContentText ->
// remove note from database, if note is blank
if (updatedTitleText.isBlank() && updatedContentText.isBlank()) {
notesRepository.deleteNote(currentNote)
return@combine
}
val updatedNote = currentNote.copy(
title = updatedTitleText,
content = updatedContentText
)
Logger.d("NotesCRUD") { "Saving Note $updatedNote" }
notesRepository.saveNote(updatedNote)
}.launchIn(this)
}
}

fun onTitleChange(newTitle: String) {
_titleText.update { newTitle }
}

fun onContentChange(newContent: String) {
_contentText.update { newContent }
}

fun clear() {
_titleText.update { "" }
_contentText.update { "" }
}

private suspend fun getOrCreateNoteWithId(id: String): Note {
val savedNotes = notesRepository.savedNotesStream.first()
val matchingNote = savedNotes.firstOrNull { it.id == id }
if (matchingNote != null) return matchingNote
return Note(
id = id,
title = "",
content = "",
createdAtTimestampMillis = Clock.System.now().toEpochMilliseconds(),
isDeleted = false
)
}

}
Loading