본문 바로가기
개발노트/Kotlin

11주차 5일차 리포지토리

by 시계속세상은아직돌아가는중 2023. 9. 22.

1. 개요

 

기존 코드에서는 프래그먼트에서 레트로핏을 통해 api를 받아오는 작업을 수행하였다.

하지만 이는 완전한 데이터 분리와는 먼 방식이며

viewmodel에서 해당 방식을 적용하는게 맞다.

 

하지만 리포지토리를 만든다면 data layer를 분리해줌으로 써 한 번 더 데이터 층을 나눌 수 있다.

 

2. retrofit

 

1. 인터페이스

interface RetrofitInterface {
    @GET("v2/search/image")
    suspend fun searchImage(
        @Query("query") query: String,
        @Query("sort") sort:String,
        @Query("size") size: Int
    ): ResultImgModel

    @GET("v2/search/vclip")
    suspend fun searchVideo(
        @Query("query") query: String?,
        @Query("sort") sort:String,
        @Query("size") size: Int
    ) : ResultVideoModel

}

2. 클라이언트

package com.example.assignmnet_img.search.retrofit

import com.example.assignmnet_img.search.SearchFragment
import com.example.assignmnet_img.unit.Unit.API_KEY
import com.example.assignmnet_img.unit.Unit.BASE_URL
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {

    val api : RetrofitInterface get() = instance.create(RetrofitInterface::class.java)

    private val instance: Retrofit
        get() {
            val httpClient = OkHttpClient.Builder().addInterceptor { chain ->
                val request: Request = chain.request()
                    .newBuilder()
                    .addHeader("Authorization", "KakaoAK ${API_KEY}")
                    .build()
                chain.proceed(request)
            }.build()

            return Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .client(httpClient)
                .build()
        }
}

 

3. repository

 

interface

package com.example.assignmnet_img.search.viewmdoel.repsitory

import com.example.assignmnet_img.search.dataclass.SearchModel

interface SearchRepository {

    suspend fun getSearchedImages(text: String): List<SearchModel>
    suspend fun getSearchVideos(text: String): List<SearchModel>

}

 데이터

package com.example.assignmnet_img.search.viewmdoel.repsitory

import com.example.assignmnet_img.search.dataclass.SearchModel
import com.example.assignmnet_img.search.retrofit.RetrofitClient

class SearchRepositoryImpl(
    private val client: RetrofitClient
) : SearchRepository {

    //데이터 영역

    override suspend fun getSearchedImages(text: String): List<SearchModel> {
        val responseImages = client.api.searchImage(text, "recency", 20)
        val responseList = responseImages.documents
        val resultList = responseList.map { document ->
            SearchModel(
                Url = document.image_url,
                label = "[IMG]",
                title = document.display_sitename,
                datetime = document.datetime
            )
        }

        return resultList
    }

    override suspend fun getSearchVideos(text: String): List<SearchModel> {
        val responseVideos = client.api.searchVideo(text, "recency", 20)
        val responseList = responseVideos.documents
        val resultList = responseList.map { document ->
            SearchModel(
                Url = document.thumbnail,
                label = "[Video]",
                title = document.title,
                datetime = document.datetime,
            )
        }

        return resultList
    }

}

변경된 뷰 모델

package com.example.assignmnet_img.search.viewmdoel

import android.content.Context
import android.widget.EditText
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.example.assignmnet_img.bookmark.provider.SharedProvider
import com.example.assignmnet_img.bookmark.provider.SharedProviderImpl
import com.example.assignmnet_img.search.dataclass.SearchModel
import com.example.assignmnet_img.search.retrofit.RetrofitClient
import com.example.assignmnet_img.search.viewmdoel.repsitory.SearchRepository
import com.example.assignmnet_img.search.viewmdoel.repsitory.SearchRepositoryImpl
import kotlinx.coroutines.launch

