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
반응형

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
반응형
728x90
반응형

이 글은 MVP 패턴과 DataBinding 라이브러리를 모르면 이해하기 힘든 글입니다.


Android에서 DataBinding은 ButterKnife, KotlinExtension과 같이 편리하고 유용하게 사용할 수 있는 Library 입니다.


저는 MVP+DataBinding 구조로 개인프로젝트를 진행하고 있습니다.

(실제로 이 방식은 deprecated 된 방식입니다.)


이렇게 MVP와 DataBinding을 사용하게 되면 View에 Click Listener를 등록한 다음 Presenter의 함수를 호출하지 않고 바로 xml에서 Presenter의 함수를 호출하는 효과를 볼 수 있습니다.

(내부적으로는 앞에 이야기 한 방식으로 하고 있습니다.)


하지만 layout에 EditText가 있고 값을 가져와서 저장해야 할 때 저는 Presenter가 아닌 View의 함수를 호출해서 사용했습니다.(ㅠㅠ)


그래서 억지로 억지로 사용하다가 오늘 Two Way Binding에 대해 알게 되었습니다.


그럼 이제 예를 통해 오늘의 주제인 DataBinding Two Way Binding에 대해 알아보겠습니다.


  


이번 글에서 예제로 사용할 화면입니다.


왼쪽 화면처럼 이름, 이메일, 폰번호를 입력하고 저장 버튼을 누르면 오른쪽 화면처럼 요약을 보여주려고 합니다.


먼저 Two Way Binding을 모른다고 가정하고 진행해보겠습니다.



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

<data>

<variable
name="view"
type="com.googry.googrydatabindingtwowaybinding.ui.unknown.UnknownContract.View"/>

<variable
name="user"
type="com.googry.googrydatabindingtwowaybinding.data.User"/>

</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">

<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/name"
android:text="@{user.name}"/>

<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email"
android:text="@{user.email}"/>

<EditText
android:id="@+id/et_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/phone"
android:text="@{user.phone}"/>

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> view.onSaveClick()}"
android:text="@string/save"/>

</LinearLayout>
</layout>


layout 입니다.


User를 받아와서 각 EditText에 값을 넣어줍니다.

UnknownContract.View를 받아와서 onClick 이벤트를 받아줍니다.


public interface UnknownContract {
interface Presenter extends BasePresenter {
void save(String name, String email, String phone);
}

interface View extends BaseView<Presenter> {
void setUser(User user);

void showSaveResult(User user);

void onSaveClick();
}

}

UnknownContract 입니다.


Presenter는 name, email, phone을 입력받는 save()를 선언합니다.


View는 setUser(), showSaveResult()와 버튼 클릭 이벤트를 받는 onSaveClick()을 선언합니다.


@Override
public void onSaveClick() {
mPresenter.save(
mBinding.etName.getText().toString(),
mBinding.etEmail.getText().toString(),
mBinding.etPhone.getText().toString()
);
}

UnknownFragment안에 onSaveClick()를 구현했습니다.

이 부분에서 EditText의 내용을 가져와 Presenter에 save()함수를 통해 전달해 줍니다.


@Override
public void save(String name, String email, String phone) {
mUser.name = name;
mUser.email = email;
mUser.phone = phone;

mUserDataSource.saveUser(mUser);
mView.showSaveResult(mUser);
}

UnknownPresenter안에 save()를 구현했습니다.


내부적으로 mUser에 값을 넣어주고 UserDataSource에 User를 저장하고 View에 결과를 보여주라고 showSaveResult()함수를 호출 합니다.


@Override
public void showSaveResult(User user) {
new AlertDialog.Builder(getContext())
.setMessage(user.toString())
.show();
}

그리고 UnknownFragment에서 AlertDialog를 통해 결과를 보여주게 됩니다.


이렇게 순서가 xml -> View.onSaveClick() -> Presenter.save() -> View.showSaveResult() 입니다.


하지만 버튼 클릭을 했을 때 각 EditText내용의 값을 가져오면서 View의 함수를 호출하는 것이 아닌 바로 Presenter의 함수를 호출 할 수 있을까요?

다르게 말하자면 View.onSaveClick()을 호출하지 않고 xml -> Presetner.save() -> View.showSaveResult()를 할 수 있을까요?


네, 바로 Two Way Binding을 하게 되면 할 수 있습니다.


