728x90
반응형

이번 2018년 11월 25일에 마루 180에서 진행한 안드로이드 컨퍼런스에서 발표한 내용 중에 BaseRecyclerView에 대해 글로 정리해보려고 이번 포스팅을 준비해봤습니다.


안드로이드 앱을 개발하면서 서버에서 데이터를 가져와서 리스트로 보여주는 기능을 자주 개발 했었는데 이런 리스트를 만드는 작업을 많이 하다 보니 공통으로 처리 할 수 있는 부분이 보이기 시작했고 그러면서 BaseRecyclerView를 만들어야겠다고 느꼈습니다.


이 글을 이해하기 위해서는 아래와 같은 지식이 필요하니 꼭 선행학습을 하고 보시면 좋을 것 같습니다.


1. RecyclerView

- RecyclerView를 어떻게 구현하는지

- RecyclerView.Adapter에서 필수로 override해야하는 함수들의 역할과 구현 방법

- RecyclerView.ViewHolder 구현 방법


2. DataBinding

- DataBinding 사용 방법

- BindingAdapter 사용 방법





언어 코드 리스트(http://help.bingads.microsoft.com/apex/index/18/ko/10004)



위에 주소에 있는 언어 코드 리스트를 RecyclerView를 통해서 보여주는 기능을 구현해보도록 하겠습니다.


일반적인 구현 방법

class Step1Adapter : RecyclerView.Adapter<Step1Adapter.ViewHolder>() {

private val items = mutableListOf<LanguageCode>()

fun replaceAll(newItems: List<LanguageCode>?) {
newItems?.let {
items.clear()
items.addAll(it)
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(parent)

override fun getItemCount() = items.size

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.onBindViewHolder(items[position])
}

inner class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.language_code_item, parent, false)
) {

fun onBindViewHolder(item: LanguageCode) {
with(itemView) {
tvCode.text = item.code
tvLanguage.text = item.language
}
}
}


}
class Step1Activity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.step1_activity)
rvContent.adapter = Step1Adapter().apply { replaceAll(getLanguageCodeList()) }
}
}


제가 처음에 많이 하던 RecyclerView의 Adapter를 구현하는 방법으로 RecyclerView.Adatper를 상속받는 CustomAdapter를 구현하고 RecyclerView.ViewHolder를 상속받는 CustomViewHolder를 구현하는 방식입니다.


CustomAdapter

- Adapter내부에 List를 가지고 있는다.

- replaceAll()과 같은 함수를 통해서 데이터를 교체한다.

- onCreateViewHolder()에서 CustomViewHolder를 생성한다.

- getItemCount()에서 list의 사이즈를 리턴한다.

- onBindViewHolder()에서 holder에 position에 맞는 데이터를 전달한다.


CustomViewHolder

- Adapter의 onCreateViewHolder에서 인자로 넘어오는 ViewGroup으로 View를 생성하여 RecyclerView.ViewHolder의 생성자에 전달한다.

- Adapter의 onBindViewHolder에서 넘어오는 데이터를 set한다.


이렇게 CustomAdapter를 따로 만들게 되면 새로운 화면에서 새로운 리스트를 보여줘야 할 때마다 계속 만들어 줘야 하는 번거로움이 있습니다.

조금 다르게 말하자면 ArrayList를 Generic을 사용하지 않고 Int를 저장하는 IntArrayList, String을 저장하는 StringArrayList를 만드는 것과 동일합니다.(조금 억지인가..)


그러면 위에 Step1Adpater와 비슷한 형태로 다른 데이터를 다른 layout으로 보여주는 CustomAdapter를 만들어본다고 가정해보겠습니다.

class CustomAdapter : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

private val items = mutableListOf<CustomItem>()

