목표
- 기존 ListView로 만들어져 있던 List를 Recycler View로 만들어 본다.
- BaseAdapter와 RecyclerAdapter의 작동원리를 알아본다.
하기 링크가 본 포스트의 선행입니다.
2022.02.05 - [Android] - Android ViewModel & ListView 사용하기
(흠... Compose 강좌 만들려고 간단히 기본 강좌 만드는 목적이었는데 이제 Recycler View라니 갈길이 너무 멀군요)
Gradle 적용
언제나 그렇듯이 Jebpack을 사용하기 위해서 관련된 라이브러리를 Gradle에 적용을 해야합니다.
https://developer.android.com/jetpack/androidx/releases/recyclerview
위의 링크를 참조해서 아래와 같이 app 폴더 하위에 build.gradle을 업데이트 해줍니다.
....
dependencies {
...
implementation 'androidx.recyclerview:recyclerview:1.2.1'
...
}
우상단 sync를 진행합니다.
Recycler Fragment 생성
기존에 TestFragment -> SecondFragment -> ThirdFragment의 순서로 작동을 했는데 아래와 같이 secondFragment와 동일한 recycleFragment를 만들고자 합니다.
위에 보면 ListView와 동일한 RecyclerView가 있는 것을 확인할 수 있습니다.
위와 같이 추가 Fragment를 생성해 줍니다.
파일명은 RecycleFragment.kt로 생성해 주고, layout은 fragment_recycle.xml을 만들어 줍니다.
두개의 파일은 아래와 같이 기존과 동일하게 만들어 줍니다.
Fragment layout xml 수정하기
기존 fragment_recycle.xml코드를 재 사용해서 수정하겠습니다.
fragment_recycle.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/fragment_second" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Next"
app:layout_constraintEnd_toEndOf="@+id/textView2"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toTopOf="@+id/textView2" />
<ListView
android:id="@+id/listview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="64dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/textView2"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/button2" />
</androidx.constraintlayout.widget.ConstraintLayout>
여기서 TextView의 Id와 기존 ListView를 RecyclerView로 변경해 줍니다.
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".RecycleFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:id="@+id/textView2R"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/fragment_recycle" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Next"
app:layout_constraintEnd_toEndOf="@+id/textView2R"
app:layout_constraintStart_toStartOf="@+id/textView2R"
app:layout_constraintTop_toTopOf="@+id/textView2R" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listviewR"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="64dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/textView2R"
app:layout_constraintStart_toStartOf="@+id/textView2R"
app:layout_constraintTop_toBottomOf="@+id/button2"
app:spanCount="1"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
여기서 가장 중요한것이
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listviewR"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="64dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/textView2R"
app:layout_constraintStart_toStartOf="@+id/textView2R"
app:layout_constraintTop_toBottomOf="@+id/button2"
app:spanCount="1"
/>
이 부분입니다.
기존 ListView를 삭제하고 RecyclerView 로 변환 했습니다.
layout을 만들었으니 만든 layout을 navigation에 연결해 줍니다.
navigation을 잊으신 그대는
2022.01.24 - [Android] - Android Fragment Navigation with Action
여기를 참고하세요
기존과 동일하게 recycleFragment를 만들어 주었습니다.
Arguments와 Action도 동일하게 만들어주세요
이제 xml로 해야할 부분이 많이 끝났습니다.
BaseAdapter vs RecycleView Adapter
이전에 ListView에서는 BaseAdapter를 사용해서 ListViewAdpater를 만들었던거 기억나시나요?
2022.02.05 - [Android] - Android ViewModel & ListView 사용하기
RecyclerView는 전용 Adapter를 사용해야 합니다.
비교해볼까요?
BaseAdapter | RecyclerView Adapter |
class ListViewAdapter(val context : Context, var list: DataViewModel) : BaseAdapter() { | class RecycleViewAdapter(val context : Context, var list: DataViewModel) : RecyclerView.Adapter<RecycleViewAdapter.Holder>() { |
getCount() | getItemCount() |
getItem() | onBindViewHolder() |
getView() | onCreateViewHolder() |
Holder를 만드는건 개발자 마음 | class Holder(val item : View) : RecyclerView.ViewHolder(item) { |
두개의 Adapter 를 상속받으면 반듯이 만들어야 하는 코드 입니다.
무언가 비슷한가요?
비슷하면서도 다른 부분이 많습니다.
특히 RecyclerView는 get대신에 on으로 시작하는 Methods명을 사용했습니다.
on은 일반적으로 Event 발생에 따른 Callback처리를 할때 on으로 시작하는 Method를 사용합니다.
예를 들자면 OnKeyListener, OnclickListener 등이 가장 대표적입니다.
그럼 get은 언제 사용될까요?
getContext(), getClass() 등 어떤 데이터를 개발자가 의도한 특정 시점에 얻어올때 사용하게 됩니다.
이 차이점을 보면 두 Adapter를 제공할때 배경을 상상해 볼 수 있습니다.
BaseAdapter가 동작하는 방법은?
BaseAdapter를 만들던 시기는
API Level1 즉, 최초 안드로이드 코드인 2008년도에 추가된 Class입니다.
이때는 아무래도 Event Base의 개발 방식보다는 선형적인 개발방식이 더 직관적이었을 것이라고 생각합니다.
그래서 간단히 ListView.java 코드를 확인해 보면 아래와 같이 getCount로 얻어온 모든 사이즈 만큼 startPosition에서 모든 View를 getView하는 것을 볼 수 있습니다.
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mListPadding.top + mListPadding.bottom;
}
// Include the padding of the list
..............
int i;
View child;
// mItemCount - 1 since endPosition parameter is inclusive
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
final AbsListView.RecycleBin recycleBin = mRecycler;
final boolean recyle = recycleOnMeasure();
final boolean[] isScrap = mIsScrap;
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
저위에 obtainView에서 i position을 바탕으로 getView하는 Code를 AbsListView.java에서 확인할 수 있습니다.
View obtainView(int position, boolean[] outMetadata) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
outMetadata[0] = false;
// Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
만약에 StartPosition이 0일경우 마지막 Size까지의 모든 View가 생성되게 됩니다.
별로 메모리나 속도에 좋지 않겠죠?
Recycler가 작동하는 방법은?
해당 내용을 이해하기 위해서는 GAP_WORK라는 것을 이해해야 합니다.
/**
* On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to
* RenderThread but before the next frame begins. We schedule prefetch work in this window.
*/
static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
내용을 보자면 UI Thread에서 RenderThread로 화면 랜더링을 위해서 Frame을 넘겨주고 나면 아주 잠시 idle 시간이 남는다고 합니다. 해당 시간에 화면에 표현할 prefetch를 스케쥴 합니다.
소스 코드를 보면서 내용을 확인해 보겠습니다.
RecyclerView.java소스의 내용입니다.
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mLayoutOrScrollCounter = 0;
mIsAttached = true;
mFirstLayoutComplete = mFirstLayoutComplete && !isLayoutRequested();
if (mLayout != null) {
mLayout.dispatchAttachedToWindow(this);
}
mPostedAnimatorRunner = false;
if (ALLOW_THREAD_GAP_WORK) {
// Register with gap worker
mGapWorker = GapWorker.sGapWorker.get();
if (mGapWorker == null) {
mGapWorker = new GapWorker();
// break 60 fps assumption if data from display appears valid
// NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
Display display = ViewCompat.getDisplay(this);
float refreshRate = 60.0f;
if (!isInEditMode() && display != null) {
float displayRefreshRate = display.getRefreshRate();
if (displayRefreshRate >= 30.0f) {
refreshRate = displayRefreshRate;
}
}
mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
GapWorker.sGapWorker.set(mGapWorker);
}
mGapWorker.add(this);
}
}
위의 코드를 보자면 displayRefreshRate를 바탕으로 Frame Interval을 계산합니다.
그리고 Gapworker를 실행하는데요.
GAPworker는 Thread로 작동하게 됩니다.
final class GapWorker implements Runnable {
GapWorker가 Frame Interval 사이에 화면에 표현할 View or Hoder를 미리 preFetch하게 됩니다.
일단 PreFetch가 작동하는 코드를 보겠습니다.
// Populate task list from prefetch data...
mTasks.ensureCapacity(totalTaskCount);
int totalTaskIndex = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() != View.VISIBLE) {
// Invisible view, don't bother prefetching
continue;
}
LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
+ Math.abs(prefetchRegistry.mPrefetchDy);
for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
final Task task;
if (totalTaskIndex >= mTasks.size()) {
task = new Task();
mTasks.add(task);
} else {
task = mTasks.get(totalTaskIndex);
}
final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
task.immediate = distanceToItem <= viewVelocity;
task.viewVelocity = viewVelocity;
task.distanceToItem = distanceToItem;
task.view = view;
task.position = prefetchRegistry.mPrefetchArray[j];
totalTaskIndex++;
}
}
GapWorker가 작동하는 코드인데요. ListView와 다른점이 보이나요?
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() != View.VISIBLE) {
// Invisible view, don't bother prefetching
continue;
}
viewCount를 갖어오고 화면에 해당 내용이 보이지 않는다면 preFetching하지 말아라 입니다.
즉, 필요한 부분만 prefetching을 하게 됩니다.
그리고 위에서 말한 idle time 즉, deadline 시간안에 prefetch 못한것은 다음 prefetch시간을 기다리게 됩니다.
RecyclerView.Recycler recycler = view.mRecycler;
RecyclerView.ViewHolder holder;
try {
view.onEnterLayoutOrScroll();
holder = recycler.tryGetViewHolderForPositionByDeadline(
position, false, deadlineNs);
if (holder != null) {
if (holder.isBound() && !holder.isInvalid()) {
// Only give the view a chance to go into the cache if binding succeeded
// Note that we must use public method, since item may need cleanup
recycler.recycleView(holder.itemView);
} else {
// Didn't bind, so we can't cache the view, but it will stay in the pool until
// next prefetch/traversal. If a View fails to bind, it means we didn't have
// enough time prior to the deadline (and won't for other instances of this
// type, during this GapWorker prefetch pass).
recycler.addViewHolderToRecycledViewPool(holder, false);
}
}
제가 본 코드의 flow가 100% 정확하지 않을 수는 있습니다. 그러나 분명한 것은 ListView의 작동방식이 선형적이라고 하면 Recycle View는 View의 가시성과 frame rate를 계산한 idle deadline시간을 바탕으로 병렬적인 작동을 하게 됩니다.
위의 작동 방식 차이가 recycle view가 기존 ListView보다 메모리 및 작동 시간에 더 많은 이점을 갖는 다고 합니다.
이와같은 행위를 ViewHodler Pattern이라고 한다고 합니다.
(패턴 맞나;;; 안드로이드 공식 메뉴얼에는 그런 언급 없는데... 아무튼 인터넷에 패턴이라고 많이들 쓰셔서 순응해봅니다.)
둘의 차이를 정리해 보자면 아래와 같습니다.
- 눈에 보이는 View 객체만 만들것인가?
- 데이터의 fetch가 일어나는 시점을 언제로 잡을 것인가?
- View와 Data의 분리
- Recycler는 Hodler와 Data의 binding이 분리되어 있습니다.
RecyclerView Adapter 개발하기
이제 RecyclerView.Adapter를 상속받는 RecycleViewAdapter.kt를 만들어 보겠습니다.
기본적인 작동방법은 기존 ListView와 동일한 방식으로 만들겠습니다.
class RecycleViewAdapter(val context : Context, var list: DataViewModel) : RecyclerView.Adapter<RecycleViewAdapter.Holder>() {
val layoutInflater : LayoutInflater = LayoutInflater.from(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return Holder.inflateHolder(parent, layoutInflater)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(list.getMessagesAt(position),position,this)
}
override fun getItemCount(): Int {
return list.size
}
class Holder(val item : View) : RecyclerView.ViewHolder(item) {
}
}
RecycleViewAdapter는 Fragement의 context와 ViewModel을 파라메터로 받습니다.
- context : LayoutInflater를 만들기 위해서 갖어 왔습니다. livedata 부분에서 언급하겠지만 context는 ViewGroup에서 받을 수 있습니다. 이해를 위해서 넣었습니다.
- list : view에 bind할 데이터입니다.
그리고 앞서 비교했던 기본 메서드 3개를 만들면 됩니다.
- onCreateViewHolder : Hodler View 객체를 만드는 부분입니다. View만 만들었지 Data를 Bind하지 않은 상태입니다.
- onBindViewHolder : 위에서 만들어진 View Holder 객체에 특정 position의 Data를 Binding하는 부분입니다.
- getItemCount : RecyclerView에 몇개의 객체가 있는지 알려주는 부분입니다.
이제 기본적인 부분을 만들었음으로 ViewHolder를 상속한 Holder 객체에 다음 두가지 기능을 만들어야 합니다.
- View를 담고있는 Holder 객체를 만들어야 합니다.
- Holder 객체에 특정 Position의 Data를 Bind해야합니다.
View를 담고있는 Holder 객체를 만들자
Holder라는 객체가 onCreateViewHolder에게 불리워 졌을때 새로운 View객체를 갖는 Holder를 생성하도록 하여야 합니다.
이를 위해서 java에서 Static과 가장 근접한 효과를 내는 compainon object를 사용하겠습니다.
class RecycleViewAdapter(val context : Context, var list: DataViewModel) : RecyclerView.Adapter<RecycleViewAdapter.Holder>() {
...
class Holder(val item : View) : RecyclerView.ViewHolder(item) {
companion object {
fun inflateHolder(parent: ViewGroup, layoutInflater: LayoutInflater) : Holder {
val view = layoutInflater.inflate(R.layout.element_single, parent, false)
return Holder(view)
}
}
fun bind(str: String,position: Int, adapter: RecycleViewAdapter){
item.findViewById<TextView>(R.id.text_string).text = str
}
....
}
Holder.inflateHolder method를 호출하면 layoutinflater를 통해서 R.layout.element_single xml layout 을 로드해 오도록 하겠습니다.
참고로 R.layout.element_single은 ListView를 만들때 사용했습니다.
2022.02.05 - [Android] - Android ViewModel & ListView 사용하기
자세한 내용은 위를 참고하시면 됩니다.
Inflate를 통해서 View객체를 생성합니다. 이때 만들어진 객체는 Row한건의 UI를 표현하기위한 것입니다.
Data Bind 하기
이제 View도 만들었으니 해당 View에 Data를 Bind해보겠습니다.
앞서 설명한것 처럼
onCreateViewHoder에서 View를 만들고
화면에 특정 Position이 표현될 것 같은 시점에 Data Prefetch를 진행합니다.
이때 불리는 event가 onBindViewHolder 입니다.
onbindViewHolder에는 Position정보가 있습니다.
이 Position정보로 Bind해보겠습니다.
class RecycleViewAdapter(val context : Context, var list: DataViewModel) : RecyclerView.Adapter<RecycleViewAdapter.Holder>() {
...
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(list.getMessagesAt(position),position,this)
}
...
class Holder(val item : View) : RecyclerView.ViewHolder(item) {
...
fun bind(str: String,position: Int, adapter: RecycleViewAdapter){
item.findViewById<TextView>(R.id.text_string).text = str
}
}
}
onBindViewHolder에 파라메터로 앞서 만들어 놓은 Holder 객체가 들어옵니다. 해당 Holder객체에 Bind할 Position정보를 바탕으로 Hoder의 Bind를 호출합니다.
holder.bind(list.getMessagesAt(position),position,this)
이 코드를 호출하면 holder객체 내 bind가 불리워 집니다.
item.findViewById<TextView>(R.id.text_string).text = str
위의 코드를 이용해서 holder안에 있는 view에 text_string view를 얻어오고 그곳에 str정보를 주입합니다.
이제 이렇게까지만 하면 List가 화면에 표시되는 것을 볼 수 있습니다.
혹시 앞에서부터 본 포스트를 보시던 분들은 이상한것을 눈치 채셨을 듯 합니다.
원래 List View의 모습과는 다릅니다. 그 이유는 ListView에 기본적으로 제공되는 theme가 Recycler View에는 동일하게 제공 되지 않기 때문입니다.
그래서 리소스에 drawable>recycle_item.xml을 만들어서 selector를 만들었습니다.
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item>
<shape android:shape="rectangle" >
<stroke
android:width="1dp"
android:color="#FF000000" />
<solid android:color="#00FFFFFF" />
<padding android:left="10dp"
android:right="10dp"
android:top="10dp"
android:bottom="10dp" />
</shape>
</item>
</selector>
위의 부분은 별도로 설명할 수 있는 기회가 있다면 별도로 포스트를 남기겠습니다.
이제 데이터 Bind까지 완료 되었으니 Holder에 Event를 추가해서 데이터를 삭제하거나 thirdFragment로 넘어가는 부분을 만들어 보겠습니다.
click event 처리하기
class RecycleViewAdapter(val context : Context, var list: DataViewModel) : RecyclerView.Adapter<RecycleViewAdapter.Holder>() {
val layoutInflater : LayoutInflater = LayoutInflater.from(context)
fun deleteAt(position: Int){
list.removeDataAt(position)
notifyDataSetChanged()
}
...
class Holder(val item : View) : RecyclerView.ViewHolder(item) {
companion object {
fun inflateHolder(parent: ViewGroup, layoutInflater: LayoutInflater) : Holder {
...
}
}
fun bind(str: String,position: Int, adapter: RecycleViewAdapter){
...
item?.setOnClickListener() {
val str = it.findViewById<TextView>(R.id.text_string).text.toString()
Toast.makeText(item.context, "clicked " + str, Toast.LENGTH_SHORT).show()
val action = RecycleFragmentDirections.actionRecycleFragmentToThirdFragment(str)
it.findNavController().navigate(action)
true
}
item?.setOnLongClickListener() {
Toast.makeText(item.context, "long clicked" + position, Toast.LENGTH_SHORT).show()
weak?.deleteAt(position)
true
}
}
}
}
위에서 deleteAt은 ViewModel에서 특정 position에 있는 데이터를 삭제하고 notifyDataSetChanged 메서드를 이용해서 리스트를 Refresh 시키는 역할을 합니다.
click event처리는 bind 내에서 정리했습니다.
lateinit var weak : RecycleViewAdapter
fun bind(str: String,position: Int, adapter: RecycleViewAdapter){
weak = WeakReference(adapter).get()!!
item.findViewById<TextView>(R.id.text_string).text = str
item?.setOnClickListener() {
val str = it.findViewById<TextView>(R.id.text_string).text.toString()
Toast.makeText(item.context, "clicked " + str, Toast.LENGTH_SHORT).show()
val action = RecycleFragmentDirections.actionRecycleFragmentToThirdFragment(str)
it.findNavController().navigate(action)
true
}
item?.setOnLongClickListener() {
Toast.makeText(item.context, "long clicked" + position, Toast.LENGTH_SHORT).show()
weak?.deleteAt(position)
true
}
}
내용은 기존 Listview와 동일하기 때문에 자세하게 다루지 않겠습니다.
그러나 WeakReference에 대한 설명을 하고자 합니다.
fun bind(str: String,position: Int, adapter: RecycleViewAdapter){
weak = WeakReference(adapter).get()!!
리스트 객체를 long click하면 삭제를 하고자 합니다.
이를 처리하기 위해서는 RecycleViewAdapter Class에서 만든 deleteAt 메서드를 사용해야 합니다.
그래서 Bind를 처리할때 Holder의 Outer Class인 RecycleViewAdapter 객체를 받게 됩니다.
이때 WeakReference를 통해서 해당 객체를 다시 받게 되는데요. 이렇게 하는 이유는 Referencing counter때문입니다.
A Class와 B Class가 서로를 참조할때 A Class가 종료되었음에도 B Class가 어디에 살아있다면 A class는 메모리에서 삭제되지 않습니다.
즉, Memory Leak이 됩니다. 자세한 내용은 아래를 참고하세요.
2020.03.27 - [Android] - android memory leak 처리
물론 위의 코드는 다음과 같이 변경하면 모든것이 해결됩니다.
// lateinit var weak : RecycleViewAdapter
fun bind(str: String,position: Int, adapter: RecycleViewAdapter){
// weak = WeakReference(adapter).get()!!
val weak = adapter
별도의 전역변수를 지양하고 bind 메서드 Scope 내에서만 참조를 유지하게 한다면 문제가 없습니다.
별것 아닌 부분이지만 이와같이 Inner class를 사용할때는 상호 참조를 주의해서 개발 진행하는 것이 필요합니다.
여기까지 정리된 git은 아래를 참조하면 됩니다.
https://github.com/theyoung/fragmentsetup/tree/802157ddd41f125bf3c85369b1abffa823ce2a95
'Android' 카테고리의 다른 글
Android ViewModel & ListView 사용하기 (0) | 2022.02.05 |
---|---|
Android View Binding (뷰 바인딩) (0) | 2022.02.03 |
Android Fragment with Arguments (0) | 2022.01.25 |
Android Fragment Navigation with Action (0) | 2022.01.24 |
Android Fragment 설정하기 (0) | 2022.01.24 |