16_위시리스트3_Room 사용법2(Graph,CRUD),swipe

소정·2024년 9월 9일
0

Android_with_compose

목록 보기
17/17

1. graph 사용

데이터 베이스 설정한 파일에 graph 사용할 것임

1.1) WishDatabase.kt

package com.lululalal.wishlist.data

import androidx.room.Database
import androidx.room.RoomDatabase

//데이터베이스 설정
@Database(
    entities = [Wish::class], //데이터 베이스 내부에 필요한 entity - 지금은 하나지만 여러개가 될 수 있음
    version = 1, //
    exportSchema = false //
)
abstract class WishDatabase : RoomDatabase() { //사용하고 있는 데이터베이스
    abstract fun WishDao() : WishDao //WishDao에 작성한 것에 접근 가능하도록 함
}

1.2) object 파일로 Graph 생성

  • 데이터베이스나 저장소의 인스턴스를 제공 및 초기화(initialize)하는 역할
  • 코틀린은 object 키워드를 사용하여 싱글톤(프로그램 내에서 단 하나의 인스턴스만 생성)을 선언

*** 그래프에서 할 일

  1. lateinit 키워드 사용해서 데이터베이스 선언(어떤 데이터 베이스인지)
  2. wishEwpository에 데이터 베이스 넘겨주기
    -> lazy 키워드를 통해 처음 사용할 때 초기화 되도록 선언 : 리소스 아껴줌
  3. databaseBuilder만들기
    -> 1에 선언한 데이터베이스에 인스턴스 만들어 할당

databaseBuilder?
- 데이터베이스의 인스턴스 생성
Room.databaseBuilder(Context, 데이터베이스에 사용할 클래스 : @Database 어노테이션을 달고 있고 RoomDatabase()를 상속 받고 있는 클래스 파일, 데이터베이스 이름 설정).build()

package com.lululalal.wishlist

import android.content.Context
import androidx.room.Room
import com.lululalal.wishlist.data.WishDatabase
import com.lululalal.wishlist.data.WishRepository

object Graph {
	//그래프에서 필요한거
    //1. lateinit 키워드 사용해서 데이터베이스 선언
    lateinit var database: WishDatabase
    //사용전에 초기화되도록 약속된 non-nullable한 데이터베이스 선언

    //2. wishRepository에 데이터 베이스 넘겨주기
    //lazy 키워드를 통해 처음 사용할 때 초기화 되도록 선언 -> 리소스 아껴줌
    val wishRepository by lazy {
        WishRepository(wishDao = database.wishDao())
    }

    //3. databaseBuilder만들기
    //데이터베이스의 인스턴스 생성 -> 인스턴스는 lateinit var database: WishDatabase에 할당됨
    //provide함수 : 호출 시 데이터베이스 초기화
    fun provide(context: Context) {
        database = Room.databaseBuilder(context,WishDatabase::class.java, "wishList.db").build()
       
//        @JvmStatic
//        public final fun <T : RoomDatabase> databaseBuilder(
//            context: Context,
//            klass: Class<T>,
//            name: String?
//        ): RoomDatabase. Builder<T>
//        Creates a RoomDatabase. Builder for a persistent database. Once a database is built, you should keep a reference to it and re-use it.
//        Params:
//        context - The context for the database. This is usually the Application context.
//        klass - The abstract class which is annotated with Database and extends RoomDatabase.
//        name - The name of the database file.
//        T - The type of the database class.
//        Returns:
//        A RoomDatabaseBuilder<T> which you can use to create the database.
    }

}

by lazy?
변수 초기화가 필요할 때에만 사용되도록함
즉 애플리케이션을 연 순간 모든 것을 처음부터 load하지않고 특정 시점에만 함
기본적으로 thread safety함 : 다양한 스레드의 접근을 받을 떄에만 초기화한다는 뜻


dependency injection(종속 항목 삽입)

  • 동일하게 코드 작업을 수행하기 위해 특정 정보 혹은 도구 필요, 동작할 때 필요한 것을 따로 찾지 않아도 찾아다 주는 헬퍼
  • 앱의 변경 사항을 더 쉽게 관리할 수 있도록 도와줌(수정사항을 찾기 위해 앱 전체를 보지 않아도 됨)

1.3) Graph 사용하기

  • 데이터베이스 생성자는 global context(Application클래스가 이 글로벌 콘텍스트 가지고 있다)에서 호출해야함
    -> 앱 전체에서 사용할 수 있어야하기 때문
  • 애플리케이션 클래스를 사용하려면 새 클래스를 생성하여 이를 서브 클래스로 만들어야함
    -> Application()을 상속받은 class파일 만들기
package com.lululalal.wishlist

import android.app.Application

