Skip to content

Commit

Permalink
navigation above MainScreen
Browse files Browse the repository at this point in the history
  • Loading branch information
softartdev committed Sep 8, 2024
1 parent 571dcaf commit 002527a
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ fun App(
SignInScreen(signInViewModel = getViewModel())
}
composable(route = AppNavGraph.Main.name) {
MainScreen(
mainViewModel = getViewModel(),
navController = navController
)
MainScreen(mainViewModel = getViewModel())
}
composable(
route = "${AppNavGraph.Details.name}/{noteId}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.softartdev.notedelight.shared.navigation.AppNavGraph
import com.softartdev.notedelight.shared.db.Note
import com.softartdev.notedelight.shared.db.TestSchema
import com.softartdev.notedelight.shared.presentation.main.MainViewModel
import com.softartdev.notedelight.shared.presentation.main.NoteListResult
Expand All @@ -40,27 +36,15 @@ import notedelight.shared_compose_ui.generated.resources.settings
import org.jetbrains.compose.resources.stringResource

@Composable
fun MainScreen(
mainViewModel: MainViewModel,
navController: NavHostController = rememberNavController()
) {
fun MainScreen(mainViewModel: MainViewModel) {
LaunchedEffect(mainViewModel) {
mainViewModel.updateNotes()
}
val noteListState: State<NoteListResult> = mainViewModel.resultStateFlow.collectAsState()
val noteListState: State<NoteListResult> = mainViewModel.stateFlow.collectAsState()
MainScreen(
noteListState = noteListState,
onItemClicked = { id: Long ->
navController.navigate(route = "${AppNavGraph.Details.name}/$id")
},
onSettingsClick = {
navController.navigate(AppNavGraph.Settings.name)
},
navSignIn = {
navController.navigate(AppNavGraph.SignIn.name) {
popUpTo(AppNavGraph.SignIn.name) { inclusive = true }
}
},
onItemClicked = mainViewModel::onNoteClicked,
onSettingsClick = mainViewModel::onSettingsClicked,
)
}

Expand All @@ -69,7 +53,6 @@ fun MainScreen(
noteListState: State<NoteListResult>,
onItemClicked: (id: Long) -> Unit = {},
onSettingsClick: () -> Unit = {},
navSignIn: () -> Unit = {}
) = Scaffold(
topBar = {
TopAppBar(
Expand All @@ -87,10 +70,14 @@ fun MainScreen(
when (val noteListResult = noteListState.value) {
is NoteListResult.Loading -> Loader()
is NoteListResult.Success -> {
val notes: List<Note> = noteListResult.result
if (notes.isNotEmpty()) NoteList(notes, onItemClicked) else Empty()
when {
noteListResult.result.isNotEmpty() -> NoteList(
noteList = noteListResult.result,
onItemClicked = onItemClicked,
)
else -> Empty()
}
}
is NoteListResult.NavSignIn -> navSignIn()
is NoteListResult.Error -> Error(err = noteListResult.error ?: "Error")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import app.cash.turbine.test
import com.softartdev.notedelight.shared.db.Note
import com.softartdev.notedelight.shared.db.NoteDAO
import com.softartdev.notedelight.shared.db.SafeRepo
import com.softartdev.notedelight.shared.navigation.AppNavGraph
import com.softartdev.notedelight.shared.navigation.Router
import com.softartdev.notedelight.shared.presentation.MainDispatcherRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
Expand All @@ -27,18 +29,19 @@ class MainViewModelTest {
val mainDispatcherRule = MainDispatcherRule()

private val mockSafeRepo = Mockito.mock(SafeRepo::class.java)
private val mockRouter = Mockito.mock(Router::class.java)
private val mockNoteDAO = Mockito.mock(NoteDAO::class.java)
private lateinit var mainViewModel: MainViewModel

@Before
fun setUp() {
Mockito.`when`(mockSafeRepo.noteDAO).thenReturn(mockNoteDAO)
mainViewModel = MainViewModel(mockSafeRepo, mockNoteDAO)
mainViewModel = MainViewModel(mockSafeRepo, mockRouter)
}

@Test
fun success() = runTest {
mainViewModel.resultStateFlow.test {
mainViewModel.stateFlow.test {
assertEquals(NoteListResult.Loading, awaitItem())

val notes = emptyList<Note>()
Expand All @@ -52,20 +55,20 @@ class MainViewModelTest {

@Test
fun navMain() = runTest {
mainViewModel.resultStateFlow.test {
mainViewModel.stateFlow.test {
assertEquals(NoteListResult.Loading, awaitItem())

Mockito.`when`(mockNoteDAO.listFlow).thenReturn(flow { throw SQLiteException() })
mainViewModel.updateNotes()
assertEquals(NoteListResult.NavSignIn, awaitItem())
Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.SignIn.name)

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun error() = runTest {
mainViewModel.resultStateFlow.test {
mainViewModel.stateFlow.test {
assertEquals(NoteListResult.Loading, awaitItem())

Mockito.`when`(mockNoteDAO.listFlow).thenReturn(flow { throw Throwable() })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import app.cash.turbine.test
import com.softartdev.notedelight.shared.presentation.MainDispatcherRule
import com.softartdev.notedelight.shared.StubEditable
import com.softartdev.notedelight.shared.anyObject
import com.softartdev.notedelight.shared.navigation.AppNavGraph
import com.softartdev.notedelight.shared.navigation.Router
import com.softartdev.notedelight.shared.usecase.crypt.CheckPasswordUseCase
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
Expand All @@ -24,43 +26,43 @@ class SignInViewModelTest {
val mainDispatcherRule = MainDispatcherRule()

private val mockCheckPasswordUseCase = Mockito.mock(CheckPasswordUseCase::class.java)
private val mockRouter = Mockito.mock(Router::class.java)

private lateinit var signInViewModel: SignInViewModel

@Before
fun setUp() {
signInViewModel = SignInViewModel(mockCheckPasswordUseCase)
signInViewModel = SignInViewModel(mockCheckPasswordUseCase, mockRouter)
}

@Test
fun showSignInForm() = runTest {
signInViewModel.resultStateFlow.test {
signInViewModel.stateFlow.test {
assertEquals(SignInResult.ShowSignInForm, awaitItem())
cancelAndIgnoreRemainingEvents()
}
}

@Test
fun navMain() = runTest {
signInViewModel.resultStateFlow.test {
signInViewModel.stateFlow.test {
assertEquals(SignInResult.ShowSignInForm, awaitItem())

val pass = StubEditable("pass")
Mockito.`when`(mockCheckPasswordUseCase(pass)).thenReturn(true)
signInViewModel.signIn(pass)
assertEquals(SignInResult.ShowProgress, awaitItem())
assertEquals(SignInResult.NavMain, awaitItem())
Mockito.verify(mockRouter).navigateClearingBackStack(route = AppNavGraph.Main.name)

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun showEmptyPassError() = runTest {
signInViewModel.resultStateFlow.test {
signInViewModel.stateFlow.test {
assertEquals(SignInResult.ShowSignInForm, awaitItem())

signInViewModel.signIn(pass = StubEditable(""))
assertEquals(SignInResult.ShowProgress, awaitItem())
assertEquals(SignInResult.ShowEmptyPassError, awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -69,13 +71,12 @@ class SignInViewModelTest {

@Test
fun showIncorrectPassError() = runTest {
signInViewModel.resultStateFlow.test {
signInViewModel.stateFlow.test {
assertEquals(SignInResult.ShowSignInForm, awaitItem())

val pass = StubEditable("pass")
Mockito.`when`(mockCheckPasswordUseCase(pass)).thenReturn(false)
signInViewModel.signIn(pass)
assertEquals(SignInResult.ShowProgress, awaitItem())
assertEquals(SignInResult.ShowIncorrectPassError, awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -84,15 +85,15 @@ class SignInViewModelTest {

@Test
fun showError() = runTest {
signInViewModel.resultStateFlow.test {
signInViewModel.stateFlow.test {
assertEquals(SignInResult.ShowSignInForm, awaitItem())

val throwable = Throwable()
Mockito.`when`(mockCheckPasswordUseCase(anyObject())).thenThrow(throwable)
signInViewModel.signIn(StubEditable("pass"))
assertEquals(SignInResult.ShowProgress, awaitItem())
assertEquals(SignInResult.ShowError(throwable), awaitItem())

Mockito.verify(mockRouter).navigate(
route = AppNavGraph.ErrorDialog.argRoute(message = throwable.message)
)
cancelAndIgnoreRemainingEvents()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
package com.softartdev.notedelight.shared.presentation.main

import com.softartdev.notedelight.shared.base.BaseStateViewModel
import com.softartdev.notedelight.shared.db.NoteDAO
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.softartdev.notedelight.shared.db.SafeRepo
import com.softartdev.notedelight.shared.navigation.AppNavGraph
import com.softartdev.notedelight.shared.navigation.Router
import io.github.aakira.napier.Napier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch

class MainViewModel(
private val safeRepo: SafeRepo,
private val noteDAO: NoteDAO,
) : BaseStateViewModel<NoteListResult>() {

override val loadingResult: NoteListResult = NoteListResult.Loading
private val router: Router
) : ViewModel() {
private val mutableStateFlow: MutableStateFlow<NoteListResult> = MutableStateFlow(
value = NoteListResult.Loading
)
val stateFlow: StateFlow<NoteListResult> = mutableStateFlow

init {
safeRepo.relaunchListFlowCallback = this::updateNotes
}

fun updateNotes() = launch(
useIdling = false,
flow = safeRepo.noteDAO.listFlow.map(NoteListResult::Success)
)
fun updateNotes() = viewModelScope.launch(Dispatchers.Main) {
safeRepo.noteDAO.listFlow
.onStart { mutableStateFlow.value = NoteListResult.Loading }
.map(transform = NoteListResult::Success)
.onEach(action = mutableStateFlow::emit)
.flowOn(Dispatchers.IO)
.catch { throwable ->
Napier.e("", throwable)
val errorName: String = throwable::class.simpleName.orEmpty()
if (errorName.contains("SQLite")) {
router.navigateClearingBackStack(AppNavGraph.SignIn.name)
} else {
mutableStateFlow.value = NoteListResult.Error(throwable.message)
}
}.launchIn(this)
}.ensureActive()

override fun errorResult(throwable: Throwable): NoteListResult {
val errorName: String = throwable::class.simpleName.orEmpty()
return when {
errorName.contains("SQLite") -> NoteListResult.NavSignIn
else -> NoteListResult.Error(throwable.message)
}
}
fun onNoteClicked(id: Long) = router.navigate(route = "${AppNavGraph.Details.name}/$id")

fun onSettingsClicked() = router.navigate(route = AppNavGraph.Settings.name)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package com.softartdev.notedelight.shared.presentation.main

import com.softartdev.notedelight.shared.db.Note

sealed class NoteListResult{
object Loading : NoteListResult()
data class Success(val result: List<Note>) : NoteListResult()
object NavSignIn : NoteListResult()
data class Error(val error: String? = null) : NoteListResult()
}
sealed interface NoteListResult {
data object Loading : NoteListResult
data class Success(val result: List<Note>) : NoteListResult
data class Error(val error: String? = null) : NoteListResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.softartdev.notedelight.shared.navigation.AppNavGraph
import com.softartdev.notedelight.shared.navigation.Router
import com.softartdev.notedelight.shared.usecase.crypt.CheckPasswordUseCase
import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
Expand All @@ -30,6 +31,7 @@ class SignInViewModel(
else -> SignInResult.ShowIncorrectPassError
}
} catch (error: Throwable) {
Napier.e("", error)
router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = error.message))
mutableStateFlow.value = SignInResult.ShowSignInForm
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.softartdev.notedelight.shared.PlatformSQLiteState
import com.softartdev.notedelight.shared.db.SafeRepo
import com.softartdev.notedelight.shared.navigation.AppNavGraph
import com.softartdev.notedelight.shared.navigation.Router
import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
Expand All @@ -26,6 +27,7 @@ class SplashViewModel(
}
)
} catch (error: Throwable) {
Napier.e("", error)
router.navigate(route = AppNavGraph.ErrorDialog.argRoute(message = error.message))
}
mutableStateFlow.value = false
Expand Down

0 comments on commit 002527a

Please sign in to comment.