fun replaceAll(newItems: List<CustomItem>?) {
newItems?.let {
items.clear()
items.addAll(it)
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(parent)

override fun getItemCount() = items.size

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.onBindViewHolder(items[position])
}

inner class ViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.custom_item, parent, false)
) {

fun onBindViewHolder(item: CustomItem) {
with(itemView) {
tvTitle.text = item.title
tvContent.text = item.content
tvTail.text = item.tail
}
}
}


}


여기서 Step1Adapter와 CustomAdapter의 차이점은

1. items의 Class

private val items = mutableListOf<LanguageCode>()

2. ViewHolder 생성자에 인자로 들어가는 layout res id

LayoutInflater.from(parent.context).inflate(R.layout.language_code_item, parent, false)

3. ViewHolder에 onBindViewHolder()의 처리

fun onBindViewHolder(item: LanguageCode) {
with(itemView) {
tvCode.text = item.code
tvLanguage.text = item.language
}
}

입니다.


그러면 어떻게 해야 위 3가지의 차이점을 공통처리할 수 있게 만들 수 있을까요?


1. Generic으로 처리

2. 매개변수로 처리

3. DataBinding으로 처리


제가 생각한 방식으로 구현한 BaseRecyclerView입니다.

abstract class BaseRecyclerView {

abstract class Adapter<ITEM : Any, B : ViewDataBinding>(
@LayoutRes private val layoutResId: Int,
private val bindingVariableId: Int? = null
) : RecyclerView.Adapter<ViewHolder<B>>() {

private val items = mutableListOf<ITEM>()

fun replaceAll(items: List<ITEM>?) {
items?.let {
this.items.run {
clear()
addAll(it)
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
object : ViewHolder<B>(
layoutResId = layoutResId,
parent = parent,
bindingVariableId = bindingVariableId
) {}

override fun getItemCount() = items.size

override fun onBindViewHolder(holder: ViewHolder<B>, position: Int) {
holder.onBindViewHolder(items[position])
}
}

abstract class ViewHolder<B : ViewDataBinding>(
@LayoutRes layoutResId: Int,
parent: ViewGroup,
private val bindingVariableId: Int?
) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
) {

protected val binding: B = DataBindingUtil.bind(itemView)!!

fun onBindViewHolder(item: Any?) {
try {
bindingVariableId?.let {
binding.setVariable(it, item)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

}

BaseRecyclerView.Adapter는

Generic으로

1. Item Class

ITEM : Any

2. ViewHolder의 Layout에 해당하는 ViewDataBinding Class

B : ViewDataBinding

매개변수로

1. ViewHolder의 layout res id

@LayoutRes private val layoutResId: Int

2. ViewHolder의 layout에 variable로 정의한 Item의 변수명 id

private val bindingVariableId: Int? = null

받게 됩니다.


BaseRecyclerView.ViewHolder는

Generic으로

1. ViewHolder의 Layout에 해당하는 ViewDataBinding Class

B : ViewDataBinding

매개변수로

1. ViewHolder의 layout res id

@LayoutRes layoutResId: Int

2. ViewGroup

parent: ViewGroup

3. ViewHolder의 layout에 variable로 정의한 Item의 변수명 id

private val bindingVariableId: Int

받게 됩니다.


ViewHolder에서 itemView를 통해 binding객체를 생성하고

protected val binding: B = DataBindingUtil.bind(itemView)!!

onBindViewHolder()에서 binding.setVariable(bindingVariableId, item)으로 layout에 variable로 선언한 변수에 데이터를 set하게 됩니다.

fun onBindViewHolder(item: Any?) {
try {
bindingVariableId?.let {
binding.setVariable(it, item)
}
} catch (e: Exception) {
e.printStackTrace()
}
}


그러면 layout xml에서 처리한 방식대로 data를 binding 시켜주게 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="languageCode"
type="com.googry.googrybaserecyclerview.LanguageCode"/>
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal">


<TextView
android:id="@+id/tvCode"
android:layout_width="120dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="@{languageCode.code}"
android:textSize="20sp"
tools:text="ko"/>


<TextView
android:id="@+id/tvLanguage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@{languageCode.language}"
android:textSize="20sp"
tools:text="한국어"/>


</LinearLayout>
</layout>



class Step2Activity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.step2_activity)
val adapter = object : BaseRecyclerView.Adapter<LanguageCode, LanguageCodeItemBinding>(
layoutResId = R.layout.language_code_item,
bindingVariableId = BR.languageCode
) {}
adapter.replaceAll(getLanguageCodeList())
rvContent.adapter = adapter
}

}

Activity에서 RecyclerView에 adapter를 set 할 때는 위와 같이만 처리해주면 됩니다.


이렇게 하면 매번 단순한 RecyclerView를 만들 때마다 CustomAdapter를 따로 만들지 않아도 되는 장점이 있습니다.



그리고 저는 DataBinding과 MVVM구조를 사용해서 프로젝트를 만드는데 그 부분에 대한 코드는 아래와 같습니다.


Activity

class Step3Activity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<Step3ActivityBinding>(this, R.layout.step3_activity)
binding.run {
setLifecycleOwner(this@Step3Activity)
vm = ViewModelProviders.of(this@Step3Activity)[LanguageCodeViewModel::class.java]
rvContent.adapter = object : BaseRecyclerView.Adapter<LanguageCode, LanguageCodeItemBinding>(
layoutResId = R.layout.language_code_item,
bindingVariableId = BR.languageCode
) {}
}
}

}

Activity는

1. binding 객체 생성

2. binding 객체에 LifecycleOwner 전달

3. layout.xml에 선언한 ViewModel 전달

4. adapter 셋팅


View(layout)

<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

<variable
name="vm"
type="com.googry.googrybaserecyclerview.step3.LanguageCodeViewModel"/>

</data>

<android.support.v7.widget.RecyclerView
android:id="@+id/rvContent"
replaceAll="@{vm.liveLanguageCodeList}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
</layout>

layout에서는 activity에서 전달 받은 ViewModel을 variable로 가지고 있고

ViewModel에 있는 data를 RecyclerView의 BindingAdapter로 선언한 replaceAll에 전달합니다.


ViewModel

class LanguageCodeViewModel : ViewModel() {

val liveLanguageCodeList = MutableLiveData<List<LanguageCode>>()

init {
liveLanguageCodeList.postValue(getLanguageCodeList())
}

}

ViewModel에는 data를 LiveData에 감싸서 가지고 있습니다.


RecyclerViewExt

@BindingAdapter("replaceAll")
fun RecyclerView.replaceAll(list: List<Any>?) {
(this.adapter as? BaseRecyclerView.Adapter<Any, *>)?.run {
replaceAll(list)
notifyDataSetChanged()
}
}

replaceAll함수는 List<Any>? 타입으로 list를 받고 RecyclerView의 adapter를 safe type cast를 하여 BaseRecyclerView.Adapter에 list를 전달하고 notifyDataSetChanged()를 호출합니다.


데이터의 흐름으로 보면 ViewModel -> layout.xml -> BindingAdapter -> BaseRecyclerView.Adapter.replaceAll() 입니다.


예제 프로젝트는 아래 링크에서 확인 할 수 있습니다.

https://github.com/sjjeong/GoogryBaseRecyclerView


아래의 링크를 참고하여 만들었습니다.

https://thdev.tech/android/2018/01/31/Recycler-Adapter-Distinguish/

https://github.com/googlesamples/android-architecture/tree/todo-mvvm-databinding



의견이 있거나 문의점이 있다면 댓글로 남겨주시거나 카카오톡ID googry로 톡주세요.






728x90
반응형
728x90
반응형

권태환



정석준

정석준_나의 안드로이드 프로젝트.pdf

제꺼는 SpeckerDeck과 SlideShare에 업로드 되지 않아서 파일로 올려요


박미륵



박상권

헤이딜러에서는 어떻게 일하나요?


오민식

오민식님 발표자료는 따로 공유되지 않았어요


노현석





 



당신의 안드로이드는 안녕하신가요?
- 우리는 항상 궁금했습니다. 
- '지금 이 안드로이드 구조가 맞을까?', '코틀린..해야하나?', 'MVP인가 MVVM인가?'
- 안드로이드 개발자들이 본인들의 이야기를 공유하는 시간을 갖습니다.
- 주니어 개발자부터 시니어 개발자까지 다양한 경험과 분야의 개발자들이 이야기합니다.




이 행사는 강의가 아닙니다
- 이 행사는 각 발표자분들의 안드로이드 이야기와 경험담을 공유하고 개발자 여러분 모두가 함께하는 행사입니다.
- ‘그래서 어쩌라고?’ 라는 생각이 드는 발표가 있을수도 있습니다. 하지만 꼭 잘하는사람, 유명한사람, 연차가 높은사람의 이야기가 정답은 아닙니다.
- 다양한 개발자들의 이야기를 듣고 간접경험 해보면서 본인만의 안드로이드 개발세계를 정리해보는 기회를 갖게 되실겁니다.
- 평소에 궁금했던 해당 안드로이드 개발분야에 대해서 알게되고 또한 묻고 답할 수 있습니다.





이런분들이 오시면 많은 도움이 됩니다.
- 자신의 커리어에 대해서 고민이 있으신 분
- 취업,이직,창업을 준비중이신 분 
- 다른 분야,회사,환경에서는 어떻게 개발하고 있는지 궁금하신 분
- 안드로이드 개발자는 아니지만 안드로이드 개발자에 대해서 이해하고 싶은 다른 플랫폼 개발자, 기획자, 디자이너, 회사 대표님




여러분의 이야기를 들려주세요

- 10분간의 짧은 시간동안 여러분의 이야기를 들려주실 라이트닝 토크 발표자를 모집합니다.

- 거창한 이야기가 아니어도 좋습니다. 실패담, 삽질기는 더 환영합니다.

- 라이트닝 토크 발표자분은 당연히 참가비가 면제됩니다. 

- 10분발표 신청: https://open.kakao.com/o/sZADYd2

라이트닝 토크 발표자 신청은 마감되었습니다.  



 




