와챠의 우당탕탕 코딩 일기장
[Android/Kotlin]Udemy 강의 정리: #7 UI 구성: Home 화면(Json 파싱, Viewpager2 + TabLayout) 본문
[Android/Kotlin]Udemy 강의 정리: #7 UI 구성: Home 화면(Json 파싱, Viewpager2 + TabLayout)
minWachya 2022. 6. 11. 16:29목차
- JSON이란?
- 홈화면 UI에 필요한 데이터 구조 설계하기
- JSON Data 객체로 변환하기
- Gson 라이브러리 사용해서 JSON Data 파싱하기
- Glide 사용해서 이미지 url 넣기
- Viewpager2 + TabLayout사용하기
- TabLauout을 indicator로 사용하기
- 옆에 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 파싱하기
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 넣기
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"