-
Notifications
You must be signed in to change notification settings - Fork 20
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
[해나] 회원가입 미션 1, 2단계 제출합니다. #15
base: hxeyexn
Are you sure you want to change the base?
Changes from all commits
e7972b6
d5fbbbb
8de0d20
97650e4
ef692fa
15e7e48
27a7a62
6844427
b266e49
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 |
---|---|---|
@@ -1 +1,10 @@ | ||
# android-signup# android-signup | ||
# android-signup | ||
|
||
## 🚀1단계 - 컴포즈 기초 | ||
- [x] 제시된 학습 테스트 완성 | ||
- [x] Preview 노출 | ||
- [x] Preview의 interactive 모드를 활용하여 버튼 클릭해보기 | ||
|
||
### 🚀2단계 - 기능 요구 사항 | ||
- [x] 회원가입 뷰 구현 | ||
- Material3 Button, TextField를 활용 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[*.{kt,kts}] | ||
ktlint_function_naming_ignore_when_annotated_with=Composable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package nextstep.signup | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.test.assert | ||
import androidx.compose.ui.test.assertCountEquals | ||
import androidx.compose.ui.test.assertIsNotEnabled | ||
import androidx.compose.ui.test.hasText | ||
import androidx.compose.ui.test.junit4.createComposeRule | ||
import androidx.compose.ui.test.onChildren | ||
import androidx.compose.ui.test.onFirst | ||
import androidx.compose.ui.test.onNodeWithTag | ||
import androidx.compose.ui.test.onNodeWithText | ||
import androidx.compose.ui.test.performClick | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import org.junit.Rule | ||
import org.junit.Test | ||
|
||
class LayoutBasicsTest { | ||
@get:Rule | ||
val composeTestRule = createComposeRule() | ||
|
||
@Test | ||
fun text() { | ||
val text = "안녕 난 컴포즈야~" | ||
composeTestRule.setContent { | ||
TextComposable() | ||
} | ||
|
||
composeTestRule | ||
.onNodeWithText(text) | ||
.assertExists() | ||
} | ||
|
||
@Test | ||
fun column() { | ||
composeTestRule.setContent { | ||
ColumnComposable() | ||
} | ||
|
||
composeTestRule.onNodeWithTag("이름") | ||
.onChildren() | ||
.assertCountEquals(3) | ||
.onFirst() | ||
.assert(hasText("깜포즈")) | ||
} | ||
|
||
@Test | ||
fun button() { | ||
composeTestRule.setContent { | ||
ButtonComposable() | ||
} | ||
|
||
val button = | ||
composeTestRule | ||
.onNodeWithTag("버튼") | ||
.performClick() | ||
|
||
button.assertIsNotEnabled() | ||
} | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
fun TextPreview() { | ||
TextComposable() | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
fun ColumnPreview() { | ||
ColumnComposable() | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
fun ButtonPreview() { | ||
ButtonComposable() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package nextstep.signup | ||
|
||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.material3.Button | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.platform.testTag | ||
import androidx.compose.ui.text.TextStyle | ||
import androidx.compose.ui.text.font.FontFamily | ||
import androidx.compose.ui.text.font.FontWeight | ||
import androidx.compose.ui.unit.sp | ||
|
||
@Composable | ||
fun TextComposable() { | ||
Text( | ||
text = "안녕 난 컴포즈야~", | ||
color = Color.Blue, | ||
style = | ||
TextStyle( | ||
fontSize = 26.sp, | ||
fontWeight = FontWeight.Bold, | ||
fontFamily = FontFamily.SansSerif, | ||
), | ||
) | ||
} | ||
|
||
@Composable | ||
fun ColumnComposable() { | ||
Column( | ||
modifier = Modifier.testTag("이름"), | ||
) { | ||
Text(text = "깜포즈", color = Color.Blue) | ||
Text(text = "킴포즈", color = Color.Cyan) | ||
Text(text = "끔포즈", color = Color.Yellow) | ||
} | ||
} | ||
|
||
@Composable | ||
fun ButtonComposable() { | ||
val enabled = remember { mutableStateOf(true) } | ||
Button( | ||
onClick = { | ||
enabled.value = false | ||
}, | ||
enabled = enabled.value, | ||
modifier = Modifier.testTag("버튼"), | ||
) { | ||
Text("클릭해주세요") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -3,44 +3,60 @@ package nextstep.signup | |||||
import android.os.Bundle | ||||||
import androidx.activity.ComponentActivity | ||||||
import androidx.activity.compose.setContent | ||||||
import androidx.compose.foundation.layout.Column | ||||||
import androidx.compose.foundation.layout.fillMaxSize | ||||||
import androidx.compose.foundation.layout.padding | ||||||
import androidx.compose.material3.MaterialTheme | ||||||
import androidx.compose.material3.Surface | ||||||
import androidx.compose.material3.Text | ||||||
import androidx.compose.runtime.Composable | ||||||
import androidx.compose.ui.Alignment | ||||||
import androidx.compose.ui.Modifier | ||||||
import androidx.compose.ui.text.input.KeyboardType | ||||||
import androidx.compose.ui.tooling.preview.Preview | ||||||
import androidx.compose.ui.unit.dp | ||||||
import nextstep.signup.ui.ButtonView | ||||||
import nextstep.signup.ui.TextFieldView | ||||||
import nextstep.signup.ui.TextView | ||||||
import nextstep.signup.ui.theme.SignupTheme | ||||||
|
||||||
class MainActivity : ComponentActivity() { | ||||||
override fun onCreate(savedInstanceState: Bundle?) { | ||||||
super.onCreate(savedInstanceState) | ||||||
|
||||||
setContent { | ||||||
SignupTheme { | ||||||
// A surface container using the 'background' color from the theme | ||||||
Surface( | ||||||
modifier = Modifier.fillMaxSize(), | ||||||
color = MaterialTheme.colorScheme.background | ||||||
modifier = | ||||||
Modifier | ||||||
.fillMaxSize() | ||||||
.padding(top = 56.dp, start = 32.dp, end = 32.dp), | ||||||
color = MaterialTheme.colorScheme.background, | ||||||
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. Surface 는 기본적으로
|
||||||
) { | ||||||
Greeting("Android") | ||||||
Column( | ||||||
horizontalAlignment = Alignment.CenterHorizontally, | ||||||
) { | ||||||
TextView(R.string.main_greeting) | ||||||
TextFieldScreen() | ||||||
ButtonView(R.string.main_sign_up, paddingTop = 42.dp) | ||||||
Comment on lines
+38
to
+40
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. 보통 Screen 은 화면 단위에 붙이는 네이밍이에요! 그러나 현재 Fragment? 같은 느낌으로 사용되고 있네요 화면 단위로 리팩토링해볼까요? 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. 그리고, TextFieldScreen 보다는 SignUpScreen 이란 네이밍은 어떠신가요! |
||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
@Composable | ||||||
fun Greeting(name: String, modifier: Modifier = Modifier) { | ||||||
Text( | ||||||
text = "Hello $name!", | ||||||
modifier = modifier | ||||||
) | ||||||
fun TextFieldScreen() { | ||||||
TextFieldView(paddingTop = 42.dp, label = R.string.main_user_name) | ||||||
TextFieldView(paddingTop = 36.dp, label = R.string.main_email, keyboardType = KeyboardType.Email) | ||||||
TextFieldView(paddingTop = 36.dp, label = R.string.main_password, keyboardType = KeyboardType.Password) | ||||||
Comment on lines
+51
to
+52
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. KeyboradType 을 잘 바꿔주셨네요. 저는 Email 타입도 지정 안했는데 덕분에 알아갑니다 👍👍👍 |
||||||
TextFieldView(paddingTop = 36.dp, label = R.string.main_password_confirm, keyboardType = KeyboardType.Password) | ||||||
} | ||||||
|
||||||
@Preview(showBackground = true) | ||||||
@Composable | ||||||
fun GreetingPreview() { | ||||||
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. Preview 의 경우 private 키워드 붙이는 걸 제안드립니다! 다른 파일에서 해당 Preview를 참조할 수 있으면 안되겠죠? 😉
Suggested change
|
||||||
SignupTheme { | ||||||
Greeting("Android") | ||||||
TextFieldScreen() | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,69 @@ | ||||||
package nextstep.signup.ui | ||||||
|
||||||
import androidx.annotation.StringRes | ||||||
import androidx.compose.foundation.layout.PaddingValues | ||||||
import androidx.compose.foundation.layout.Spacer | ||||||
import androidx.compose.foundation.layout.fillMaxWidth | ||||||
import androidx.compose.foundation.layout.padding | ||||||
import androidx.compose.foundation.text.KeyboardOptions | ||||||
import androidx.compose.material3.Button | ||||||
import androidx.compose.material3.ButtonDefaults | ||||||
import androidx.compose.material3.MaterialTheme | ||||||
import androidx.compose.material3.Text | ||||||
import androidx.compose.material3.TextField | ||||||
import androidx.compose.material3.TextFieldDefaults | ||||||
import androidx.compose.runtime.Composable | ||||||
import androidx.compose.runtime.getValue | ||||||
import androidx.compose.runtime.mutableStateOf | ||||||
import androidx.compose.runtime.remember | ||||||
import androidx.compose.runtime.setValue | ||||||
import androidx.compose.ui.Modifier | ||||||
import androidx.compose.ui.res.stringResource | ||||||
import androidx.compose.ui.text.input.KeyboardType | ||||||
import androidx.compose.ui.unit.Dp | ||||||
import androidx.compose.ui.unit.dp | ||||||
import nextstep.signup.ui.theme.Blue50 | ||||||
|
||||||
@Composable | ||||||
fun TextView( | ||||||
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. 마찬가지로 XML TextView 와 헷갈릴 수 있을 것 같습니다! 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. 저는 StringRes 보다는 String 으로 값을 넘기는걸 선호합니다! 예시를 들어볼게요! TextView 의 경우 더더욱 기본 컴포넌트이기에 다른 컴포넌트에 비해 훨씬 유연성이 높아야한다고 생각해요! 따라서, String 타입으로 변경하는걸 제안드립니다 💪 |
||||||
@StringRes description: Int, | ||||||
) { | ||||||
Text( | ||||||
text = stringResource(id = description), | ||||||
style = MaterialTheme.typography.titleLarge, | ||||||
) | ||||||
} | ||||||
|
||||||
@Composable | ||||||
fun TextFieldView( | ||||||
paddingTop: Dp = 0.dp, | ||||||
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. 보통 paddingTop 과 같은 세부사항을 직접 넘기지 않고 Modifier를 사용하여 좀 더 유연하게 UI 설정을 할 수 있어요!
Suggested change
근데 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. 위와 마찬가지로 그래서, |
||||||
@StringRes label: Int, | ||||||
keyboardType: KeyboardType = KeyboardType.Text, | ||||||
) { | ||||||
var input by remember { mutableStateOf("") } | ||||||
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. TextView 가 직접 상태를 관리하는 대신 부모로 상태를 올려볼까요? 이를 상태 호이스팅기법이라 합니다. 질문1) 왜 상태 호이스팅을 해야할까요?? 힌트) 만약 유저 정보를 사용자에게 입력받아 서버로 보내려면 어떻게 해야될까요? 🤔 |
||||||
Spacer(modifier = Modifier.padding(top = paddingTop)) | ||||||
TextField( | ||||||
value = input, | ||||||
onValueChange = { input = it }, | ||||||
modifier = Modifier.fillMaxWidth(), | ||||||
label = { Text(text = stringResource(id = label)) }, | ||||||
keyboardOptions = KeyboardOptions(keyboardType = keyboardType), | ||||||
colors = TextFieldDefaults.colors(focusedIndicatorColor = Blue50), | ||||||
) | ||||||
} | ||||||
|
||||||
@Composable | ||||||
fun ButtonView( | ||||||
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 를 동시에 사용하는 프로젝트에서 컴포저블 함수가 |
||||||
@StringRes description: Int, | ||||||
paddingTop: Dp, | ||||||
) { | ||||||
Spacer(modifier = Modifier.padding(top = paddingTop)) | ||||||
Button( | ||||||
onClick = {}, | ||||||
modifier = Modifier.fillMaxWidth(), | ||||||
colors = ButtonDefaults.buttonColors(containerColor = Blue50), | ||||||
contentPadding = PaddingValues(vertical = 15.dp), | ||||||
) { | ||||||
Text(text = stringResource(id = description)) | ||||||
} | ||||||
} |
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.
1단계를 완벽하게 수행해주셨네요!