API 불러오는 연습을 하고있다.
라이브러리를 하나도 사용하지 않는 HttpURLConnection -> Gson 라이브러리 사용 -> Rtrofit2까지 바꿔보면서 진행했다. Retrofit2는 Api 불러올 때 많이 사용해봤어서 익숙했지만 HttpURLConnection은 처음 사용해보았다.
1. HttpURLConnection
HttpURLConnection 을 사용해 보았고, 아래 공식문서를 참고하여 작성해 보았다.
HttpURLConnection | Android Developers
developer.android.com
setRequestProperty("User-Agent", "Mozilla/5.0") 이 부분 때문에 시간을 많이 썼다. 저 부분을 작성하지 않았더니 403에러가 자꾸 떴다. 내 API 키가 잘못된건가 싶어서 재발급도 받았지만 똑같은 오류가 계속 나타났다. 그때부터 구글링을 하기 시작했고, Stack Overflow에서 발견하여 문제를 해결할 수 있었다.
아래는 혹시나 나중에 잊어버릴까봐 복붙해놓은 코드!
라이브러리 없이 통신하려니 낯설어서 힘들긴했는데 그래도 해고 나니 라이브러리가 얼마나 삶을 편하게 해주는게 깨닫게 되었다.
class BusinessTabFragment : Fragment() {
private var _binding: FragmentBusinessTabBinding? = null
private val binding get() = _binding!!
private val dataList: MutableList<NewsData> = mutableListOf()
private val apiKey = BuildConfig.api_key
private val url = "https://newsapi.org/v2/top-headlines?category=business&country=us"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBusinessTabBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fetchNewsData()
setRecyclerView()
}
private fun fetchNewsData() {
lifecycleScope.launch(Dispatchers.IO) {
val urlConnection = URL(url).openConnection() as? HttpURLConnection
urlConnection?.apply {
requestMethod = "GET"
setRequestProperty("X-Api-Key", apiKey)
setRequestProperty("User-Agent", "Mozilla/5.0")
try {
if (urlConnection.responseCode == HttpURLConnection.HTTP_OK) {
val responseData = urlConnection.inputStream.bufferedReader().use { it.readText() }
val jsonObject = JSONObject(responseData)
val articlesArray = jsonObject.getJSONArray("articles")
val newNewsDataList: MutableList<NewsData> = mutableListOf()
for (i in 0 until articlesArray.length()) {
val item = articlesArray.getJSONObject(i)
val title = item.optString("title", "No Title")
val description = item.optString("description", "No Description")
val urlToImage = item.optString("urlToImage", "")
val publishedAt = item.optString("publishedAt", "")
newNewsDataList.add(NewsData(urlToImage, title, description, publishedAt))
}
withContext(Dispatchers.Main) {
dataList.clear()
dataList.addAll(newNewsDataList)
binding.rvBusiness.adapter?.notifyItemChanged(position)
}
} else {
Log.e("Error", "HTTP Error: ${urlConnection.responseCode}")
}
} catch (e: Exception) {
Log.e("Error", "Exception: ${e.message}")
} finally {
urlConnection.disconnect()
}
}
}
}
private fun setRecyclerView() {
val adapter = NewsAdapter(dataList)
binding.rvBusiness.adapter = adapter
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
2. Gson
이번에는 Gson 라이브러리를 사용하였다. Gson에는 여러가지 장점있지만 자동매핑 기능이 있다는게 가장 좋은듯하다.
HttpURLConnection 사용하여 Json파일을 불러오고 Gson을 활용해서 자동으로 값이 대입되어 들어가도록 했다.
이렇게 했더니 훨씬 코드가 간결해졌다.
GitHub - google/gson: A Java serialization/deserialization library to convert Java Objects into JSON and back
A Java serialization/deserialization library to convert Java Objects into JSON and back - GitHub - google/gson: A Java serialization/deserialization library to convert Java Objects into JSON and back
github.com
class BusinessTabFragment : Fragment() {
private var _binding: FragmentBusinessTabBinding? = null
private val binding get() = _binding!!
private var newsList: MutableList<NewsData> = mutableListOf()
private val apiKey = BuildConfig.api_key
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBusinessTabBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
startData()
}
private fun startData() {
lifecycleScope.launch(Dispatchers.IO) {
val category = getString(R.string.tv_home_business)
newsList = fetchNewsData(category)
withContext(Dispatchers.Main) {
binding.rvBusiness.adapter = NewsAdapter(newsList)
}
}
}
private fun fetchNewsData(category: String): MutableList<NewsData> {
val url = URL("https://newsapi.org/v2/top-headlines?category=$category&country=us")
val urlConnection = (url.openConnection() as? HttpURLConnection)?.apply {
setRequestProperty("User-Agent", "Mozilla/5.0")
setRequestProperty("X-Api-Key", apiKey)
requestMethod = "GET"
}
return try {
when (urlConnection?.responseCode) {
HttpURLConnection.HTTP_OK -> successHttp(urlConnection)
HttpURLConnection.HTTP_MOVED_PERM -> showErrorMessage("301오류 URI이 변경 되었습니다.")
HttpURLConnection.HTTP_NOT_FOUND -> showErrorMessage("404오류 데이터를 가져오는데 실패했습니다.")
else -> showErrorMessage("네트워크 오류가 발생했습니다.")
}
} catch (e: Exception) {
Log.e("통신오류", "오류가 발생했습니다: ${e.message}")
mutableListOf()
} finally {
urlConnection?.disconnect()
}
}
private fun successHttp(urlConnection: HttpURLConnection): MutableList<NewsData> {
val gson = Gson()
val reader = InputStreamReader(urlConnection.inputStream)
val jsonResponse = gson.fromJson(reader, JsonObject::class.java)
val articlesJsonArray = jsonResponse.getAsJsonArray("articles")
return articlesJsonArray.map { gson.fromJson(it, NewsData::class.java) }.toMutableList()
}
private fun showErrorMessage(message: String): MutableList<NewsData> {
lifecycleScope.launch(Dispatchers.Main) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
return mutableListOf()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
3. Retrofit2
retrofit도 문서를 한번 읽어보았고, 최대한 이대로 하려고 했으나 코루틴을 사용하여야 해서 suspend를 사용하였다. 처음에는 retrofit이 어렵다고 생각했는데 계속 사용하다 보니 제일 쉬운듯하다.
여러가지의 @Query가 있을 경우 더더욱 좋다!
API 인터페이스 정의 -> 응답 데이터 클래스 정의 -> Retrofit 인스턴스 생성 및 설정 -> 자동매핑
ApiRepository()파일은 나중에 다른 쿼리의 API를 더 불러올지도 몰라서 작성한것인데 단순하게 하나만 불러오는거라면 필요없다.
Retrofit
A type-safe HTTP client for Android and Java
square.github.io
interface ApiFetch {
@Headers("X-Api-Key:$api_key")
@GET("v2/top-headlines")
suspend fun getNewsList(@Query("category") category: String, @Query("country") country: String = "us"): Response<NewsResponse>
companion object {
fun create(): ApiFetch {
return Retrofit.Builder()
.baseUrl("https://newsapi.org")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiFetch::class.java)
}
}
}
class ApiRepository() {
private val client = ApiFetch.create()
suspend fun getNewsList(category: String) = client.getNewsList(category)
}
@Parcelize
data class NewsResponse(
val status: String,
val totalResults: Int,
val articles: List<NewsData>
) : Parcelable
@Parcelize
data class NewsData(
val title: String,
val content: String,
val urlToImage: String,
val publishedAt: String
) : Parcelable
class BusinessTabFragment : Fragment() {
private var _binding: FragmentBusinessTabBinding? = null
private val binding get() = _binding!!
private val netWorkRepository = ApiRepository()
private var newsList: MutableList<NewsData> = mutableListOf()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBusinessTabBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
startData()
}
private fun startData() {
lifecycleScope.launch(Dispatchers.Main) {
try {
val response = withContext(Dispatchers.IO) {
netWorkRepository.getNewsList(getString(R.string.home_business))
}
if (response.isSuccessful) {
response.body()?.articles?.let { newsList.addAll(it) }
binding.rvBusiness.adapter = NewsAdapter(newsList)
} else {
httpResponse(response.code(), requireContext())
}
} catch (e: Exception) {
Log.e("startData", "예외 발생: ${e.message}")
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun httpResponse(responseCode: Int, context: Context) {
val message = when (responseCode) {
HttpURLConnection.HTTP_MOVED_PERM -> getString(context, R.string.Extenstion_http_301)
HttpURLConnection.HTTP_BAD_REQUEST -> getString(context, R.string.Extenstion_http_400)
HttpURLConnection.HTTP_UNAUTHORIZED -> getString(context, R.string.Extenstion_http_401)
HttpURLConnection.HTTP_FORBIDDEN -> getString(context, R.string.Extenstion_http_403)
HttpURLConnection.HTTP_NOT_FOUND -> getString(context, R.string.Extenstion_http_404)
else -> getString(context, R.string.Extenstion_http_else)
}
showToast(message, context)
}
fun getString(context: Context, stringResId: Int): String {
return context.getString(stringResId)
}
fun showToast(message: String, context: Context) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
위에는 suspend를 사용한것이고, 아래는 비동기적으로 불러올 때 사용하는 예시이다.
interface NewsService {
@GET("/v2/top-headlines")
fun getTopHeadlines(@Query("category") category: String): Call<List<Article>>
}
// Activity or Fragment
val newsService = NewsService.create()
newsService.getTopHeadlines("business").enqueue(object : Callback<List<Article>> {
override fun onResponse(call: Call<List<Article>>, response: Response<List<Article>>) {
}
override fun onFailure(call: Call<List<Article>>, t: Throwable) {
}
})
'안드로이드앱' 카테고리의 다른 글
NewsApp 팀프로젝트 회고록 작성(12월 11일 ~ 12월 29일) (1) | 2023.12.31 |
---|---|
PayAPP 팀프로젝트 회고록 작성(11월 20일 ~ 12월 8일) (0) | 2023.12.22 |
안드로이드 지라프(GIRAFFE) , 고슴도치(hedgehog) 버전 API키 숨기기 (0) | 2023.12.13 |
안드로이드 날씨앱 위젯만들기 도전 1! (0) | 2023.11.16 |
안드로이드 코틀린 프로젝트 날씨앱 만들기! (2) | 2023.11.13 |