Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#11 week7 #12

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
38 changes: 38 additions & 0 deletions app/src/main/java/org/sopt/and/core/component/BaseViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.sopt.and.core.component

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

abstract class BaseViewModel<State : UiState, SideEffect : UiSideEffect, Event : UiEvent> :
ViewModel() {

private val initialState: State by lazy { createInitialState() }
abstract fun createInitialState(): State

private val _uiState = MutableStateFlow(initialState)
val uiState: StateFlow<State> = _uiState.asStateFlow()

private val _sideEffect = MutableSharedFlow<SideEffect>()
val sideEffect: SharedFlow<SideEffect> = _sideEffect.asSharedFlow()

fun setState(reduce: State.() -> State) {
_uiState.value = _uiState.value.reduce()
}

fun setSideEffect(builder: () -> SideEffect) {
viewModelScope.launch { _sideEffect.emit(builder()) }
}

fun setEvent(event: Event) {
viewModelScope.launch { handleEvent(event) }
}

protected abstract suspend fun handleEvent(event: Event)
}
3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/core/component/UiEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.core.component

interface UiEvent
3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/core/component/UiSideEffect.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.core.component

interface UiSideEffect
3 changes: 3 additions & 0 deletions app/src/main/java/org/sopt/and/core/component/UiState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.sopt.and.core.component

interface UiState
103 changes: 51 additions & 52 deletions app/src/main/java/org/sopt/and/feature/login/LoginScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,50 +42,65 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import kotlinx.coroutines.flow.collectLatest
import org.sopt.and.R
import org.sopt.and.UiState
import org.sopt.and.core.component.DescriptionText
import org.sopt.and.core.component.DividerWithText
import org.sopt.and.core.component.textfield.CustomEmailTextField
import org.sopt.and.core.component.textfield.CustomPwTextField
import org.sopt.and.domain.entity.LoginInfo
import org.sopt.and.feature.login.model.LoginContract
import org.sopt.and.feature.login.viewmodel.LoginViewModel
import org.sopt.and.ui.theme.ANDANDROIDTheme

@Composable
fun LoginScreen(navController: NavController) {
val viewModel: LoginViewModel = hiltViewModel()

val loginEmail by viewModel.email.collectAsState()
val loginPassword by viewModel.password.collectAsState()
var passwordVisible by remember { mutableStateOf(false) }
fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltViewModel()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NavController ๊ฐ์ฒด ์ž์ฒด๋ฅผ ๋„˜๊ธฐ๋Š” ๊ฒƒ๋ณด๋‹ค ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋„˜๊ฒจ์ฃผ์‹œ๋Š” ๊ฒŒ ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์š”!
NavController ๊ฐ์ฒด ์ž์ฒด๋ฅผ ๋„˜๊ธฐ๊ฒŒ ๋˜๋ฉด LoginScreen์ด ๋„ค๋น„๊ฒŒ์ด์…˜๊ณผ ๊ด€๋ จ๋œ ์ฑ…์ž„๋„ ๊ฐ€์ง€๊ฒŒ ๋˜์–ด์„œ ๊ทธ ์—ญํ• ์ด ๋ฌด๊ฑฐ์›Œ ์ง‘๋‹ˆ๋‹ค.
๋˜ํ•œ, ํ…Œ์ŠคํŠธ ๋“ฑ์ด ์–ด๋ ต๊ฒŒ ๋˜์–ด์š”!

val uiState by viewModel.uiState.collectAsState()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collectAsStateWithLifecycle์„ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค ~ (์™œ์ธ์ง€๋Š” ์„ธ๋ฏธ๋‚˜ ์ž๋ฃŒ ์ฐธ๊ณ  !!)

val context = LocalContext.current
val loginState by viewModel.loginState.collectAsState()
var passwordVisible by remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }

LaunchedEffect(Unit) {
viewModel.sideEffect.collectLatest { sideEffect ->
when (sideEffect) {
is LoginContract.LoginSideEffect.NavigateToMyPageWithToken -> {
saveAuthToken(context, sideEffect.token)
navController.navigate("mypage") {
popUpTo("login") { inclusive = true }
}
}

is LoginContract.LoginSideEffect.ShowSnackbar -> {
snackbarHostState.showSnackbar(sideEffect.message)
}
}
}
}

