와챠의 우당탕탕 코딩 일기장

[Android/Kotlin]Udemy 강의 정리: #7 UI 구성: Home 화면(Json 파싱, Viewpager2 + TabLayout) 본문

코딩 일기장/Android(Kotlin)

[Android/Kotlin]Udemy 강의 정리: #7 UI 구성: Home 화면(Json 파싱, Viewpager2 + TabLayout)

minWachya 2022. 6. 11. 16:29
반응형

목차

  1. JSON이란?
  2. 홈화면 UI에 필요한 데이터 구조 설계하기
  3. JSON Data 객체로 변환하기
  4. Gson 라이브러리 사용해서 JSON Data 파싱하기
  5. Glide 사용해서 이미지 url 넣기
  6. Viewpager2 + TabLayout사용하기
    1. TabLauout을 indicator로 사용하기 
    2. 옆에 item도 살짝 보이게 구현하기

 


1. JSON이란?

  • https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/JSON
  • JavaScript Object Notation (JSON)은 Javascript 객체 문법.
  • 네트워크 통신에서 데이터 주고받는 포맷으로 많이 사용됨.
  • key:value로 표현(String, boolean, int, array등)
  • 이런 json 데이터를 앱에서 받아서 하는 일: 객체(json object)로 변환, 변수로 저장하기
{
  "squadName": "Super hero squad",
  "homeTown": "Metro City",
  "formed": 2016,
  "secretBase": "Super tower",
  "active": true,
  "members": [
    {
      "name": "Molecule Man",
      "age": 29,
      "secretIdentity": "Dan Jukes",
      "powers": [
        "Radiation resistance",
        "Turning tiny",
        "Radiation blast"
      ]
    },
    {
      "name": "Madame Uppercut",
      "age": 39,
      "secretIdentity": "Jane Wilson",
      "powers": [
        "Million tonne punch",
        "Damage resistance",
        "Superhuman reflexes"
      ]
    },
    {
      "name": "Eternal Flame",
      "age": 1000000,
      "secretIdentity": "Unknown",
      "powers": [
        "Immortality",
        "Heat Immunity",
        "Inferno",
        "Teleportation",
        "Interdimensional travel"
      ]
    }
  ]
}

홈 화면 만들기 대작전


2. 홈화면 UI에 필요한 데이터 구조 설계하기

1. main 오른쪽 클릭 > New > Folder > Assets Folder 

: Json, 각종 미디어 파일 추가 가능

 

2. home.json file 추가

아래 화면 참고하면서 json data 만들기

{
  "title": {
    "text": "minWachya님, 이런 상품 어때요?",
    "icon_url": "https://user-images.githubusercontent.com/61674991/173049282-bbae9702-4ef6-4cc8-8671-22bf64c2da47.png"
  },
  "top_banners": [
    {
      "background_image_url": "https://user-images.githubusercontent.com/61674991/173049405-c47ae022-a536-4f49-b151-e8f3aa82ab9a.png",
      "badge": {
        "label": "기획전",
        "background_color": "#52514d"
      },
      "label": "따스한 겨울\n준비하기",
      "product_detail": {
        "brand_name": "twg.official",
        "label": "캐시미어 100% 터틀넥 스웨터",
        "discount_rate": 9,
        "price": 102000,
        "thumbnail_image_url": "https://user-images.githubusercontent.com/61674991/173049400-b2f06b08-5cc3-46c9-bc95-c5e1b0e2e2ec.png",
        "product_id": "FW-twg-sweater-1"
      }
    },
    {
      "background_image_url": "https://user-images.githubusercontent.com/61674991/173049373-4c1c9535-c85b-41d1-908d-891cc9c0520e.png",
      "badge": {
        "label": "기획전",
        "background_color": "#967a6d"
      },
      "label": "나만의\n홈 오피스",
      "product_detail": {
        "brand_name": "Desk",
        "label": "슬림 데스크 800",
        "discount_rate": 16,
        "price": 160000,
        "thumbnail_image_url": "https://user-images.githubusercontent.com/61674991/173049397-6eb21f9f-50e3-4f1a-b439-4b5bb06a2345.png",
        "product_id": "desk-1"
      }
    }
  ]
}

 

+ 이미지 링크 어떻게 얻나: 깃허브 이슈 사용하기

