Room DB를 UI와 연동하기

2023. 1. 30. 16:57Android Jetpack

  • Repository에 Room DAO를 조작하기위해 구현한다.
package com.example.kakaobook.ui.view.repository

import androidx.lifecycle.LiveData
import com.example.kakaobook.ui.view.data.model.Book
import com.example.kakaobook.ui.view.data.model.SearchResponse
import retrofit2.Response

interface BookSearchRepository {

    suspend fun searchBooks(
        query: String,
        sort: String,
        page: Int,
        size: Int,
    ): Response<SearchResponse>

    //Room
    suspend fun  insertBooks(book: Book)
    suspend fun  deleteBooks(book: Book)

    fun getFavoriteBooks(): LiveData<List<Book>>



}

 

인터페이스를 구현할 repository클래스에 작성해주도록하겟습니다.

package com.example.kakaobook.ui.view.repository

import androidx.lifecycle.LiveData
import com.example.kakaobook.ui.view.data.api.RetrofitInstance.api
import com.example.kakaobook.ui.view.data.db.BookSearchDatabase
import com.example.kakaobook.ui.view.data.model.Book
import com.example.kakaobook.ui.view.data.model.SearchResponse
import retrofit2.Response

class BookSearchRepositoryImpl(private val db: BookSearchDatabase) : BookSearchRepository {
    override suspend fun searchBooks(
        query: String,
        sort: String,
        page: Int,
        size: Int
    ): Response<SearchResponse> {
        return api.searchBooks(query, sort, page, size)
    }

    override suspend fun insertBooks(book: Book) {
       db.bookSearchDao().insertBook(book)
    }

    override suspend fun deleteBooks(book: Book) {
       db.bookSearchDao().deleteBook(book)
    }

    override fun getFavoriteBooks(): LiveData<List<Book>> {
        return db.bookSearchDao().getFavoriteBooks()
    }

}

-> 생성자로 BookSearchDatabase를 받아서 dao를 통해서 메소드를 구현하면된다.

 그리고 메인액티비티에서 Repository를 생성할때 BookSearchDatabase 객체를 전달해주면된다.

package com.example.kakaobook.ui.view.ui.view

import android.os.Bundle
import android.provider.ContactsContract.Data
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.room.Database
import com.example.kakaobook.R
import com.example.kakaobook.databinding.ActivityMainBinding
import com.example.kakaobook.ui.view.data.db.BookSearchDatabase
import com.example.kakaobook.ui.view.repository.BookSearchRepositoryImpl
import com.example.kakaobook.ui.view.ui.view.viewmodel.BookSearchViewModel
import com.example.kakaobook.ui.view.ui.view.viewmodel.BookSearchViewModelProvideFactory


class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    lateinit var bookSearchViewModel: BookSearchViewModel
    private lateinit var navController: NavController
    private lateinit var appBarConfiguration: AppBarConfiguration
    private lateinit var db: BookSearchDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setupJetpackNavigation()


        db = BookSearchDatabase.getInstance(this)
        val bookSearchRepository = BookSearchRepositoryImpl(db)
        val factory = BookSearchViewModelProvideFactory(bookSearchRepository)

        //뷰모델 적용
        bookSearchViewModel = ViewModelProvider(this, factory)[BookSearchViewModel::class.java]
    }

    private fun setupJetpackNavigation() {
        val navHostFragment = supportFragmentManager.findFragmentById(R.id.booksearch_nav_host_fragment) as NavHostFragment
        navController=  navHostFragment.navController
        binding.bottomNavigationMemu.setupWithNavController(navController)

        appBarConfiguration = AppBarConfiguration(navController.graph)
        setupActionBarWithNavController(navController,appBarConfiguration)
    }

    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
}



 

 

 

  • ViewModel
package com.example.kakaobook.ui.view.ui.view.viewmodel

import android.annotation.SuppressLint
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.kakaobook.ui.view.data.model.Book
import com.example.kakaobook.ui.view.data.model.SearchResponse
import com.example.kakaobook.ui.view.repository.BookSearchRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


class BookSearchViewModel(
    private val bookSearchRepository: BookSearchRepository
) : ViewModel() {

    //Api
    private val _searchResult = MutableLiveData<SearchResponse>()

    val searchResult: LiveData<SearchResponse> = _searchResult


    // 코루틴 백그라운드 작업을 용이하게함.
    @SuppressLint("SuspiciousIndentation")
    fun searchBooks(query: String) = viewModelScope.launch(Dispatchers.IO) {
        val response = bookSearchRepository.searchBooks(query, "accuracy", 1, 20)
        if (response.isSuccessful) {
            response.body()?.let {
                _searchResult.postValue(it)
            }
        }
    }

    //Room
    fun saveBook(book: Book) = viewModelScope.launch(Dispatchers.IO) {
        bookSearchRepository.insertBooks(book)
    }
    fun deleteBook(book: Book) = viewModelScope.launch(Dispatchers.IO) {
        bookSearchRepository.deleteBooks(book)

    }
    val favoriteBooks: LiveData<List<Book>> = bookSearchRepository.getFavoriteBooks()
}

repository에서 만든 suspend함수를 viewmodelscope안에서  코루틴스코프를 통해  실행하도록한다.

getFavoriteBooks()를 읽어온 데이터는 favoritebooks에 가지고 있도록하겟습니다.

 

 

 

  • Book Fragment에 플로팅 버튼을 만들어 화면에 표시하도록 하겟습니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".ui.view.ui.view.BookFragment">

    <WebView
        android:id="@+id/webview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/btn_favorite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:src="@drawable/ic_baseline_favorite_24"
        android:backgroundTint="@color/black"
        android:layout_marginBottom="60dp"
        android:layout_marginEnd="20dp"/>


