이번 결과물은 최종 결과물이 아닌 이번 파트의 중간결과물이다.
# 중간 결과물 미리보기
현재 중간 결과물 상태는, 인터파크에서 제공하는 API를 사용하여 베스트셀러 책들을 가져와 리사이클러뷰에 띄우는 작업을 완료했다.
다음에는 책 검색 API를 이용하여 책도 검색하는 시스템을 만들 것이다. 이번에는 공부한게 꽤 많아서 끊어서 정리해야 할거 같았다.
# 구현 순서
인터파크 도서 API에 API요청(Retrofit사용) -> 받은 값 dataclass로 정의 -> 리사이클러뷰 어댑터에 리스트 전달 -> 리사이클러뷰에 띄어짐
# 알게 된 것
- retrofit
- gson
- DTO 구조
- glide
- viewBinding
- inflate
- layoutinflate
- RecyclerView
# Retrofit사용하여 API 정보 받아오기 ( request, response )
## 사용하기전
추가해준다.
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation 'com.google.code.gson:gson:2.9.0'
<uses-permission android:name="android.permission.INTERNET"/>
## DTO
API는 HTTP통신으로 이루어지기 때문에 서버에 request를 요청해야하는데 Retrofit이 그 행동을 도와준다. 일단 인터파크API에 가서 key를 발급받고, POSTMAN을 활용하여 정보가 어떻게 넘어오는지 확인한다. 베스트셀러 API를 요청했을떄 대강 이런식으로 넘어온다.
{
"title": "인터파크도서검색결과",
"link": "http://bsearch.interpark.com/dsearch/book.jsp?query=ios&sch=book&order_tp=accuracy&contextCheckboxUseYn=N&filter_disp_no=028&titleCheckboxUseYn=N&personCheckboxUseYn=N&entrNmCheckboxUseYn=N&isbnCheckboxUseYn=N",
"language": "ko",
"copyright": "Copyright ⓒ 2009 INTERPARK INT All rights reserved.",
"pubDate": "3 Apr 2022 14:16:36 GMT",
"imageUrl": "http://bimage.interpark.com/renewPark/topGnb/logo.jpg",
"totalResults": 131,
"startIndex": 1,
"itemsPerPage": 10,
"maxResults": 10,
"queryType": "title",
"query": "ios",
"searchCategoryId": "100",
"searchCategoryName": "국내도서",
"returnCode": "000",
"returnMessage": "정상",
"item": [
{
"itemId": 210531192,
"title": "아이폰으로 용돈벌기 1",
"description": "아이폰으로 용돈벌기 1 출판사 서평2009년 11월 28일 아이폰이 한국에 출시되면서 IT업계는 엄청난 변화를 맞이하게 되었습니다. 아이폰은 한국에서는 별 볼일 없을 것이라는 예상도 있었지만 아이폰 출시 이후 점차 스마트폰의 비율이 높아져갔고 사람들도 스마트폰의 장점에 매료되었습니다.iOS 개발이 개발자들 사이에서 인기를 얻기 시작하면서 시중에 많은 책들이 등장했지만, 막상 실제 앱을 개발하려고 하면 어려운 점이 많습니다. 일례로 개발지식만 다루는 책에서는 앱 스토어 등록 절차나 광고 또는 홍보 방법이 설명되지 않아 실제로 앱을 만들고 판매하는 내용을 알고자 하는 독자들이 아쉬움을 토로하기도 합니다. 본 도서에서는 크게 3가지의 방향을 추구하고 있습니다.애플 생태계 경험이 책의 목표는 독자가 iOS의 ‘개발...",
"pubDate": "20120525",
"priceStandard": 25000,
"priceSales": 22500,
"discountRate": "10",
"saleStatus": "절판",
"mileage": "1250",
"mileageRate": "6",
"coverSmallUrl": "http://bimage.interpark.com/goods_image/1/1/9/2/210531192h.jpg",
"coverLargeUrl": "http://bimage.interpark.com/goods_image/1/1/9/2/210531192s.jpg",
"categoryName": "",
"publisher": "아이콕스(iCox)",
"customerReviewRank": 10,
"author": "",
"translator": "",
"isbn": "9788996852117",
"link": "http://book.interpark.com/blog/integration/product/itemDetail.rdo?prdNo=210531192&refererType=8303&bookblockname=bpmain_in&booklinkname=wg_search_A9EA8BB6A5DDF72EBC08BFC34A12C2A6E3FDF06F155E65B848F94062B6581B9F",
"mobileLink": "http://m.book.interpark.com/view.html?PRD_NO=210531192&SHOP_NO=0000400000",
"additionalLink": "http://book.interpark.com/gate/ippgw.jsp?goods_no=210531192&biz_cd=",
"reviewCount": 1
},
{
"itemId": 216762202,
"title": "iOS 컴포넌트와 프레임워크 실전 프로그래밍",
//...
}, //...
]
}
큰 { } 안에 해당 요청 API에 관한 값이 들어있고, 그 안에 item을 배열로 넘겨줘 도서의 정보를 보내준다. 이러한 구조를 DTO(Data Transfer Object) 라고한다고 한다. 저번에 DAO도 잠깐 공부했었는데 로직을 가지고 있지 않은 순수한 정보객체라고 한다. 베스트셀러DTO와 서치DTO가 필요하다. 또 item을 저장한 model도 필요한데 book이라는 모델로 만들면 괜찮을거 같다.
### model 만들기
// model/Book
import com.google.gson.annotations.SerializedName
data class Book(
@SerializedName("itemId") val id : Long,
@SerializedName("title") val title : String,
@SerializedName("description")val description : String,
@SerializedName("coverSmallUrl")val coverSmallUrl : String
)
@SerializedName는 API에서 받아오는 이름과 다를때 지정해 줄 수 있다. 같다면 굳이 지정해주지 않아도 된다.
### DTO 만들기
// SearchBook DTO
import com.google.gson.annotations.SerializedName
data class SearchBookDto(
@SerializedName("title") val title : String,
@SerializedName("item") val books : List<Book>
)
// BestSeller DTO
import com.google.gson.annotations.SerializedName
data class BestSellerDto(
@SerializedName("title") val title : String,
@SerializedName("item") val books: List<Book>
)
## Retrofit
이제 retrofit이 API를 요청하면 그 값들을 저장할 그릇(?)을 다 만들
었다. api를 요청하는 Service를 만들자.
retrofit에 가면 어떻게 사용하는지 알려주는데, service인터페이스를 만들어 어떻게 쿼리를 보낼지를 만들어줘야한다.
// api/BookService
interface BookService {
// 책 검색 API
@GET("api/search.api?output=json")
fun getBooksByName(
@Query("key") apiKey: String,
@Query("query") keyword: String
): Call<SearchBookDto>
// 베스트 셀러 API
@GET("api/bestSeller.api?output=json&categoryId=100")
fun getBestSellerBooks(
@Query("key") apiKey : String,
): Call<BestSellerDto>
}
서비스에서 정의시킨 함수를 요청하면 @GET에 정의한 기본쿼리 + 함수의 인자로 적은 쿼리를 보낸다. 반환값으로는 아까 만들었던 Dto를를 받게한다.
### request보내고 response받기
// 빌더 만들기
val retrofit = Retrofit.Builder()
.baseUrl("https://book.interpark.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
val bookService = retrofit.create(BookService::class.java)
// 보낼 요청 함수 호출하기
bookService.getBestSellerBooks("발급받은 API키")
.enqueue(object : Callback<BestSellerDto> {
override fun onResponse(call: Call<BestSellerDto>, response: Response<BestSellerDto>) {
// 성공적으로 응답 받았을때 행동 정의
}
override fun onFailure(call: Call<BestSellerDto>, t: Throwable) {
}
})
## 인터페이스에 대한 의문
여기서 의문이 생긴게, 인터페이스는 안에 내용이 구현 돼 있지 않은, 즉 이 함수를 구현해서 사용해라라고 명한 것이라고 알 고 있었다. 그러면 내가만든 인터페이스인 BookService에서 두가지 쿼리 요청 함수를 만들었고 그 내부에 대해서는 정의하지 않았다. 그리고 그리고 쿼리를 보내고 사용하는 어떠한 행동도 내가 정의하지 않았다. 난 인터페이스만 정의했는데 이게 어떻게 작동하는 것일까 의문이었다.
그렇다면 내가 정의한 인터페이스를 어디선가 사용하고 있다는건데 찾아보려고 했지만 이것에대해서는 찾을 수 없었다. 여튼 그래서 인터페이스 구글링 열심히 해봤는데, 가장 중요한건 매개체가 된다는 것이다. 아마 리사이클러뷰에서 버튼같은거 정의하다보면 확실히 알 수 있을거 같은데, 아직은 감만 잡았다.. ㅠ
# Glide
API는 당연히 사진을 URL이미지 형태로 보내준다. 우리가 많이 써본거처럼 url를 누르면 이미지가 튀어나오는거다. 이걸 어떻게 ImageView에 적용해야할까?라는 의문에 대한 답이 Glide이다. 코드 몇줄만으로 아주 쉽게 해결해준다.
먼저 설정에 추가해준다.
implementation 'com.github.bumptech.glide:glide:4.11.0'
//glide를 이용하여 url이미지 적용
Glide.with(binding.bookImageView.context)
.load(bookModel.coverSmallUrl)
.into(binding.bookImageView)
# viewBinding
findViewByID에서 자유로워질 수 있다. 근데 이것도 쓰다보니까 익숙해져서... viewBinding으로 하려니까 더 헷갈리는거 같지만, 새로운거에 익숙해져야 한다. 사실 옛날에 프로젝트할때 써봤는데 그때는 체계적으로 공부하지 않아서 엄청 오류가 많이 떴었다.
//gradle.app android{ ... } 에 추가
viewBinding {
enabled = true
}
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.sampleTextView.setText("이렇게 쉽게 접근이 가능합니다")
}
사실 이것만 보면 쉬운데, inflate랑 layoutInflater랑 context의 개념이 너무 많이 등장해서(리사이클러뷰에서도...) 좀 찾아 봤었다.
viewBinding을 켜주면, xml명이 activity_camel_case.xml이라면 activity에서 ActivityCamelCase라는 것이 자동으로 생성되는데 이게 xml이 view객체로 바뀐 것이라고 할 수 있다.
viewBinding을 쓰지 않아도, 액티비티에서 xml(레이아웃)의 뷰에 접근할 수 있는 것도, 어디선가 xml이 객체화 되어 메모리에 올라온다는 뜻이다. 그걸 setContentView에서 해주고 있었던 것인데, inflate를 이용해서 그 작업을 해주는 것이다. --> inflate는 부풀리다라는 뜻을 가지고 있다.
즉 아래와 같이 inflate를 써서 해당 레이아웃에 접근하도록 하는데
ItemBookBinding.inflate(LayoutInflater.from(parent.context),parent,false)
매개변수의 순서대로
- (1) inflater: LayoutInflater
- (2) parent: 뷰를 붙일 ViewGroup. 사용하는 뷰에 따라 다른 값을 사용한다.
- (3) attachToParent: Boolean 값.
이 된다.
# RecyclerView
리사이클러뷰를 뷰 바인딩을 사용해서 만드니까 조금 헷갈렸다. 일단 코드를 기록해두도록 하겠다.
class BookAdapter : ListAdapter<Book,BookAdapter.BookViewHolder>(diffUtil) {
inner class BookViewHolder(private val binding : ItemBookBinding ) : RecyclerView.ViewHolder(binding.root){
fun bind(bookModel : Book){
binding.titleTextView.setText(bookModel.title)
binding.descptionTextVIew.setText(bookModel.description)
//glide를 이용하여 url이미지 적용
Glide.with(binding.bookImageView.context)
.load(bookModel.coverSmallUrl)
.into(binding.bookImageView)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookViewHolder {
return BookViewHolder(ItemBookBinding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: BookViewHolder, position: Int) {
holder.bind(currentList[position])
}
companion object{
val diffUtil = object :DiffUtil.ItemCallback<Book>() {
override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean {
return oldItem.itemId == newItem.itemId
}
}
}
}
일단 getItemCount가 없다는 점과 diffUtill을 만들어 준 것이 달랐다. 이건 viewBinding사용과는 별개인거 같다.
나머지의 흐름자체는 똑같았으나,
비슷하면 비슷하고 다르면 다를 수 있는데, R.layout.item_quote로 접근하던걸, layoutinflater로 접근한것의 차이가 있다.
// 저번(pagerView2)에서 사용헀던 코드
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuoteViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_quote, parent, false)
return QuoteViewHolder(view)
}
## currentList
리사이클러뷰에서
// 1
override fun onBindViewHolder(holder: BookViewHolder, position: Int) {
holder.bind(currentList[position])
}
// 2
override fun onBindViewHolder(holder: QuoteViewHolder, position: Int) {
holder.bind(quotes[position])
}
이렇게 list명을 직접사용해도 되고, 제공되는 currentList로 간단하게 사용할 수 있다.
# 느낀점
- context 개념
- inflate 개념
- layoutInflater개념
- 인터페이스(interface)개념
공부하면서 해당 부분이 나오면 더욱 신경쓰면서 봐야할거 같다.
레트로핏
인터페이스
Glide