그럼 이제 Two Way Binding을 하는 방법에 대해 알아보겠습니다.


private User mUser;

@Override
public void start() {
mView.setUser(mUser);
}

Presenter안에 User가 있고 View에 User객체를 전달해줍니다.


그리고 Contract.View에 있는 setUser()라는 함수의 구현부분은 이렇게 생겼습니다.

@Override
public void setUser(User user) {
mBinding.setUser(user);
}

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

<data>

<variable
name="presenter"
type="com.googry.googrydatabindingtwowaybinding.ui.known.KnownContract.Presenter"/>

<variable
name="user"
type="com.googry.googrydatabindingtwowaybinding.data.User"/>

</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/name"
android:text="@={user.name}"/>

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/email"
android:text="@={user.email}"/>

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/phone"
android:text="@={user.phone}"/>

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> presenter.save()}"
android:text="@string/save"/>

</LinearLayout>
</layout>

저렇게 binding에 넣어준 User를 @{}가 아닌 @={}로 각 EditText에 binding 하면 됩니다.


이렇게 했을 경우 EditText에서 text가 변경 될 때 마다 User안에 값을 변경하게 됩니다.


User객체는 Presenter에서 관리하고 있기 때문에 그럼 각 EditText에서 값을 가져와 Presenter에 전달해줄 필요가 없게 됩니다.


그래서 바로 버튼에 onClick에서 presenter.save()를 호출 할 수 있게 되는 것입니다.




이렇게 Two Way Binding을 사용하게 되면 바로 Presenter의 함수를 호출하여 함수 호출의 depth가 낮아지게 되는 효과를 볼 수 있습니다.


예제 코드는 https://github.com/sjjeong/GoogryDataBindingTwoWay 에서 확인하실 수 있습니다.

728x90
반응형
728x90
반응형

앱을 사용하다 보면 사용자에게 무엇 인가를 선택하게 만들어야하는 순간이 옵니다.


하지만 화면을 따로 만들거나 DialogFragment로 만들려면 다소 귀찮은게 한두가지가 아닙니다. ㅠㅠ


그래서 이번 포스팅에서는 아주 간단한 방법으로 selection popup을 만드는 것을 소개 하려고 합니다.


저는 지금까지 AlertDialog는 Message만 넣을 수 있는줄 알았습니다.


하지만 SingleChoice와 MultiChoice도 만들 수 있다는 것을 최근에 알았습니다.


먼저 layout은 기본으로 생성해주는 activity_main.xml을 사용하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.googry.googryselectpopup.MainActivity">

<TextView
android:id="@+id/tv_sports"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

</android.support.constraint.ConstraintLayout>


SingleChoice는 AlertDialog에 setItems를 사용해 만들 수 있습니다.

setItems는 String[]나 CharSequence[]를 인자로 받습니다.

public class MainActivity extends AppCompatActivity {

private String[] mSports = {"야구","축구","농구","수영","테니스","골프","탁구","볼링","당구","태권도","유도","검도"};
private TextView mTvSports;
private AlertDialog mSportSelectDialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mTvSports = (TextView) findViewById(R.id.tv_sports);
mTvSports.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mSportSelectDialog.show();
}
});

mSportSelectDialog = new AlertDialog.Builder(MainActivity.this )
.setItems(mSports, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
mTvSports.setText(mSports[i]);
}
})
.setTitle("title")
.setPositiveButton("확인",null)
.setNegativeButton("취소",null)
.create();

}
}

그리고 OnClickListener를 통해 선택된 아이템의 index를 넘겨 받게 됩니다.


실행 화면



MultiChoice는 AlertDialog에 setMultiChoiceItems를 사용해 만들 수 있습니다.


이 함수는 setItems와는 다르게 boolean[]를 추가로 받습니다.

그 이유는 이전에 선택된 것을 보여주기 위해서 입니다.

public class MainActivity extends AppCompatActivity {

private String[] mSports = {"야구","축구","농구","수영","테니스","골프","탁구","볼링","당구","태권도","유도","검도"};
private boolean[] mSportsSelected = new boolean[mSports.length];
private TextView mTvSports;
private AlertDialog mSportSelectDialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mTvSports = (TextView) findViewById(R.id.tv_sports);
mTvSports.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mSportSelectDialog.show();
}
});