Comment on lines +62 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ด๋Ÿฐ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒ ๋„ค์š”! ๋ฐฐ์›Œ๊ฐ‘๋‹ˆ๋‹ค!!

Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color(0xFF1B1B1B))
.padding(horizontal = 10.dp)
) {
LoginTopBar()
Spacer(modifier = Modifier.padding(top = 30.dp))
Spacer(modifier = Modifier.height(30.dp))

CustomEmailTextField(
value = loginEmail,
onValueChange = { viewModel.updateEmail(it) },
value = uiState.email,
onValueChange = { viewModel.setEvent(LoginContract.LoginEvent.UpdateEmail(it)) },
placeholder = stringResource(R.string.login_email_id)
)
Spacer(modifier = Modifier.padding(top = 10.dp))

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

Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd
) {
CustomPwTextField(
value = loginPassword,
onValueChange = { viewModel.updatePassword(it) },
placeholder = stringResource(R.string.login_setting_password),
// passwordVisible = passwordVisible,
// padding = PaddingValues(vertical = 10.dp)
value = uiState.password,
onValueChange = { viewModel.setEvent(LoginContract.LoginEvent.UpdatePassword(it)) },
placeholder = stringResource(R.string.login_setting_password)
)
Text(
text = if (passwordVisible) "hide" else "show",
Expand All @@ -95,52 +110,36 @@ fun LoginScreen(navController: NavController) {
.clickable { passwordVisible = !passwordVisible }
)
}
Spacer(modifier = Modifier.padding(top = 30.dp))

NavigateToMain {
val userInfo = LoginInfo(loginEmail, loginPassword)
viewModel.submitLogin(userInfo)
}

when (val state = loginState) {
is UiState.Success -> {
LaunchedEffect(state.data) {
val authToken = state.data?.token
if (authToken != null) {
snackbarHostState.showSnackbar(context.getString(R.string.login_success))
saveAuthToken(context, authToken)
navController.navigate("mypage") {
popUpTo("login") { inclusive = true }
}
} else {
snackbarHostState.showSnackbar("๋กœ๊ทธ์ธ ์‹คํŒจ")
}
}
}

is UiState.Failure -> {
LaunchedEffect(state.errorMessage) {
snackbarHostState.showSnackbar(state.errorMessage)
}
}
Spacer(modifier = Modifier.height(30.dp))

else -> Unit
NavigateToMain {
viewModel.setEvent(
LoginContract.LoginEvent.SubmitLogin(
uiState.email,
uiState.password
)
)
}
Spacer(modifier = Modifier.height(20.dp))

Spacer(modifier = Modifier.padding(top = 20.dp))
ThreeTextsWithDividers(
modifier = Modifier.fillMaxWidth(),
stringResource(R.string.login_find_id),
stringResource(R.string.login_setting_password_again),
stringResource(R.string.sign_up),
text1 = stringResource(R.string.login_find_id),
text2 = stringResource(R.string.login_setting_password_again),
text3 = stringResource(R.string.sign_up),
Comment on lines +128 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์กฐ๊ธˆ ๋” ์ง๊ด€์ ์ธ ๋„ค์ด๋ฐ์„ ์‚ฌ์šฉํ•ด ์ฃผ์…”๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™๋„ค์š” !

onSignUpClick = { navController.navigate("signup") }
)

DividerWithText(stringResource(R.string.login_join_with_social_account))

Image(
painter = painterResource(id = R.drawable.ic_social_login),
contentDescription = "Social Login",
contentDescription = "Social Login"
)
Spacer(modifier = Modifier.padding(top = 20.dp))

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

DescriptionText(stringResource(R.string.login_join_social_account_description))
SnackbarHost(hostState = snackbarHostState)
}
Expand Down
51 changes: 20 additions & 31 deletions app/src/main/java/org/sopt/and/feature/login/LoginViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,51 +1,40 @@
package org.sopt.and.feature.login
package org.sopt.and.feature.login.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.sopt.and.UiState
import org.sopt.and.domain.repository.LoginRepository
import org.sopt.and.core.component.BaseViewModel
import org.sopt.and.domain.entity.LoginInfo
import org.sopt.and.domain.entity.Token
import org.sopt.and.domain.repository.LoginRepository
import org.sopt.and.feature.login.model.LoginContract.LoginEvent
import org.sopt.and.feature.login.model.LoginContract.LoginSideEffect
import org.sopt.and.feature.login.model.LoginContract.LoginState
import javax.inject.Inject

@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginRepository: LoginRepository
) : ViewModel() {

private val _email = MutableStateFlow("")
val email: StateFlow<String> = _email

private val _password = MutableStateFlow("")
val password: StateFlow<String> = _password
) : BaseViewModel<LoginState, LoginSideEffect, LoginEvent>() {

private val _loginState = MutableStateFlow<UiState<Token>>(UiState.Loading)
val loginState: StateFlow<UiState<Token>> = _loginState
override fun createInitialState() = LoginState()

private val _authToken = MutableStateFlow<String?>(null)
val authToken: StateFlow<String?> = _authToken

fun updateEmail(newEmail: String) {
_email.value = newEmail
}

fun updatePassword(newPassword: String) {
_password.value = newPassword
override suspend fun handleEvent(event: LoginEvent) {
when (event) {
is LoginEvent.UpdateEmail -> setState { copy(email = event.email) }
is LoginEvent.UpdatePassword -> setState { copy(password = event.password) }
is LoginEvent.SubmitLogin -> submitLogin(event.email, event.password)
}
}

fun submitLogin(userInfo: LoginInfo) {
private fun submitLogin(email: String, password: String) {
setState { copy(isLoading = true, errorMessage = null) }
viewModelScope.launch {
loginRepository.postLogin(userInfo)
loginRepository.postLogin(LoginInfo(email, password))
.onSuccess { response ->
_loginState.emit(UiState.Success(response))
_authToken.emit(response.token)
setSideEffect { LoginSideEffect.NavigateToMyPageWithToken(response.token) }
}
.onFailure { exception ->
_loginState.emit(UiState.Failure("๋กœ๊ทธ์ธ ์‹คํŒจ"))
.onFailure {
setSideEffect { LoginSideEffect.ShowSnackbar("๋กœ๊ทธ์ธ ์‹คํŒจ") }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.sopt.and.feature.login.model

import org.sopt.and.core.component.UiEvent
import org.sopt.and.core.component.UiSideEffect
import org.sopt.and.core.component.UiState

class LoginContract {
data class LoginState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val errorMessage: String? = null
) : UiState

sealed class LoginEvent : UiEvent {
data class UpdateEmail(val email: String) : LoginEvent()
data class UpdatePassword(val password: String) : LoginEvent()
data class SubmitLogin(val email: String, val password: String) : LoginEvent()
}

sealed class LoginSideEffect : UiSideEffect {
data class NavigateToMyPageWithToken(val token: String) : LoginSideEffect()
data class ShowSnackbar(val message: String) : LoginSideEffect()
}
}
38 changes: 23 additions & 15 deletions app/src/main/java/org/sopt/and/feature/mypage/MyPageViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
package org.sopt.and.feature.mypage
package org.sopt.and.feature.mypage.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.sopt.and.core.component.BaseViewModel
import org.sopt.and.domain.repository.MyPageRepository
import org.sopt.and.feature.mypage.model.MyPageContract.MyPageEvent
import org.sopt.and.feature.mypage.model.MyPageContract.MyPageSideEffect
import org.sopt.and.feature.mypage.model.MyPageContract.MyPageState
import javax.inject.Inject

@HiltViewModel
class MyPageViewModel @Inject constructor(
private val myPageRepository: MyPageRepository
) : ViewModel() {
) : BaseViewModel<MyPageState, MyPageSideEffect, MyPageEvent>() {

private val _hobby = MutableStateFlow<String?>(null)
val hobby: StateFlow<String?> = _hobby.asStateFlow()
override fun createInitialState() = MyPageState()

fun loadHobby(token: String) {
override suspend fun handleEvent(event: MyPageEvent) {
when (event) {
is MyPageEvent.LoadHobby -> loadHobby(event.token)
}
}

private fun loadHobby(token: String) {
setState { copy(isLoading = true) }
viewModelScope.launch {
val result = myPageRepository.getMyHobby(token)
result.onSuccess { response ->
_hobby.value = response.hobby
}.onFailure {
_hobby.value = "๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."
}
myPageRepository.getMyHobby(token)
.onSuccess { response ->
setState { copy(hobby = response.hobby, isLoading = false) }
}
.onFailure {
setState { copy(hobby = "", isLoading = false) }
setSideEffect { MyPageSideEffect.ShowErrorToast("๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ•จ") }
}
}
}
}
Loading