[Android/Kotlin]Retrofit을 활용한 Kakao api로 이미지 받아오기 과제

정민수·2024년 1월 26일

컨셉 유지하기 위해 중간중간 얼렁뚱땅 넘어가는 부분이 있어요. 절ㄷ ㅐ 귀찮아서 그런거아니고 몰라서 그런건 맞음

처음부터 끝까지 과정을 무에서 유로 흘러가는 과정을 담아볼까 한다. 하나하나 세세하게 한달 뒤 또 까먹고 전전긍긍 하고있을게 분명하기 때문에ㅎㅎ

1일차 : UI구성 및 기능..

우선 viewPager2와 tabLayout 활용

gradle에 추가

    //viewPager2
    implementation("androidx.viewpager2:viewpager2:1.0.0")

레트로핏 때 필요한거 미리 넣어놓기

    //retrofit
    implementation("com.google.code.gson:gson:2.10.1")
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.10.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")

인터넷 권한 받아오기 그냥 한번에 넣어놓을게요 manifest에 넣으면 됨.

<uses-permission android:name="android.permission.INTERNET"/>

오키 여기까지 하고 까먹은거 있으면 추가로 넣어야지
fragment필요한거 생성해주시고 나는 이미지검색프래크먼트랑 저장소 프래그먼트 2개 만듬.

viewPagerAdapter

package com.android.searchproject

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter

class ViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {

    var fragments : ArrayList<Fragment> = ArrayList()

    override fun getItemCount(): Int {
        return fragments.size
    }

    override fun createFragment(position: Int): Fragment {
        return fragments[position]
    }

    fun addFragment(fragment: Fragment){
        fragments.add(fragment)
        notifyItemInserted(fragments.size - 1)
    }

    fun removeFragment(){
        fragments.removeLast()
        notifyItemRemoved(fragments.size)
    }
}

MainActivity.kt

package com.android.searchproject

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.viewpager2.widget.ViewPager2
import com.android.searchproject.databinding.ActivityMainBinding
import com.google.android.material.tabs.TabLayoutMediator

class MainActivity : AppCompatActivity() {


    private lateinit var binding: ActivityMainBinding


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initViewPager()
    }
    private fun initViewPager() {
        var viewPager2Adapter = ViewPagerAdapter(this)
        viewPager2Adapter.addFragment(SearchFragment())
        viewPager2Adapter.addFragment(StorageFragment())

        binding.viewPager.apply {
            adapter = viewPager2Adapter

            registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)
                }
            })
        }
        TabLayoutMediator(binding.tapLayout, binding.viewPager) { tab, position ->
            when (position) {
                0 -> {
                    tab.text = "이미지 검색"
                }
                1 -> {
                    tab.text = "좋아요 보관함"
                }
            }
        }.attach()

    }
}

MainActivity.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="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/black"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="HaruSearch"
            android:textColor="@color/white"
            android:textSize="30sp"
            android:textStyle="bold" />
    </androidx.appcompat.widget.Toolbar>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/constraintLayout2"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#C5C5C5"
        android:paddingHorizontal="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar">

        <EditText
            android:id="@+id/et_main_search"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:paddingStart="5dp"

            android:hint=" 키워드"
            android:layout_marginEnd="10dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/btn_main_search"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btn_main_search"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="검색"
            android:backgroundTint="#505050"
            android:textSize="16sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>



    
    

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/constraintLayout2"
        app:layout_constraintBottom_toTopOf="@+id/tapLayout"/>



    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tapLayout"
        android:layout_width="409dp"
        android:layout_height="wrap_content"
        tools:layout_editor_absoluteX="1dp"
        tools:layout_editor_absoluteY="682dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Search_recyclerview.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"
    android:padding="15dp">

    <ImageView
        android:id="@+id/search_Image"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:scaleType="centerCrop"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/search_favorite"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_marginTop="5dp"
        android:layout_marginEnd="5dp"
        android:visibility="gone"
        android:src="@drawable/favorite"
        app:layout_constraintEnd_toEndOf="@+id/search_Image"
        app:layout_constraintTop_toTopOf="@+id/search_Image" />

    <TextView
        android:id="@+id/search_name"
        tools:text="name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="1dp"
        android:layout_marginTop="2dp"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/search_Image"/>

    <TextView
        android:id="@+id/search_datetime"
        tools:text="dateandTime"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="1dp"
        android:textSize="15sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/search_name" />
</androidx.constraintlayout.widget.ConstraintLayout>

이렇게 일단 대충 UI를 구성해주었다.

Kakao API 받아오기

나의 어플리케이션을 생성해서 받아와준다.

Search DataClass 생성해주기.


데이터 클래스를 작성할 때는 카카오에서 제공해주는 문서에 들어가면 친절하게 이름과 타입을 표로 만들어 놓았다. 우리는 이걸 보고 옮겨 적어주기만 하면된다.

package com.android.searchproject

import android.os.Parcelable
import android.provider.DocumentsContract
import com.google.gson.annotations.SerializedName

data class SearchResponse(
    @SerializedName("documents")
    val documents: MutableList<DocumentsContract.Document>?,
    @SerializedName("meta")
    val metaData: MetaData?
)

data class MetaData(
    @SerializedName("total_count")
    val totalCount : Int,
    @SerializedName("pageable_count")
    val pageableCount : Int,
    @SerializedName("is_end")
    val isEnd : Boolean
)