 1. 시간 및 장소

- 시간: 2018년 11월 25일(일요일) 13:00 ~ 18:40

- 장소: 마루180 지하1층 이벤트홀 (지도보기)

서울특별시 강남구 역삼로 180 (역삼역 도보 10분)

 

 

 




2. 발표자 소개 





안드로이드 AAC-ViewModel 자세히 살펴보기
- 안드로이드 AAC-ViewModel을 상세하게 살펴보고, 코틀린에서 간단하게 inject하기 위한 방법들을 살펴봅니다.
 
 
권태환
현) RPG코리아 '요기요' 안드로이드 개발자
6년차 안드로이드 개발자로 GDG Seoul 운영진
- 2년전부터 코틀린 학습 및 실 프로젝트에 적용하여 사용중
- '꿈 많은 개발자가 되자!' 블로그를 통해 안드로이드 개발에 관련한 다양한 글 업로드중




나의 안드로이드 프로젝트 구조

- 작년 5월부터 안드로이드 개발자로 일하면서 안드로이드 프로젝트 구조에 대해 고민해왔던 경험에 관해 이야기 합니다.


정석준

- 현) 코인원 안드로이드 개발자

- 안드로이드 개발 2년차

- 코인헬퍼(코인 가격 간편 보기 앱) 개발





