영상목록 API 만들고, 가져오기(Retrofit2)

LeeEunJae·2022년 8월 5일
1

Study Kotlin

목록 보기
8/20

📌 실행 결과

mocky 를 이용해서 가상 API를 만드는 법은 이전에 올린 게시물이 있으므로 생략 하겠습니다.
https://velog.io/@dldmswo1209/Mocky-로-가상-API-만들고-적용하기Retrofit2-사용

📌 샘플 비디오 url

https://gist.github.com/deepakpk009/99fd994da714996b296f11c3c371d5ee
영상은 깃허브에 공유되어 있는 url 을 약간 수정해서 사용했습니다.

{
    "videos": [
        {
          "description": "Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.\n\nLicensed under the Creative Commons Attribution license\nhttps://www.bigbuckbunny.org",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
          ,
          "subtitle": "By Blender Foundation",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg",
          "title": "Big Buck Bunny"
        },
        {
          "description": "The first Blender Open Movie from 2006",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
          ,
          "subtitle": "By Blender Foundation",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ElephantsDream.jpg",
          "title": "Elephant Dream"
        },
        {
          "description": "HBO GO now works with Chromecast -- the easiest way to enjoy online video on your TV. For when you want to settle into your Iron Throne to watch the latest episodes. For $35.\nLearn how to use Chromecast with HBO GO and more at google.com/chromecast.",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"
          ,
          "subtitle": "By Google",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerBlazes.jpg",
          "title": "For Bigger Blazes"
        },
        {
          "description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for when Batman's escapes aren't quite big enough. For $35. Learn how to use Chromecast with Google Play Movies and more at google.com/chromecast.",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4"
          ,
          "subtitle": "By Google",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerEscapes.jpg",
          "title": "For Bigger Escape"
        },
        {
          "description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV. For $35.  Find out more at google.com/chromecast.",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4"
          ,
          "subtitle": "By Google",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerFun.jpg",
          "title": "For Bigger Fun"
        },
        {
          "description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for the times that call for bigger joyrides. For $35. Learn how to use Chromecast with YouTube and more at google.com/chromecast.",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4"
          ,
          "subtitle": "By Google",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerJoyrides.jpg",
          "title": "For Bigger Joyrides"
        },
        {
          "description": "Introducing Chromecast. The easiest way to enjoy online video and music on your TV—for when you want to make Buster's big meltdowns even bigger. For $35. Learn how to use Chromecast with Netflix and more at google.com/chromecast.",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4"
          ,
          "subtitle": "By Google",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerMeltdowns.jpg",
          "title": "For Bigger Meltdowns"
        },
        {
          "description": "Sintel is an independently produced short film, initiated by the Blender Foundation as a means to further improve and validate the free/open source 3D creation suite Blender. With initial funding provided by 1000s of donations via the internet community, it has again proven to be a viable development model for both open 3D technology as for independent animation film.\nThis 15 minute film has been realized in the studio of the Amsterdam Blender Institute, by an international team of artists and developers. In addition to that, several crucial technical and creative targets have been realized online, by developers and artists and teams all over the world.\nwww.sintel.org",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
          ,
          "subtitle": "By Blender Foundation",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/Sintel.jpg",
          "title": "Sintel"
        },
        {
          "description": "Smoking Tire takes the all-new Subaru Outback to the highest point we can find in hopes our customer-appreciation Balloon Launch will get some free T-shirts into the hands of our viewers.",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4"
          ,
          "subtitle": "By Garage419",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/SubaruOutbackOnStreetAndDirt.jpg",
          "title": "Subaru Outback On Street And Dirt"
        },
        {
          "description": "Tears of Steel was realized with crowd-funding by users of the open source 3D creation tool Blender. Target was to improve and test a complete open and free pipeline for visual effects in film - and to make a compelling sci-fi film in Amsterdam, the Netherlands.  The film itself, and all raw material used for making it, have been released under the Creatieve Commons 3.0 Attribution license. Visit the tearsofsteel.org website to find out more about this, or to purchase the 4-DVD box with a lot of extras.  (CC) Blender Foundation - https://www.tearsofsteel.org",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4"
          ,
          "subtitle": "By Blender Foundation",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/TearsOfSteel.jpg",
          "title": "Tears of Steel"
        },
        {
          "description": "The Smoking Tire heads out to Adams Motorsports Park in Riverside, CA to test the most requested car of 2010, the Volkswagen GTI. Will it beat the Mazdaspeed3's standard-setting lap time? Watch and see...",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4"
          ,
          "subtitle": "By Garage419",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/VolkswagenGTIReview.jpg",
          "title": "Volkswagen GTI Review"
        },
        {
          "description": "The Smoking Tire is going on the 2010 Bullrun Live Rally in a 2011 Shelby GT500, and posting a video from the road every single day! The only place to watch them is by subscribing to The Smoking Tire or watching at BlackMagicShine.com",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4"
          ,
          "subtitle": "By Garage419",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/WeAreGoingOnBullrun.jpg",
          "title": "We Are Going On Bullrun"
        },
        {
          "description": "The Smoking Tire meets up with Chris and Jorge from CarsForAGrand.com to see just how far $1,000 can go when looking for a car.The Smoking Tire meets up with Chris and Jorge from CarsForAGrand.com to see just how far $1,000 can go when looking for a car.",
          "sources": 
            "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4"
          ,
          "subtitle": "By Garage419",
          "thumb": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/WhatCarCanYouGetForAGrand.jpg",
          "title": "What care can you get for a grand?"
        }
      ]
}

