티스토리 뷰
이번에는 Naver Developers에서 제공해주는 API 중 네이버 검색 (뉴스)을 사용해보려고 합니다.
1. 활용신청
https://developers.naver.com/apps/#/register
다음과 같이 설정을 하여서 신청을 하시면 됩니다.
등록을 하고 나면 Application 텝에 들어가시면
이렇게 나오는 것을 확인할 수 있을 겁니다.
클릭해서 들어가면 Client ID와 Client Secret의 키 값을 확인할 수 있습니다.
2. Android에 Key 등록
local.properties 하단부에 다음과 같이 추가해줍니다.
client_id="위에서 나온 Client ID 키 값을 추가해줍니다."
client_secret="위에서 나온 Client Secret 키 값을 추가해줍니다."
app수준의 build.gradle에 다음의 내용을 추가합니다.
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
android {
...
defaultConfig {
...
buildConfigField "String", "CLIENT_ID", properties['client_id']
buildConfigField "String", "CLIENT_SECRET", properties['client_secret']
}
}
Sync Now하고 빌드 한 번 진행하면
BuildConfig.CLIENT_ID
BuildConfig.CLIENT_SECRET
이렇게 사용이 가능해집니다.
3. Retrofit 적용
이제 본격적으로 Network 통신을 위해 Retrofit을 적용해봅니다.
https://developers.naver.com/docs/serviceapi/search/news/news.md#%EB%89%B4%EC%8A%A4
해당 내용을 통해 어떤 데이터를 넣어줘야 하는지, 어떤 값을 받을 수 있는지 확인이 가능합니다.
data class NewsResult(
val lastBuildDate : String, // 검색 결과를 생성한 시간이다.
val total : Int, // 검색 결과 문서의 총 개수를 의미한다.
val start : Int, // 검색 결과 문서 중, 문서의 시작점을 의미한다.
val display : Int, // 검색된 검색 결과의 개수이다.
val items : List<NewsItems>, //개별 검색 결과이며 title, originallink, link, description, pubDate를 포함한다.
)
data class NewsItems(
val title : String, // 개별 검색 결과
@SerializedName("originallink")
val originalLink: String, // 검색 결과 문서의 제공 언론사 하이퍼텍스트 link를 나타낸다.
val link: String, // 검색 결과 문서의 제공 네이버 하이퍼텍스트 link를 나타낸다.
val description : String, //검색 결과 문서의 내용을 요약한 패시지 정보이다. 문서 전체의 내용은 link를 따라가면 읽을 수 있다. 패시지에서 검색어와 일치하는 부분은 태그로 감싸져 있다.
val pubDate : String // 검색 결과 문서가 네이버에 제공된 시간이다.
)
우선 결괏값을 data class로 만들어 줍니다.
그다음 호출을 위해 interface를 만들건대 그전에 호출 예시를 한번 보겠습니다.
curl "https://openapi.naver.com/v1/search/news.xml?query=%EC%A3%BC%EC%8B%9D&display=10&start=1&sort=sim" \ -H "X-Naver-Client-Id: {애플리케이션 등록 시 발급받은 client id 값}" \ -H "X-Naver-Client-Secret: {애플리케이션 등록 시 발급받은 client secret 값}" -v
저는 여기서 한 개의 API를 쓸 거라서 BaseUrl은 아무 곳에서 끊어도 무관하지만
다른 API들도 봤을 때 https://openapi.naver.com/v1/ 여기까지가 공통적이기 때문에
const val NAVER_API_BASE_URL = "https://openapi.naver.com/v1/"
BaseUrl은 위와 같이 설정하였습니다.
그다음 해당 API는 GET 방식이고 추가 주소로는"search/news.json"가 있습니다.
요청 변수는 query만 필수 값이고 나머지는 선택이므로 원하는 방식으로 넣어 줍니다.
저는 그냥 다 했습니다~ㅎㅎ
마지막으로
\ -H "X-Naver-Client-Id: {애플리케이션 등록 시 발급받은 client id 값}" \ -H "X-Naver-Client-Secret: {애플리케이션 등록 시 발급받은 client secret 값}" -v
이 부분을 보면 네이버 API는 거의 모든 API에서 위와 같은 헤더 값을 요청하는 것으로 알고 있습니다.
interface NaverService {
@Headers(value = [
"X-Naver-Client-Id: ${BuildConfig.CLIENT_ID}",
"X-Naver-Client-Secret: ${BuildConfig.CLIENT_SECRET}"
])
@GET("search/news.json")
suspend fun getNews(
@Query("query") query : String, // 검색을 원하는 문자열로서 UTF-8로 인코딩한다.
@Query("display") display : Int, // 검색 결과 출력 건수 지정, 기본값 10 최대값 100
@Query("start") start : Int, // 검색 시작 위치 기본값 1, 최대값 1000까지 가능
@Query("sort") sort : String // 정렬 옵션: sim (유사도순), date (날짜순)
) : Response<NewsResult>
}
이전 예제에서는 Header가 없었어서 이번에는 Headers를 이용하여 추가를 해보았습니다.
2022.04.28 - [안드로이드/코드] - 도로명 주소 개발센터 API 사용해보기1 : Api활용 신청, Retrofit
기본 사용법 관련해서는 도로명 주소 사용해보기 포스터에서 진행하였으므로 생략하겠습니다~
4. Hilt 적용
Hilt 역시 도로명 주소 사용해보기에서 다루었지만 이번 예제를 통해 내용을 빠진 보충 해보려고 합니다~
@Provides
@Singleton
fun provideAddressRetrofit(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
) : Retrofit =
Retrofit.Builder()
.baseUrl(Constants.ADDRESS_API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build()
@Provides
@Singleton
fun provideNaverRetrofit(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
) : Retrofit =
Retrofit.Builder()
.baseUrl(Constants.NAVER_API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build()
만약 이렇게 두 가지 BaseUrl을 사용해야 하는 경우라고 했을 때 Hilt를 적용하려고 하면 문제가 발생합니다.
같은 Retrofit을 반환하고 있기 때문에 Hilt입장에서는 어떤 것을 적용해야 할지 알 수 없습니다.
이런 경우를 대비하여 Hilt는 다음과 같이 준비를 해두었습니다.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AddressRetrofit
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class NaverRetrofit
위와 같이 annotation을 생성해주시고
@AddressRetrofit
@Provides
@Singleton
fun provideAddressRetrofit(
...
) : Retrofit =
...
@NaverRetrofit
@Provides
@Singleton
fun provideNaverRetrofit(
...
) : Retrofit =
...
@Provides위에 만든 annotation을 각각 넣어 구분을 해줍니다.
@Provides
@Singleton
fun provideNaverService(@NaverRetrofit retrofit: Retrofit) : NaverService =
retrofit.create(NaverService::class.java)
그 Retrofit을 불러올 때도 어떤 Retrofit을 불러오는 것이다라는 것을 확실하게 알려줍니다.
이렇게 하면 같은 리턴 값이라도 구분하여 사용이 가능해집니다.
5. 전체 코드
이것 이후에는 이전 글과 특별히 다른 부분이 없어서 혹시 필요하신 분을 위해 전체 코드를 올립니다~
1) data class
data class NewsResult(
val lastBuildDate : String, // 검색 결과를 생성한 시간이다.
val total : Int, // 검색 결과 문서의 총 개수를 의미한다.
val start : Int, // 검색 결과 문서 중, 문서의 시작점을 의미한다.
val display : Int, // 검색된 검색 결과의 개수이다.
val items : List<NewsItems>, //개별 검색 결과이며 title, originallink, link, description, pubDate를 포함한다.
) {
fun mapper() = items.map { it.mapper() }
}
data class NewsItems(
val title : String, // 개별 검색 결과
@SerializedName("originallink")
val originalLink: String, // 검색 결과 문서의 제공 언론사 하이퍼텍스트 link를 나타낸다.
val link: String, // 검색 결과 문서의 제공 네이버 하이퍼텍스트 link를 나타낸다.
val description : String, //검색 결과 문서의 내용을 요약한 패시지 정보이다. 문서 전체의 내용은 link를 따라가면 읽을 수 있다. 패시지에서 검색어와 일치하는 부분은 태그로 감싸져 있다.
val pubDate : String // 검색 결과 문서가 네이버에 제공된 시간이다.
) {
fun mapper() = NewsContents(
title = title,
url = originalLink,
description = description
)
}
data class NewsContents(
val title: String,
val url: String,
val description: String
)
2) NaverService.kt
interface NaverService {
@Headers(value = [
"X-Naver-Client-Id: ${BuildConfig.CLIENT_ID}",
"X-Naver-Client-Secret: ${BuildConfig.CLIENT_SECRET}"
])
@GET("search/news.json")
suspend fun getNews(
@Query("query") query : String, // 검색을 원하는 문자열로서 UTF-8로 인코딩한다.
@Query("display") display : Int, // 검색 결과 출력 건수 지정, 기본값 10 최대값 100
@Query("start") start : Int, // 검색 시작 위치 기본값 1, 최대값 1000까지 가능
@Query("sort") sort : String // 정렬 옵션: sim (유사도순), date (날짜순)
) : Response<NewsResult>
}
3) NaverClient.kt
class NaverClient @Inject constructor(
private val service: NaverService
) {
suspend fun getNews(
query: String,
display: Int,
start: Int,
sort: String,
successListener: (NewsResult) -> Unit,
failureListener: () -> Unit
) = try {
val result = service.getNews(
query = query,
display = display,
start = start,
sort = sort
)
if (result.isSuccessful) {
result.body()?.let(successListener) ?: failureListener
} else {
failureListener()
}
} catch (e: Exception) {
e.printStackTrace()
failureListener()
}
}
4) NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(
loggingInterceptor: HttpLoggingInterceptor
) : OkHttpClient =
OkHttpClient.Builder()
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.build()
@Provides
@Singleton
fun provideGsonConvertFactory() : GsonConverterFactory = GsonConverterFactory.create()
@AddressRetrofit
@Provides
@Singleton
fun provideAddressRetrofit(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
) : Retrofit =
Retrofit.Builder()
.baseUrl(Constants.ADDRESS_API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build()
@NaverRetrofit
@Provides
@Singleton
fun provideNaverRetrofit(
okHttpClient: OkHttpClient,
gsonConverterFactory: GsonConverterFactory
) : Retrofit =
Retrofit.Builder()
.baseUrl(Constants.NAVER_API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.build()
@Provides
@Singleton
fun provideLoggingInterceptor() : HttpLoggingInterceptor =
HttpLoggingInterceptor().apply {
level = if(BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
@Provides
@Singleton
fun provideAddressService(@AddressRetrofit retrofit: Retrofit) : AddressService =
retrofit.create(AddressService::class.java)
@Provides
@Singleton
fun provideAddressClient(addressService: AddressService) : AddressClient =
AddressClient(addressService)
@Provides
@Singleton
fun provideNaverService(@NaverRetrofit retrofit: Retrofit) : NaverService =
retrofit.create(NaverService::class.java)
@Provides
@Singleton
fun provideNaverClient(naverService: NaverService) : NaverClient =
NaverClient(naverService)
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AddressRetrofit
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class NaverRetrofit
}
5) NewsRepository.kt
class NewsRepository @Inject constructor(
private val client: NaverClient
) {
suspend fun getNews(
query: String,
display: Int,
start: Int,
sort: String,
successListener: (NewsResult) -> Unit,
failureListener: () -> Unit
) {
client.getNews(
query = query,
display = display,
sort = sort,
start = start,
successListener = successListener,
failureListener = failureListener
)
}
}
6) NewsViewModel.kt
@HiltViewModel
class NewsViewModel @Inject constructor(
private val repository: NewsRepository
) : BaseViewModel<NewsViewModel.Event>() {
private var start = 1
private val display = 20
private var isMore = true
fun getNews(
) = viewModelScope.launch {
if (isMore.not()) return@launch
repository.getNews(
query = "포켓몬",
display = display,
start = start,
sort = SIMILAR,
successListener = { result ->
if(result.total > start) {
isMore = true
start += display
}
event(Event.Success(result.mapper()))
},
failureListener = {
event(Event.Failure)
}
)
}
sealed class Event {
data class Success(val result: List<NewsContents>): Event()
object Failure : Event()
}
companion object {
const val SIMILAR = "sim"
}
}
7) NewsActivity.kt
@AndroidEntryPoint
class NewsActivity : BaseViewModelActivity<ActivityNewsBinding, NewsViewModel>(R.layout.activity_news) {
override val viewModel: NewsViewModel by viewModels()
private lateinit var adapter : NewsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
repeatOnStarted {
viewModel.eventFlow.collect { event -> eventHandler(event) }
}
initViews()
viewModel.getNews()
}
private fun initViews() = with(binding) {
title = getString(R.string.news)
layoutTop.btnBack.setOnClickListener { onBackClick(it) }
adapter = NewsAdapter { url ->
startActivity(
createIntent(NewsWebViewActivity::class.java).also {
it.putExtra(Constants.URL, url)
}
)
}
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!recyclerView.canScrollVertically(1) ) {
viewModel.getNews()
}
}
})
}
private fun eventHandler(event: NewsViewModel.Event) {
when(event) {
is NewsViewModel.Event.Failure -> {
toast(getString(R.string.error_news_search))
}
is NewsViewModel.Event.Success -> {
adapter.addNewsItems(event.result)
}
}
}
8) NewsAdapter.kt
class NewsAdapter(
private val listener : (String) -> Unit
) : RecyclerView.Adapter<NewsAdapter.NewsViewHolder>() {
private val list = mutableListOf<NewsContents>()
inner class NewsViewHolder(private val binding : ItemNewsBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(news : NewsContents) = with(binding) {
newsContents = news
if (adapterPosition % 2 == 0) {
root.setBackgroundResource(R.color.black)
} else {
root.setBackgroundResource(R.color.light_black)
}
root.setOnClickListener {
listener(news.url)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder =
NewsViewHolder(ItemNewsBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
holder.bind(list[position])
}
override fun getItemCount(): Int = list.size
fun addNewsItems(list: List<NewsContents>) {
list.forEach {
this.list.add(it)
notifyItemChanged(list.lastIndex)
}
}
}
9) activity_news.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="activity"
type="com.ezen.lolketing.view.main.news.NewsActivity" />
<variable
name="title"
type="String" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".view.main.news.NewsActivity">
<include
android:id="@+id/layoutTop"
layout="@layout/layout_top"
app:isBackVisible="@{true}"
app:title="@{title}" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/layoutTop"
tools:listitem="@layout/item_news" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
10) item_news
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="newsContents"
type="com.ezen.lolketing.model.NewsContents" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@color/black">
<TextView
android:id="@+id/txtTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/sub_color"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:html="@{newsContents.title}"
tools:text="기사 제목" />
<TextView
android:id="@+id/txtDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:maxLines="2"
app:html="@{newsContents.description}"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/txtTitle"
app:layout_constraintStart_toStartOf="@id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/txtTitle"
tools:text="뉴스 정보" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
'안드로이드 > 코드' 카테고리의 다른 글
Compose 미니 프로젝트 : Pokedex (0) | 2022.06.30 |
---|---|
Compose 기초 2 : Text, Image, LazyColumn + Card (0) | 2022.06.30 |
데이터 바인딩 사용해보기 (0) | 2022.05.28 |
도로명 주소 개발센터 API 사용해 보기 4: AAC 사용해 보기 (0) | 2022.05.17 |
도로명 주소 개발센터 API 사용해 보기 3: Coroutine 사용해 보기 (0) | 2022.05.11 |
- Total
- Today
- Yesterday
- Kotlin
- Duplicate class found error
- Gradient
- Compose BottomSheetDialog
- Fast api
- 웹뷰
- LazyColumn
- Compose 네이버 지도 api
- Compose ConstraintLayout
- Compose MotionLayout
- WebView
- Row
- Compose QRCode Scanner
- Compose BottomSheetScaffold
- Compose BottomSheet
- Compose Naver Map
- 포켓몬 도감
- Pokedex
- compose
- 안드로이드 구글 지도
- Compose 네이버 지도
- Retrofit
- Duplicate class fond 에러
- 안드로이드
- Worker
- Android Compose
- WorkManager
- Compose ModalBottomSheetLayout
- Android
- column
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |