와챠의 우당탕탕 코딩 일기장

[Compose] 1-1. Jetpack Compose Basics 본문

코딩 일기장/Android(Kotlin)

[Compose] 1-1. Jetpack Compose Basics

minWachya 2025. 3. 26. 15:49
반응형

JetPack Compose 공부를 해보려고 한다.(드디어~!)

Android Developer가 제공해주는 JetPack Compose Basics 1 강의를 따라서 공부해봤다.

 

<목차>

  1. Compose의 정의
  2. Modifier의 정의
  3. 컴포저블 함수의 state를 관리하는 방법
  4. 성능 기준에 맞는 목록을 만드는 방법
  5. 애니메이션을 추가하는 방법
  6. 앱 스타일과 테마를 지정하는 방법
  7. 기타: 참고하면 좋을 사이트들

아래는 목차에 따른 강의 내용을 정리해본 것이다.


1. Compose의 정의

  • @Composable 어노테이션을 fun 앞에 붙임으로써 컴포저블 함수로 만들 수 있다.
    • @Composable: 지속적으로 UI를 업데이트하고 유지관리하기 위해 함수에 특수 지원을 추가하도록 Compose에 알려주는 역할
  • 컴포저블 함수 내에서 다른 컴포저블 함수를 호출할 수 있다.
  • UI를 만들 수 있다.
import androidx.compose.material3.Text

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

 


2. Modifier의 정의

  • Modifier는 1. UI 구성 요소를 꾸밀 수 있고, 2. 상호작용을 추가할 수 있다.
    • 아래는 공식문서에서 Modifier를 설명한 것: 
    • 컴포저블의 크기, 레이아웃, 동작 및 모양 변경
    • 접근성 라벨과 같은 정보 추가
    • 사용자 입력 처리
    • 요소를 클릭 가능, 스크롤 가능, 드래그 가능 또는 확대/축소 가능하게 만드는 높은 수준의 상호작용 추가
  • 아래 예제에선 padding을 사용하여 UI에 padding을 추가한 모습이다.
  • 여러 modifier의 목록을 확인하고 싶다면 Compose modifier 목록을 참고하면 좋을 것 같다.
import androidx.compose.material3.Surface
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}


3. 컴포저블 함수의 state를 관리하는 방법

  • 위 영상처럼 클릭 상태를 관리하려면? recomposition에도 영향을 받지 않는 remember를 사용해 state를 관리해야 한다.
  • recomposition
    • recomposition: @Composable 함수가 데이터를 UI로 변환하는 과정이다. 데이터가 변경되면 Compose는 새 데이터로 UI를 업데이트한다.
    • remember: recomposition을 방지하는 데 사용된다. 즉, recomposition되어도 상태가 유지된다.
    • (회전, 다크모드로 변경, bottom까지 스크롤 후 top으로 이동 등의) 변경 사항에서도 state를 유지하기 위해서는 rememberSaveable를 사용하면 된다.
import androidx.compose.runtime.saveable.remember

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

 

  • State Hoisting(상태 호이스팅)
    • State(상태) Hoisting(끌어올리기): 상태를 관리하는(Stateful) Compose을 상태를 관리하지 않도록(Stateless) 만들기 위한 디자인 패턴이다. 즉, 자식 Compose의 state를 부모(호출하는 곳)로 끌어올리는 것이다.
    • 상태를 호이스팅할 때의 장점
      • 상태가 중복되지 않음
      • 버그 발생 방지
      • 컴포저블 재사용 가능
      • 테스트 쉬워짐 
    •  + by: 위임: 매번 .value(getter)를 입력할 필요가 없도록 해줌
@Composable
fun MyApp(modifier: Modifier = Modifier) {
	
    // 관리하는 상태 끌어올리기(부모에 위치시켜서 자식에게 전달)
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
        	// 자식에게 전달
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier
                .padding(vertical = 24.dp),
            // 전달받은 상태 사용
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

 


4. 성능 기준에 맞는 목록을 만드는 방법

 

  • 기존에선 이런 list를 만들기 위해 RecyclerView를 사용했었다. Compose에서 이와 같은 역할을 하는 것은 LazyColumn, LazyRow이다! 위 예제에서는 LazyColumn을 사용했다.
  • LazyColumn은 필요한 데이터만 렌더링하여 메모리 사용을 줄여 성능을 향상시킨다. 이는 대량의 데이터를 다룰 때 효과적이다.
    • RecyclerView와의 차이점:
      • LazyColumn: 스크롤 시에만 아이템을 렌더링하여 메모리 사용 최적화함
      • RecyclerView: 뷰 재사용을 통해 메모리 관리를 지원하지만, 여전히 모든 아이템을 메모리에 로드 및 유지해야함
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(100) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

5. 애니메이션을 추가하는 방법

  • 애니메이션을 적용하기 위해 animateDpAsState 컴포저블을 사용한다.
  • animateDpAsState은 애니메이션이 완료될 때까지 객체의 value가 계속 업데이트되는 state를 반환한다. (dp값 사용)
  • animateDpAsState의 매개변수인 animationSpec에서 애니메이션을 맞춤설정할 수 있다.
  • Compose의 애니메이션: 컴포즈에서 사용할 수 있는 애니메이션. 다양한 유형의 애니메이션을 참고할 수 있음
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

	// expanded에 따라 애니메이션 적용되는 부분
    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        // 애니메이션 맞춤 설정
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                // expanded에 따른 애니메이션 적용
                .padding(bottom = extraPadding.coerceAtLeast(0.dp)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

 


6. 앱 스타일과 테마를 지정하는 방법

앱 스타일 지정

  • ui/theme/Theme.kt 파일의 (패키지 이름)Theme을 보면, MaterialTheme을 사용하는 것을 알 수 있다.
  • MaterialTheme Material 디자인 원칙을 반영한 컴포저블 함수다.
  • UI에서 아래와 같이 (패키지 이름)Theme을 사용함으로써 스타일을 적용한다.
  • 즉, Theme을 수정하면 앱 스타일을 변경할 수 있다.
    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }
  • MaterialTheme은 세 가지 속성을 가지고 있다: colorScheme, typography, shapes
  • 이 중 typography를 사용하여 텍스트 스타일을 변경해보면 다음 코드와 같다.

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                // 이 부분!
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }
  • 해당 스타일에서 조금만 변경하고 싶다면, copy 함수를 사용하면 된다.
