Room DB를 UI와 연동하기
2023. 1. 30. 16:57ㆍAndroid 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을 실행하도록하엿습니다.
- 결과화면
'Android Jetpack' 카테고리의 다른 글
계산기 만들어보기 (Livedata+ViewModel+DataBinding+BindingAdapter) (0) | 2023.02.15 |
---|---|
코루틴 Flow와 StateFlow (0) | 2023.01.30 |
책 검색결과 저장을 위한 Room DB 구현해보기 (0) | 2023.01.27 |
Safe args로 Fragment 간 데이터 전달하기 (1) | 2023.01.26 |
Navigation으로 UI의 화면전환 구현하기 (0) | 2023.01.25 |