안드로이드를 공부해보고자 무작정 kotlin 으로 DB 데이터 리스트 출력부터 해보았다.
(여기저기 구글링하여 많은 블로그들의 종합적인 소스를 기반으로 했기때문에 틀린 소스일 수 있음)
OkHttp
효율적으로 데이터를 로드하고 대역폭을 절약할 수 있게한다.
- OkHttp는 기본적으로 효율적인 HTTP 클라이언트이며 HTTP/2 지원을 통해 동일한 호스트에 대한 모든 요청이
소켓을 공유할 수 있다. - 연결 풀링은 요청 지연 시간을 줄인다. (HTTP/2를 사용할 수 없는 경우)
- 투명 GZIP는 다운로드 크기를 줄인다.
- 응답 캐시는 반복 요청에 대한 네트워크를 완전히 방지한다.
라고 OkHttp 공식 홈페이지를 번역기로 돌렸다...
그리고 Android 5.0+(API 레벨 21+) 및 Java 8+에서 작동한다.
Retrofit
Retrofit은 OkHttp에 의존하며 A type-safe HTTP client for Android and Java
라고 Retrofit 공식 홈페이지에서 말하듯이 네트워크 통신을 하여 데이터를 필요한 형태로 안전하게 받을 수 있다는 의미
Recyclerview
Recyclerview는 ListView
에서 좀더 유연해진 버전이라고 한다.
구글링 도중 완벽하게 설명되어있는 것 같은 블로그를 찾아서 자세한 설명은 참고하길 바란다. ListView vs Recyclerview
짧고 굵게 설명하자면 리스트를 만드는데 재사용이 좋아진 부분, LayoutManager와 ViewHolder 패턴의 의무적인 사용, Item에 대한 뷰의 변형이나 애니메이션할 수 있는 개념이 추가
(아래 이미지는 위 블로그에서 사용된 이미지)
app
app에 retrofit2, okhttp3, recyclerview 를 추가한다.
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc03"
implementation 'com.squareup.okhttp3:okhttp:3.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-gson:2.7.2'
Retrofit
은 결과를 원하는 형태로 받기 위해 converter-gson
을 같이 추가한다
Recyclerview Layout
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintVertical_bias="0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Recyclerview를 표현할 Layout을 생성한다.
Item Layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/rowNo"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/rowName"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/rowSection"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/rowRegDate"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"/>
</LinearLayout>
리스트에서 row 마다 표시할 Item Layout을 생성한다. 여기서는 Text만 표시하기로함
Pojo Class (Java Beans)
test 용으로 php로 DB에서 데이터를 json으로 출력한 것을 가져오기 위한 Gson 형태의 Pojo Class
class Items {
@SerializedName("No")
@Expose
private var no: String? = null
@SerializedName("Name")
@Expose
private var name: String? = null
@SerializedName("Section")
@Expose
private var section: String? = null
@SerializedName("RegDate")
@Expose
private var regDate: String? = null
fun getNo(): String? {
return no
}
fun setNo(no: String?) {
this.no = no
}
fun getName(): String? {
return name
}
fun setName(name: String?) {
this.name = name
}
fun getSection(): String? {
return section
}
fun setSection(section: String?) {
this.section = section
}
fun getRegDate(): String? {
return regDate
}
fun setRegDate(regDate: String?) {
this.regDate = regDate
}
override fun toString(): String {
return "ClassPojo [RegDate = " + regDate + ", Name = " + name + ", Section = " + section + ", No = " + no + "]"
}
}
Pojo Class 2
구조는 다음과 같다
{
curPage : 1,
result : [
{
no: 1,
name: 2,
section: 3,
regDate: 4
},
{
},
...
n,
]
}
class ItemsList {
@SerializedName("curPage")
@Expose
private var curPage: String? = null
@SerializedName("result")
@Expose
private var items: List<Items?>? = null
fun getCurPage(): String? {
return curPage
}
fun setCurPage(curPage: String?) {
this.curPage = curPage
}
fun getResult(): List<Items?>? {
return items
}
fun setResult(items: List<Items?>?) {
this.items = items
}
override fun toString(): String {
return "ClassPojo [result = $items, curPage = $curPage]"
}
}
RetrofitService
API 통신할 interface 정의
interface RetrofitService {
@GET("/v1/test")
fun getList(
@Query("curPage") curPage : Int,
@Query("setRow") setRow: Int,
@Query("setLimit") setLimit: Int
): Call<ItemsList>
}
MainActivity
class MainActivity : AppCompatActivity() {
# retrofit
private lateinit var retrofit : Retrofit
# retrofit service
private lateinit var myAPI : RetrofitService
# adapter
private lateinit var adapter: MainAdapter
# api query
private lateinit var query: HashMap<String, Int>
# infinite scroll listener
private lateinit var scrollListener: EndlessRecyclerViewScrollListener
# LinearLayoutManager
private val linearLayoutManager = LinearLayoutManager(this)
# TAG
companion object {
const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "on create!")
setContentView(R.layout.activity_main)
adapter = MainAdapter() # adapter 객체 생성
requestMain() # 최초 데이터 호출
recyclerView.adapter = adapter # adapter 등록
recyclerView.layoutManager = linearLayoutManager # layoutManager 등록
}
# retrofit api 호출
private fun listener(query: HashMap<String, Int>) {
retrofit = RetrofitClient.getInstnace()
myAPI = retrofit.create(RetrofitService::class.java)
var result : ItemsList = ItemsList() # response 결과를 받을 변수 선언
Runnable {
# retrofitService interface에서 호출
myAPI.getList(
query.get("curPage")!!,
query.get("setRow")!!,
query.get("setLimit")!!
).enqueue(object : Callback<ItemsList> {
override fun onFailure(call: Call<ItemsList>, t: Throwable) {
# 호출 실패 시
Log.d(TAG, t.message)
}
override fun onResponse(call: Call<ItemsList>, response: Response<ItemsList>) {
# 호출 성공 시
Log.d(TAG, "response : ${response.body()!!.toString()}")
result = response.body()!!
if (result.getResult()!!.isNotEmpty()) {
# 결과값 세팅
adapter.setItems((result.getResult() as ArrayList<Items>?)!!)
} else {
Log.d(TAG, "errorBody : ${response.errorBody()}")
Log.d(TAG, "message : ${response.message()}")
Log.d(TAG, "status_code : ${response.code()}")
Log.d(TAG, "response_url : ${response.raw().request().url().url()}")
}
}
})
}.run()
}
private fun requestMain() {
# 최초 호출
var query: HashMap<String, Int> = hashMapOf("curPage" to 1, "setRow" to 0, "setLimit" to 10)
listener(query)
# 스크롤 이벤트
scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
requestPagingMain(query, totalItemsCount + 10)
}
}
recyclerView.addOnScrollListener(scrollListener)
scrollListener.resetState()
}
private fun requestPagingMain(query: HashMap<String, Int>, offset: Int) {
# 추가 호출 시
query.put("setRow", offset)
listener(query)
}
# retrofit 생성
object RetrofitClient {
private val API_URL = "https://www.test.co.kr/"
private var instance : Retrofit? = null
private val gson = GsonBuilder().setLenient().create()
fun getInstnace() : Retrofit {
if(instance == null){
instance = Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return instance!!
}
}
}
Adapter
class MainAdapter(): RecyclerView.Adapter<CustomViewHolder>(){
private var item = ArrayList<Items>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val v = layoutInflater.inflate(R.layout.item_layout, parent, false)
return CustomViewHolder(v)
}
override fun getItemCount(): Int = item.size
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder?.bindItems(items[position])
}
fun setItems(newItem : ArrayList<Items>) {
this.item.addAll(newItem)
# 값이 변경된 것을 알림 (이거 안하면 안됨)
notifyDataSetChanged()
}
}
//뷰홀더 : 안에 정의할 수 있고 밖에서 정의 할 수 있다.
class CustomViewHolder(val view : View) : RecyclerView.ViewHolder(view){
// 받은 뷰에 아이템 바인딩
fun bindItems(data : Items){
view.findViewById<TextView>(R.id.rowNo).text = data.getNo()
view.findViewById<TextView>(R.id.rowName).text = data.getName()
view.findViewById<TextView>(R.id.rowSection).text = data.getSection()
view.findViewById<TextView>(R.id.rowRegDate).text = data.getRegDate()
}
}
EndlessRecyclerViewScrollListener
해당 소스는 구글링해서 얻었다.
abstract class EndlessRecyclerViewScrollListener : RecyclerView.OnScrollListener {
// The minimum amount of items to have below your current scroll position
// before loading more.
private var visibleThreshold = 5
// The current offset index of data you have loaded
private var currentPage = 0
// The total number of items in the dataset after the last load
private var previousTotalItemCount = 0
// True if we are still waiting for the last set of data to load.
private var loading = true
// Sets the starting page index
private val startingPageIndex = 0
var mLayoutManager: RecyclerView.LayoutManager
constructor(layoutManager: LinearLayoutManager) {
mLayoutManager = layoutManager
}
constructor(layoutManager: GridLayoutManager) {
mLayoutManager = layoutManager
visibleThreshold *= layoutManager.spanCount
}
constructor(layoutManager: StaggeredGridLayoutManager) {
mLayoutManager = layoutManager
visibleThreshold *= layoutManager.spanCount
}
private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
var maxSize = 0
for (i in lastVisibleItemPositions.indices) {
if (i == 0) {
maxSize = lastVisibleItemPositions[i]
} else if (lastVisibleItemPositions[i] > maxSize) {
maxSize = lastVisibleItemPositions[i]
}
}
return maxSize
}
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
var lastVisibleItemPosition = 0
val totalItemCount = mLayoutManager.itemCount
if (mLayoutManager is StaggeredGridLayoutManager) {
val lastVisibleItemPositions =
(mLayoutManager as StaggeredGridLayoutManager).findLastVisibleItemPositions(null)
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions)
} else if (mLayoutManager is GridLayoutManager) {
lastVisibleItemPosition =
(mLayoutManager as GridLayoutManager).findLastVisibleItemPosition()
} else if (mLayoutManager is LinearLayoutManager) {
lastVisibleItemPosition =
(mLayoutManager as LinearLayoutManager).findLastVisibleItemPosition()
}
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
currentPage = startingPageIndex
previousTotalItemCount = totalItemCount
if (totalItemCount == 0) {
loading = true
}
}
// If it’s still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && totalItemCount > previousTotalItemCount) {
loading = false
previousTotalItemCount = totalItemCount
}
// If it isn’t currently loading, we check to see if we have breached
// the visibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
currentPage++
onLoadMore(currentPage, totalItemCount, view)
loading = true
}
}
// Call this method whenever performing new searches
fun resetState() {
currentPage = startingPageIndex
previousTotalItemCount = 0
loading = true
}
// Defines the process for actually loading more data based on page
abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?)
}
흐름정리
흐름은 좀 더 공부한 후에 수정할 예정....
'Mobile > Android' 카테고리의 다른 글
Android HASHKEY 얻는 법 (0) | 2019.12.21 |
---|---|
HTTP, Async, Gson, 이미지 라이브러리 & Json Parse (0) | 2019.12.15 |
Dialog ProgressBar (0) | 2019.12.15 |