티스토리 뷰

Android/Kotlin

[Kotlin]  Retrofit2  +  Coroutine

혀가 길지 않은 개발자 2020. 8. 2. 22:55
반응형

Retrofit2  +  Coroutine


build.gradle (Module: app)

android {
    compileOptions {
    	sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    // Retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.7.1'
    implementation 'com.squareup.retrofit2:converter-gson:2.7.1'
    implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'

    // OkHttp3
    implementation 'com.squareup.okhttp3:logging-interceptor:4.3.1'

    // Kotlin Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4'
}

 

 

 

 

 

 

 

 

https://newsapi.org

 

News API - A JSON API for live news and blog articles

Search worldwide news articles and headlines from all over the web in real-time with our free News API.

newsapi.org

회원가입 후 로그인

 

뉴스 정보를 읽어올 때 필요한 API key

 

 

 

 

 

 

 

 

 

 

Postman

https://newsapi.org/v2/everything?q=Nigeria&sortBy=publishedAt&apiKey=********************************

입력 시 Json 형식의 데이터가 나온다.

{
    "status": "ok",
    "totalResults": 9388,
    "articles": [
        {
            "source": {
                "id": null,
                "name": "Vanguard"
            },
            "author": "David O Royal",
            "title": "Adeboye, Martins, PFN, others welcome re-opening of worship centres",
            "description": "By Sam Eyoboka GENERAL Overseer of the Redeemed Christian Church of God, RCCG, Pastor Enoch Adejare Adeboye, Catholic Archbishop of Lagos, Most Rev. Alfred Adewale Martins, the Lagos State branch of the Pentecostal Fellowship of Nigeria, PFN were the first se…",
            "url": "https://www.vanguardngr.com/2020/08/adeboye-martins-pfn-others-welcome-re-opening-of-worship-centres/",
            "urlToImage": "https://i2.wp.com/www.vanguardngr.com/wp-content/uploads/2020/03/Catholic-Church.jpg?fit=1920%2C1280&ssl=1",
            "publishedAt": "2020-08-02T13:43:17Z",
            "content": "An empty church\r\nBy Sam Eyoboka\r\nGENERAL Overseer of the Redeemed Christian Church of God, RCCG, Pastor Enoch Adejare Adeboye, Catholic Archbishop of Lagos, Most Rev. Alfred Adewale Martins, the Lago… [+4874 chars]"
        },
        
        
        ...
        
        
        {
            "source": {
                "id": null,
                "name": "Vanguard"
            },
            "author": "Oboh",
            "title": "I’m ashamed people misinterpret ‘waiving sovereignty’ in $500m Chinese loan, says Amaechi",
            "description": "Minister of Transportation, Rotimi Amaechi, has said he is ashamed that some people can’t understand that the clause ‘waiving sovereignty’ in the loan agreement between Nigeria and China is only a contract term, a sovereign guarantee that assures payback acco…",
            "url": "https://www.vanguardngr.com/2020/08/im-ashamed-people-misinterprete-waiving-sovereignty-in-500m-chinese-loan-says-amaechi/",
            "urlToImage": "https://i0.wp.com/www.vanguardngr.com/wp-content/uploads/2019/02/Vanguard_Live_Backdrop.jpg?fit=1920%2C1080&ssl=1",
            "publishedAt": "2020-08-02T11:32:14Z",
            "content": "Minister of Transportation, Rotimi Amaechi, has said he is ashamed that some people cant understand that the clause waiving sovereignty in the loan agreement between Nigeria and China is only a contr… [+4112 chars]"
        }
    ]
}

Json 형식으로 된 데이터를 자바 POJO 클래스로 변환 필요

 

 

 

 

 

 

 

 

Json 형식으로 된 데이터를 복사한 후

www.jsonschema2pojo.org 에 들어가서 붙여넣기

 

 

 

 

버튼 클릭 후

 

 

 

 

Gson 형식의 POJO 클래스로 변환 성공

 

 

 

 

 

 

 

 

 

LatestNews.kt

package com.jwsoft.kotlinproject

import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

data class LatestNews (

    @SerializedName("status")
    @Expose
    val status: String,

    @SerializedName("totalResults")
    @Expose
    val totalResults: Int,

    @SerializedName("articles")
    @Expose
    val articles: List<Article>

)

 

 

 

 

 

 

Article.kt

package com.jwsoft.kotlinproject

import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

data class Article (
    @SerializedName("source")
    @Expose
    val source: Source,

    @SerializedName("author")
    @Expose
    val author: String,

    @SerializedName("title")
    @Expose
    val title: String,

    @SerializedName("description")
    @Expose
    val description: String,

    @SerializedName("url")
    @Expose
    val url: String,

    @SerializedName("urlToImage")
    @Expose
    val urlToImage: String,

    @SerializedName("publishedAt")
    @Expose
    val publishedAt: String,

    @SerializedName("content")
    @Expose
    val content: String
)

 

 

 

 

 

Source.kt

package com.jwsoft.kotlinproject

import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

data class Source (

    @SerializedName("id")
    @Expose
    val id: Any,

    @SerializedName("name")
    @Expose
    val name: String

)

 

 

 

 

 

 

 

 

NewsApiService.kt

package com.jwsoft.kotlinproject

import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object NewsApiService {
    //creating a Network Interceptor to add api_key in all the request as authInterceptor
    private val interceptor = Interceptor { chain ->
        val url = chain.request().url.newBuilder().addQueryParameter("apiKey", "API_KEY").build()
        val request = chain.request()
            .newBuilder()
            .url(url)
            .build()
        chain.proceed(request)
    }

    // we are creating a networking client using OkHttp and add our authInterceptor.
    private val apiClient = OkHttpClient().newBuilder().addInterceptor(interceptor).build()

    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder().client(apiClient)
            .baseUrl("https://newsapi.org/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build()
    }

    val newsApi: NewsApiInterface = getRetrofit().create(NewsApiInterface::class.java)

}

Singleton Pattern 사용.

API 요청 시 interceptor 변수가 파라미터 쿼리로 API_KEY 를 추가해서 보냄.

.addCallAdapterFactory(CoroutineCallAdapterFactory()) 는

Deferred<Response<LatestNews>> 리턴타입을 사용하기 위함.

 

 

참고

github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter

 

JakeWharton/retrofit2-kotlin-coroutines-adapter

A Retrofit 2 adapter for Kotlin coroutine's Deferred type. - JakeWharton/retrofit2-kotlin-coroutines-adapter

github.com

 

 

 

.addCallAdapterFactory(CoroutineCallAdapterFactory()) 없이

suspend 추가 시 Response<LatestNews> 로 사용 가능. 

 

github.com/square/retrofit/blob/master/CHANGELOG.md#version-260-2019-06-05

 

square/retrofit

A type-safe HTTP client for Android and the JVM. Contribute to square/retrofit development by creating an account on GitHub.

github.com

stackoverflow.com/questions/56473539/retrofit-2-6-0-exception-java-lang-illegalargumentexception-unable-to-create-c

 

Retrofit 2.6.0 exception: java.lang.IllegalArgumentException: Unable to create call adapter for kotlinx.coroutines.Deferred

I have a project with Kotlin coroutines and Retrofit. I had these dependencies: implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.squareup.retrofit2:converter-gson:2.5.0'

stackoverflow.com

 

 

 

 

 

NewsApiService.kt

에러 발생

 

 

build.gradle (Module: app)

android {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

추가 시 에러 해결.

 

 

 

stackoverflow.com/questions/59488983/why-i-still-get-cannot-inline-bytecode-built-with-jvm-target-1-8-into-bytecode

 

Why I still get "Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6"

I am developing an Android project with Kotlin and Dagger 2. I have a NetworkModule it is supposed to provide a singleton instance of Retrofit. in which I define all those provider functions. All ...

stackoverflow.com

 

 

 

 

 

 

 

 

 

NewsApiInterface.kt

package com.jwsoft.kotlinproject

import kotlinx.coroutines.Deferred
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface NewsApiInterface {
    //fetches latest news with the required query params
    @GET("v2/everything")
    fun fetchLatestNewsAsync(@Query("q") query: String,
                             @Query("sortBy") sortBy : String) : Deferred<Response<LatestNews>>
}

.addCallAdapterFactory(CoroutineCallAdapterFactory()) 추가 시

Deferred<Response<LatestNews>> 리턴타입을 사용.

 

 

package com.jwsoft.kotlinproject

import kotlinx.coroutines.Deferred
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query

interface NewsApiInterface {
    //fetches latest news with the required query params
    @GET("v2/everything")
    suspend fun fetchLatestNewsAsync(@Query("q") query: String,
                             @Query("sortBy") sortBy : String) : Response<LatestNews>
}

.addCallAdapterFactory(CoroutineCallAdapterFactory()) 없이

suspend 추가 시 Response<LatestNews> 로 사용 가능.

코루틴이 사용된 이유는 해당 API 가 응답이 올 때까지 기다리게 하기 위함.

 

 

 

 

 

 

 

Output.kt

package com.jwsoft.kotlinproject

import java.lang.Exception

sealed class Output<out T : Any> {
    data class Success<out T : Any>(val output : T) : Output<T>()
    data class Error(val exception: Exception) : Output<Nothing>()
}

sealed class 사용

 

 

 

 

 

 

 

 

BaseRepository.kt

package com.jwsoft.kotlinproject

import android.util.Log
import retrofit2.Response
import java.io.IOException

open class BaseRepository {

    suspend fun <T : Any> safeApiCall(call : suspend()-> Response<T>, error : String) : T?{
        val result = newsApiOutput(call, error)
        var output : T? = null

        when(result){
            is Output.Success ->
                output = result.output
            is Output.Error -> Log.e("Error", "The $error and the ${result.exception}")
        }
        
        return output
    }

    private suspend fun<T : Any> newsApiOutput(call: suspend()-> Response<T> , error: String) : Output<T>{
        val response = call.invoke()

        return if (response.isSuccessful)
            Output.Success(response.body()!!)
        else
            Output.Error(IOException("OOps .. Something went wrong due to  $error"))
    }

}

 

 

 

 

 

 

 

NewRepo.kt

package com.jwsoft.kotlinproject

class NewsRepo(private val apiInterface: NewsApiInterface) : BaseRepository() {

    //get latest news using safe api call
    suspend fun getLatestNews() : MutableList<Article>?{
        return safeApiCall(
            //await the result of deferred type
            call = {apiInterface.fetchLatestNewsAsync("Nigeria", "publishedAt").await()},
            error = "Error fetching news"
            //convert to mutable list
        )?.articles?.toMutableList()
    }

}

NewRepo 클래스는 safeApiCall() 함수를 호출하기 위해 BaseRepository 클래스를 상속한다.

파라미터로 NewsApiInterface 를 사용한다.

 

safeApiCall() 에 파라미터를 넣고 호출하면 await() 를 사용하여 NewsApiInterface 의 결과를 기다린다.

이게 가능한 이유는 fetchLastestNews() 함수의 리턴 타입이 코루틴의 Deffered 타입이기 때문이다.

 

 

 

 

 

 

 

NewsViewModel.kt

package com.jwsoft.kotlinproject

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

class NewsViewModel : ViewModel() {

    //create a new Job
    private val parentJob = Job()
    //create a coroutine context with the job and the dispatcher
    private val coroutineContext : CoroutineContext get() = parentJob + Dispatchers.Default
    //create a coroutine scope with the coroutine context
    private val scope = CoroutineScope(coroutineContext)
    //initialize news repo
    private val newsRepository : NewsRepo = NewsRepo(NewsApiService.newsApi)
    //live data that will be populated as news updates
    val newsLiveData = MutableLiveData<MutableList<Article>>()

    fun getLatestNews() {
        ///launch the coroutine scope
        scope.launch {
            //get latest news from news repo
            val latestNews = newsRepository.getLatestNews()
            //post the value inside live data
            newsLiveData.postValue(latestNews)

        }
    }
    
    fun cancelRequests() = coroutineContext.cancel()

}

 

 

 

 

 

 

 

NewsViewModelFactory.kt

package com.jwsoft.kotlinproject

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class NewsViewModelFactory : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return NewsViewModel() as T
    }
}

 

 

 

 

 

 

 