mSportSelectDialog = new AlertDialog.Builder(MainActivity.this )
.setMultiChoiceItems(mSports, mSportsSelected, new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i, boolean b) {
mSportsSelected[i] = b;
}
})
.setTitle("title")
.setPositiveButton("확인",null)
.setNegativeButton("취소",null)
.create();

}
}

그리고 OnMultiChoiceClickListener를 통해 아이템을 선택 할 때 마다 선택한 아이템의 index와 선택유무인 boolean을 넘겨 받게 됩니다.


실행화면



저는 지금까지 간단한 SingleChoice나 MultiChoice도 DialogFragment로 직접 만들어서 사용했는데 이런게 있는줄 너무 늦게 알았습니다.


여러분들도 간단한 입력은 이런식으로 처리하세요~~

728x90
반응형
728x90
반응형

구글은 Android 8.0 O버전을 Oreo로 공식 발표를 했습니다. 



이번 포스팅에서는 Oreo에서 추가된 새로운 기능들 중에 몇가지에 대해 소개 하려고 합니다.


- 알림

Android 8.0에서 알림이 크게 변경되었습니다.


알림채널을 사용해 알림을 카테고리화 할 수 있습니다.

이 기능을 사용하면 사용자는 카테고리별로 소리, 진동, 알림의 중요도를 관리할 수 있습니다.

그리고 기존에 알림의 on/off을 카테고리별로 지정할 수 있기 때문에 사용자는 자신이 받고싶은 알람만 받을 수 있습니다.


  



알림뱃지를 사용해 사용자가 아직 반응하지 않은 알림을 앱 아이콘을 통해 보여줍니다.

알림뱃지는 알림 도트라고도 불립니다.


그 외에도 다시알림(Snoozing), 알림제한시간(Timeout), 알림설정(Setting), 알림닫기이벤트(dismissal), 배경색상(Backgournd color), 메시징스타일(Messaging style) 등이 있습니다.


- PIP(Picture-in-Picture) Mode

PIP는 기존에 Android TV에서 제공되던 기능입니다. 주로 동영상 재생에 사용되는 다중창 모드입니다.

PIP는 다중 창 수명 주기를 따라갑니다.



- 다운로드 가능한 폰트(Downloadable Fonts)

Android 8.0과 Android Support Library 26을 사용하면 이 기능을 사용할 수 있습니다.

이 기능을 사용함으로써 얻을 수 있는 이점은 APK size를 줄이고 이로인해 앱 설치 성공률을 올릴 수 있고 동일한 폰트을 여러 앱이 공유할 수 있어서 유저의 데이터, 폰의 저장공간을 절약할 수 있습니다.


이 기능은 Google Play service 버전 11 이상에서만 가능합니다.


- 폰트의 Resource화

개발자 홈페이지는 XML의 글꼴이라 나와있는데 저는 폰트의 Resource화가 더 맞는거 같습니다.

기존에 Android에서 폰트르 적용하기위해 assets폴더 아래에 폰트파일을 넣어서 사용해야 했지만 이제 res폴더 아래 font폴더를 만든 후 폰트 파일을 넣으면 @font나 R.font로 접근할 수 있습니다.


- TextView 자동 크기 조절

TextView의 크기에 따라 텍스트 사이즈를 자동으로 늘리거나 줄일 수 있습니다.

