본문 바로가기
Android

안드로이드 Jetpack Compose 기본 Composable 모아보기 Row / Column / Box / LazyColumn / Image / Card / Scaffold / TextField / Button / Navigation

by 봉이로그 2023. 12. 14.

Row (수평) Horizontal

Row는 CSS로 비유하면 flex box의 direction row와 동일하다.

Modifier로 속성을 지정할수 있다.

Row(
	modifier = Modifier.background(color = Color.Blue).fillMaxSize(),
) {
	Text("Hello")
	Text("World")
}

Column (수직) Vertical

Column은 CSS로 비유하면 flex box의 direction column과 동일하다.

row와 동일하게 Modifier로 속성을 지정한다.

 

예시

 Column (
 	modifier = Modifier
        .fillMaxSize()
    	.background(color = Color.Blue)
    	.padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalAggrangement = Arrangement.SpaceBetween,
 ) {
 	Text("Hello")
    Text("World")
 }


setContent {

val scrollState = rememberScrollState()

Column(
  modifier = Modifier
         .background(color = Color.Green)
         .fillMaxWidth()
         .verticalScroll(scrollState)
      ) {
           for (i in 1..50) {
              Text("텍스트 $i")
           }
        }
 }

 

Spacer

공간을 설정하는 컴포저블

 

Spacer(Modifier.width(16.dp))

 

LazyColumn

스크롤링이 제공되는 Column 컴포저블이다.

setContent {
	LazyColumn(
    	modifier = Modifier.bacakground(color = Color.Green).fillMaxWidth(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
    	item {
        	Text("Header")
        }
    
    	items(50) { index ->
        	Text("텍스트 $index")
        }
        
        item {
        	Text("Footer")
        }
    }
}

Box

Box는 XML로 비유하면 FrameLayout이라고 할수 있다.

 

Box안에 있는 컨텐츠들이 겹쳐서 나타나게 된다. 

중첩해서 사용해서 레이아웃에서 각 요소의 위치를 지정하여 활용한다.

Box에 contentAlignment가 지정되어있어도, 각 자식에 개별적으로 Modifier.align이나 Alignment가 지정되어있으면 최상위 부모Box의 contentAlignment가 무시된다.

그러나 propagateMinConstraints 속성(기본값 false)을 true로 하게 될시 자식 요소에 지정된 Alignment는 전부 무시되고 부모 요소의 contentAlignment가 자식 컴포즈에서 모두 적용되게 된다.

 

setContent {
	Box(
    	modifier = Modifier.background(color = Color.Green).fillMaxWidth().height(200.dp),
        contentAlignment = Alignment.TopStart
    ) {
    	Text("안녕")
        
       	Box(
        	modifier = Modifier.fillMaxSize().padding(16.dp),
            contentAlignment = Alignment.BottomEnd
        ) {
	        Text("헬로우 안드로이드")
        }
    }
}

 

Image/ Card

import androidx.compose.runtiem.getValue // by를 사용하기위해
import androidx.compose.runtiem.setValue // by를 사용하기위해

...
setContent {
 var isFavorite by rememberSaveable { // rememberSaveable를 사용해서 화면을 돌려도 state가 유지된다
 	mutableStateOf(false)
 }

	ImageCard(
	    modifier = Modifier
                    .fillMaxWidth(0.5f)
                    .padding(16.dp),
    	isFavorite = isFavorite,
    ) { favorite -> 
    	isFavorite = favorite
    }
}


@Composable
fun ImageCard(
	modifier: Modifier = Modifier,
	isFavorite: Boolean,
    onTabFavorite: (Boolean) -> Unit, // return은 없는 콜백함수
) {
//	val isFavorite = remember {
//  	mutableStateOf(false)
//   }
 
	Card(
    // 0.5f는 절반을 차지
    	modifier = modifier,
    	shape = RoundedCornerShape(8.dp),
        elevation = 5.dp,
    ) {
    	Box(
        	modifier = Modifier.height(200.dp),
        ) {
        	Image(
            	painter = painterResource(id = R.drawable.파일명),
                contentDescription = "이미지설명",
                contentScale = ContentScale.Crop,
            )
            Box(
            	modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.TopEnd,
            ) {
            	IconButton(onClick = { 
                	// isFavorite.value = !isFavorite.value
//                    isFavorite = !isFavorite // by를 사용해서 value를 생략가능
				onTabFavorite.invoke(!isFavorite); // invoke는 생략가능
                }) {
//                	Icon(imageVector = if(isFavorite.value) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
                   	Icon(imageVector = if(isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, // by를 사용해서 value를 생략가능
                    contentDescription = "아이콘 설명",
                    tint = Color.White,
                    )
                }
            }
            
        }
    }
}

 

 

이미지 카드 컴포저블의 상태는 내려가고, 이벤트는 올라가는 패턴의 단방향 데이터 흐름이다. 단방향 데이터 흐름을 따르면 UI에 상태를 표

시하는 컴포저블과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있다.

 

이 개념이 상태 호이스팅이다.

상태 호이스팅은 컴포저블을 Stateless로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴이다.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

 

 

TextField

...

class MainActivity: ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        setContent {
            val textValue : MutableState<String> = remember {
           		mutableStateOf("")
            }
        	Column(
            	modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
            	TextField(
                 value = textValue.value,
                 onValueChange = {
                 textValue.value = it
                 },
                )
                Button(onClick = { }) {
                	Text("클릭!!");
                }
            }
        }
    }
}

 