</androidx.constraintlayout.widget.ConstraintLayout>

 

 

  •  bookfragment에 플로팅버튼을 클릭햇을때 저장하도록 하겟습니다.
package com.example.kakaobook.ui.view.ui.view

import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebViewClient
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import com.example.kakaobook.databinding.FragmentBookBinding
import com.example.kakaobook.ui.view.ui.view.viewmodel.BookSearchViewModel
import com.google.android.material.snackbar.Snackbar


class BookFragment : Fragment() {
    private var mbinding : FragmentBookBinding? = null
    private val binding get() = mbinding!!

    private val args by navArgs<BookFragmentArgs>()
    private lateinit var  bookSearchViewModel:BookSearchViewModel


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mbinding = FragmentBookBinding.inflate(inflater, container, false)
        return binding.root
    }


    @SuppressLint("SetJavaScriptEnabled")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bookSearchViewModel=(activity as MainActivity).bookSearchViewModel


        binding.webview.apply {
            webViewClient = WebViewClient()
            settings.javaScriptEnabled = true
            settings.useWideViewPort = true
            loadUrl(args.book.url)
        }
         binding.btnFavorite.setOnClickListener {
             bookSearchViewModel.saveBook(args.book)
             Snackbar.make(view,"저장 완료",Snackbar.LENGTH_SHORT).show()
         }
    }

    override fun onPause() {
        binding.webview.onPause()
        super.onPause()
    }

    override fun onResume() {
        super.onResume()
        binding.webview.onResume()
    }

    override fun onDestroyView() {
        mbinding = null
        super.onDestroyView()


    }

}

-> 액티비티에 뷰모델을 전달받도록하고, favorite 버튼을 클릭햇을때  전달받은 args.book을 savebook에 저장하면된다.

 

 

  • favorite Fragment에 저장된 책을 표시하도록 화면을 만들어줍니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".ui.view.ui.view.FavoriteFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/favorite_book"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:scrollbars="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

  • favoriteFrgment에 리사이클러뷰와 뷰모델을 설정해준다.
package com.example.kakaobook.ui.view.ui.view

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.kakaobook.databinding.FragmentFavoriteBinding
import com.example.kakaobook.ui.view.data.model.Book
import com.example.kakaobook.ui.view.ui.view.adapter.BookSearchAdapter
import com.example.kakaobook.ui.view.ui.view.viewmodel.BookSearchViewModel
import com.google.android.material.snackbar.Snackbar


class FavoriteFragment : Fragment() {
    private var mbinding: FragmentFavoriteBinding? = null
    private val binding get() = mbinding!!

    private lateinit var bookSearchViewModel: BookSearchViewModel
    private lateinit var bookSearchAdapter: BookSearchAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mbinding = FragmentFavoriteBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bookSearchViewModel = (activity as MainActivity).bookSearchViewModel

        setupRecyclerView()
        setupTocuhHelper(view)

        //뷰모델에있는 favoirtebooks를 관찰해서 리사이클러 뷰 데이터 를 갱신해준다.
        bookSearchViewModel.favoriteBooks.observe(viewLifecycleOwner) {
            bookSearchAdapter.submitList(it)
        }
    }

    private fun setupRecyclerView() {
        bookSearchAdapter = BookSearchAdapter()
        binding.favoriteBook.layoutManager = LinearLayoutManager(context)
        binding.favoriteBook.adapter = bookSearchAdapter

        bookSearchAdapter.setOnItemClickListener {
            findNavController().navigate(
                FavoriteFragmentDirections.actionFragmentFavoriteToFrgmentBook(it)
            )
        }
    }

    //왼쪽으로 스와이프하면 데이터가 삭제 하도록 해준다.
    private fun setupTocuhHelper(view: View) {

        val itemTouchHelperCallback =
            object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
                override fun onMove(
                    recyclerView: RecyclerView,
                    viewHolder: RecyclerView.ViewHolder,
                    target: RecyclerView.ViewHolder
                ): Boolean {
                    return true
                }

                override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                    val position = viewHolder.adapterPosition
                    val book = bookSearchAdapter.currentList[position]

                    bookSearchViewModel.deleteBook(book)
                    Snackbar.make(view, "삭제 완료", Snackbar.LENGTH_SHORT).apply {
                        setAction("취소") {
                            bookSearchViewModel.saveBook(book)
                        }
                    }.show()
                }
            }
        ItemTouchHelper(itemTouchHelperCallback).apply {
            attachToRecyclerView((binding.favoriteBook))
        }
    }

    override fun onDestroyView() {
        mbinding = null
        super.onDestroyView()


    }

}

리사이클러뷰아이템을 swipe하는데는 simplecallback을 사용합니다.

SimpleCallback에 instance를 만들어주고 attchToRecyclerView로  연결해주면 swipe나 drag동작을 인식시켜준다.

초기화할때 드래그방향은 dragdirs고 0으로 주면서 드래그방향은 사용하지않도록햇습니다.  스와이프방향은 왼쪽만 사용하도록햇습니다.

onsiped함수에 스와이프할때 삭제하도록 구현햇습니다.

진행상태를 snackbar를 통해 삭제 메세지를 띄우는 표시햇고, 데이터를 지운상태를 다시 되돌릴수있도록 setaction을 써서 savebook을 실행하도록하엿습니다.


  •  결과화면