Kotlin puzzler
- 코틀린 퀴즈들을 통해서 문법에 조금 더 익숙해지기
 

 

박미륵 
현) 레이니스트 '뱅크샐러드' 안드로이드 개발자
안드로이드 개발 5년차
- 컨퍼런스에서 세션듣기를 좋아하고 함수형 프로그래밍과 테스팅에 관심을 가지고 있습니다.





헤이딜러에서는 어떻게 일하나요?
- 헤이딜러 서비스를 운영중인 PRND컴퍼니에서 피쳐가 시작되서 끝나는 과정에 대해서 이야기 합니다.
- 기획 -> 디자인 ->  개발 -> 리뷰 -> QA -> 배포 -> 테스트 의 프로세스에 대해 이야기 합니다.

 

박상권 
현) PRND컴퍼니 안드로이드 개발팀장
- 안드로이드 개발 8년차
- 대기업, 증권회사, 스타트업 근무
- 중고폰 관련 서비스 '셀폰' 창업 경험
- 개발자대회 및 창업대회 다수 수상
- 박상권의 삽질블로그 운영(https://gun0912.tistory.com)
- 30개이상의 개인앱 출시 및 운영




Kotlin, AAC 및 RxJava2와 함께한 신규 어플리케이션 개발기
- 큰 팀에서 열심히 일하던 개발자가, 혼자서 밑바닥 부터 개발을 하겠다고 팀을 뛰쳐 나왔습니다. 
- 마치 망망대해에 던져진 느낌입니다. 어떻게 해야 혼자서 유지 보수가 가능한 꿈만 같은 코드를 짤 수 있을까요? 
- 반복 작업에 질린 개발자가 깨끗한 코드를 목표로 신기술을 적용해가는 과정을 다룹니다.
 

 

오민식
현) 하이퍼커넥트 안드로이드 개발자 
- 신사업 안드로이드 1인 개발 
- 자칭 코틀린 덕후
- 밑바닥 부터 만들며 고통 받기를 즐김