Scaffold

Material 디자인에 뼈대가 되는 컴포저블 

SnackBar나 FloatingActionButton을 사용하기위해서는 Sacffold를 감싸서 사용하게 된다.

scaffoldState는 scaffoldd의 상태이다.

rememberScaffoldState는 최근의 scaffold 상태를 저장해서 기억한다.

showSnackbar 함수는 suspend 함수로써 정지함수라고 한다. 이걸 실행하기위해선 코루틴을 사용해야한다.

 

...

class MainActivity: ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
        setContent {            
            // 구조분해
            val (text, setValue) = remember {
            mutableStateof("")
            }
            
            val scaffoldState = rememberScaffoldState()
            val scope = rememberCorutineScope()
            val keyboardController = LocalSoftwareKeyboardController.current

            Scaffold(
            	scaffoldState = scaffoldState,
            ) { Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
                ) {
                    TextField(
                    value = text,
                    onValueChange = setValue,
                    )
                        Button(onClick = {
                        keyboardController?.hide() // 키보드를 숨김
                        scope.luaunch { //코루틴 안에서 suspend함수 실행
                        	scaffoldState.snackbarHostState.showSnackbar("Hello $text")
                            }
                        }) {
                        Text("클릭!!");
                        }
                }
            }
        }
    }
}

 

Navigation

모듈수준의 build.gradle(:app)에 dependencies 추가

build.gradle(:app)

...

dependencies {
    val nav_version = "2.5.3"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

 

...

class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        setContent {
        	val navController = rememberNavController()
            
            NavHost(
            navController = navController,
            startDestination = "first",
            ) {
            	composable("first") {
                	FirstScreen(navController)
                }
            	composable("second") {
                	SecondScreen(navController)
                }                
                // 넘겨받을 값을 중괄호로
            	composable("third/{value}") { backStackEntry ->
                	ThridScreen(
                        navController = navController,
                        value = backStackEntry.arguments?.getString("value") ?: "",
                    )
                }                
            }
        }
    }
}

@Composable
fun FirstScreen(navController : NavController) {
	val (value, setValue) = remember {
    	mutableStateOf("")
    }

	Column(
    	modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
    	Text(text = "첫화면")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick ={ 
        	navController.navigate("second")
        }) {
        	Text("두번째 !")
        }
        Spacer(modifer = Modifier.height(16.dp))
        TextField(value = value, onValueChange = setValue)
        Button(onClick ={
             	if(value.isNotEmpty()) {
            		navController.navigate("third/$value")
            	}
        }) {
        	Text("세번째 !")
        }        
    }
}

@Composable
fun SecondScreen(navController : NavController) {
	Column(
    	modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
    	Text(text = "두번째 화면")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick ={ 
            // 둘중 하나 사용
            navController.navigateUp()
            navController.popBackStack()
        }) {
        	Text("뒤로가기 !")
        }
    }
}

@Composable
fun ThirdScreen(navController : NavController,value: String) {
	Column(
    	modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
    	Text(text = "세번째 화면")
        Spacer(modifier = Modifier.height(16.dp))
        Text(value)
        Button(onClick ={
            navController.navigateUp()
        }) {
        	Text("뒤로가기 !")
        }        
    }
}

 

Reference

https://www.youtube.com/watch?v=kFOZKIwn5KU&list=PLxTmPHxRH3VV8lJq8WSlBAhmV52O2Lu7n&index=3