1. 깃허브 이슈탭 클릭 > Nes issue 클릭

2. 이미지 드로그 앤 드롭하면 자동으로 url 생성~ 업로드는 안 해도 됨!!


3. JSON data를 객체로 변환하기

1.  asset 폴더 접근해서 데이터 읽어기 위한 AssetLoader.kt 생성

AssetLoader.kt

package com.example.shoppi

import android.content.Context

class AssetLoader {
    // 오류 처리 포함
    fun getJsonString(context: Context, fileName: String): String? {
        // 성공, 실패로 나뉘는 작업 처리: runCatching
        // 성공 시 result, 실패 시 null 반환
        return kotlin.runCatching {
            loadAsset(context, fileName)
        }.getOrNull()
    }

    // 자원 읽어오기
    private fun loadAsset(context: Context, fileName: String): String {
        // context 통해 app 전역에서 사용할 수 있는 정보에 접근 가능
        // + 리소스, 데이터베이스같은 시스템 자원에 접근 가능
        // json 파일 열고 자원사용하기(use 사용: 자원 정리 도와줌)
        return context.assets.open(fileName).use { inputStream ->
            val size = inputStream.available()  // inputStream에 data 실제로 존재하는지?
            val bytes = ByteArray(size) // 크기가 sizw인 ByteArray 생성
            inputStream.read(bytes)     // inputStream의 ByteArray를 bytes 객체에 복사
            String(bytes)   // String으로 변경하여 리턴
        }
    }
}

4. Gson 라이브러리 사용해서 JSON Data 파싱하기

(gson 깃허브 링크)

 

1. 라이브러리 추가

// Gson
implementation 'com.google.code.gson:gson:2.9.0'

 

2. json 타입에 맞는 객체 생성

package com.example.shoppi

import com.google.gson.annotations.SerializedName

// home.json과 같은 데이터 구조
// 변수명은 json의 key값과 같게
// 다르게 하고 싶다면 @SerializedName 어노테이션 사용
data class HomeData(
    val title: Title,
    @SerializedName("top_banners") val topBanners: List<Banner>,

)

data class Title(
    val text: String,
    @SerializedName("icon_url") val iconUrl: String
)

data class Banner(
    @SerializedName("background_image_url") val backgroundImageUrl: String,
    val badge: BannerBadge,
    val label: String,
    @SerializedName("product_detail") val productDetail: ProductDetail
)

data class BannerBadge(
    val label: String,
    @SerializedName("background_color") val backgroundColor: String
)

data class ProductDetail(
    @SerializedName("brand_name") val brandName: String,
    val label: String,
    @SerializedName("discount_rate") val discountRate: Int,
    val price: Int,
    @SerializedName("thumbnail_image_url") val thumbnailImageUrl: String,
    @SerializedName("product_id") val productId: String
)

 

3. 파싱하기

val assetLoader = AssetLoader()
val homeJsonString = assetLoader.getJsonString(requireContext(), "home.json")
if(!homeJsonString.isNullOrEmpty()) {
    val gson = Gson()
    val homeData = gson.fromJson(homeJsonString, HomeData::class.java)

    toolbarTitle.text = homeData.title.text
    GlideApp.with(this@HomeFragment)
        .load(homeData.title.iconUrl)
        .into(toolbarIcon)
    viewpager.adapter = HomeBannerAdapter().apply {
        submitList(homeData.topBanners)
    }
}

5. Glide 사용해서 이미지 url 넣기

Glide 공식문서

 

1. Manifest에 권한 추가

<!--Glide 사용 위한 권한-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

2. gradle(:app)에 라이브러리 추가

// Glide
implementation ("com.github.bumptech.glide:glide:$glideVersion")
kapt "com.github.bumptech.glide:compiler:$glideVersion"

+ kapt 컴파일 위한 플러그인 추가

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'	// 이거 추가
}

3. gradle(:project)에 버전 추가

ext {
    glideVersion = '4.12.0'
}

 

4. AppGlide 모듈 상속받은 클래스 생성하기

package com.example.shoppi

import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule

@GlideModule
class GlideModule: AppGlideModule() { }

 

~컴파일 한 번 해주기~

 

 

5. 사용

GlideApp.with(this@HomeFragment)
                .load(iconUrl)
                .into(toolbarIcon)

6. Viewpager2 + TabLayout사용하기