class SearchViewModel(
    private val contextProvider: SharedProvider,
    private val repository: SearchRepository
) : ViewModel() {

    private val _searchList: MutableLiveData<List<SearchModel>> = MutableLiveData()
    val searchList: LiveData<List<SearchModel>> get() = _searchList

    val searchText: MutableLiveData<String> = MutableLiveData()

    private fun findItem(item: SearchModel?): SearchModel? {
        val currentList = searchList.value.orEmpty()

        if (item != null) {
            return currentList.find {
                it.datetime == item.datetime && //혹시 모를 고유 아이디 부여가 꼬일 시 방어 기제로 상세한 비교
                        it.Url == item.Url &&
                        it.title == item.title
            }
        }
        return null
    }

    private fun clearList() {
        val currentList = searchList.value.orEmpty().toMutableList()
        currentList.clear()
        _searchList.value = currentList
    }

    private fun findIndex(item: SearchModel?): Int {

        val currentList = searchList.value.orEmpty().toMutableList()

        val findItem = findItem(item)

        return currentList.indexOf(findItem)

    }

    private fun sortList(baseList: MutableList<SearchModel>): List<SearchModel> {
        val sortedList = baseList.sortedByDescending { it.datetime }
        return sortedList
    }

    suspend fun doSearch(keyword: String) {

        clearList()

        viewModelScope.launch {

            val resultImagesList = repository.getSearchedImages(keyword)
            val resultVideosList = repository.getSearchVideos(keyword)

            val currentList = searchList.value.orEmpty().toMutableList().apply {
                addAll(resultImagesList)
                addAll(resultVideosList)
            }
            val sortedList = sortList(currentList)

            _searchList.value = sortedList
        }
    }

    fun updateItem(item: SearchModel?) {
        if (item == null) return

        val currentList = searchList.value.orEmpty().toMutableList()
        val index = findIndex(item)

        currentList[index] = item

        _searchList.value = currentList
    }

    fun compareUpdateItem(item: SearchModel?) {
        if (item == null) return
        if (findItem(item) == null) return
        else updateItem(item)

    }

    fun updateText(text: String) {
        searchText.value = text
    }

    fun saveSearchText(text: String) {
        val sharedPrf = contextProvider.getSharedPreferences("name_search_text")
        val edit = sharedPrf.edit().apply {
            putString("key_search_text", text)
            apply()
        }
    }

    fun loadSearchText(view: EditText) {
        val sharedPrf = contextProvider.getSharedPreferences("name_search_text")
        val text = sharedPrf.getString("key_search_text", "")
        view.setText(text)
    }

}

class SearchViewModelFactory(
    private val context: Context
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(SearchViewModel::class.java)) {
            return SearchViewModel(
                SharedProviderImpl(context),
                SearchRepositoryImpl(RetrofitClient)
            ) as T
        } else {
            throw IllegalArgumentException("Not found ViewModel class.")
        }
    }


}

 

1. retorfit은 비동기 작동을한다.

2. 비동기 작업을 원활히 하기 위해서는 코루틴을 사용하게된다. -> suspend fun

3. suspend fun을 사용함으로써 retrofit의 원활한 사용이 가능해진다!

 

리포지토리를 사용하면서 주의할 점은 리포지토리는 비지니스 모델을 분리하기 위함이므로 라이브 데이터를 직접 사용하지 않는다. 즉 리포지토리 안에 라이브 데이터를 선언해서 사용할 수는 있으나, 이는 데이터를 관리하는 뷰모델에서 조정하는 것이 맞다.

 

또한 인터페이스를 통해 구현되는 메소드들은 합칠 수 있으면 합쳐주어 간소화 시키는 것 역시 중요하다.

 

한 눈에 보기 편한 코드포맷을 짜도록 노력해봐야한다.

 

따라서 레트로핏의 인터페이스에서는 콜이 아닌 직접적인 클래스를 사용하기 위해 suspend fun을 사용해주었으며

suspend fun을 사용함으로써 비동기 작업을 좀 더 직관적으로 꾸릴 수 있게 되었다.

 

또한 리포지토리를 사용함으로써 데이터 레이아웃 층을 나누었으며 여기서 더 나아가 퍼사드 패턴을 사용하여 도메인층까지 분리가 되었다.

 

이에 대한 개념은 좀 더 이해가 필요할 것이다.

 

참고 사이트

 

https://velog.io/@lifeisbeautiful/Android-MVVM-%ED%8C%A8%ED%84%B4-Retrofit-%EA%B0%80%EC%9D%B4%EB%93%9C

 

[Android] MVVM 패턴 Retrofit 가이드 Kotlin

Android MVVM 패턴 Retrofit 가이드

velog.io

https://nuritech.tistory.com/16

 

[Kotlin] Coroutine suspend function 은 대체 뭐야?

목차 suspend 는 무엇인가. 사전을 찾아보면, '중지하다' 라는 뜻의 단어다. 그렇다면, coroutine 에서의 suspend keyword 는 무엇을 의미할까? a function that could be started, paused, and resume. 시작하고, 멈추고,

nuritech.tistory.com

https://refactoring.guru/ko/design-patterns/facade

 

퍼사드 패턴

/ 디자인 패턴들 / 구조 패턴 퍼사드 패턴 다음 이름으로도 불립니다: Facade 의도 퍼사드 패턴은 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터

refactoring.guru