-
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
[Feature/week_1_compose]: 1주차 과제 Compose 구성 #8
base: develop_compose
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,11 +1,92 @@ | ||||||||||||||||||||||||||||||||
package org.sopt.dosopttemplate | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import androidx.appcompat.app.AppCompatActivity | ||||||||||||||||||||||||||||||||
import android.os.Bundle | ||||||||||||||||||||||||||||||||
import android.widget.Toast | ||||||||||||||||||||||||||||||||
import androidx.activity.ComponentActivity | ||||||||||||||||||||||||||||||||
import androidx.activity.compose.setContent | ||||||||||||||||||||||||||||||||
import androidx.activity.viewModels | ||||||||||||||||||||||||||||||||
import androidx.compose.runtime.Composable | ||||||||||||||||||||||||||||||||
import androidx.compose.ui.tooling.preview.Preview | ||||||||||||||||||||||||||||||||
import androidx.lifecycle.flowWithLifecycle | ||||||||||||||||||||||||||||||||
import androidx.lifecycle.lifecycleScope | ||||||||||||||||||||||||||||||||
import androidx.navigation.NavHostController | ||||||||||||||||||||||||||||||||
import androidx.navigation.compose.NavHost | ||||||||||||||||||||||||||||||||
import androidx.navigation.compose.composable | ||||||||||||||||||||||||||||||||
import androidx.navigation.compose.rememberNavController | ||||||||||||||||||||||||||||||||
import kotlinx.coroutines.flow.launchIn | ||||||||||||||||||||||||||||||||
import kotlinx.coroutines.flow.onEach | ||||||||||||||||||||||||||||||||
import org.sopt.dosopttemplate.feature.home.HomeScreen | ||||||||||||||||||||||||||||||||
import org.sopt.dosopttemplate.feature.login.LoginScreen | ||||||||||||||||||||||||||||||||
import org.sopt.dosopttemplate.feature.signup.SignUpScreen | ||||||||||||||||||||||||||||||||
import org.sopt.dosopttemplate.ui.theme.DoEuijinKwakTheme | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
class MainActivity : ComponentActivity() { | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
enum class Screen { | ||||||||||||||||||||||||||||||||
LOGIN, | ||||||||||||||||||||||||||||||||
SIGNUP, | ||||||||||||||||||||||||||||||||
Home, | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
private val viewModel by viewModels<MainViewModel>() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
class MainActivity : AppCompatActivity() { | ||||||||||||||||||||||||||||||||
override fun onCreate(savedInstanceState: Bundle?) { | ||||||||||||||||||||||||||||||||
super.onCreate(savedInstanceState) | ||||||||||||||||||||||||||||||||
setContentView(R.layout.activity_main) | ||||||||||||||||||||||||||||||||
setContent { | ||||||||||||||||||||||||||||||||
DoEuijinKwakTheme { | ||||||||||||||||||||||||||||||||
val navController = rememberNavController() | ||||||||||||||||||||||||||||||||
Comment on lines
+31
to
+37
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.
Suggested change
컴포즈에서는 delegate 방식이 아닌 함수 내에서 변수 선언방식으로 뷰모델을 불러올 수도 있습니다. 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. 오호 처음알았네요 그러면 저 viewmodel은 어떤 생명주기를 가지고있나요?? |
||||||||||||||||||||||||||||||||
NavHost( | ||||||||||||||||||||||||||||||||
navController = navController, | ||||||||||||||||||||||||||||||||
startDestination = Screen.LOGIN.name, | ||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||
composable(Screen.LOGIN.name) { | ||||||||||||||||||||||||||||||||
LoginScreen( | ||||||||||||||||||||||||||||||||
navController = navController, | ||||||||||||||||||||||||||||||||
viewModel = viewModel, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
composable(Screen.SIGNUP.name) { | ||||||||||||||||||||||||||||||||
SignUpScreen( | ||||||||||||||||||||||||||||||||
navController = | ||||||||||||||||||||||||||||||||
navController, | ||||||||||||||||||||||||||||||||
viewModel = viewModel, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
composable(Screen.Home.name) { | ||||||||||||||||||||||||||||||||
HomeScreen( | ||||||||||||||||||||||||||||||||
navController = navController, | ||||||||||||||||||||||||||||||||
viewModel = viewModel, | ||||||||||||||||||||||||||||||||
Comment on lines
+44
to
+58
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. 여러개의 화면에서 하나의 ViewModel을 공유하면 한 화면의 상태를 다른화면도 알 수 있는 상황도 있을 것 같네요. 괜찮긴 하지만 굳이 이걸 다 알아야하나라는 생각도 있습니다. 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. 맞습니다... 근데 보통 Auth관련 로직은 AppViewModel에 넣고 처리하는게 일반적인 방법이라고 생각되어 위처럼 구성해보았습니다 |
||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
collectEvent(navController) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
private fun collectEvent(navController: NavHostController) { | ||||||||||||||||||||||||||||||||
viewModel.event.flowWithLifecycle(lifecycle).onEach { | ||||||||||||||||||||||||||||||||
when (it) { | ||||||||||||||||||||||||||||||||
is MainContract.MainSideEffect.ShowToast -> { | ||||||||||||||||||||||||||||||||
Toast.makeText(this, it.message, Toast.LENGTH_SHORT).show() | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
is MainContract.MainSideEffect.LoginSuccess -> { | ||||||||||||||||||||||||||||||||
Toast.makeText(this, "로그인 성공!", Toast.LENGTH_SHORT).show() | ||||||||||||||||||||||||||||||||
navController.navigate(Screen.Home.name) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
is MainContract.MainSideEffect.RegistrationSuccess -> { | ||||||||||||||||||||||||||||||||
Toast.makeText(this, "회원가입 성공!", Toast.LENGTH_SHORT).show() | ||||||||||||||||||||||||||||||||
navController.popBackStack() // 로그인 화면으로 돌아갑니다. | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
}.launchIn(lifecycleScope) | ||||||||||||||||||||||||||||||||
Comment on lines
+68
to
+84
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. 이 방식은 주로 View에서 많이 사용하는 방식이고 Compose에서는 상태를 구독받을 때 val state by viewModel.state.collectStateWithLifecycle() 로 구독을 많이 받습니다. (그리고 이벤트도 거의 안쓰이고 대부분 UiState 선에서 다 해결할 수 있습니다.) 참고로 지난 DroidKaigi 샘플앱에서도 SharedFlow를 사용한 코드는 없었네요. |
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
@Preview(showBackground = true) | ||||||||||||||||||||||||||||||||
@Composable | ||||||||||||||||||||||||||||||||
fun GreetingPreview() { | ||||||||||||||||||||||||||||||||
DoEuijinKwakTheme {} | ||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.sopt.dosopttemplate | ||
|
||
class MainContract { | ||
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. 굳이 클래스로 한번더 wrapping한 이유가 있나요? |
||
data class MainState( | ||
val id: String = "", | ||
val registerId: String = "", | ||
val pw: String = "", | ||
val registerPw: String = "", | ||
val nickname: String = "", | ||
) | ||
|
||
sealed interface MainSideEffect { | ||
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. SideEffect는 보통 악영향, 예상치 못한 문제 이런곳에 사용하는 네이밍 아닌가요? 인터페이스 이름을 MainSideEffect로 하신 이유를 알고 싶습니다! 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. 의학용어에서는 그렇게 사용하긴 하지만 CS에서 사이드 이펙트라는 것은 어떤 행위에 대한 부수 효과(의도치 않는 생산물?) 정도로 받아들이면 좋을 것 같네요. 함수형 프로그래밍에서 자주 다뤄지는 개념입니다 (순수함수와 사이드 이펙트라는 용어로 말이죠) |
||
data class ShowToast(val message: String) : MainSideEffect | ||
object LoginSuccess : MainSideEffect | ||
object RegistrationSuccess : MainSideEffect | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package org.sopt.dosopttemplate | ||
|
||
import androidx.lifecycle.ViewModel | ||
import androidx.lifecycle.viewModelScope | ||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.asSharedFlow | ||
import kotlinx.coroutines.flow.asStateFlow | ||
import kotlinx.coroutines.launch | ||
|
||
class MainViewModel : ViewModel() { | ||
private val _state = MutableStateFlow(MainContract.MainState()) | ||
val state get() = _state.asStateFlow() | ||
|
||
private val _event = MutableSharedFlow<MainContract.MainSideEffect>() | ||
val event get() = _event.asSharedFlow() | ||
|
||
private fun emitToast(message: String) { | ||
viewModelScope.launch { _event.emit(MainContract.MainSideEffect.ShowToast(message)) } | ||
} | ||
|
||
fun updateState(newState: MainContract.MainState) { | ||
_state.value = newState | ||
} | ||
|
||
fun login() { | ||
val state = state.value | ||
when { | ||
state.id.isEmpty() || state.id != state.registerId -> emitToast("아이디를 확인해주세요.") | ||
state.pw.isEmpty() || state.pw != state.registerPw -> emitToast("비밀번호를 확인해주세요.") | ||
else -> viewModelScope.launch { | ||
_event.emit(MainContract.MainSideEffect.LoginSuccess) | ||
} | ||
} | ||
} | ||
|
||
fun register() { | ||
val id = state.value.registerId | ||
val pw = state.value.registerPw | ||
val nickname = state.value.nickname | ||
|
||
when { | ||
id.length !in 6..10 -> emitToast("ID는 6~10 글자로 입력해주세요.") | ||
pw.length !in 8..12 -> emitToast("비밀번호는 8~12 글자로 입력해주세요.") | ||
nickname.isBlank() -> emitToast("닉네임을 입력해주세요.") | ||
else -> completeRegistration(id, pw, nickname) | ||
} | ||
} | ||
Comment on lines
+26
to
+48
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. 잘 생각을 해보면...String의 길이 비교나 id, password 일치 조건등의 비교등이 뷰모델에서 가지는 역할이라고 볼 수 있을까요? |
||
|
||
fun clearRegisterInfo() { | ||
updateState(state.value.copy(registerId = "", registerPw = "", nickname = "")) | ||
} | ||
|
||
private fun completeRegistration(id: String, pw: String, nickname: String) { | ||
viewModelScope.launch { | ||
updateState(state.value.copy(id = id, pw = pw, nickname = nickname)) | ||
_event.emit(MainContract.MainSideEffect.RegistrationSuccess) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,101 @@ | ||||||||||||||||||||||||||
package org.sopt.dosopttemplate.feature.home | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
import androidx.compose.foundation.layout.Column | ||||||||||||||||||||||||||
import androidx.compose.foundation.layout.Row | ||||||||||||||||||||||||||
import androidx.compose.foundation.layout.Spacer | ||||||||||||||||||||||||||
import androidx.compose.foundation.layout.height | ||||||||||||||||||||||||||
import androidx.compose.foundation.layout.padding | ||||||||||||||||||||||||||
import androidx.compose.foundation.layout.size | ||||||||||||||||||||||||||
import androidx.compose.material.icons.Icons | ||||||||||||||||||||||||||
import androidx.compose.material.icons.filled.Person | ||||||||||||||||||||||||||
import androidx.compose.material3.ExperimentalMaterial3Api | ||||||||||||||||||||||||||
import androidx.compose.material3.Icon | ||||||||||||||||||||||||||
import androidx.compose.material3.Scaffold | ||||||||||||||||||||||||||
import androidx.compose.material3.Text | ||||||||||||||||||||||||||
import androidx.compose.runtime.Composable | ||||||||||||||||||||||||||
import androidx.compose.runtime.collectAsState | ||||||||||||||||||||||||||
import androidx.compose.ui.Alignment | ||||||||||||||||||||||||||
import androidx.compose.ui.Modifier | ||||||||||||||||||||||||||
import androidx.compose.ui.graphics.Color | ||||||||||||||||||||||||||
import androidx.compose.ui.text.TextStyle | ||||||||||||||||||||||||||
import androidx.compose.ui.text.font.FontWeight | ||||||||||||||||||||||||||
import androidx.compose.ui.unit.dp | ||||||||||||||||||||||||||
import androidx.compose.ui.unit.sp | ||||||||||||||||||||||||||
import androidx.navigation.NavController | ||||||||||||||||||||||||||
import org.sopt.dosopttemplate.MainViewModel | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@OptIn(ExperimentalMaterial3Api::class) | ||||||||||||||||||||||||||
@Composable | ||||||||||||||||||||||||||
fun HomeScreen( | ||||||||||||||||||||||||||
modifier: Modifier = Modifier, | ||||||||||||||||||||||||||
navController: NavController, | ||||||||||||||||||||||||||
viewModel: MainViewModel, | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
Scaffold { innerPadding -> | ||||||||||||||||||||||||||
HomeContent( | ||||||||||||||||||||||||||
modifier = modifier.padding(innerPadding), | ||||||||||||||||||||||||||
navController = navController, | ||||||||||||||||||||||||||
viewModel = viewModel, | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Composable | ||||||||||||||||||||||||||
fun HomeContent( | ||||||||||||||||||||||||||
modifier: Modifier, | ||||||||||||||||||||||||||
navController: NavController, | ||||||||||||||||||||||||||
viewModel: MainViewModel, | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
Comment on lines
+43
to
+48
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.
Suggested change
+) 이렇게 작성하면 Preview를 그릴 수 없겠죠? 어떻게 하면 HomeContent의 Preview를 그릴 수 있게 만들 수 있을까요? |
||||||||||||||||||||||||||
val state = viewModel.state.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.
Suggested change
|
||||||||||||||||||||||||||
Column( | ||||||||||||||||||||||||||
modifier = modifier | ||||||||||||||||||||||||||
.padding(horizontal = 16.dp) | ||||||||||||||||||||||||||
.padding(top = 32.dp), | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
Row( | ||||||||||||||||||||||||||
verticalAlignment = Alignment.CenterVertically, | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
Icon( | ||||||||||||||||||||||||||
imageVector = Icons.Default.Person, | ||||||||||||||||||||||||||
contentDescription = "Person Icon", | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
Spacer(modifier = Modifier.size(8.dp)) | ||||||||||||||||||||||||||
Text( | ||||||||||||||||||||||||||
text = state.value.nickname, | ||||||||||||||||||||||||||
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.
Suggested change
collect를 할 때 by 문법을 활용하면 바로 value에 접근할 수 있습니다. |
||||||||||||||||||||||||||
style = TextStyle( | ||||||||||||||||||||||||||
fontWeight = FontWeight(700), | ||||||||||||||||||||||||||
fontSize = 20.sp, | ||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||
color = Color.Black, | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
Spacer(modifier = Modifier.size(16.dp)) | ||||||||||||||||||||||||||
Text( | ||||||||||||||||||||||||||
text = "컴포즈 반가워", | ||||||||||||||||||||||||||
style = TextStyle( | ||||||||||||||||||||||||||
fontSize = 20.sp, | ||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||
color = Color.Black, | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
Spacer(modifier = Modifier.height(24.dp)) | ||||||||||||||||||||||||||
Text( | ||||||||||||||||||||||||||
text = "ID", | ||||||||||||||||||||||||||
style = TextStyle( | ||||||||||||||||||||||||||
fontWeight = FontWeight(700), | ||||||||||||||||||||||||||
fontSize = 30.sp, | ||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
Spacer(modifier = Modifier.height(8.dp)) | ||||||||||||||||||||||||||
Text(text = state.value.id) | ||||||||||||||||||||||||||
Spacer(modifier = Modifier.height(24.dp)) | ||||||||||||||||||||||||||
Text( | ||||||||||||||||||||||||||
text = "PW", | ||||||||||||||||||||||||||
style = TextStyle( | ||||||||||||||||||||||||||
fontWeight = FontWeight(700), | ||||||||||||||||||||||||||
fontSize = 30.sp, | ||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||
Spacer(modifier = Modifier.height(8.dp)) | ||||||||||||||||||||||||||
Text(text = state.value.pw) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} |
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.