Android + DI + Koin = Easy?!
- Android 개발시 DI 적용은 하고 싶은데 Dagger는 진입 장벽이 높아서 고민하고 계시지 않으신가요? 
- 좀 더 쉽게 DI 적용이 가능한 KOIN을 알려드립니다. 본 발표에서는 Library 소개 및 사용한 경험에 대해서 이야기 합니다. 

 

노현석 
현) 야놀자 안드로이드 개발자
- GDG Korea Android Organizer
- 활동명: 프루(Pluu)







3. 행사 시간표


시간

발표자

제목

설명

13:00 ~ 13:30

(30분)

권태환

안드로이드 AAC-ViewModel 자세히 살펴보기

안드로이드 AAC-ViewModel을 상세하게 살펴보고, 
코틀린에서 간단하게 inject하기 위한 방법들을 살펴봅니다.

휴식시간(10분)

 

 

 

13:40 ~ 14:00

(20분) 

정석준 

나의 안드로이드 프로젝트 구조

- 작년 5월부터 안드로이드 개발자로 일하면서 안드로이드 프로젝트 구조에 대해 고민해왔던 경험에 관해 이야기 합니다.

휴식시간(10분)

 

 

 

14:10 ~ 14:40

(30분)

박미륵 

 Kotlin puzzler

코틀린 퀴즈들을 통해서 문법에 조금 더 익숙해지기

https://www.slideshare.net/MireukPark/kotlin-puzzler

휴식시간(10분)


 

14:50 ~ 15:10

(20분)

-

발표자 Q&A

- 발표자분들에게 궁금한점을 남겨주시면 답변해드립니다.

http://slido.com, 코드: ted1

휴식시간(10분)

 

 

 

 15:20 ~ 15:50

(30분)

박상권 

헤이딜러에서는 어떻게 일하나요?

- 헤이딜러 서비스를 운영중인 PRND컴퍼니에서 피쳐가 시작되서 끝나는 과정에 대해서 이야기 합니다.

- 기획 -> 디자인 ->  개발 -> 리뷰 -> QA -> 배포 -> 테스트 의 프로세스에 대해 이야기 합니다.

휴식시간(10분)

 

 

 

16:00 ~ 16:30

(30분)

오민식 