(공식문서)

ViewPager2 + Indicator

 

1. 라이브러리 추가

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

 

2. xml 코드 짜기

 <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/viewpager_home_banner"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/viewpager_home_banner_indicator"
                android:layout_width="0dp"
                android:layout_height="45dp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/viewpager_home_banner"
                app:tabBackground="@drawable/selector_viewpager_indicator"
                app:tabGravity="center"
                app:tabIndicatorHeight="0dp" />

        </androidx.constraintlayout.widget.ConstraintLayout>

 

3. Viewpager2 Adapter 생성

package com.example.shoppi

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

// ListAdapter: 데이터의 리스트 받아서 순차적으로 viewholder와 바인딩함.
// 레이아웃 유지한 채로 데이터만 업데이트
// BannerDiffCallback: 스크롤 변경됨에 따라서 데이터 변경 확인하고 업데이트
class HomeBannerAdapter: ListAdapter<Banner, HomeBannerAdapter.HomeBannerViewHolder>(BannerDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeBannerViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_home_banner, parent, false)
        return HomeBannerViewHolder(view)
    }

    override fun onBindViewHolder(holder: HomeBannerViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class HomeBannerViewHolder(view: View): RecyclerView.ViewHolder(view) {
        private val bannerImageView = view.findViewById<ImageView>(R.id.iv_banner_image)
       
        fun bind(banner: Banner) {
            GlideApp.with(itemView)
                .load(banner.backgroundImageUrl)
                .into(bannerImageView)
        }
    }
}

// BannerDiffCallback: 스크롤 변경됨에 따라서 데이터 변경 확인하고 업데이트
// Banner 객체가 같은지 다른지 체크 기준 정하기
class BannerDiffCallback: DiffUtil.ItemCallback<Banner>() {
    // 제품 id 같으면 같은 제품
    override fun areItemsTheSame(oldItem: Banner, newItem: Banner): Boolean {
        return oldItem.productDetail.productId == newItem.productDetail.productId
    }
    // 제품 id도 같을 때 다른 내용들도 모두 같은지?(업데이트 되었을 수도 있으니까)
    override fun areContentsTheSame(oldItem: Banner, newItem: Banner): Boolean {
        return oldItem == newItem
    }

}

 

4. 어댑터에 데이터 연결

viewpager.adapter = HomeBannerAdapter().apply {
    submitList(homeData.topBanners)
}

6-1. TabLayout을 indicator로 설정하기

1.. selector 만들기

drawable > selector_viewpager_indecator.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/selected_viewpager_indicator" android:state_selected="true" />
    <item android:drawable="@drawable/unselected_viewpager_indicator" android:state_selected="false" />
</selector>

selected_viewpager_indicator.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:innerRadius="0dp"
    android:shape="ring"
    android:thickness="4.5dp"
    android:useLevel="false">
    <solid android:color="@color/shoppi_black_01" />
</shape>

unselected_viewpager_indicator.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:innerRadius="0dp"
    android:shape="ring"
    android:thickness="3dp"
    android:useLevel="false">
    <solid android:color="@color/shoppi_grey_05" />
</shape>

2. xml에서 Tablayout에 아래 속성 추가

app:tabBackground="@drawable/selector_viewpager_indicator"

6-2. 옆의 item도 살짝 보이게 구현하기

1. res > values > dimens.xml 추가

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="viewpage_item_width">312dp</dimen>
    <dimen name="viewpage_item_margin">16dp</dimen>
</resources>

 

2. 코드

// dp -> px
val pageWidth = resources.getDimension(R.dimen.viewpage_item_width)
val pageMargin = resources.getDimension(R.dimen.viewpage_item_margin)
val screenWidth = resources.displayMetrics.widthPixels
// 얼만큼 살짝 보일지 계산
viewpager.offscreenPageLimit = 3
val offset = screenWidth - pageWidth - pageMargin
viewpager.setPageTransformer { page, position ->
    page.translationX = position * -offset
}
TabLayoutMediator(viewpagerIndicator, viewpager) { tab, position -> }.attach()

컴포넌트의 반복되는 레이아웃 이름: item_xxx

+imageView는 항상 scaleType 설정해주기!!

 

TextView:ellipsize

TextView text 길이 길어질 때 ... 표시

android:ellipsize="end"

 

반응형
Comments