import androidx.compose.ui.text.font.FontWeight

Text(
    text = name,
    // 기존 headlineMedium에서 fontWeight만 변경
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

 

  • 다크 모드 테스트해보려면 @Preview에 UI_MODE_NIGHT_YES 를 추가하면 된다.

import android.content.res.Configuration.UI_MODE_NIGHT_YES

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES, 	// 이 부분!
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

 

앱 테마 지정

  • 테마(다크모드, 기본모드)를 지정하려면 먼저 ui/theme 폴더에 있는 Color.kt에 사용할 색상을 추가한 뒤, (패키지 이름) Theme에서 아래 코드와 같이 색상을 지정해주면 된다.
  • 색상이 잘 적용됐는지 확인해보려면 미리보기가 아닌, 앱을 실제로 실행해보아야 한다. (미리보기에는 동적 색상이 사용되기 때문)
private val DarkColorScheme = darkColorScheme(
	// Blue, Navy... => Colors.kt에서 지정한 색
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    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)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

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

7. 기타: 참고하면 좋을 사이트들

  • Material Design: 사용자 인터페이스와 환경을 만들기 위해 구글이 만든 디자인 시스템
    • Components 탭에서 다양한 컴포넌트들의 가이드라인들을 볼 수도 있고, 정확한 용어를 알 수 있어서 좋은듯!
  • Compose Material Catalog: Material Design 구성요소, 테마, Jetpack Compose 의 표준을 참조할 수 있는 앱
    • 다양한 컴포넌트들을 테마에 따라 볼 수 다르게 볼 수 있고, 직접 동작하는 예시들을 볼 수 있어서 앱 개발할 때 참고하기 조을듯요
  • Compose modifier 목록: 정렬, 애니메이션, 배치, 클릭 또는 스크롤 가능 여부 지정 등에 사용할 수 있는 여러 modifier의 전체 목록
  • Compose 이해: Compose에 대한 더 자세한 내용을 확인할 수 있다! 
  • Jetpack Compose용 Kotlin: JetPack Compose에서 kotlin이 어떻게 활용되는지 참고하기 조음
  • Compose의 애니메이션: 컴포즈에서 사용할 수 있는 애니메이션. 다양한 유형의 애니메이션을 참고할 수 있음

전체 코드

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
        	// BasicsCodelabTheme을 적용함
            BasicsCodelabTheme { // fillMaxSize: 너비, 높이 모두 채우기
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
	// rememberSaveable: 상태 변경에도 유지 가능한 변수
    // by: 위임: Delegate Pattern을 자동으로 구현해줌
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
	
    // color: 배경색으로 MaterialTheme~을 따름
    Surface(modifier, color = MaterialTheme.colorScheme.background) {
    	// 앱 처음 시작 시 온보딩 화면을 보여줌
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

// 온보딩 화면
@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            // onClick 메서드 상속.
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(100) { "$it" }
) {
	// LazyColumn: 필요한 데이터만 렌더링하여 메모리 사용을 줄임 => 스크롤 성능 향상
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by rememberSaveable { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name, style = MaterialTheme.typography.headlineMedium.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }
            )
        }
    }
}

// ------프리뷰----------

@Preview(
    showBackground = true,		// true: 뒷 배경을 흰 색으로
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,	// UI_MODE_NIGHT_YES: 다크 모드 확인
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

후기

음냐... flutter랑 비슷한 느낌? 아니 비슷하다기보단 같은듯... 컴포넌트들만으로 개발을 한다는 것...

view 따로 실제 데이터 따로 관리하는 것에서 이렇게 한꺼번에 같이 개발할 수 있다는 게 넘 편한듯

글고 preview에서 실제 동작 바로 확인할 수 있는 거 이 기능이 왜 대체 지금 생긴 거지;;;;

그동안 컴파일때문에 버린 시간들이여(하지만 쉴 수 있어서 꿀이었음)...

반응형
Comments