728x90
이번 심화주차 팀 프로젝트로 YouTube API를 이용한 앱을 만들기로 했다.
내가 맡은 부분은 가장 인기 있는 비디오 리스트를 api로 받아오고, 카테고리 별 비디오 리스트를 분류, 해당 카테고리의 채널 썸네일을 보여주는 작업을 맡았다.
우선 TrendFramgnet 에서 한국에서 가장 인기있는 영상들을 불러오는 작업을 했다.
RetrofitInstance
더보기
package com.brandon.playvideo_app.data.api
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
private const val BASE_URL = "https://www.googleapis.com/youtube/v3/"
object RetrofitInstance {
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).build())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: YouTubeApi by lazy { retrofit.create(YouTubeApi::class.java) }
}
YouTubeApi
더보기
package com.brandon.playvideo_app.data.api
import com.brandon.playvideo_app.data.model.CategoryVideoModel
import com.brandon.playvideo_app.data.model.ChannelByCategoryModel
import com.brandon.playvideo_app.data.model.TrendVideoModel
import retrofit2.http.GET
import retrofit2.http.Query
private const val PART = "snippet"
private const val CHART = "mostPopular"
private const val MAX_RESULT = 20
private const val REGION = "KR"
private const val API_KEY = //API//
private const val HL = "ko_KR" //hl 매개변수는 API 응답의 텍스트 값에 사용할 언어를 지정합니다. 기본값은 en_US입니다.
private const val VIDEO_CATEGORY_ID =
"0" //videoCategoryId 매개변수는 차트를 검색해야 하는 동영상 카테고리를 식별합니다. 이 매개변수는 chart 매개변수와만 함께 사용할 수 있습니다. 기본적으로 차트는 특정 카테고리로 제한되지 않습니다. 기본값은 0입니다.
interface YouTubeApi {
@GET("videos")
suspend fun getTrendingVideos(
@Query("part") part: String = PART,
@Query("chart") chart: String = CHART,
@Query("maxResults") maxResults: Int = MAX_RESULT,
@Query("regionCode") regionCode: String = REGION,
@Query("videoCategoryId") videoCategoryId: String = VIDEO_CATEGORY_ID,
@Query("key") apiKey: String = API_KEY
): TrendVideoModel
}
TrendViewModel
더보기
package com.brandon.playvideo_app.data.model
import com.google.gson.annotations.SerializedName
data class TrendVideoModel(
@SerializedName("etag")
val etag: String,
@SerializedName("items")
val items: List<Item>,
@SerializedName("kind")
val kind: String,
@SerializedName("nextPageToken")
val nextPageToken: String,
@SerializedName("pageInfo")
val pageInfo: PageInfo
)
data class Item(
@SerializedName("etag")
val etag: String,
@SerializedName("id")
val id: String,
@SerializedName("kind")
val kind: String,
@SerializedName("snippet")
val snippet: Snippet
)
data class Snippet(
@SerializedName("categoryId")
val categoryId: String,
@SerializedName("channelId")
val channelId: String,
@SerializedName("channelTitle")
val channelTitle: String,
@SerializedName("defaultAudioLanguage")
val defaultAudioLanguage: String,
@SerializedName("defaultLanguage")
val defaultLanguage: String,
@SerializedName("description")
val description: String,
@SerializedName("liveBroadcastContent")
val liveBroadcastContent: String,
@SerializedName("localized")
val localized: Localized,
@SerializedName("publishedAt")
val publishedAt: String,
@SerializedName("tags")
val tags: List<String>,
@SerializedName("thumbnails")
val thumbnails: Thumbnails,
@SerializedName("title")
val title: String
)
data class Thumbnails(
@SerializedName("default")
val default: Default,
@SerializedName("high")
val high: High?,
@SerializedName("maxres")
val maxres: Maxres?,
@SerializedName("medium")
val medium: Medium?,
@SerializedName("standard")
val standard: Standard?
)
data class Standard(
@SerializedName("height")
val height: Int,
@SerializedName("url")
val url: String,
@SerializedName("width")
val width: Int
)
data class PageInfo(
@SerializedName("resultsPerPage")
val resultsPerPage: Int,
@SerializedName("totalResults")
val totalResults: Int
)
data class Medium(
@SerializedName("height")
val height: Int,
@SerializedName("url")
val url: String,
@SerializedName("width")
val width: Int
)
data class Maxres(
@SerializedName("height")
val height: Int,
@SerializedName("url")
val url: String,
@SerializedName("width")
val width: Int
)
data class Localized(
@SerializedName("description")
val description: String,
@SerializedName("title")
val title: String
)
data class High(
@SerializedName("height")
val height: Int,
@SerializedName("url")
val url: String,
@SerializedName("width")
val width: Int
)
data class Default(
@SerializedName("height")
val height: Int,
@SerializedName("url")
val url: String,
@SerializedName("width")
val width: Int
)
TrendFragment
더보기
package com.brandon.playvideo_app.ui.trend
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.brandon.playvideo_app.R
import com.brandon.playvideo_app.data.api.RetrofitInstance
import com.brandon.playvideo_app.databinding.ToolbarCommonBinding
import com.brandon.playvideo_app.databinding.TrendFragmentBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
class TrendFragment : Fragment() {
private var _binding: TrendFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var videoAdapter: VideoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = TrendFragmentBinding.inflate(inflater, container, false)
initRecyclerView()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbarBinding = ToolbarCommonBinding.bind(view.findViewById(R.id.included_tool_bar))
toolbarBinding.toolbarCommon.inflateMenu(R.menu.library_tool_bar_menu)
toolbarBinding.toolbarCommon.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.search -> {
true
}
R.id.setting -> {
true
}
// 다른 메뉴 아이템에 대해서도 필요한 경우 추가할 수 있음
else -> false
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun initRecyclerView() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
//API 통신 부분
val responseData = RetrofitInstance.api.getTrendingVideos().items
videoAdapter = VideoAdapter(responseData)
}
binding.recyclerView.apply {
adapter = videoAdapter
layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
}
}
}
}
VideoAdapter
더보기
package com.brandon.playvideo_app.ui.trend
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.brandon.playvideo_app.data.model.Item
import com.brandon.playvideo_app.databinding.ItemTrendBinding
import com.bumptech.glide.Glide
class VideoAdapter(private val items: List<Item>) :
RecyclerView.Adapter<VideoAdapter.VideoHolder>() {
inner class VideoHolder(private val binding: ItemTrendBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
with(binding) {
tvVideoTitle.text = item.snippet.title
tvChannelTitle.text = item.snippet.channelTitle
//Glide 라이브러리 사용
Glide.with(itemView).load(item.snippet.thumbnails.maxres?.url ?: item.snippet.thumbnails.default.url).into(ivThumbnail)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoHolder {
val inflater =
parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val binding = ItemTrendBinding.inflate(inflater, parent, false)
return VideoHolder(binding)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: VideoHolder, position: Int) {
holder.bind(items[position])
}
}
item_trend.xml
더보기
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_thumbnail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:adjustViewBounds="true"
android:minHeight="300dp"
android:scaleType="fitXY"
app:layout_constraintTop_toBottomOf="@id/tv_channel_title" />
<TextView
android:id="@+id/tv_video_title"
style="@style/video_title_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="22dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_thumbnail"
tools:text="The Beauty of Existence - Heart Touching Nasheed" />
<TextView
android:id="@+id/tv_channel_title"
style="@style/channel_title_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="22dp"
app:layout_constraintBottom_toTopOf="@id/iv_thumbnail"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="channelTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
종속성 추가
// retrofit2
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation ("com.google.code.gson:gson:2.10.1")
// Glide
implementation("com.github.bumptech.glide:glide:4.16.0")
// 코루틴
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
// navigation
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
youtube data v3 를 사용했고, API 사용 방법은 아래 링크를 참고 했다.
https://developers.google.com/youtube/v3/docs?apix=true&hl=ko