본문 바로가기

Android

[Android] 검색어 저장 기능 구현해보기(dagger-hilt, room, coroutine, mvvm)

지난 포스팅에서 RxJava로 naver api를 이용하여 영화 검색 기능을 만들어 보았다.

2021.03.22 - [Android] - [Android] Naver검색 api - RxJava, Retrofit, MVVM, Hilt

 

이번에는 영화를 검색했을 때 최근 검색어 자동저장을 Room을 이용해 만들어보았다.

이번에도 RxJava로 구현을 하려고 했으나 먼저 Coroutine으로 작성하는 법을 포스팅하고 RxJava를 더 공부한 다음 리팩토링 하고자 한다.

 

 

1) build.gradle(module: app) dependency 추가

- 다음과 같이 Room 의존성을 추가해준다.

ext {
   retrofit_version = "2.9.0"
   lifecycle_version = "2.2.0"
   room_version = "2.3.0-alpha04"
}
    
//Room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
implementation "androidx.room:room-ktx:$room_version"

 

2) Entity 생성

- Room을 이용하여 테이블에 데이터를 저장하기 위해 Entity를 만들어준다.

- Entity 어노테이션을 통해서 table name을 지정해주고 저장하고자 하는 데이터 형식대로 데이터 클래스를 만들어준다.

- PrimaryKey는 참조를 위해서 필수적으로 존재해야한다.

(autoGenerate = true)로 설정을 하면 Entity가 새로 생길 때마다 Primary key는 자동으로 생성된다.

- ColumnInfo 어노테이션을 통해서 원하는 컬럼명을 지정해줄 수 있다.

@Entity(tableName = "search_history")
data class SearchEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int? = null,
    @ColumnInfo(name = "search_query")
    val searchQuery: String
)

 

3) Dao 작성

- Entity로 설정한 테이블에 대하여 쿼리문을 작성하는 인터페이스이다.

- Coroutine을 사용하기 위해서 dao내의 함수는 suspend function을 작성해준다.

- onConflict는 데이터베이스 충돌이 날 경우 어떻게 처리할 것인지를 명시하는 속성이다.

- 다음과 같이 데이터를 가져올 때는 Query문에 해당하는 쿼리문을 넣어주고, 데이터를 삽입할 때는 Insert, 삭제는 Delete를 사용한다.

@Dao
interface SearchDao {
    @Query("SELECT * FROM search_history")
    suspend fun getAll(): List<SearchEntity>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(searchList: SearchEntity)

    @Delete
    suspend fun delete(searchEntity: SearchEntity)
}

 

4) Repository 생성

- Hilt를 통해 앞서 만들었던 Dao를 주입받아 Repository를 생성한다.

- 이 때도 마찬가지로 Coroutine에서 실행될 함수이기에 suspend function으로 작성해준다.

class SearchHistoryRepository @Inject constructor(
    private val searchDao: SearchDao
){
    suspend fun insert(searchEntity: SearchEntity) = searchDao.insert(searchEntity)
    suspend fun getAll() = searchDao.getAll()
    suspend fun delete(searchEntity: SearchEntity) = searchDao.delete(searchEntity)
}

 

5) Database 생성

- Database는 Room을 실제로 구현하는 부분이다.

- RoomDatabase 인스턴스를 만드는 작업은 매우 비싼 작업이기 때문에 싱글톤으로 만드는 것을 권장하고 있다.

- 먼저 RoomDatabase를 상속받는 추상클래스로 만들어주고 @Database 어노테이션을 달아준다.

- 이후 사용할 Entity를 배열형태로 입력해준다.

@Database(entities = [SearchEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getSearchDao(): SearchDao
}

 

6) Hilt Module

- Dao, Database, Repository 객체들은 다음과 같이 같이 외부에서 만들어 주입해주었다.

@Module
@InstallIn(ApplicationComponent::class)
object DatabaseModule {

    @Provides
    fun provideSearchDao(appDatabase: AppDatabase) = appDatabase.getSearchDao()

    @Provides
    @Singleton
    fun provideAppDatabase(@ApplicationContext context: Context) =
        Room.databaseBuilder(context, AppDatabase::class.java, "app database")
            .fallbackToDestructiveMigration()
            .build()

    @Provides
    @Singleton
    fun provideSearchHistoryRespository(searchDao: SearchDao): SearchHistoryRepository =
        SearchHistoryRepository(searchDao)
}

 

 

7) ViewModel에서 데이터 조회 및 삽입 구현

class SearchMovieViewModel @ViewModelInject constructor(
    private val searchMovieRepository: SearchMovieRepository,
    private val searchHistoryRepository: SearchHistoryRepository
    ) : ViewModel() {

    private val _movieList = MutableLiveData<MovieResponseModel>()
    val movieList: LiveData<MovieResponseModel>
        get() = _movieList

    val searchQuery: MutableLiveData<String> = MutableLiveData<String>()

    private val _genreMovie = MutableLiveData<Int>()
    val genreMovie: LiveData<Int>
        get() = _genreMovie

    private val _country = MutableLiveData<String>()
    val country: LiveData<String>
    get() = _country

    private val _queryList = MutableLiveData<List<SearchEntity>>()
    val queryList: LiveData<List<SearchEntity>>
    get() = _queryList


    init {
        getSearchHistory()
    }

    fun changeCountryFilter(country: String) {
        _country.value = country
    }

    @SuppressLint("CheckResult")
    fun getMovieList() {
        searchMovieRepository.getMovieList(
            searchQuery.value!!,
            country.value
            ).subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                _movieList.postValue(it)
                insertHistory()
                getSearchHistory()
            }, {
                Log.e("error :", it.stackTraceToString())
            })
    }

    private fun insertHistory() {
        viewModelScope.launch {
            searchHistoryRepository.insert(
                SearchEntity(
                    null,
                    searchQuery.value!!
                )
            )
        }
    }

    fun getSearchHistory() {
        viewModelScope.launch {
            _queryList.postValue(searchHistoryRepository.getAll())
        }
    }
}