Kotlin, AAC 및 RxJava2와 함께한 신규 어플리케이션 개발기 

- 큰 팀에서 열심히 일하던 개발자가, 혼자서 밑바닥 부터 개발을 하겠다고 팀을 뛰쳐 나왔습니다. 

- 마치 망망대해에 던져진 느낌입니다. 어떻게 해야 혼자서 유지 보수가 가능한 꿈만 같은 코드를 짤 수 있을까요? 

- 반복 작업에 질린 개발자가 깨끗한 코드를 목표로 신기술을 적용해가는 과정을 다룹니다.

휴식시간(10분)

 

 

 

 16:40 ~ 17:10

(30분)

노현석 

Android + DI + Koin = Easy?!

- Android 개발시 DI 적용은 하고 싶은데 Dagger는 진입 장벽이 높아서 고민하고 계시지 않으신가요?  

- 좀 더 쉽게 DI 적용이 가능한 KOIN을 알려드립니다. 본 발표에서는 Library 소개 및 사용한 경험에 대해서 이야기 합니다. 

휴식시간(10분)

 

 

 

 17:20 ~ 18:00

(40분)

 -

 라이트닝 토크

1. 나윤호: 머테리얼 디자인 2.0 적용해보기

- 지난 5월에 발표된 구글 머테리얼 디자인 2.0을 간단하게 알아보고, 이를 개인 프로젝트에 적용했던 경험을 공유합니다.


2. 최예찬: Android 개발자가 왜 거기서 나와...?

- 개발에 입문하고 4년간 안드로이드 외 다양한 분야들을 오고가며 공부한 경험, 그 안에서 느꼈던 장점과 시각을 공유드리고자 합니다.


3. 백수사랑: 텍스트에 스타일 끼얹기 Part1

- 마크업 태그와 Spannable 을 활용하여 안드로이드의 여러 뷰 컴포넌트내에 HTML 스타일의 시각적 효과를 적용할 수 있는 방법을 공유해보려고 합니다.


4. 윤정현: 스타트업 에서 주니어 개발자 혼자 하나의 서비스를 책임지는 방법 

- 주니어 개발자가 스타트업에서 일하면서 쉽게 겪어보지 못한 경험에 대해 얘기합니다. 

 휴식시간(10분)

 

 

 

18:10 ~ 18:30
(20분)

-

 발표자 Q&A

- 발표자분들에게 궁금한점을 남겨주시면 답변해드립니다.

http://slido.com, 코드: ted2 


  

제 발표는 아래와 같은 피드백이 있었어요.


#좋았던점

적용하는 방법에 대한 예시를 들어 설명해 주셔서 많은 도움이 되었습니다

recyclerview의 반복되는 함수를 base로 만든 경험을 잘 들었습니다.

본인의 경험을 살려 재미있게 설명을 잘 해주셨고 개인적으로 원하는 내용이여서 만족스럽습니다 

발표가 귀에 쏙쏙 편하게 들림

Adapter 클래스를 예로 들어 모듈화 처리하는 내용이 흥미로웠습니다. 

자신의 힘들었던 내용의 고민

타인의 고민을 통해 배움

프로젝트를 진행할때 어떠한구조로 진행되야하는지 알아서 좋았습니다

개인 경험의 공유 중심

지금 고민하는 것과 비슷했다

안드로이드 구조 변화에 대한 내용을 들어서 좋았습니다.

한 번쯤 고민할만한 공감되는 이야기라서 좋았습니다.

MVP, MVVM 패턴을 모두 겪으면서 각각의 장단점을 잘 설명해주셔서 좋았습니다. 현재 MVP로 개발하고 있는 저로써는 차후 MVVM으로의 전환 또한 생각하게 된 계기가 되었습니다.

다른 곳에서 보지 못했던 유용한 예제

저 또한 주니어 개발자로서 안드로이드 처음 메인을 맡으면서 겪고 있는 비슷한 이야기들을 공유해 주셔서 좋았습니다.

