Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into github-actions-only
Browse files Browse the repository at this point in the history
  • Loading branch information
Frank1234 committed Jul 31, 2024
2 parents 782d685 + faf0f71 commit 6fe178f
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ suspend fun <T> handleAction(
onError: suspend (ActionResult.Error) -> Unit,
) {
when (action) {
is ActionResult.Success -> onSuccess(action.result)
is ActionResult.Success -> onSuccess(action.data)
is ActionResult.Error -> onError(action)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ sealed class ActionResult<out T : Any?> {
data object NotImplemented : Error(Exception("API error format not implemented"))
}

data class Success<T : Any?>(val result: T) : ActionResult<T>()
data class Success<T : Any?>(val data: T) : ActionResult<T>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package nl.q42.template.actionresult.domain

fun <T> ActionResult<T>.getDataOrNull(): T? = when (this) {
is ActionResult.Error -> null
is ActionResult.Success -> result
is ActionResult.Success -> data
}

/**
Expand All @@ -12,7 +12,7 @@ fun <T> ActionResult<T>.getDataOrNull(): T? = when (this) {
*/
fun <S, T> ActionResult<S>.map(mapper: (S) -> T): ActionResult<T> = when (this) {
is ActionResult.Error -> this
is ActionResult.Success -> ActionResult.Success(mapper(this.result))
is ActionResult.Success -> ActionResult.Success(mapper(this.data))
}

/**
Expand All @@ -22,6 +22,5 @@ fun <S, T> ActionResult<S>.map(mapper: (S) -> T): ActionResult<T> = when (this)
*/
fun <S, T> ActionResult<List<S>>.mapList(mapper: (S) -> T): ActionResult<List<T>> = when (this) {
is ActionResult.Error -> this
is ActionResult.Success -> ActionResult.Success(this.result.map { mapper(it) })
is ActionResult.Success -> ActionResult.Success(this.data.map { mapper(it) })
}

Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package nl.q42.template.data.user

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import nl.q42.template.actionresult.domain.ActionResult
import nl.q42.template.actionresult.domain.getDataOrNull
import nl.q42.template.actionresult.domain.map
import nl.q42.template.data.user.local.UserLocalDataSource
import nl.q42.template.data.user.local.model.UserEntity
import nl.q42.template.data.user.local.model.mapToUser
import nl.q42.template.data.user.remote.UserRemoteDataSource
import nl.q42.template.domain.user.model.User
import nl.q42.template.domain.user.repo.UserRepository
import nl.q42.template.actionresult.domain.ActionResult
import nl.q42.template.actionresult.domain.getDataOrNull
import nl.q42.template.actionresult.domain.map
import javax.inject.Inject

internal class UserRepositoryImpl @Inject constructor(
private val userRemoteDataSource: UserRemoteDataSource,
private val userLocalDataSource: UserLocalDataSource,
) : UserRepository {

override suspend fun getUser(): ActionResult<User> {
override suspend fun fetchUser(): ActionResult<Unit> {

// get remotely
val userEntityActionResult = userRemoteDataSource.getUser()
Expand All @@ -25,7 +26,9 @@ internal class UserRepositoryImpl @Inject constructor(
userLocalDataSource.setUser(userEntity)
}

// send response back to caller (note that it's often better to expose a Flow instead).
return userEntityActionResult.map(UserEntity::mapToUser)
// we send back unit, the user needs to be observed
return userEntityActionResult.map { }
}

override fun getUserFlow(): Flow<User?> = userLocalDataSource.getUserFlow().map { it?.mapToUser() }
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package nl.q42.template.data.user.local

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import nl.q42.template.data.user.local.model.UserEntity
import javax.inject.Inject

internal class UserLocalDataSource @Inject constructor() {

private val userFlow = MutableStateFlow<UserEntity?>(null) // this is dummy code, replace it with your own local storage implementation.

fun setUser(userEntity: UserEntity) {
// store in DB or preferences here: use preferences for simple key-values, for more complex objects, use a Room DB.

// usually you store in DataStore or DB here...

userFlow.update { userEntity } // this is dummy code, replace it with your own local storage implementation.
}

fun getUserFlow(): Flow<UserEntity?> = userFlow
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package nl.q42.template.data.user.local.model

import nl.q42.template.domain.user.model.EmailAddress
import nl.q42.template.domain.user.model.User

internal data class UserEntity(val email: String)

internal fun UserEntity.mapToUser() = User(email = email)
internal fun UserEntity.mapToUser() = User(email = EmailAddress(email))
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package nl.q42.template.domain.user.model

data class User(val email: String)
@JvmInline
value class EmailAddress(val value: String)

data class User(val email: EmailAddress)
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package nl.q42.template.domain.user.repo

import nl.q42.template.domain.user.model.User
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import nl.q42.template.actionresult.domain.ActionResult
import nl.q42.template.domain.user.model.User

interface UserRepository {
suspend fun getUser(): ActionResult<User>
suspend fun fetchUser(): ActionResult<Unit>
fun getUserFlow(): Flow<User?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ package nl.q42.template.domain.user.usecase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.q42.template.actionresult.domain.ActionResult
import nl.q42.template.domain.user.model.User
import nl.q42.template.domain.user.repo.UserRepository
import javax.inject.Inject

// A UseCase models an action so the name should begin with a verb. For Flows, use: GetSomethingFlowUseCase
class GetUserUseCase @Inject constructor(private val userRepository: UserRepository) {
class FetchUserUseCase @Inject constructor(private val userRepository: UserRepository) {

suspend operator fun invoke(): ActionResult<User> = withContext(Dispatchers.Default) {
userRepository.getUser()
suspend operator fun invoke(): ActionResult<Unit> = withContext(Dispatchers.Default) {
userRepository.fetchUser()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nl.q42.template.domain.user.usecase

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import nl.q42.template.domain.user.model.User
import nl.q42.template.domain.user.repo.UserRepository
import javax.inject.Inject

class GetUserFlowUseCase @Inject constructor(private val userRepository: UserRepository) {

operator fun invoke(): Flow<User?> =
userRepository
.getUserFlow()
.flowOn(Dispatchers.Default)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,44 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.aakira.napier.Napier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import nl.q42.template.actionresult.data.handleAction
import nl.q42.template.domain.user.usecase.GetUserUseCase
import nl.q42.template.domain.user.usecase.FetchUserUseCase
import nl.q42.template.domain.user.usecase.GetUserFlowUseCase
import nl.q42.template.feature.home.R
import nl.q42.template.navigation.AppGraphRoutes
import nl.q42.template.navigation.viewmodel.RouteNavigator
import nl.q42.template.ui.home.destinations.HomeSecondScreenDestination
import nl.q42.template.ui.presentation.ViewStateString
import java.lang.RuntimeException
import javax.inject.Inject

@HiltViewModel
class HomeViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase,
private val fetchUserUseCase: FetchUserUseCase,
private val getUserFlowUseCase: GetUserFlowUseCase,
private val navigator: RouteNavigator,
) : ViewModel(), RouteNavigator by navigator {

private val _uiState = MutableStateFlow<HomeViewState>(HomeViewState.Empty)
private val _uiState = MutableStateFlow<HomeViewState>(HomeViewState())
val uiState: StateFlow<HomeViewState> = _uiState.asStateFlow()

init {
loadUser()
startObservingUserChanges()
fetchUser()
}

fun onScreenResumed() {
}

fun onLoadClicked() {
loadUser()
}

private fun loadUser() {
viewModelScope.launch {

_uiState.update { HomeViewState.Loading }

handleAction(
getUserUseCase(),
onError = { _uiState.update { HomeViewState.Error } },
onSuccess = { result ->
_uiState.update {
HomeViewState.Data(
ViewStateString.Res(R.string.emailTitle, result.email)
)
}
},
)
}
fetchUser()
}

fun onOpenSecondScreenClicked() {
Expand All @@ -69,4 +54,27 @@ class HomeViewModel @Inject constructor(
fun onOpenOnboardingClicked() {
navigateTo(AppGraphRoutes.onboarding)
}

fun fetchUser() {
viewModelScope.launch {

_uiState.update { it.copy(showError = false, isLoading = true) }

handleAction(
action = fetchUserUseCase(),
onError = { _uiState.update { it.copy(showError = true, isLoading = false) } },
onSuccess = { _uiState.update { it.copy(isLoading = false) } },
)
}
}

private fun startObservingUserChanges() {
getUserFlowUseCase().filterNotNull().onEach { user ->
_uiState.update {
it.copy(
userEmailTitle = ViewStateString.Res(R.string.emailTitle, user.email.value)
)
}
}.launchIn(viewModelScope)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package nl.q42.template.presentation.home

import nl.q42.template.ui.presentation.ViewStateString

sealed class HomeViewState {
data class Data(val userEmailTitle: ViewStateString? = null) : HomeViewState()
object Loading : HomeViewState()
object Error : HomeViewState()
object Empty : HomeViewState()
}
data class HomeViewState(
val userEmailTitle: ViewStateString? = null,
val isLoading: Boolean = false,
val showError: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package nl.q42.template.ui.home

import androidx.compose.foundation.layout.Arrangement.Center
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import nl.q42.template.presentation.home.HomeViewState
import nl.q42.template.ui.compose.get
import nl.q42.template.ui.presentation.toViewStateString
Expand All @@ -35,15 +33,10 @@ internal fun HomeContent(
/**
* This is dummy. Use the strings file IRL.
*/
when (viewState) {
is HomeViewState.Data -> viewState.userEmailTitle?.let { userEmailTitle ->
Text(text = userEmailTitle.get())
}
viewState.userEmailTitle?.get()?.let { Text(text = it) }

HomeViewState.Empty -> {}
HomeViewState.Error -> Text(text = "Error")
HomeViewState.Loading -> Text(text = "Loading")
}
if (viewState.isLoading) CircularProgressIndicator()
if (viewState.showError) Text(text = "Error")

Button(onClick = onLoadClicked) {
Text("Refresh")
Expand All @@ -55,41 +48,29 @@ internal fun HomeContent(
Button(onClick = onOpenOnboardingClicked) {
Text("Open onboarding")
}

Spacer(modifier = Modifier.height(32.dp))

Text(text = "NOTE: when cloning this template, set up your own Firebase project and replace google-services.json")
}
}

@PreviewLightDark
@Composable
private fun HomeContentErrorPreview() {
PreviewAppTheme {
HomeContent(HomeViewState.Error, {}, {}, {})
HomeContent(HomeViewState(showError = true), {}, {}, {})
}
}

@PreviewLightDark
@Composable
private fun HomeContentLoadingPreview() {
PreviewAppTheme {
HomeContent(HomeViewState.Loading, {}, {}, {})
HomeContent(HomeViewState(isLoading = true), {}, {}, {})
}
}

@PreviewLightDark
@Composable
private fun HomeContentEmptyPreview() {
PreviewAppTheme {
HomeContent(HomeViewState.Empty, {}, {}, {})
}
}

@PreviewLightDark
@Composable
private fun HomeContentDataPreview() {
PreviewAppTheme {
HomeContent(HomeViewState.Data("[email protected]".toViewStateString()), {}, {}, {})
HomeContent(HomeViewState(userEmailTitle = "[email protected]".toViewStateString()), {}, {}, {})
}
}
Loading

0 comments on commit 6fe178f

Please sign in to comment.