📌 VideoModel.kt

서버로부터 영상 리스트를 가져오는데 필요한 데이터 클래스 입니다.

data class VideoModel(
    val title: String,
    val sources: String,
    val subtitle: String,
    val thumb: String,
    val description: String
)

📌 VideoDto.kt

DTO (Data Transfer Object)
객체 단위로 데이터를 관리 하기 위함


import com.dldmswo1209.youtube.model.VideoModel

data class VideoDto(
    val videos: List<VideoModel>
)

📌 VideoService.kt

통신할 API의 Http 메소드를 정의하는 service interface

import com.dldmswo1209.youtube.dto.VideoDto
import retrofit2.Call
import retrofit2.http.GET

interface VideoService {
    @GET("/v3/9dda1018-377f-4974-bf51-bcb513a4e851")
    fun listVideos(): Call<VideoDto>
}

📌 VideoAdapter.kt

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.dldmswo1209.youtube.R
import com.dldmswo1209.youtube.model.VideoModel


class VideoAdapter : ListAdapter<VideoModel, VideoAdapter.ViewHolder>(diffUtil) {
    inner class ViewHolder(private val view: View): RecyclerView.ViewHolder(view){

        fun bind(item: VideoModel){
            val titleTextView = view.findViewById<TextView>(R.id.titleTextView)
            val subTitleTextView = view.findViewById<TextView>(R.id.subTitleTextView)
            val thumbnailImageView = view.findViewById<ImageView>(R.id.thumbnailImageView)

            titleTextView.text = item.title
            subTitleTextView.text = item.subtitle
            Glide.with(thumbnailImageView.context)
                .load(item.thumb)
                .into(thumbnailImageView)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_video,parent,false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object{
        val diffUtil = object: DiffUtil.ItemCallback<VideoModel>(){
            override fun areItemsTheSame(oldItem: VideoModel, newItem: VideoModel): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: VideoModel, newItem: VideoModel): Boolean {
                return oldItem == newItem
            }
        }
    }
}

📌 MainActivity.kt

RecyclerView 에 어답터를 연결시켜주고, getVideoList() 메소드 안에서 Retrofit 인스턴스를 생성하고 비디오 리스트를 서버로부터 가져오는 메소드를 호출

package com.dldmswo1209.youtube

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import com.dldmswo1209.youtube.adapter.VideoAdapter
import com.dldmswo1209.youtube.databinding.ActivityMainBinding
import com.dldmswo1209.youtube.dto.VideoDto
import com.dldmswo1209.youtube.service.VideoService
import retrofit2.*
import retrofit2.converter.gson.GsonConverterFactory

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var videoAdapter : VideoAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        supportFragmentManager.beginTransaction()
            .replace(R.id.fragmentContainer, PlayerFragment())
            .commit()

