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

[해나] 회원가입 미션 1, 2단계 제출합니다. #15

Open
wants to merge 9 commits into
base: hxeyexn
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
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를 활용
2 changes: 2 additions & 0 deletions app/.editorConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*.{kt,kts}]
ktlint_function_naming_ignore_when_annotated_with=Composable
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}
Expand Down
78 changes: 78 additions & 0 deletions app/src/androidTest/java/nextstep/signup/LayoutBasicsTest.kt
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 {
Copy link

Choose a reason for hiding this comment

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

1단계를 완벽하게 수행해주셨네요!

@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()
}
54 changes: 54 additions & 0 deletions app/src/androidTest/java/nextstep/signup/TestComposable.kt
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("클릭해주세요")
}
}
38 changes: 27 additions & 11 deletions app/src/main/java/nextstep/signup/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link

Choose a reason for hiding this comment

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

Surface 는 기본적으로 MaterialTheme.colorScheme.surface 로 지정되어있습니다!

MaterialTheme.colorScheme.background 로 color 를 바꿔주신 이유가 따로 있으실까요??

) {
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
Copy link

Choose a reason for hiding this comment

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

보통 Screen 은 화면 단위에 붙이는 네이밍이에요! 그러나 현재 Fragment? 같은 느낌으로 사용되고 있네요

화면 단위로 리팩토링해볼까요?

Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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() {
Copy link

Choose a reason for hiding this comment

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

Preview 의 경우 private 키워드 붙이는 걸 제안드립니다! 다른 파일에서 해당 Preview를 참조할 수 있으면 안되겠죠? 😉

Suggested change
fun GreetingPreview() {
private fun SignUpScreenPreview() {

SignupTheme {
Greeting("Android")
TextFieldScreen()
}
}
69 changes: 69 additions & 0 deletions app/src/main/java/nextstep/signup/ui/ViewComposable.kt
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(
Copy link

Choose a reason for hiding this comment

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

마찬가지로 XML TextView 와 헷갈릴 수 있을 것 같습니다!

Copy link

Choose a reason for hiding this comment

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

저는 StringRes 보다는 String 으로 값을 넘기는걸 선호합니다!
StringRes 의 경우 정적이기 때문에 동적으로 String 이 변환돼서 온다면 이를 해결할 방법이 없기 때문입니다.

예시를 들어볼게요!
만약, ViewModel 에서 userName을 보내주고 TextView 에서 userName를 보여주고 싶다고 해봅시다. 그런데 description 가 StringRes 라 표시해줄 방법이 없겠네요 😢

TextView 의 경우 더더욱 기본 컴포넌트이기에 다른 컴포넌트에 비해 훨씬 유연성이 높아야한다고 생각해요! 따라서, String 타입으로 변경하는걸 제안드립니다 💪

@StringRes description: Int,
) {
Text(
text = stringResource(id = description),
style = MaterialTheme.typography.titleLarge,
)
}

@Composable
fun TextFieldView(
paddingTop: Dp = 0.dp,
Copy link

@murjune murjune Oct 11, 2024

Choose a reason for hiding this comment

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

보통 paddingTop 과 같은 세부사항을 직접 넘기지 않고 Modifier 를 설정하고 넘겨줍니다!

Modifier를 사용하여 좀 더 유연하게 UI 설정을 할 수 있어요!

Suggested change
paddingTop: Dp = 0.dp,
modifier: Modifier = Modifier,

근데 Modifer 의 역할이 무엇일까요??

Copy link

Choose a reason for hiding this comment

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

위와 마찬가지로 TextFieldView 는 네이밍에서 유추해본다면 프로젝트 전역에서 사용할 수 있는 기본 컴포넌트라는 생각이 들어요.

그래서, SignUpTextField 로 네이밍 변경 제안드려봅니다 🤔

@StringRes label: Int,
keyboardType: KeyboardType = KeyboardType.Text,
) {
var input by remember { mutableStateOf("") }
Copy link

@murjune murjune Oct 11, 2024

Choose a reason for hiding this comment

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

TextView 가 직접 상태를 관리하는 대신 부모로 상태를 올려볼까요? 이를 상태 호이스팅기법이라 합니다.

질문1) 왜 상태 호이스팅을 해야할까요??
질문2) StateLess 컴포저블과 StateFull 컴포저블의 차이는 뭘까요? 그리고 어떤 차이가 있을까요?

힌트) 만약 유저 정보를 사용자에게 입력받아 서버로 보내려면 어떻게 해야될까요? 🤔

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(
Copy link

@murjune murjune Oct 11, 2024

Choose a reason for hiding this comment

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

View 와 Compose 를 동시에 사용하는 프로젝트에서 컴포저블 함수가 xxxView 라는 네이밍이라면 헷갈릴 것 같아요!
따라서, 저는 기존 전통 View 시스템과의 구분을 위해 Compose 에서는 View 라는 일반적으로 postfix 를 사용하지 않습니다.

@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))
}
}
4 changes: 3 additions & 1 deletion app/src/main/java/nextstep/signup/ui/theme/Color.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ val Pink80 = Color(0xFFEFB8C8)

val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Pink40 = Color(0xFF7D5260)

val Blue50 = Color(0xFF2196F3)
51 changes: 24 additions & 27 deletions app/src/main/java/nextstep/signup/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package nextstep.signup.ui.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
Expand All @@ -9,23 +8,20 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)

private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
private val DarkColorScheme =
darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
)

private val LightColorScheme =
lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
Expand All @@ -34,29 +30,30 @@ private val LightColorScheme = lightColorScheme(
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
*/
)

@Composable
fun SignupTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
content: @Composable () -> Unit,
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

darkTheme -> DarkColorScheme
else -> LightColorScheme
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
content = content,
)
}
Loading