(출처: https://academy.realm.io/kr/posts/android-oreo-new-features/)



- Adaptive Icon

이 기능을 사용하게 되면 사용자에게 더 좋은 시각 적인 효과를 불 수 있습니다.


- 통합 레이아웃 Margin And Padding

이제는 양옆이나 위아래를 동시에 지정 할 수 있는 xml속성이 생겼습니다.

하지만 xxxStart xxxEnd에는 영향을 주지 않기 때문에 사용을 할 때 주의해야 합니다.



- findViewById()

기존 findViewById는 View의 type에 맞게 type casting을 해줘야 했지만 이제 findViewById가 View 대신 <T extends View> T를 반환하기 때문에 type casting을 할 필요가 없어졌습니다.



이 외에도 Android 8.0에 추가되고 개선 된 많은 기능들이 있습니다.


더 알고 싶으신 분들은 아래 출처 링크를 타고 더 많은 정보를 얻어보세요.


출처

https://www.android.com/versions/oreo-8-0/

https://developer.android.com/about/versions/oreo/index.html

https://android-developers.googleblog.com/2017/08/introducing-android-8-oreo.html

https://academy.realm.io/kr/posts/android-oreo-new-features/

728x90
반응형
728x90
반응형

앱을 만들면서 Intro, Splash화면은 사용자가 어떤 앱을 실행했는지 알려주는 좋은 기능입니다.


하지만 가끔 검정, 회색, 흰색의 화면이 잠깐 나온 다음에 스플래시 화면이 나오는 앱이 많습니다.


그래서 이번 포스팅에서는 스플래시 화면을 따로 만들기 보다는 처음에 나오는 검정, 회색, 흰색 화면을 이용해 스플래시 화면을 만들고자 합니다.


1. background_splash.xml 만들기

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

<item
android:drawable="@color/white"/>

<item>
<bitmap
android:gravity="center"
android:src="@mipmap/ic_launcher"/>
</item>

</layer-list>

res/drawable 안에 background.splash.xml를 만든 다음에 위 코드를 넣어줍니다.


이렇게 하면 배경은 흰색이고 가운데에 앱 로고가 들어가게 됩니다.


2. SplashTheme 만들기

<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/background_splash</item>
</style>

res/values/styles.xml에 NoActionBar를 상속받는 SplashTheme를 만들어 줍니다.


이렇게 하면 저 테마를 적용한 Activity는 Background로 우리가 만든 background_splash를 보여주게 됩니다.


3. 빈 Activity 만들기

public class SplashActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

startActivity(new Intent(getApplicationContext(), MainActivity.class));
finish();
}
}

이렇게 setContentView()도 하지 않고 바로 startActivity()를 실행하여 MainActivity로 넘어가줍니다.

SplashActivity는 finish()를 해줍니다.


4. Manifest.xml에 SplashActivity 선언하기

<activity
android:name=".SplashActivity"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

이제 2번에서 만든 Theme를 3번에서 만든 Activity에 넣어주고 3번에서 만든 Activity를 시작 Activity로 만들어줍니다.


5. 테스트 해보기



이제 Handler.postDelayed()에서 벗어나봅시다!


출처

https://www.bignerdranch.com/blog/splash-screens-the-right-way/

https://android.jlelse.eu/right-way-to-create-splash-screen-on-android-e7f1709ba154

728x90
반응형
728x90
반응형

이 글은 Realm을 사용하며 Java8을 사용하기 위해 jackOption을 설정한 개발자를 위한 글이다.


Realm을 추가하고 앱을 실행하면 Application에서 Realm.init()부터 에러가 나는것을 볼 수 있다.


java.lang.ExceptionInInitializerError: RealmTransformer doesn't seem to be applied.

                                           Please update the project configuration to use the Realm Gradle plugin.

   See https://realm.io/news/android-installation-change/

at io.realm.RealmConfiguration.<clinit>(RealmConfiguration.java:78)

at io.realm.RealmConfiguration$Builder.initializeBuilder(RealmConfiguration.java:474)

at io.realm.RealmConfiguration$Builder.<init>(RealmConfiguration.java:460)

at io.realm.Realm.init(Realm.java:201)


만약 저 링크에 있는 것처럼 Gradle 설정을 잘 따라 했다면 JackOption을 사용했는지 확인해봐야 한다.


만약 본인이 Lambda을 사용하기 위해 아래와 같이 JackOption을 사용했다면

Retrolambda를 사용해야 Realm을 사용 할 수 있다.


이 에러를 해결하기 위해서는 jackOptions을 제거하고 Retrolambda를 추가해야한다.


본인 프로젝트 build.gradle




Module build.gradle




출처

https://blog.realm.io/android-installation-change/

https://stackoverflow.com/questions/41153879/realm-android-realmtransformer-doesnt-seem-to-be-applied

https://github.com/evant/gradle-retrolambda

http://tiii.tistory.com/5

728x90
반응형
728x90
반응형

Realm은 Android에서 사용하던 SQLite와는 다른 C++ 코어 기반 DB입니다.