        videoAdapter = VideoAdapter()
        getVideoList()
        binding.mainRecyclerView.apply {
            adapter = videoAdapter
            layoutManager = LinearLayoutManager(context)
        }
    }

    private fun getVideoList(){
        val retrofit = Retrofit.Builder() // Retrofit 인스턴스 생성
            .baseUrl("https://run.mocky.io/")
            .addConverterFactory(GsonConverterFactory.create()) // GsonConverter : Json 타입의 응답결과를 객체로 매핑해주는 Converter
            .build()

        retrofit.create(VideoService::class.java).also {
            it.listVideos() // listVideos() 메소드 호출
                .enqueue(object: Callback<VideoDto>{ // enqueue 로 통신 실행
                    override fun onResponse(call: Call<VideoDto>, response: Response<VideoDto>) {
                        if(!response.isSuccessful){
                            // 통신 실패
                            Log.d("testt", "response fail")
                            return
                        }
                        response.body()?.let {
                            videoAdapter.submitList(it.videos)
                        }

                    }

                    override fun onFailure(call: Call<VideoDto>, t: Throwable) {
                        // 통신 실패
                    }
                })
        }

    }
}

✅ 문제점

activity_main.xml 을 MotionLayout 으로 만들었기 때문에 화면을 드래그하면 RecyclerView가 스크롤 되지 않고, MotionLayout의 motion이 동작하게 된다.

📌 해결 방법

MotionLayout 을 커스텀해서 터치된 영역과 모션이 동작해야 할 터치 영역을 비교해서 모션이 동작해야 할 터치 영역이 아니라면, 모션 동작을 하지 않도록 커스텀 한다.

import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout

class CustomMotionLayout(context: Context, attributeSet: AttributeSet? = null): MotionLayout(context, attributeSet) {
    private var motionTouchStarted = false // motion 을 동작하기 위한 영역이 터치가 되었는지 확인하는 flag 변수
    private val mainContainerView by lazy {
        findViewById<View>(R.id.mainContainerLayout)
    }
    private val hitRect = Rect()

    init {
        setTransitionListener(object: TransitionListener{
            override fun onTransitionStarted(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int
            ) {}

            override fun onTransitionChange(
                motionLayout: MotionLayout?,
                startId: Int,
                endId: Int,
                progress: Float
            ) {}

            override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
                // 모션이 끝나면 motionTouchStarted 를 초기화
                motionTouchStarted = false
            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout?,
                triggerId: Int,
                positive: Boolean,
                progress: Float
            ) {
            }
        })
    }
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when(event.actionMasked){
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL->{
                motionTouchStarted = false
                return super.onTouchEvent(event)
            }
        }
        if(!motionTouchStarted){
            mainContainerView.getHitRect(hitRect) // getHitRect() 호출시 hitRect 에 mainContainerView 의 hitRect 값을 넣어 줌
            motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt()) // hitRect 영역 안에 터치가 되었는지 확인
        }
        return super.onTouchEvent(event) && motionTouchStarted // motionTouchStarted 가 true 인 경우에만 모션이 동작하도록 함
    }

    private val gestureListener by lazy{
        object : GestureDetector.SimpleOnGestureListener(){
            override fun onScroll(
                e1: MotionEvent,
                e2: MotionEvent,
                distanceX: Float,
                distanceY: Float
            ): Boolean {
                mainContainerView.getHitRect(hitRect)
                return hitRect.contains(e1.x.toInt(), e1.y.toInt())
            }
        }
    }

    private val gestureDetector by lazy{
        GestureDetector(context, gestureListener)
    }
    override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
        return gestureDetector.onTouchEvent(event)

    }
}
profile
매일 조금씩이라도 성장하자

0개의 댓글