MainActivity.kt

package com.jwsoft.kotlinproject

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MainActivity : AppCompatActivity() {
    private lateinit var newsViewModel: NewsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // create instance of view model factory
        val viewModelFactory = NewsViewModelFactory()
        
        // Use view ModelFactory to initialize view model
        newsViewModel =  ViewModelProvider(this@MainActivity, viewModelFactory).get(NewsViewModel::class.java)
       
        newsViewModel.getLatestNews()
        newsViewModel.newsLiveData.observe(this, Observer {
            //bind your ui here
            Log.e("news count", it.size.toString())
            Log.e("author", it[0].author)
            Log.e("title", it[0].title)
        })

        btnRefresh.setOnClickListener {
            newsViewModel.getLatestNews()
        }
    }

}

실행 결과

 

 

 

 

 

 

참고

medium.com/hacktive-devs/making-network-calls-on-android-with-retrofit-kotlins-coroutines-72fd2594184b

 

Making Network calls on Android with Retrofit + Kotlin’s Coroutines

In recent years, the world of Android development has seen many changes. One of these changes is kotlin’s Coroutines. Last year, the…

medium.com

 

 

 

 

반응형
댓글
  • 프로필사진 미뉴엣 안녕하세요?

    위의 소스를 그대로 복사하여 프로젝트를 만들어봤는데 앱이 실행중에 다운이 됩니다.

    왜 이런 현상이 이러나는지 이해할 수가 없어요..^^

    혹시 짐작가는데라도 있을가요?
    2021.08.09 14:33
댓글쓰기 폼