class WishListApp : Application() {

    override fun onCreate() {
        super.onCreate()
        Graph.provide(this)
    }

}

1.4) 프로젝트가 application class 인식하게 하기

  • 매니패스트에 등록하기
  • 앱이 db를 생성하려고 할 때마다 충돌이 발생하지 않도록하려면 application tag 내에서 이 작업을 수행해야됨


1.5) viewModel에 Repository 전달하기

만들어둔 듀 모델 매개변수에 초기상태로 Graph에 작성해둔 리포지포리 넣어주기

class WishViewModel(
    //pass로 전달하는 WishRepository를 따로 설정할 필요가 없도록 초기 상태를 설정
    private val wishRepository: WishRepository = Graph.wishRepository
):ViewModel() {

	내부 코드는 1에 있음
}

1.6) AddEditDetailView수정

  • RoomDatabase에 데이터 저장되도록 수정

스낵바 사용해보기
1. 스낵바 변수 선언

	val snackMessage = remember {
        mutableStateOf("")
    }

AddEditDetailView.kt 수정

  1. 데이터 베이스에 데이터 저장하는 작업 등 비동기로 실행하기 위한 스코프 변수 선언
val scope = rememberCoroutineScope()
  1. Scaffold State 변수 선언 및 Scaffold에 상태 유지용 매개변수로 넣기
    -> Scaffold : UI의 외관에 대한 디테일이 포함되어 있는 부분
    -> UI 상태를 유지하기 위한 변수
    -> scaffoldState 사용하기 위해 import androidx.compose.material.Scaffold 임포트
val ScaffoldState = rememberScaffoldState()

Scaffold( // AppBar의 onBackNavClicked()은 Scaffold에 작성
        scaffoldState = scaffoldState, //Scaffold에 상태 유지용 매개변수로 넣기
        topBar = { AppBarView(title =
                if (id != 0L) stringResource(id = R.string.update_wish)
                else stringResource(id = R.string.add_wish)
            )
            {
                navController.navigateUp()
                // navigateUp() => HomeView로 돌아감
                // 사용자를 이전에 있던 화면으로 돌아가게 하는 것을 의도
            }
        }
    ) {
    	...
    }

  1. 추가 수정 역할하는 버튼에 위에 선언한 변수를 사용하여 DB에 데이터 추가하기 & 추가 후 페이지 이동
Button(onClick =
                { /*데이터 데이스  Room에 저장하기*/
                    if (wishViewModel.wishTitleState.isNotEmpty()
                        && wishViewModel.wishDescriptionState.isNotEmpty()) {
                        if (id != 0L) {
                            //TODO 기존 데이터 업데이트
                        } else {
                            //TODO 데이터 추가
                            wishViewModel.addWish(
                                Wish(
                                    title = wishViewModel.wishTitleState.trim(),
                                    description = wishViewModel.wishDescriptionState.trim()
                                )
                            )
                            snackMessage.value = "등록 완료"
                        }
                    } else {
                        //TODO 필드 작성 & wish 항목 생성
                        snackMessage.value = "항목 생성을 위해 필드를 작성하세요"
                    }

                    //생성 후 페이지 이동
                    scope.launch {
                        scaffoldState.snackbarHostState.showSnackbar(snackMessage.value)
                        navController.navigateUp()
                    }
                },
                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
                    )
                )
            }

1.7) list 목록 DB에서 읽어오기

HomeView.kt 파일에 list 만드는 부분인 LazyColumn 수정하기

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 = "추가")
            }
        }
    ) {
        //위시 리스트를 가져오기
        val wishList = viewModel.getAllWishes.collectAsState(initial = listOf()) //빈 목록으로 초기화

        LazyColumn(modifier = Modifier
            .fillMaxSize()
            .padding(it)){
                items(wishList.value) {
                    wish -> WishItem(wish = wish) {
                }
            }
        }
    }
}

1.8) 상세보기 화면

  • 리스트를 클릭하면 이동해야하기 때문에 HomeView.kt에서 작업
  • HomeView에서 리스트 클릭 시 navigaion을 통해 해당 디테일 페이지로 이동

1) HomeView.kt
floating 버튼 클릭할 땐 "+/0L" 붙여서 보내고 리스트 클릭할 땐 "+/id" 로 보내서 항상성 유지

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+"/0L")
                }) {
                Icon(imageVector = Icons.Default.Add, contentDescription = "추가")
            }
        }
    ) {
        //위시 리스트를 가져오기
        val wishList = viewModel.getAllWishes.collectAsState(initial = listOf()) //빈 목록으로 초기화

        LazyColumn(modifier = Modifier
            .fillMaxSize()
            .padding(it)){
                items(wishList.value) {
                    wish -> WishItem(wish = wish) {
                        val id = wish.id
                        navController.navigate(Screen.AddScreen.route + "/$id")
                }
            }
        }
    }
}