안드로이드 아키텍처 입문자의 시선으로 구조를 변경하게 된 계기나 내용 구성이 알아듣기 쉽고 공감이 많이 되었습니다. 향후 개발을 함에 있어서 많은 도움이 될 것 같습니다. 재미있게 잘 들었습니다.

기본을 잘 설명해줘서 좋았습니다.

프로젝트 전반적으로 고민했던 부분에 대해서 공감했습니다.

"자신의 사례를 통해서 현업에서는 어떻게 하는지 알 수 있어 좋았다.

특히 리싸이클러뷰는 매우 만족했다."

MVVM + Databinding에 대한 개념과 적용하면서 겪은 내용들을 잘 설명해주심

프로젝트 구조경험을 들어서 좋았습니다.

MVP 페턴을 적용했을때 좋았던 점이랑 문제점을 지적하고 그 해결책으로 MVVM패턴을 적용한 방식이 좋았습니다



#아쉬웠던점

코드가 잘 보이지 않아 아쉬웠습니다.

조금 설명이 부족한 부분이 있어서 아쉬웠습니다

코드로만 접근하는 방식보다는 쉬운 개념정도부터 정리해주시면서 접근해주신다면 더 좋지 않을까 하는 피드백 드려봅니다. 

정리가 될던 느낌이다.

"단순 유행하니까? 쓴다 라는 등의 초급자 스피커같았다. 보다 명확한이유. 아키텍처를 통해 이로운점이나 단점도 소개필요 하다고 생각합니다. 또한 BaseAdaper는 저는 매우비효율적이라고 생각합니다. 실무에서 새로운사람이왔을경우 저러한 랩핑 클래스를 익히고 쓰려면 그만큼의 시간이듭니다. 강의에서 소개시켜준 랩핑코드는 필요한코드인지 한번 되새김이 필요해보입니다. 내년에는 좀더 퀄리티있는 스피커가

되시길.. 아키택쳐 패턴을 하면서 테스트코드 짜기 쉽다. 실무에서 쓰는방법등 이런 소개를 개인적으로는 듣고싶네요"

결론을 알 수가 없어서 아쉬웠습니다.

조금 시간이 짧았던게 아쉬웠습니다.

조그만 더 상세하게 해줬으면 했습니다.

MVP, MVVM을 사용한 경험에 대해서는 예전부터 많은 세션들이 있었기에 특별한 인사이트를 경험하진 못했던 것 같아요

728x90
반응형
728x90
반응형

Activity에서 키보드가 올라오고 화면크기를 변경하기 위해 AndroidManifest.xml에 android:windowSoftInputMode에 adjustResize를 설정해줍니다.



위에 화면처럼 EditText를 선택하고 키보드가 올라오면 아래 로그인 버튼이 보이지 않아 사용자가 스크롤 해야 하는 문제가 있습니다.

(스크롤 하기 완전 귀찮아..)


이번 포스팅에서는 키보드가 보여질 때 화면을 키보드 크기 만큼 위로 스크롤 하는 방법에 대해 설명하고자 합니다.


andorid softkeyboard height라는 키워드로 구글링을 하던 중에 Keyboard Handling on Android 글을 보고 키보드가 보이고 사라지는 이벤트를 만들었습니다.


제가 예제로 만든 앱의 layout은 아래와 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sv_root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="20dp">


<ImageView
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_marginTop="40dp"
android:src="@mipmap/ic_launcher" />

<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:hint="@string/hint_id"
android:inputType="text"
android:lines="1"
android:textSize="24sp" />

<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:hint="@string/hint_password"
android:inputType="textPassword"
android:lines="1"
android:textSize="24sp" />

<Button
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/login" />

<LinearLayout
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingEnd="8dp"
android:paddingStart="8dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sign_up" />

<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/forgot_your_password" />
</LinearLayout>
</LinearLayout>

</ScrollView>

