-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: develop
Are you sure you want to change the base?
Feat/#11 week7 #12
Changes from all commits
20d473f
a6506b1
8d11065
a147228
d9bb720
9b76ec1
3470dbd
dbb4668
aab2278
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package org.sopt.and.core.component | ||
|
||
interface UiEvent |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package org.sopt.and.core.component | ||
|
||
interface UiSideEffect |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package org.sopt.and.core.component | ||
|
||
interface UiState |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()) { | ||
val uiState by viewModel.uiState.collectAsState() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
|
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() | ||
} | ||
} |
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("๋ถ๋ฌ์ค์ง ๋ชปํจ") } | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NavController ๊ฐ์ฒด ์์ฒด๋ฅผ ๋๊ธฐ๋ ๊ฒ๋ณด๋ค ์ด๋ฅผ ์ฌ์ฉํ๋ ํจ์๋ฅผ ๋๊ฒจ์ฃผ์๋ ๊ฒ ์ข์ ๊ฒ ๊ฐ์์!
NavController ๊ฐ์ฒด ์์ฒด๋ฅผ ๋๊ธฐ๊ฒ ๋๋ฉด LoginScreen์ด ๋ค๋น๊ฒ์ด์ ๊ณผ ๊ด๋ จ๋ ์ฑ ์๋ ๊ฐ์ง๊ฒ ๋์ด์ ๊ทธ ์ญํ ์ด ๋ฌด๊ฑฐ์ ์ง๋๋ค.
๋ํ, ํ ์คํธ ๋ฑ์ด ์ด๋ ต๊ฒ ๋์ด์!