Coroutine + Retrofit | Coroutine + Room

개발/안드로이드 2021. 7. 22. 21:00 Posted by 아는 개발자

Coroutine + Retrofit

 

Retrofit 2.6.0 버전부터 suspend 함수로 api를 작성할 수 있게 됐다. 다른 Retrofit 인터페이스처럼 어노테이션을 추가하고 suspend 함수를 추가하면 빌드 될 때 Retrofit 에서 전처리한다. 

 

interface LibraryApi {
    @GET("/1.0/new")
    suspend fun getNew() : BookListResp
}

 

suspend로 쓰였기 때문에, api 를 호출하는 부분에서도 suspend 함수를 받아서 처리할 수 있다. 예로 Repository 인 경우 suspend 함수를 이용해서 아래 코드로 표현이 가능하다. withContext를 받아서 I/O 쓰레드에서 실행하도록 변경해 Main 쓰레드 안전성이 보장됐다.

 

class LibraryRepository(
    private val apiProvider: ApiProvider
) {

    private val libraryApi by lazy { apiProvider.createApi(LibraryApi::class.java) }

    suspend fun loadNew(): BookListResp = withContext(Dispatchers.IO) {
        return@withContext libraryApi.getNew()
    }

 

Coroutine + Room 

 

Room에서 데이터 변화에 따라 UI를 바꾸거나 특정 로직을 실행해야할 때가 있다. Room 에서는 LiveData와 Flow를 이용하는 두가지 방법을 제공하는데, 이 포스트에서는 Flow를 활용한 버전만 다룬다. 

 

@Dao
interface BookSearchDao {
    @Query("select * from BookSearch order by BookSearch.createdAt desc")
    fun selectBookSearchList(): Flow<List<BookSearch>?>
    
class LibraryRepository(
    private val bookSearchDao: BookSearchDao
) {
    suspend fun loadBookSearchHistory(): Flow<List<BookSearch>?> = withContext(Dispatchers.IO) {
        return@withContext bookSearchDao.selectBookSearchList()
    }

 

Dao의 리턴타입을 Flow로 싸고 Repository 에서는 suspend 함수를 이용해 IO 쓰레드에서 실행하도록 변경하고 리턴했다. Flow는 RxJava의 Flowable과 같아서 내부 DB에 변경사항이 생기면 스트림을 따라서 알림을 준다. 

 

class SearchViewModel @Inject constructor(
    private val libraryRepository: LibraryRepository
): ViewModel() {
    fun loadHistory() {
        viewModelScope.launch {
            libraryRepository.loadBookSearchHistory()
                .distinctUntilChanged()
                .collect { list ->
                    searchHistory.value = list ?: listOf()
                }
        }
    }

 

ViewModel에선 loadBookSearchHistory() 함수의 리턴값인 Flow를 viewModelScope 내에서 subscribe 한다. 변화가 있을 때마다 collect 내부의 바디 코드가 실행된다. viewModelScop 으로 실행했기 때문에 ViewModel이 종료되면 subscribe도 자동으로 종료된다.

728x90

'개발 > 안드로이드' 카테고리의 다른 글

RxJava dispose()  (0) 2021.09.16
ListAdapter, DiffUtil  (0) 2021.08.20
Coroutine + Retrofit | Coroutine + Room  (0) 2021.07.22
suspend fun  (0) 2021.07.22
Single, Maybe, Completable  (0) 2021.07.04
Serializable 과 Parcelable  (0) 2021.06.19

안드로이드 개발중 Gson과 Retrofit을 이용해 Json 데이터를 주고 받을 때 이런 에러를 보게 되는 경우가 있다 

 

Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 15 path $.documents
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:224)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
  at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)

 

이 에러는 서버로 부터 전달 받은 타입과 내가 예상한 타입이 맞지 않아서 발생하는 문제다. 특히 위 에러는 나는 Object가 올것으로 기대 했는데 실제 데이터가 배열인 경우다. 아래 코드를 보면 나는 검색 결과에 대한 클래스에서 documents 인자를 하나의 오브젝트로 받고 있다. 

 

interface LibraryApi {
    @GET("/v3/search/book")
    fun search(
        @Query(value = "query") query: String,
        @Query(value = "sort") sort: String?,
        @Query(value = "page") page: Int?,
        @Query(value = "size") size: Int?,
        @Query(value = "target") target: String?
    ): Single<SearchResp>
}

@Keep
data class SearchResp(val documents: BookMeta, val meta: SearchMeta)

 

그런데 실제 서버로부터 응답은 documents는 하나의 오브젝트가 아니라 배열로 날라온다. 클라이언트에서 기대한 데이터 타입이 달라서 발생하는 에러다.

 

{
  "documents": [
    {
      "authors": [
        "전석"
      ],
      "contents": "주식투자의 성공 요소 중 중요한 것은 바로 수익을 낼 수 있는 종목을 볼 줄 아는 눈을 갖는 것이다. 지금 상승하거나 혹은 상승을 준비 중인 종목들이 본격적으로 상승하기 전에, 세력은 어쩔 수 없이 흔적을 남기게 된다. 『개미대학 세력의 매집원가 구하기』는 그런 종목을 구별해 낼 줄 아는 능력을 키울 수 있게 한다. 처음 기술적 분석을 공부하다가 힘들다고 포기하는 부분이 바로 '캔들과 거래량'인데, 이것을 실전에 적용하려면 암기가 아닌 원리의 이해가",
      "datetime": "2017-0
728x90