dependencies {
...
// GoogleMap
implementation("com.google.android.gms:play-services-maps:18.2.0")
// (옵션) GPS 이용해서 위치 확인
implementation("com.google.android.gms:play-services-location:21.1.0")
}
ListAdapter는 RecyclerView.Adapter의 확장된 형태로, RecyclerView의 데이터를 리스트 형태로 관리하며, 효율적인 데이터 리스트 업데이트를 위해 DiffUtil을 내부적으로 사용한다. 기존의 RecyclerViewAdapter와 ListAdapter의 주요 차이점은 데이터의 변경을 관리하는 방식에 있다.
조금 더 보태자면 기존의 RecyclerViewAdapter는 정적인, 즉 고정된 데이터를 보여주기에 최적화되어 있고 ListAdapter는 데이터가 동적으로 추가되고 삭제되는 일들이 많을 때 효율적으로 동작하기 위해 최적화되어있다고 생각하면 된다.
RecyclerViewAdapter와의 차이점
RecyclerViewAdapter
데이터 변경을 감지하고 UI를 업데이트하기 위해서는 개발자가 명시적으로 notifyDataSetChanged(), notifyItemInserted(), notifyItemRemoved() 등의 메서드를 호출해야 한다.
데이터가 변경될 때마다 전체 리스트를 업데이트하거나, 특정 위치의 데이터가 변경되었다는 것을 직접 알려줘야 한다.
성능상의 이유로, 대량의 데이터가 변경될 때 비효율적일 수 있다. ( 모든 아이템을 전부 다시 그리기 때문)
ListAdapter
내부적 으로 AsyncListDiffer를 사용하여 데이터의 변경사항을 비동기적으로 계산하고, 필요한 최소한의 업데이트를 RecyclerView에 알려준다. (DiffUtil 메소드를 통해서 변경된 부분만 뷰를 그림)
개발자는 단순히 새로운 리스트를 submitList() 메서드를 통해 전달하면, ListAdapter가 나머지를 처리합니다.
데이터의 변경 사항이 자동으로 계산되어 효율적인 업데이트가 가능합니다.
DiffUtil이란?
DiffUtil은 두 리스트 간의 차이점을 계산하는 유틸리티 클래스로, 어떤 항목이 추가, 제거, 변경되었는지를 찾아낸다. 이를 위해 DiffUtil.Callback을 상속받은 클래스를 만들어서 두 리스트의 항목이 같은지(areItemsTheSame()), 내용이 같은지(areContentsTheSame())를 비교하는 로직을 구현해야 한다.
DiffUtil은 이러한 정보를 바탕으로 최소한의 업데이트로 RecyclerView를 갱신할 수 있는 'DiffResult'를 생성합니다. 이 결과는 RecyclerView의 어댑터에 적용되어, 효율적으로 UI를 업데이트할 수 있게 도와준다.
ListAdapter를 사용하면 이 모든 과정을 더 쉽게 처리할 수 있게 되어, 데이터가 변경될 때마다 RecyclerView를 더 효율적으로 업데이트할 수 있습니다.
ListAdapter 사용법
class ContactAdapter(var viewType: Int) :
ListAdapter<ContactData, RecyclerView.ViewHolder>(diffUtil) {
companion object {
private val diffUtil = object : DiffUtil.ItemCallback<ContactData>() {
override fun areItemsTheSame(oldItem: ContactData, newItem: ContactData): Boolean {
// 연락처가 같은지 확인
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: ContactData, newItem: ContactData): Boolean {
// 모든 필드가 같은지 확인 (data class의 equals 사용)
return oldItem == newItem
}
}
const val TYPE_LIST = 0
const val TYPE_GRID = 1
}
override fun getItemViewType(position: Int): Int {
return viewType
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// ViewHolder 생성 로직
return when (viewType) {
TYPE_LIST -> ListViewHolder(
ItemRecyclerViewListBinding.inflate(
LayoutInflater.from(
parent.context
), parent, false
)
)
TYPE_GRID -> GridViewHolder(
ItemRecyclerViewGridBinding.inflate(
LayoutInflater.from(
parent.context
), parent, false
)
)
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// ViewHolder 바인딩 로직
when (holder.itemViewType) {
TYPE_LIST -> (holder as ListViewHolder).bind(getItem(position))
TYPE_GRID -> (holder as GridViewHolder).bind(getItem(position))
}
}
// ViewHolder 구현
inner class ListViewHolder(private val binding: ItemRecyclerViewListBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(contact: ContactData) {
/* 아이템 바인딩 로직 */
}
}
inner class GridViewHolder(private val binding: ItemRecyclerViewGridBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(contact: ContactData) {
/* 아이템 바인딩 로직 */
}
}
위 코드는 연락처 앱의 어댑터를 구성한 코드이다. 2개의 뷰 타입을 볼 수 있게 하기 위해 ListViewHolder와 GridViewHolder를 생성한 점을 제외하면 일반 리스트어댑터의 구성과 동일하다.
ListAdapter를 상속받아서 그 안에 데이터와 뷰 홀더를 넣어준다. 기존에 RecyclerViewAdapter에서는 인자로 dataList를 받아왔지만 ListAdapter는 변경이 있을 때 submitList를 통해 리스트를 받아오기에 인자가 필요없다.
내부적으로 getItem 메소드를 통해서 ListAdapter의 currentList를 받아와 RecyclerViewAdapter의 mItems[position].Item 을 대체한다.
어댑터 구현이 완료되면 어댑터를 세팅하고 어댑터에 데이터를 넣을 때는아래와 같다.
object ContactManager {
val contactList: MutableList<ContactData> = mutableListOf()
fun getList(): List<ContactData> {
return contactList.toList()
}
}
contactAdapter.submitList(ContactManager.getList())
중요한 점은 기존의 contactList는 MutableList이지만 호출할 때는 toList()로 변환하여 List형태로 submit한다는 점이다.
ListAdapter의 DiffUtil 이 받아오는 리스트의 주소값이 같으면 동작을 안하기 때문에 toList로 변환하여서 주소값을 변경시켜주어서 다른 리스트라는 것을 알려주어야 한다.
3. 파일 형태로 저장하기 (주로 FileWriter나 BufferedWriter 등을 사용)
여기선 SharedPreferences에 대해서 알아보자.
sharedPreferences란 무엇일까?
안드로이드 앱 개발을 할 때 데이터 저장이 필요한 순간은 반드시 온다.
많은 양의 데이터나 중요한 데이터들은 DB에 저장하는게 바람직하지만 보통 간단한 프로그램 설정 정보들은 sharedPreferences를 이용해서 데이터를 저장한다.
안드로이드에서 기본적으로 제공하는 클래스이기에 따로 gradle설정 필요x
한줄 요약 :
프로그램의 설정 정보 (사용자의 옵션 선택 사항 이나 프로그램의 구성 정보)를 영구적으로 저장하는 용도로 사용
예) 알림 권한 설정 여부, 자동 로그인 설정 여부 등
sharedPreferences 특징
간단한 키-값 쌍의 데이터를 저장하기 위한 메커니즘을 제공
기본적으로 SharedPreferences는 내부적으로 XML 파일로 데이터를 저장
주로 앱의 구성 설정, 사용자 기본 설정, 애플리케이션 상태 등을 저장하는 데 사용
사용 방법
SharedPreferences에 데이터 저장하기
private fun saveData() {
val pref = getSharedPreferences("pref", MODE_PRIVATE)
val editor = pref.edit() //수정 모드
// 첫 번째는 key, 두 번째는 value
editor.putString("name", binding.etText.text.toString())
editor.apply() // 저장 완료
}
pref라는 변수에 SharedPreferences 객체 저장 pref는 파일 이름이며 뒤에 모드는 현재 이 파일은 앱 내에서만 접근 가능하도록 하는 모드다. ( + 모드에 대한 설명은 따로 밑에 적어두었어요.)
.edit() 은 SharedPreferences의 Editro 객체를 반환하는 메소드로, 파일을 변경할 수 있게 만들어주는 녀석이라고 생각하면 된다.
.put자료형을 통해서 첫 번째 인자에 키 값을 넣고 두 번째 인자에 실제 저장할 value값을 넣어주면 된다.
.apply()로 변경 사항을 저장한다. (저장하는 방법은 [apply, commit] 두 가지가 있다. 둘의 차이도 따로 밑에 정리해놨다.)
SharedPreferences에 있는 데이터 가져오기
private fun loadData() {
val pref = getSharedPreferences("pref", 0)
// 첫 번째는 key, 두 번째는 데이터가 없을 경우 default value -> 저장하지 않은 경우도 생각해야 한다.
binding.etText.setText(pref.getString("name", ""))
}
저장한 데이터를 세팅할 때는 마찬가지로 pref라는 변수에 해당 파일이 담긴 객체를 전달해준다.
뒤에 0은 MODE_PRIVATE을 의미하며 0으로 대체하여 사용가능하다.
애플리케이션에서 SharedPreferences 데이터 저장 확인 방법
Device Explorer에 가서 data -> data -> [현재 프로젝트 이름] 을 찾는다.
프로젝트를 열어보면 shared_prefs 폴더가 있다. 그 안에 우리가 저장한 XML 파일을 찾아볼 수 있다.
pref.xml을 열어보면 저장한 데이터 map 형태로 잘 담겨있다.
SharedPreferences 모드 설명
SharedPreferences의 mode 매개변수는 해당 SharedPreferences 파일의 액세스 권한 및 공유 범위를 정의합니다.
모드의 종류로는 다음과 같이 4가지가 있다.
MODE_PRIVATE
MODE_WORLD_READABLE
MODE_WORLD_WRITEABLE
MODE_MULTI_PROCESS
// 인터페이스 내용
@IntDef(flag = true, prefix = { "MODE_" }, value = {
MODE_PRIVATE,
MODE_WORLD_READABLE,
MODE_WORLD_WRITEABLE,
MODE_MULTI_PROCESS,
})
@Retention(RetentionPolicy.SOURCE)
public @interface PreferencesMode {}
현재 MODE_PRIVATE 을 제외한 옵션은 Deprecated(사용이 권장되지 않음)되었다.
MODE_WORLD_READABLE 이 모드를 사용하면 다른 앱에서 해당 SharedPreferences 파일을 읽을 수 있다.
MODE_WORLD_WRITEABLE 이 모드를 사용하면 다른 앱에서 해당 SharedPreferences 파일을 수정할 수 있다.
위의 두가지 모드는 현재 보안 이슈로 인해 Deprecated되었고, API 레벨 23에서는 사용 중지되었습니다.
MODE_MULTI_PROCESS는다중 프로세스 간에 SharedPreferences 파일을 공유할 수 있도록 한다. API 레벨 23에서 Deprecated 되었고, API 레벨 30에서는 완전히 사용이 중지되었다. 이를 대체하기 위해, 다중 프로세스 간에 데이터를 공유해야 하는 경우에는 ContentProvider, BroadcastReceiver 및 일부 동기화 기술을 사용하는 것이 권장된다고 한다.
commit, apply 차이
commit 모드
commit 메소드는 동기적으로 동작하며, 변경 사항을 즉시 SharedPreferences에 기록한다.
모든 변경 사항이 저장소에 성공적으로 쓰여질 때까지 commit 메소드는 블록된다. 따라서 호출자는 commit의 반환 값을 통해 변경 사항이 성공적으로 저장되었는지 여부를 확인할 수 있다.
boolean success = editor.commit()
if (success) {
// 변경 사항이 성공적으로 저장됨
} else {
// 변경 사항 저장 실패
}
apply 모드
apply 메소드는 비동기적으로 동작하며, 변경 사항을 즉시 메모리에 적용한 다음, 백그라운드에서 비동기적으로 디스크에 저장소에 커밋한다.
apply 메소드는 즉시 반환되므로 호출자는 변경 사항의 저장 성공 여부에 대한 피드백을 받을 수 없다. 따라서 호출자는 비동기적으로 변경 사항이 저장되는 것을 기다리거나 확인할 방법이 없다.
editor.apply()
// 변경 사항이 비동기적으로 저장되므로 피드백이 없음
일반적으로, 변경 사항이 성공 여부를 확인하고 결과에 따라 추가 작업이 필요한 경우에는 commit을 사용하고, 결과에 대한 피드백이 필요하지 않거나 비동기적으로 처리해도 무방한 경우에는 apply을 사용하는 것이 적절하다.