2) Navigation.kt
NavHost 안 화면이 될 composable route수정 및 arguments 추가
get방식으로 보내지는 주소로 변경 및 쿼리 변수의 속성 지정

NavHost(
        navController = navController,
        startDestination = Screen.HomeScreen.route
    ) {
        //화면이 될 composable 추가
        composable(Screen.HomeScreen.route) {
            HomeView(navController, viewModel)
        }

        composable(Screen.AddScreen.route+"/{id}",
            arguments = listOf(
                navArgument("id") {
                    type = NavType.LongType
                    defaultValue = 0L
                    nullable = false
                }
            )
            ) { entry ->
            val id = if(entry.arguments != null) entry.arguments!!.getLong("id") else 0L
            AddEditDetailView(id = id, wishViewModel = viewModel, navController = navController)
        }
    }

3) AddEditDetailView.kt
id != 0L일 때 기존 데이터 보여주고 수정 하기

3-1. fun AddEditDetailView에 id로 wish 가져오는 경우와 0L로 가져오는 경우 viewModel의 값 셋팅하는 부분 추가

//wishViewModel 셋팅하는 부분 추가 
if (id != 0L) {
        val wish = wishViewModel.getByWishId(id).collectAsState(initial = Wish(0L,"","")) //초기값 설정
        wishViewModel.wishTitleState = wish.value.title
        wishViewModel.wishDescriptionState = wish.value.description
    } else {
        wishViewModel.wishTitleState = ""
        wishViewModel.wishDescriptionState = ""
    }

3-2. 데이터 엡데이트 부분 수정
wishViewModel에작성되어 있는 updateWish을 사용하여 업데이트

Button(onClick =
                { /*데이터 데이스  Room에 저장하기*/
                    if (wishViewModel.wishTitleState.isNotEmpty()
                        && wishViewModel.wishDescriptionState.isNotEmpty()) {
                        if (id != 0L) {
                            //TODO 기존 데이터 업데이트
                            wishViewModel.updateWish(
                                Wish(
                                    id = id,
                                    title = wishViewModel.wishTitleState.trim(),
                                    description = wishViewModel.wishDescriptionState.trim()
                                )
                            )
                        } else {

1.9) 스와이프 사용

1) 스와이프 이용한 데이터 삭제

  • HomeView.kt에서 리스트를 스와이프시 삭제하는 로직 추가
  • dismissState 추가 하여 dismiss 상태 확인 하도록함
  • items에 key 설정하여 원하는 item이 삭제되도록 한다
LazyColumn(modifier = Modifier
            .fillMaxSize()
            .padding(it)){
                //items에 key 설정하면 원하는 item이 삭제가 됨
                items(wishList.value, key={wish->wish.id}) {
                    wish ->
                    //스와이프 삭제
                    //삭제를 위한 코드
                    val dismissState = rememberDismissState(
                        confirmStateChange = {
                            if(it == DismissValue.DismissedToEnd
                                || it == DismissValue.DismissedToStart) {
                                viewModel.deleteWish(wish)
                            }
                            true //상태변화 확인
                        }
                    )

                    SwipeToDismiss(
                        state = dismissState,
                        background = {
                            //val => state에서 작동하기 위해 변수 var 아닌 val로 선언
                            val color by animateColorAsState(
                                if (dismissState.dismissDirection == DismissDirection.EndToStart) Color.Red else Color.Transparent,
                                label = ""
                            )
                            val alignment = Alignment.CenterEnd
                            Box(
                                modifier = Modifier
                                    .fillMaxSize()
                                    .background(color)
                                    .padding(horizontal = 20.dp),
                                contentAlignment = alignment //contentAlignment는 Box에 {}가 있어야만 됨
                            ) {
                                Icon(Icons.Default.Delete,
                                    contentDescription = "삭제",
                                    tint = Color.White)
                            }
                        },
                        //방향 - 왼쪽에서부터 쓸지 오늘쪽에서부터 쓸지 정하는 것
                        directions = setOf(DismissDirection.EndToStart),
                        //어느 시점에서 dismiss 할 지
                        dismissThresholds = {FractionalThreshold(1f)},
                        //쓸어 넘긴 후 삭제할 내용
                        dismissContent = {
                            WishItem(wish = wish) {
                                val id = wish.id
                                navController.navigate(Screen.AddScreen.route + "/$id")
                            }
                        }
                    )
            }
        }
    }

cf) 참고
https://tutorials.eu/storing-data-permanently-part-2-2-day-14-android-14-masterclass/

profile
보조기억장치

0개의 댓글