example의 scope(범위는) Activity 안에 있어야 합니다. 그리고 해당 에러는 함수들이 제대로 정의되지 않아서 나오는 에러입니다. 제가 기본적인 샘플을 올려드릴게요. 참고하셔서 더 깔금하게 만드시면 좋을 것 같습니다.
// 기본적으로 데이터를 가져오는 부분과 화면을 담당하는 부분은 데이터 클래스도 분리해서 사용하는게 좋습니다. 구조가 같더라고 말이죠.
// 님의 코드를 기반으로 임의로 만든 클래스입니다. 클래스 이름이 *Schema로 끝나는 이유는 이 클래스는 데이터 레이어에서 사용되는 클래스라고 알려주는 명명규칙입니다.
data class DateSchema(
val value: String,
val year: Int
)
// 이건 UseCase라고 불리는 클래스입니다. 이 클래스는 데이터 레이어(여기서는 FirebaseDatabase)를 가져와서 비지니스 로직을 구현합니다.
// 이 클래스는 아주 중요합니다. 따라서 이 클래스를 중점으로 유닛테스트를 하면 좋습니다.
// 이 클래스는 옵저버 패턴을 사용하고 있습니다. 님이 딱히 RxJava나 coroutine같은 걸 안쓰시는 것 같아 콜백을 사용하여 처리할 수 있는 방법으로 구현했습니다.
// Listener 인터페이스를 구현한 클래스는 모두 옵저버가 될 수 있으며, 이 클래스의 인스턴스에 registerListener를 통하여 등록을 해주면, 이벤트가 발생할 때 콜백을 통해
// 알려주게 됩니다. 등록을 해제할 때는 unregisterListener 를 호출합니다.
// 생성자에 FirebaseDatabase를 넘겨주고 있는데, 이렇게 하면 나중에 유닛테스트를 할 때, test double이나 Mock을 사용하여 테스트를 만들기가 아주 쉬워집니다.
// 더 나아가 FirebaseDatabase 자체도 다른 클래스 안에 집어넣어서 직접 사용하지 않게 할 수도 있습니다. 이게 더 좋은 구조입니다. 어떤 라이브러리를 사용할 때는 가능하면
// 직접 사용하기보다는 한번 다른 클래스로 감싸서 사용하는 것이 의존성을 훨씬 줄여줄 수 있는 더 좋은 코드입니다.
class GetCalendarUseCase(
private val firebaseDatabase: FirebaseDatabase
) {
interface Listener {
fun onCalendarFetched(dates: List<DateSchema>)
fun onCalendarFetchFailed(e: DatabaseError)
}
private val listeners = hashSetOf<Listener>()
fun registerListener(listener: Listener) {
listeners.add(listener)
}
fun unregisterListener(listener: Listener) {
listeners.remove(listener)
}
fun fetch() {
val schemas = listOf<DateSchema>()
// 파이어베이스에서 데이터를 가져오는 부분. 이 부분은 제가 알 수 없는 부분이라 님의 코드를 그대로 붙여넣었습니다.
// 꼭 확인해 보셔야할 부분은 20번이나 루프를 돌아야 하는게 이상합니다. 이러면 속도가 떨어집니다. 제 생각에는 한번에 원하시는 리스트를 가져올 수 있는 쿼리가 반드시 있을 것 같습니다.
for( i in 1..20){
val myRef: DatabaseReference = database.getReference("List/list$i")
myRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val value = dataSnapshot?.value
//example.add(Date("$value", 2022))
//recycler_view.adapter?.notifyDataSetChanged()
//recycler_view.adapter = DateAdapter(example.sortedBy { it.name }) { date ->
//}
//recycler_view.adapter?.notifyDataSetChanged()
}
override fun onCancelled(e: DatabaseError) {
notifyCalendarFetchFailed(e)
}
})
}
// 위에서 리스트를 가져오신 다음 등록된 리스너에게 알려줍니다.
notifyCalendarFetched(schemas)
//에러시, notifyCalendarFetchFailed() 호출
}
// 리스너들에게 데이터를 가져왔다는 이벤트 통보
private fun notifyCalendarFetched(schemas: List<DateSchema>) {
for (listener in listeners) {
listener.onCalendarFetched(schemas)
}
}
// 리스너들에게 데이터 가져오기에 실패했다는 이벤트 통보
private fun notifyCalendarFetchFailed(error: DatabaseError) {
for (listener in listeners) {
listener.onCalendarFetchFailed(error)
}
}
}
========= 여기서부터는 뷰레이어입니다.
// 어댑터에 사용할 클래스. 이름은 님이 코드만 봐서는 더 좋은 이름이 바로 떠오르지 않아 이렇게 주었습니다. 가능하면 좀 더 명확한 이름이 좋습니다.
// 다시 한번 말씀드리지만, clean code를 작성하는데 Naming은 정말 중요합니다.
data class DateItem(
val value: String,
val year: String
)
// 생성자에 List를 넘겨주는 부분을 사용하지 않아서 뺏습니다. 그리고 lambda 생성자 변수의 이름도 좀 더 명확하게 주었습니다.
class DateAdapter(
private val dateItemClicked: (item: DateItem) -> Unit
): RecyclerView.Adapter<DateViewHolder>() {
private val calendarItems = arrayListOf<DateItem>()
// 파이어베이스에서 데이터를 받으면 어댑터를 갱신해야 하므로 이 부분은 꼭 필요합니다.
fun submitList(items: List<DateItem>) {
calendarItems.clear()
calendarItems.addAll(items)
notifyDataSetChanged()
}
override fun getItemCount(): Int = calendarItems.size
// 아주 드물지만 리사이클러뷰의 레이아웃이 완전히 준비되기 전에 아이템에 액세할 경우가 생깁니다. 이때 position이 -1이 되기 때문에 방어적인 코딩차원에서 null을 리턴할 수 있도록 했고,
// onViewBindHolder에서 null인 경우는 무시를 하고 있습니다.
private fun getItem(position: Int): DateItem? = calendarItems.getOrNull(position)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DateViewHolder {
// 뷰바인딩을 뷰홀더로 넘겨줍니다.
val itemBinding = ItemDateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DateViewHolder(itemBinding, dateItemClicked)
}
override fun onBindViewHolder(holder: DateViewHolder, position: Int) {
val item = getItem(position) ?: return
holder.bind(item)
}
}
// adpaterPosition은 리스트 아이템 구성에 따라 다른 값을 리턴할 수 있기 때문에 전 개인적으로 사용하지 않습니다. 대신 bind에서 리스너를 초기화 줍니다. 이건 개인적인 스타일입니다.
class DateViewHolder(
private val binding: ItemDateBinding,
private val dateItemClicked: (item: DateItem) -> Unit
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: DateItem) {
binding.titleTxt.text = item.value
itemView.setOnClickListener {
dateItemClicked(item)
}
}
}
// 액티비티입니다.
class MainActivity : AppCompatActivity(), GetCalendarUseCase.Listener {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private val context: Context get() = this@MainActivity
private val calendarAdapter = DateAdapter {
}
private val dateItemMapper by lazy { DateItemMapper() }
private lateinit var getCalendarUseCase: GetCalendarUseCase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupDependencies()
setupViews()
}
private fun setupDependencies() {
getCalendarUseCase = GetCalendarUseCase(FirebaseDatabase.getInstance())
}
private fun setupViews() {
binding.calendarRcv.apply {
adapter = calendarAdapter
}
}
override fun onDestroy() {
// RecyclerView의 Adapter를 명시적으로 해제해주지 않으면 생각보다 많은 상황에서 메모리 누수가 발생합니다. 그래서 강제로 null로 만들어서 가비지 콜렉터가 메모리를 회수할 수 있도록 도와줍니다.
binding.calendarRcv.adapter = null
super.onDestroy()
}
// onStop에서 getCalendarUseCase에 등록하고 onStop에서 등록해제를 하는 부분은 아주 중요합니다. 이렇게 하는 이유는 앱이 백그라운드로 빠지면 이벤트를 더이상 받지 않게 하기 위함이고
// MainActivity가 종료되면 자연스럽게 옵저버를 등록해제해서 가비지 콜렉터가 메모리를 회수할 수 있도록 해주기 위함입니다.
override fun onStart() {
super.onStart()
getCalendarUseCase.registerListener(this)
fetchCalendar()
}
private fun fetchCalendar() {
getCalendarUseCase.fetch()
}
override fun onStop() {
super.onStop()
getCalendarUseCase.unregisterListener(this)
}
override fun onCalendarFetched(dates: List<DateSchema>) {
calendarAdapter.submitList(dateItemMapper.schemasToUiEntities(dates))
}
override fun onCalendarFetchFailed(e: DatabaseError) {
// show error message
}
}