키보드가 보여질 때 화면을 스크롤 하기 위해서 최상단에는 ScrollView를 추가했습니다.



저는 KeyboardVisibilityUtils라는 클래스를 만들고 Activity에서 가져다 사용 했습니다.

import android.graphics.Rect
import android.view.ViewTreeObserver
import android.view.Window

class KeyboardVisibilityUtils(
private val window: Window,
private val onShowKeyboard: ((keyboardHeight: Int) -> Unit)? = null,
private val onHideKeyboard: (() -> Unit)? = null
) {

private val MIN_KEYBOARD_HEIGHT_PX = 150

private val windowVisibleDisplayFrame = Rect()
private var lastVisibleDecorViewHeight: Int = 0


private val onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
window.decorView.getWindowVisibleDisplayFrame(windowVisibleDisplayFrame)
val visibleDecorViewHeight = windowVisibleDisplayFrame.height()

// Decide whether keyboard is visible from changing decor view height.
if (lastVisibleDecorViewHeight != 0) {
if (lastVisibleDecorViewHeight > visibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX) {
// Calculate current keyboard height (this includes also navigation bar height when in fullscreen mode).
val currentKeyboardHeight = window.decorView.height - windowVisibleDisplayFrame.bottom
// Notify listener about keyboard being shown.
onShowKeyboard?.invoke(currentKeyboardHeight)
} else if (lastVisibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX < visibleDecorViewHeight) {
// Notify listener about keyboard being hidden.
onHideKeyboard?.invoke()
}
}
// Save current decor view height for the next call.
lastVisibleDecorViewHeight = visibleDecorViewHeight
}

init {
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
}

fun detachKeyboardListeners() {
window.decorView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
}

visibleDecorViewHeight는 현재 화면의 height 값입니다.

만약 키보드가 올라와 있는 상태라면 visibleDecorViewHeight는 키보드가 올라오기 전 height보다 작습니다.

if (lastVisibleDecorViewHeight > visibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX) {
// Calculate current keyboard height (this includes also navigation bar height when in fullscreen mode).
val currentKeyboardHeight = window.decorView.height - windowVisibleDisplayFrame.bottom
// Notify listener about keyboard being shown.
onShowKeyboard?.invoke(currentKeyboardHeight)
} else if (lastVisibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX < visibleDecorViewHeight) {
// Notify listener about keyboard being hidden.
onHideKeyboard?.invoke()
}

이전에 보여준 화면 height현재 화면 height + 최소 키보드 크기 값 보다 크면 키보드가 올라온 것으로 보고 키보드가 보였다는 이벤트를 전달하고

이전에 보여준 화면 height + 최소 키보드 크기 값현재 화면 height 보다 작으면 키보드가 내려간 것으로 보고 키보드가 사라졌다는 이벤트를 전달 합니다.


그리고 MainActivity는 아래처럼 작성했습니다.

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity() {

private lateinit var keyboardVisibilityUtils: KeyboardVisibilityUtils

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
keyboardVisibilityUtils = KeyboardVisibilityUtils(window,
onShowKeyboard = { keyboardHeight ->
sv_root.run {
smoothScrollTo(scrollX, scrollY + keyboardHeight)
}
})
}

override fun onDestroy() {
keyboardVisibilityUtils.detachKeyboardListeners()
super.onDestroy()
}


onCreate에서 KeyboardVisibilityUtils클래스를 만들 때 인자로 window를 전달하고 onShowKeyboard(키보드가 보여질 때 해당 코드가 호출)를 통해 키보드가 보여지는 이벤트를 받아서 ScrollView를 keyboardHeight만큼 스크롤 하고 있습니다.



이제 EditText를 선택하면 자동으로 화면이 위로 스크롤 됩니다.



출처: https://pspdfkit.com/blog/2016/keyboard-handling-on-android/


android keyboard handling

android keyboard scroll up

android keyboard event

728x90
반응형

+ Recent posts