핵심적으로 배울것
Scaffold?
top bar(상단바) 모양을 설정하는데 사용하는 composable, 이를 사용해 Floating >Action Button과 Bottom Bar(하단바)도 만들 수 있다Room 데이터 베이스?
스마트폰에 데이터를 영구적으로 저장
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath ("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
}
}
plugins {
id("com.android.application") version "8.1.3" apply false
id("com.android.library") version "8.1.3" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.google.devtools.ksp") version "1.8.21-1.0.11" apply false
}
1-2) 앱 수준 gradle에 플러그인 추가
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
dependencies {
val nav_version = "2.7.7"
// val compose_version = "1.6.0-alpha08"
val room = "2.6.1"
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
implementation("androidx.activity:activity-compose:1.9.1")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation ("androidx.navigation:navigation-compose:$nav_version")
//room
ksp("androidx.room:room-compiler:$room") //플러그인 추가한 ksp 사용
implementation("androidx.room:room-ktx:$room")
implementation("androidx.room:room-runtime:$room")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Scaffold로 만들 수 있는 것들
topBar, bottomBar, snackbarHost, floatingActionButton 등을 만들수 있다
바를 만들 수 있는 Scaffold 함수를 사용하여 top bar를 만든다.
Scaffold는 paddingValues를 넘겨 받음
package com.lululalal.wishlist
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeView() {
Scaffold(
topBar = { AppBarView("WishList") }
) {
LazyColumn(modifier = Modifier.fillMaxSize().padding(it)){
}
}
}
package com.lululalal.wishlist
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeView() {
val context = LocalContext.current
Scaffold(
topBar = { AppBarView("WishList", {
Toast.makeText(context, "클릭", Toast.LENGTH_SHORT).show()
}) },
floatingActionButton = {
FloatingActionButton(
modifier = Modifier.padding(all=20.dp),
contentColor = Color.White,
backgroundColor = Color.Black,
onClick = { /* TODO 화면 추가 또는 수정 */}) {
Icon(imageVector = Icons.Default.Add, contentDescription = "추가")
}
}
) {
LazyColumn(modifier = Modifier
.fillMaxSize()
.padding(it)){
}
}
}
topbar의 모양을 담당하는 파일, 표시할 타이틀과 버튼클릭 이벤트 가짐
TopAppBar 사용하여 모양을 잡는다
package com.lululalal.wishlist
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.TopAppBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class) //TopAppBar 사용하기 위해서 추가해야됨
@Composable
fun AppBarView(
title:String,
onBackNavClicked:() -> Unit = {} //초기에는 아무것도 안할거라 비워둠
) {
TopAppBar(
title = {
Text(text = title,
color = colorResource(id = R.color.white),
modifier = Modifier
.padding(start = 4.dp)
.heightIn(max = 24.dp))
},
elevation = 3.dp,
backgroundColor = colorResource(id = R.color.app_bar_color),
// navigationIcon =
)
}
package com.lululalal.wishlist
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class) //TopAppBar 사용하기 위해서 추가해야됨
@Composable
fun AppBarView(
title:String,
onBackNavClicked:() -> Unit = {} //초기에는 아무것도 안할거라 비워둠
) {
//나중에 이 이아콘을 보이거나 보이지 않도록 수정하기 위해 따로 정의
val navigationIcon : (@Composable () -> Unit)? = {
//필요할 때만 보이도록 설정
if (!title.contains("WishList")) {
IconButton(onClick = { onBackNavClicked() }) {
Icon(
imageVector = Icons.Default.ArrowBack,
tint = Color.White,
contentDescription = "home"
)
}
} else {
null
}
}
TopAppBar( //TopAppBar를 따로 관리하면 유연함을 확보할 수 있음
title = {
Text(text = title,
color = colorResource(id = R.color.white),
modifier = Modifier
.padding(start = 4.dp)
.heightIn(max = 24.dp))
},
elevation = 3.dp,
backgroundColor = colorResource(id = R.color.app_bar_color),
navigationIcon = navigationIcon
)
}
package com.lululalal.wishlist.data
data class Wish(
val id : Long =0L,
val title:String,
val description:String
)
//항목 이미지 만들기
@Composable
fun WishItem(wish: Wish, onClick: () -> Unit) {
//onClick 하면 디테일 페이지로 이동
Card(modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, start = 8.dp, end = 8.dp)
.clickable {
onClick()
},
elevation = 10.dp,
backgroundColor = Color.White,
) {
Column(modifier = Modifier
.padding(16.dp)) {
Text(text = wish.title, fontWeight = FontWeight.ExtraBold)
Text(text = wish.description)
}
}
}
더미 데이터를 만들고 화면에 어떻게 표시하는 지 확인하기
// 더미 데이터 생성
package com.lululalal.wishlist.data
data class Wish(
val id : Long =0L,
val title:String,
val description:String
)
object DummyWish {
val wishList = listOf<Wish>(
Wish(1,"aaaa","aaaaaaaaaaaaaaaaa"),
Wish(2,"bbbbb","test"),
Wish(3,"ccccc","test test test"),
Wish(4,"dddd","testtesttesttesttest"),
)
}
// 홈 뷰에 더미 데이터 LazyColumn에 호출
// HomeView.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeView() {
val context = LocalContext.current
Scaffold(
topBar = { AppBarView("WishList", {
Toast.makeText(context, "클릭", Toast.LENGTH_SHORT).show()
}) },
floatingActionButton = {
FloatingActionButton(
modifier = Modifier.padding(all=20.dp),
contentColor = Color.White,
backgroundColor = Color.Black,
onClick = { /* TODO 화면 추가 또는 수정 */}) {
Icon(imageVector = Icons.Default.Add, contentDescription = "추가")
}
}
) {
LazyColumn(modifier = Modifier
.fillMaxSize()
.padding(it)){
items(DummyWish.wishList) {
wish -> WishItem(wish = wish) {
}
}
}
}
}
NavHost와 NavController를 포함한 Composable
package com.lululalal.wishlist
import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
import androidx.lifecycle.viewmodel.compose.viewModel //네비게이션에서 사용할 뷰모델 임포트는 이거
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
//NavHost와 NavController를 포함한 Composable
@Composable
fun Navigation(viewModel: WishViewModel = viewModel(),
navController: NavHostController = rememberNavController()) {
//NavController에 초기상태 rememberNavController()를 전달 해둠
//기본적으로 ViewModel을 쓰고 생성하는 navController 객체들을 기억하라고 명령해둠
NavHost(
navController = navController,
startDestination = Screen.HomeScreen.route
) {
//화면이 될 composable 추가
composable(Screen.HomeScreen.route) {
HomeView(navController, viewModel)
}
composable(Screen.AddScreen.route) {
AddEditDetailView(id = 0L, wishViewModel = viewModel, navController = navController)
}
}
}
navigation에 등록할 화면 목록
package com.lululalal.wishlist
sealed class Screen(val route:String) { //navigation을 위해 사용할 화면 등록
//sealed - 상속 불가능 하도록
object HomeScreen : Screen("home_screen")
object AddScreen : Screen("add_screen")
}
메인 함수가 로드되면 네비게이션을 호출해 화면 부르기
package com.lululalal.wishlist
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.lululalal.wishlist.ui.theme.WishListTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WishListTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Navigation()
}
}
}
}
}
데이터와 UI간의 소통 책임 - 데이터 저장, 로드, 수집, 수정 등등
mutableStateOf사용 하려면 ->getValue와 setValue import해야한다
package com.lululalal.wishlist
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class WishViewModel:ViewModel() {
//데이터와 UI간의 소통 책임 - 데이터 저장, 로드, 수집, 수정 둥둥
var wishTitleState by mutableStateOf("")
// mutableStateOf사용 ->getValue와 setValue import
var wishDescriptionState by mutableStateOf("")
fun onWishTitleChanged(newString: String) { //외부에서 받아온 string
wishTitleState = newString // mutableStateOf 타입인 변수에 덮어쓰기
}
fun onWishDescriptionChanged(newString: String) {
wishDescriptionState = newString
}
}
위시 리스트 추가 및 수정 버튼 클릭 시 보일 화면 작성
WishTextField라는 함수를 만들어 OutlinedTextField를 커스텀 하여 사용해보기
AddEditDetailView 함수에서 viwemodel에 작성한 사용자에게 받아 서버에 저장할 값들을 셋팅한다
AppBar 안에 작성한 뒤로가기 버튼 클릭 시 실행 할 행동은 AddEditDetailView의 Scaffold 안에 작성함.
package com.lululalal.wishlist
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddEditDetailView(
id : Long,
wishViewModel: WishViewModel,
navController: NavController) {
Scaffold( // AppBar의 onBackNavClicked()은 Scaffold에 작성
topBar = { AppBarView(title =
if (id != 0L) stringResource(id = R.string.update_wish)
else stringResource(id = R.string.add_wish)
)
{
navController.navigateUp()
// navigateUp() => HomeView로 돌아감
// 사용자를 이전에 있던 화면으로 돌아가게 하는 것을 의도
}
}
) {
Column(
modifier = Modifier
.padding(it)
.wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.height(10.dp))
WishTextField("title",
value = wishViewModel.wishTitleState,
onValueChanged = { //onValueChanged -> 일어날 일 적기
wishViewModel.onWishTitleChanged(it)
//it = editText에 사용자가 쓴 값
})
Spacer(modifier = Modifier.height(10.dp))
WishTextField("Description",
value = wishViewModel.wishDescriptionState,
onValueChanged = {
wishViewModel.onWishDescriptionChanged(it)
//it = editText에 사용자가 쓴 값
})
Spacer(modifier = Modifier.height(20.dp))
Button(onClick =
{ /*데이터 데이스 Room에 저장하기*/
if (wishViewModel.wishTitleState.isNotEmpty()
&& wishViewModel.wishDescriptionState.isNotEmpty()) {
//기존 데이터 업데이트
} else {
//데이터 추가
}
},
modifier = Modifier.fillMaxWidth()) {
//버튼 텍스트를 표시하되 뭔가를 받아왔는지에 따라 달라질것
Text(
text =
if (id != 0L) stringResource(id = R.string.update_wish)
else stringResource(id = R.string.add_wish),
style = TextStyle(
fontSize = 18.sp
)
)
}
}
}
}
@Composable
fun WishTextField(
label:String,
value:String,
onValueChanged:(String) -> Unit //텍스트에 보이는 것을 수정할 수 있도록해줌
) {
OutlinedTextField(value = value,
onValueChange = onValueChanged,
label = {
Text(text = label,
color = Color.Black)
},
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), //키보드 타입 지정 (이메일, 텍스트, 넘버 등등)
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = Color.Blue,
focusedBorderColor = Color.Green,
unfocusedBorderColor = Color.Black,
cursorColor = Color.Magenta,
focusedLabelColor = Color.Green,
unfocusedLabelColor = Color.Black
) //개별 색성 정의 가능 : 텍스트 색상, 테두리 강조 색상, 테두리 강조 취소 색상 등
)
}
@Preview
@Composable
fun WishTextFieldPrev() {
WishTextField("title","aaa", {})
}
package com.lululalal.wishlist
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.lululalal.wishlist.data.DummyWish
import com.lululalal.wishlist.data.Wish
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeView(
navController: NavController,
viewModel: WishViewModel
) {
val context = LocalContext.current
Scaffold(
topBar = { AppBarView("WishList", {
}) },
floatingActionButton = {
FloatingActionButton(
modifier = Modifier.padding(all=20.dp),
contentColor = Color.White,
backgroundColor = Color.Black,
onClick = {
navController.navigate(Screen.AddScreen.route)
}) {
Icon(imageVector = Icons.Default.Add, contentDescription = "추가")
}
}
) {
LazyColumn(modifier = Modifier
.fillMaxSize()
.padding(it)){
items(DummyWish.wishList) {
wish -> WishItem(wish = wish) {
}
}
}
}
}
//항목 이미지 만들기
@Composable
fun WishItem(wish: Wish, onClick: () -> Unit) {
//onClick 하면 디테일 페이지로 이동
Card(modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, start = 8.dp, end = 8.dp)
.clickable {
onClick()
},
elevation = 10.dp,
backgroundColor = Color.White,
) {
Column(modifier = Modifier
.padding(16.dp)) {
Text(text = wish.title, fontWeight = FontWeight.ExtraBold)
Text(text = wish.description)
}
}
}
package com.lululalal.wishlist
import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
import androidx.lifecycle.viewmodel.compose.viewModel //네비게이션에서 사용할 뷰모델 임포트는 이거
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
//NavHost와 NavController를 포함한 Composable
@Composable
fun Navigation(viewModel: WishViewModel = viewModel(),
navController: NavHostController = rememberNavController()) {
//NavController에 초기상태 rememberNavController()를 전달 해둠
//기본적으로 ViewModel을 쓰고 생성하는 navController 객체들을 기억하라고 명령해둠
NavHost(
navController = navController,
startDestination = Screen.HomeScreen.route
) {
//화면이 될 composable 추가
composable(Screen.HomeScreen.route) {
HomeView(navController, viewModel)
}
composable(Screen.AddScreen.route) {
AddEditDetailView(id = 0L, wishViewModel = viewModel, navController = navController)
}
}
}