(출처: https://speakerdeck.com/realm/realm-introduction-seoul-meetup-10)



그리고 기존 RDB개념이 아닌 Object를 저장하는 개념입니다.


  

(출처: https://speakerdeck.com/realm/realm-introduction-seoul-meetup-10)



이렇게 사용함으로써 RDB개념에 대해 잘 모르는(저도 잘 모릅니다) 개발자에게 Realm은 그냥 객체를 사용하는데 알아서 DB에 쓰고 읽는 효과를 볼 수 있습니다.


하지만 제가 Realm을 쓰면서 놀란게 하나가 있습니다.


바로 모델 클래스를 상속 시킬수가 없다는 것입니다.


나닛?!


그래서 이번 블로그에서는 Realm을 사용하면서 발생하는 이슈중 하나인 RealmObject 상속으로 인한 모델 구조 변경에 대해서 이야기하려고합니다.


1. is-a관계 has-a관계로 만들기


예를 들어 Log클래스와 User클래스가 있고 User클래스는 Log클래스를 상속 받는 구조 입니다.

public class Log extends RealmObject {
public long createdTs;
public long updatedTs;
}

public class User extends Log {
public String name;
public int age;
public String email;
}

is-a관계로 모델을 구성 했을 때 Realm에서는 이런 오류가 발생합니다.

Valid model classes must either extend RealmObject or implement RealmModel.





출처: https://github.com/realm/realm-java/issues/2691

출처: https://github.com/realm/realm-java/issues/761


is-a관계가 언제 될지는 모르겠지만 아마 안될꺼같아요.


그래서 모델 클래스를 has-a관계로 바꿔야합니다.

public class User extends RealmObject {
public Log log;
public String name;
public int age;
public String email;
}


2. 상속구조 구현하기


is-a관계를 has-a관계로 바꾸면서 다형성(Polymorphism)과 유사한 형태를 만들어 보려고 합니다.


public interface ILog extends RealmModel {
Log getLog();
}
public interface IUser extends ILog {
User getUser();
}

ILog와 IUser를 만들고 IUser가 ILog를 상속받게 합니다.


public class Log extends RealmObject implements ILog{
public long createdTs;
public long updatedTs;

@Override
public Log getLog() {
return this;
}
}
public class User extends RealmObject implements IUser{
public Log log;
public String name;
public int age;
public String email;

@Override
public Log getLog() {
return log;
}

@Override
public User getUser() {
return this;
}
}

그리고 Log가 ILog를 실체화하고 User가 IUser를 실체화합니다.


이렇게 되면 Interface를 이용해서 다형성을 유지 할 수 있게 됩니다.


그리고 객체에 접근하기 위해서 getter를 선언해줍니다.


3. 동일한 primary key주기


Realm에서 객체를 update하기 위해서는 primary key가 선언되어 있어야 합니다.


이렇게 했을때 has-a관계에 있는 모델들이 동일한 primary key를 가지고 있어야만 is-a관계처럼 동일한 디비를 가질수 있게 됩니다.


public class Log extends RealmObject implements ILog {
public long createdTs;
public long updatedTs;
@PrimaryKey
private long logId;

public Log(long logId) {
this.logId = logId;
}

public Log() { }

public long getLogId() {
return logId;
}

@Override
public Log getLog() {
return this;
}
}
public class User extends RealmObject implements IUser {
public Log log;
public String name;
public int age;
public String email;
@PrimaryKey
private long logId;

public User(long logId) {
this.logId = logId;
log = new Log(logId);
}

public User() { }

public long getLogId() {
return logId;
}

@Override
public Log getLog() {
return log;
}

@Override
public User getUser() {
return this;
}
}

이렇게 하면 logId는 생성자를 통해서만 set할 수 있고 User와 Log가 동일한 logId를 가질 수 있습니다.


기본 생성자는 Realm에서 사용하기 떄문에 선언해줘야합니다.


4. auto increment 만들기


Realm realm = Realm.getDefaultInstance();
Number number = realm.where(Log.class).max("logId");
long logId = number == null ? 0 : number.longValue() + 1;
User user = new User(logId);

이렇게 하게 되면 Log의 마지막 logId값 보다 1큰 수를 logId로 가질 수 있게 됩니다.


5. 끝으로


이런식으로 기존 is-a관계를 has-a관계로 바꾸면서 primary key도 unique하게 가질 수 있는 구조를 만들었습니다.


하지만 이 방법이 정말 옳은지는 잘 모르겠습니다.


만약 이 포스팅을 보고 이 구조로 구현을 한다고 하시면 한번 더 고민해주셨으면 좋겠습니다.


여러분들의 많은 의견과 태클 감사하겠습니다.

728x90
반응형

+ Recent posts