@Parcelize
data class Document(
    @SerializedName("collection")
    val collection: String,
    @SerializedName("datetime")
    val dateTime: String,
    @SerializedName("display_sitename")
    val displaySiteName: String,
    @SerializedName("doc_url")
    val docUrl: String,
    @SerializedName("height")
    val height: Int,
    @SerializedName("image_url")
    val imageUrl: String,
    @SerializedName("thumbnail_url")
    val thumbnailUrl: String,
    @SerializedName("width")
    val width: Int,
): Parcelable

Parcelable오류 (해결!!)

Parcelable이 정의가 되지 않았다는 오류가 생겼다.

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id ("kotlin-parcelize")
    id ("kotlin-kapt")
}

플러그인에 밑에 2줄을 추가해주니 해결 됨.

@SerializedName이 무엇인가요?

아 찾아봤는데 정리하기 귀찮다. 나중에 생각나면 다시 정리해야지

Retrofit 객체 생성, NetworkClient.kt

레트로핏에 대한 자세한 내용은 이전에 작성한 자료를 보시면 됩니다.
(아직 작성 안했고 이 자리에 작성하는대로 링크 넣어야징~~)

object NetWorkClient {

    private const val BASE_URL = "https://dapi.kakao.com/"


    private fun createOkHttpClient(): OkHttpClient {
        val interceptor = HttpLoggingInterceptor()

        if (BuildConfig.DEBUG)
            interceptor.level = HttpLoggingInterceptor.Level.BODY
        else
            interceptor.level = HttpLoggingInterceptor.Level.NONE

        return OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .addNetworkInterceptor(interceptor)
            .build()
    }

    private val searchRetrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)//
        .addConverterFactory(GsonConverterFactory.create()).client(
            createOkHttpClient()
        ).build()

    val searchNetwork: NetworkInterface = searchRetrofit.create(NetworkInterface::class.java)

}

Base_URL의 코드는 기본정보의 URL에서 가지고 오면 된다. (위에 사진 있음.)

NetworkInterface

interface NetworkInterface {

    @Headers("Authorization: API")
    @GET("v2/search/image")
    suspend fun searchImage(@QueryMap param: HashMap<String, String>) : SearchResponse
}

API에는 어플리케이션 생성하고 받은 REST_API 넣으면 됨.

SearchAdapter

package com.android.searchproject

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.android.searchproject.databinding.SearchRecyclerviewBinding
import com.bumptech.glide.Glide
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class SearchAdapter(private val context: Context, private val results: MutableList<Document>, private val favoriteItems: MutableList<Document>) :
    RecyclerView.Adapter<SearchAdapter.SearchViewHolder>() {

    interface SearchThumbnailClickListener {
        fun onClick(view: View, position: Int)
    }

    var searchThumbnailClickListener: SearchThumbnailClickListener? = null

    private val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.getDefault())
    private val outputFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())

    inner class SearchViewHolder(private val binding: SearchRecyclerviewBinding) :
        RecyclerView.ViewHolder(binding.root) {
        val image = binding.searchImage
        val name = binding.searchName
        val dateTime = binding.searchDatetime
        val favorite = binding.searchFavorite
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
        return SearchViewHolder(
            SearchRecyclerviewBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }

    override fun getItemCount(): Int {
        return results.size
    }

    override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
        holder.image.setOnClickListener {
            searchThumbnailClickListener?.onClick(holder.favorite, position)
        }

        Glide.with(context)
            .load(results[position].thumbnailUrl)
            .into(holder.image)
        holder.name.text = results[position].displaySiteName
        holder.dateTime.text =
            outputFormat.format(inputFormat.parse(results[position].dateTime) as Date)
        holder.favorite.isVisible = (favoriteItems.find { it == results[position] } != null)
    }
}

SearchFragment

아 몰라

2일차

우선 어제처럼 TIL을 작성하면 너무 비효율적인 것 같음.
오늘은 주말이지만 어제 해결하지 못한 오류가 계속 생각나서 코드 쳐다 봄.

오류 (ViewPager2 does not support direct child views)


처음엔 setFragment를 viewPager에 진행했었음.
애뮬레이터 실행은 되는데 검색어에 입력하고 검색버튼을 누르면 앱이 꺼지는 오류가 발생함


이렇게 사이트 검색은 가능하지만 ViewPager2 does not support direct child views와 같은 오류가 발생함. ViewPager2위에 직접적으로 자식뷰를 생성하는건 안된다고 한다

main_activity_xml. 그래서 뷰페이저를 프레임 레이아웃으로 감싸고

<FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/constraintLayout2"
        app:layout_constraintBottom_toTopOf="@+id/tapLayout">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp" />

    </FrameLayout>

frameLayout위에 리사이클러뷰가 올라가질 수 있도록 함.

검색이 가능해졌다!!

일단 오늘은 여기까지 하고 3일차 목표

3일차 목표

이미지 클릭시 좋아요 보관함으로 이동
키보드 숨김처리
엔터 클릭 시 검색
리스트에서 특정 이미지를 선택하면 특별한 표시를 보여주도록 구현합니다

profile
응애...아무것도 모르는 개발자 흉내라도 내고 싶은 비전공자입니다.

0개의 댓글