15_위시리스트(Scaffold 앱바커스텀/앱바 백버튼/OutlinedTextField 커스텀)

소정·2024년 8월 26일
0

Android_with_compose

목록 보기
15/17

핵심적으로 배울것
Scaffold?
top bar(상단바) 모양을 설정하는데 사용하는 composable, 이를 사용해 Floating >Action Button과 Bottom Bar(하단바)도 만들 수 있다

Room 데이터 베이스?
스마트폰에 데이터를 영구적으로 저장

1. 프로젝트에 사용할 것들 dependency

  1. kapt 키워드 대신 KSP 사용하기 위해 플러그인 추가
    참고) https://developer.android.com/build/migrate-to-ksp?hl=ko#kts
    1-1) 프로젝트 수준 gradle에 플러그인 추가
// 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")
}

  1. 사용할 것들 임플리먼트
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")
}

2. HomeView 및 Scaffold

Scaffold로 만들 수 있는 것들
topBar, bottomBar, snackbarHost, floatingActionButton 등을 만들수 있다

2-1) HomeView.kt 파일 생성

바를 만들 수 있는 Scaffold 함수를 사용하여 top bar를 만든다.
Scaffold는 paddingValues를 넘겨 받음

  1. 광고 화면 만들기
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)){

        }
    }
}

  1. 플로팅 버튼 추가
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)){

        }
    }
}


2-2) AppBar.kt 생성

topbar의 모양을 담당하는 파일, 표시할 타이틀과 버튼클릭 이벤트 가짐
TopAppBar 사용하여 모양을 잡는다

  1. 앱바 설정하기
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 =
    )
}

  1. 앱바에 뒤로가기 버튼 추가 & 홈 빼고 다른 화면에서만 보이도록 설정
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
    )
}

3. 플로팅 버튼 클릭 시 생성될 wish item

  1. list row에 표시될 data class 생성
package com.lululalal.wishlist.data

data class Wish(
    val id : Long =0L,
    val title:String,
    val description:String
)

  1. 카드뷰 모양 만들기
//항목 이미지 만들기
@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)
        }
    }
}

  1. 더미 데이터 넣어서 표시

더미 데이터를 만들고 화면에 어떻게 표시하는 지 확인하기

// 더미 데이터 생성
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) {

                }
            }
        }
    }
}

4. 화면 전환을 위한 navigation 준비

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)
        }
    }

}

Screen.kt

navigation에 등록할 화면 목록

package com.lululalal.wishlist

sealed class Screen(val route:String) { //navigation을 위해 사용할 화면 등록
    //sealed - 상속 불가능 하도록
    object HomeScreen : Screen("home_screen")
    object AddScreen : Screen("add_screen")
}

mainActivity.kt

메인 함수가 로드되면 네비게이션을 호출해 화면 부르기

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()
                }
            }
        }
    }
}

5. WishViewModel 작성과 AddEditDetailView.kt 작성

5-1) WishViewModel

데이터와 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
    }

}

5-2)AddEditDetailView.kt

위시 리스트 추가 및 수정 버튼 클릭 시 보일 화면 작성

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", {})
}

6. 홈뷰와 작성뷰 연동

6-1) HomeView에 navController를 매개변수로 추가 및 floatingActionButton버튼 클릭 시 이동 추가

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)
        }
    }
}

6-2) Navigation에 HomeView 매개변수 빈 부분 수정

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)
        }
    }

}
profile
보조기억장치

0개의 댓글