2021.05.22 - [안드로이드/개발] - [Android 개발일지] MVVM 패턴으로 Todo, Done List 만들기 - (1) 기획, Mockup
이제 Mockup한 대로 전체적인 레이아웃을 만들어보려고 한다.
android {
buildFeatures {
viewBinding true
}
}
dependencies {
// 뷰페이저 2
implementation "androidx.viewpager2:viewpager2:1.0.0"
}
기본적으로 익스텐션 대신 뷰바인딩을 사용할 것이고,
뷰페이저를 사용하기 위하여 종속성을 추가해준다.
2021.05.12 - [안드로이드/기본] - [Android] ViewPager2와 TabLayout 사용하여 레이아웃 만들기
기본적으로 뷰페이저와 탭레이아웃을 사용하여 스와이프 되는 레이아웃을 구현하려고 하므로
위 링크에서 서술한 방식으로 레이아웃을 만들어준다.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_search"
android:title="Search"
android:icon="@drawable/ic_baseline_search_24"
android:iconTint="@color/white"
app:showAsAction="ifRoom"
tools:targetApi="o"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>
검색창으로 사용할 메뉴를 만들어주고
class TodoListFragment : Fragment() {
private var binding : FragmentTodoListBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// 상단 메뉴 추가
setHasOptionsMenu(true)
// 뷰바인딩
binding = FragmentTodoListBinding.inflate(inflater,container,false)
return binding!!.root
}
// 서치바 추가
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main_menu,menu)
val search = menu?.findItem(R.id.menu_search)
val searchView = search?.actionView as? SearchView
}
// 프래그먼트는 뷰보다 오래 지속 . 프래그먼트의 onDestroyView() 메서드에서 결합 클래스 인스턴스 참조를 정리
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
}
TodoListFragment에서 뷰바인딩 후,
onCreateOptionMenu를 오버라이드 하고, setHasOptionsMenu(true) 를 추가하여 서치바를 추가해준다.
첫번째 탭에 검색창이 잘 구현된 것을 확인할 수 있고
스와이프나 탭 클릭 시 다른 탭으로 잘 이동된다.
plugins {
id 'kotlin-kapt'
}
dependencies {
def room_version = "2.3.0"
// room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// room 코루틴
implementation "androidx.room:room-ktx:$room_version"
def lifecycle_version = "2.3.0"
// ViewModel - 라이프 사이클
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData - 데이터의 변경 사항을 알 수 있음
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// 뷰모델 생성하기 쉽게 해줌
implementation 'androidx.fragment:fragment-ktx:1.1.0'
}
Room, 뷰모델, 라이브데이터를 사용하기위해 추가해준다.
여기선 Room에 대해 간략하게만 서술하며
2021.05.12 - [안드로이드/AAC, MVVM] - [Android] Room 활용하여 데이터 저장하기
여기서 자세한 내용을 확인할 수 있다.
@Entity
data class Memo(
val check : Boolean,
val content : String
){
@PrimaryKey(autoGenerate = true)
var id = 0
}
메모 내용과 그 일을 했는지 체크할 수 있는 체크 변수를 만들어주고, id는 자동으로 할당한다.
@Dao
interface MemoDao {
// OnConflictStrategy.IGNORE = 동일한 아이디가 있을 시 무시
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addUser(memo : Memo)
@Update
suspend fun updateUser(memo : Memo)
@Delete
suspend fun deleteUser(memo : Memo)
@Query("SELECT * FROM Memo ORDER BY id ASC")
fun readAllData() : Flow<List<Memo>>
@Query("SELECT * FROM Memo WHERE content LIKE :searchQuery")
fun searchDatabase(searchQuery : String) : Flow<List<Memo>>
}
TodoList를 만들기 위한 쿼리들을 만들어준다.
/* entities = 사용할 엔티티 선언, version = 엔티티 구조 변경 시 구분해주는 역할
exportSchema = 스키마 내보내기 설정 */
@Database(entities = [Memo::class], version = 1, exportSchema = false)
abstract class MemoDatabase : RoomDatabase(){
abstract fun memoDao() : MemoDao
companion object{
/* @Volatile = 접근가능한 변수의 값을 cache를 통해 사용하지 않고
thread가 직접 main memory에 접근 하게하여 동기화. */
@Volatile
private var instance : MemoDatabase? = null
// 싱글톤으로 생성 (자주 생성 시 성능 손해). 이미 존재할 경우 생성하지 않고 바로 반환
fun getDatabase(context : Context) : MemoDatabase? {
if(instance == null){
synchronized(MemoDatabase::class){
instance = Room.databaseBuilder(
context.applicationContext,
MemoDatabase::class.java,
"memo_database"
).build()
}
}
return instance
}
}
}
싱글톤으로 데이터베이스를 생성해준다.
// 앱에서 사용하는 데이터와 그 데이터 통신을 하는 역할
class MemoRepository(private val memoDao: MemoDao) {
val readAllData : Flow<List<Memo>> = memoDao.readAllData()
suspend fun addUser(memo: Memo){
memoDao.addUser(memo)
}
suspend fun updateUser(memo: Memo){
memoDao.updateUser(memo)
}
suspend fun deleteUser(memo: Memo){
memoDao.deleteUser(memo)
}
fun searchDatabase(searchQuery: String): Flow<List<Memo>> {
return memoDao.searchDatabase(searchQuery)
}
}
Dao에서 정의한 메소드들을 사용하기 위해 저장소를 만들어준다.
뷰모델에서 호출되어 DB에 직접적으로 접근하여 통신한다.
// 뷰모델은 DB에 직접 접근하지 않아야함. Repository 에서 데이터 통신.
class MemoViewModel(application: Application) : AndroidViewModel(application) {
val readAllData : LiveData<List<Memo>>
private val repository : MemoRepository
init{
val memoDao = MemoDatabase.getDatabase(application)!!.memoDao()
repository = MemoRepository(memoDao)
readAllData = repository.readAllData.asLiveData()
}
fun addUser(memo : Memo){
viewModelScope.launch(Dispatchers.IO) {
repository.addUser(memo)
}
}
fun updateUser(memo : Memo){
viewModelScope.launch(Dispatchers.IO) {
repository.updateUser(memo)
}
}
fun deleteUser(memo : Memo){
viewModelScope.launch(Dispatchers.IO) {
repository.deleteUser(memo)
}
}
fun searchDatabase(searchQuery: String): LiveData<List<Memo>> {
return repository.searchDatabase(searchQuery).asLiveData()
}
}
프래그먼트와 연결하여
뷰에 사용할 데이터를 가공해주는 Viewmodel를 생성한다.
데이터의 입력,수정,삭제 등은 viewmodelscope에서 비동기로 처리한다.
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_baseline_edit_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
첫번째 탭에 Fab를 만들어준다.
Fab 클릭 시 메모를 할 수 있는 창이 뜨도록 할 것이다.
class TodoListFragment : Fragment() {
private var binding : FragmentTodoListBinding? = null
private val memoViewModel: MemoViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// 상단 메뉴 추가
setHasOptionsMenu(true)
// 뷰바인딩
binding = FragmentTodoListBinding.inflate(inflater,container,false)
binding!!.addButton.setOnClickListener {
Toast.makeText(activity, "테스트", Toast.LENGTH_SHORT).show()
onFabClicked()
}
return binding!!.root
}
// Fab 클릭시 다이얼로그 띄움
fun onFabClicked(){
val memo = Memo(false,"테스트")
memoViewModel.addUser(memo)
}
Fab가 클릭되었을 때 다이얼로그를 띄워야하지만, 일단 테스트를 위해 임의의 값을 저장해보았다.
프래그먼트에서 toast의 context는 activity를 사용하며
뷰모델은 프래그먼트 ktx를 사용했기 때문에
private val memoViewModel: MemoViewModel by viewModels() 로 쉽게 초기화 할 수 있다.
2020.09.10 - [안드로이드/기본] - [Android] 안드로이드 스튜디오 SQLite Database 경로(위치)
위 링크의 방법으로 DB를 확인해보면,
잘 생성되었음을 확인할 수 있다.
https://github.com/HanYeop/TodoneList