맨땅에 코딩

'학습의 숲' 소소한 개발일지 - 과일 누적 랭킹 본문

KAU 2023 (2학년)/객체지향프로그래밍

'학습의 숲' 소소한 개발일지 - 과일 누적 랭킹

나는 푸딩 2024. 7. 25. 21:52

FruitshowRepository

🍀 Firebase Realtime Database에서 데이터를 불러와서 이를 LiveData로 변환하는 역할을 한다. 이를 통해 ViewModel에서 이 LiveData를 관찰하여 데이터가 변경될 때마다 UI를 업데이트할 수 있다. 이렇게 하면 데이터를 가져오는 로직과 UI 로직을 분리하여 코드의 가독성과 유지보수성을 향상시킬 수 있다.

 

package com.example.forestlearning.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.forestlearning.FruitShowData
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener

class FruitshowRepository {
    // Firebase Realtime Database 레퍼런스 선언
    private val db: DatabaseReference = FirebaseDatabase.getInstance().getReference("Users")

    fun fetchFruitData(): LiveData<List<FruitShowData>> {
        // LiveData 선언. 이 LiveData는 Firebase에서 가져온 과일 데이터를 갖게 됨
        val fruitDataList = MutableLiveData<List<FruitShowData>>()
        // Firebase Realtime Database에서 데이터를 불러오는 이벤트 리스너 설정
        db.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                // 데이터가 변경되었을 때 호출되는 메소드
                val temp = mutableListOf<FruitShowData>()
                for (userSnapshot in dataSnapshot.children) {
                    // 사용자 이름과 과일 개수를 가져옴
                    val userName = userSnapshot.child("name").getValue(String::class.java) ?: ""
                    var totalFruits = 0

                    // 각 날짜별 과일 개수를 더함
                    val dayFruitSnapshot = userSnapshot.child("Dayfruit")
                    for (dateSnapshot in dayFruitSnapshot.children) {
                        for (fruitSnapshot in dateSnapshot.children) {
                            val fruitNum = fruitSnapshot.getValue(Int::class.java) ?: 0
                            totalFruits += fruitNum
                        }
                    }

                    // 가져온 데이터를 FruitShowData 객체로 만들어 temp에 추가
                    val item = FruitShowData(userName, totalFruits)
                    temp.add(item)
                }

                // 과일 개수를 기준으로 데이터를 정렬하고 _fruitData에 추가
                temp.sortByDescending { it.fruitnum }
                fruitDataList.value = temp
            }

            override fun onCancelled(databaseError: DatabaseError) {
                // 데이터를 가져오는데 실패했을 때 처리
            }
        })
        return fruitDataList
    }
}

 

  1. Firebase Realtime Database 레퍼런스 선언: Firebase Realtime Database는 클라우드 호스팅 NoSQL 데이터베이스로, 데이터를 JSON 형식으로 저장하고 모든 클라이언트에게 실시간으로 데이터를 제공한다. 여기서는 "Users"라는 노드에 대한 참조를 가져온다.

  2. LiveData 선언: LiveData는 앱의 생명주기를 인식하는 데이터 홀더 클래스다. LiveData는 액티비티, 프래그먼트, 서비스의 생명주기 상태를 인식하고, 생명주기가 활성 상태인 컴포넌트에만 업데이트를 알린다. 여기서는 과일 데이터를 가지고 있는 LiveData를 선언하고 있다.

  3. 데이터 가져오기: fetchFruitData() 메소드에서는 Firebase Realtime Database에서 데이터를 불러온다. addValueEventListener를 사용하여 데이터변경사항을 실시간으로 감지하고, 변경사항이 감지되면 onDataChange() 메소드가 호출된다.

  4. 데이터 처리: onDataChange() 메소드에서는 DataSnapshot 객체를 통해 Firebase의 데이터를 읽는다. DataSnapshot은 데이터베이스의 특정 시점의 데이터 상태를 포함한다. 여기서는 "Users" 노드의 모든 자식노드를 순회하며, 각 사용자의 이름과 과일의 개수를 가져온다. 그리고 이 정보를 FruitShowData 객체로 만들어서 리스트에 추가한다.

  5. 데이터 정렬 및 LiveData 업데이트: 모든 데이터를 가져와서 FruitShowData 객체로 만든 후에는 과일의 개수를 기준으로 리스트를 정렬한다. 그 후, 이 리스트를 _fruitData에 설정하여 LiveData의 값을 업데이트한다. 이렇게 하면 LiveData를 관찰하고 있는 모든 옵저버에게 알림이 전달되어 UI를 업데이트할 수 있다.

  6. 오류 처리: onCancelled() 메소드에서는 데이터를 가져오는 도중 오류가 발생했을 때의 처리를 작성할 수 있다. 현재는 아무런 처리도 하지 않고 있다.

FruitshowViewModel

🍀 FruitshowRepository를 통해 Firebase Realtime Database에서 데이터를 가져와 이를 LiveData로 제공한다. 이를 통해 View에서는 LiveData를 관찰하여 데이터가 변경될 때마다 UI를 업데이트할 수 있다. 이렇게 함으로써 데이터를 가져오는 로직과 UI 로직을 분리하여 코드의 가독성과 유지보수성을 향상시킬 수 있다.

 

package com.example.forestlearning.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.forestlearning.FruitShowData
import com.example.forestlearning.repository.FruitshowRepository
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener

class FruitshowViewModel : ViewModel() {
    // FruitshowRepository 인스턴스를 생성하여 repository 변수에 저장
    private val repository = FruitshowRepository()

    // repository를 통해 과일 데이터를 가져와 LiveData 형태로 저장
    // 과일 데이터가 변경될 때마다 자동으로 업데이트
    val fruitData: LiveData<List<FruitShowData>> = repository.fetchFruitData()
}

 

  1. Repository 인스턴스 생성: FruitshowViewModel 클래스 내에서 FruitshowRepository의 인스턴스를 생성한다. FruitshowRepository는 데이터를 가져오는 로직을 담당하며, 이를 ViewModel에서 사용하게 된다. 이렇게 함으로써 데이터를 가져오는 로직과 UI 로직을 분리하여 코드의 가독성과 유지보수성을 향상시킬 수 있다.

  2. LiveData 가져오기: fruitDataFruitshowRepository에서 가져온 LiveData를 참조한다. LiveData는 변경 가능한 데이터를 보유하고 있는 데이터 홀더 클래스로, LiveData 객체에 저장된 데이터가 변경되면 구독자에게 알림을 보낸다. 이를 통해 UI를 최신 상태로 유지할 수 있다. 여기서는 FruitshowRepository에서 Firebase Realtime Database에서 가져온 데이터를 LiveData로 변환한 것을 참조하게 된다.

FruitshowFragment

🍀 과일 데이터를 보여주는 화면을 구성하고, 사용자의 검색 요청을 처리하는 역할을 한다. 이를 통해 사용자는 과일 데이터를 오름차순으로 확인하고 사용자 이름을 검샐할 수 있다. 이 클래스는 ViewModel을 통해 데이터를 가져오며, 이 데이터를 RecyclerView에 표시한다. 이렇게 함으로써 UI 로직과 데이터 처리 로직을 분리하여 코드의 가독성과 유지보수성을 향상시킬 수 있다.

 

package com.example.forestlearning

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.forestlearning.databinding.FragmentFruitshowBinding
import com.example.forestlearning.viewmodel.FruitshowViewModel

class FruitshowFragment : Fragment() {
    // 바인딩 객체. UI 컴포넌트에 접근할 수 있게 함
    var binding: FragmentFruitshowBinding? = null

    // ViewModel 객체. UI에서 사용할 데이터를 제공
    private val viewModel: FruitshowViewModel by viewModels()

    // 뷰를 생성하는 메소드
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 바인딩 객체를 초기화하고 루트 뷰를 반환
        binding = FragmentFruitshowBinding.inflate(inflater, container, false)
        return binding?.root
    }

    // 뷰가 생성된 후 호출되는 메소드
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // RecyclerView의 Adapter를 생성하고 설정
        val adapter = FruitshowAdapter()
        binding?.fruitShowRecycler?.apply {
            layoutManager = LinearLayoutManager(context)
            this.adapter = adapter
        }

        // ViewModel의 fruitData를 관찰하여 데이터가 변경될 때마다 Adapter에 반영
        viewModel.fruitData.observe(viewLifecycleOwner) { data ->
            adapter.fruitData = data
        }

        // 검색 버튼의 클릭 이벤트를 설정
        binding?.searchBtn?.setOnClickListener {
            val searchName = binding?.searchName?.text.toString()
            if (searchName.isNotEmpty()) {
                // 검색어가 비어있지 않을 경우 검색 수행
                adapter.search(searchName)
            } else {
                // 검색어가 비어있을 경우 전체 목록 출력
                adapter.search("")
            }
        }
    }

    // 뷰가 파괴될 때 호출되는 메소드
    override fun onDestroyView() {
        super.onDestroyView()
        // 뷰가 파괴될 때 바인딩 객체를 null로 설정
        binding = null
    }
}

 

  1. 바인딩 객체 선언: binding 객체는 FragmentFruitshowBinding 클래스의 인스턴스입니다. 이 객체를 통해 뷰에 선언된 UI 컴포넌트에 접근할 수 있다.
  2. ViewModel 객체 선언: viewModel 객체는 FruitshowViewModel 클래스의 인스턴스입니다. 이 객체를 통해 UI에 필요한 데이터를 가져온다.
  3. onCreateView 메소드: 이 메소드에서는 Fragment의 뷰를 생성한다. FragmentFruitshowBinding.inflate() 메소드를 통해 바인딩 객체를 초기화하고 뷰를 생성한다.
  4. onViewCreated 메소드: 이 메소드에서는 뷰가 생성된 후 필요한 설정한다. RecyclerView LayoutManagerAdapter를 설정하며, ViewModelLiveData를 관찰하여 데이터가 변경될 때마다 Adapter에 반영한다. 또한, 검색 버튼의 클릭 이벤트를 설정한다.
  5. onDestroyView 메소드: 이 메소드에서는 Fragment의 뷰가 파괴될 때 필요한 정리 작업을 합니다. 바인딩 객체를 null로 설정하여 메모리 누수를 방지한다.

FruitshowAdapter

🍀 RecyclerView에서 사용하는 데이터와 아이템 뷰를 연결하는 역할을 한다. 이 클래스는 원본 데이터와 검색 결과 데이터를 관리하며, 검색 기능을 제공한다. 이를 통해 사용자는 원하는 데이터만 RecyclerView에서 볼 수 있다. 이 클래스는 ViewHolder를 통해 아이템 뷰를 재사용하여 메모리 사용량을 최적화하고, 스크롤 성능을 향상시킨다.

 

package com.example.forestlearning

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.forestlearning.databinding.FruitshowListBinding

class FruitshowAdapter : RecyclerView.Adapter<FruitshowAdapter.Holder>() {
    private var _fruitData = listOf<FruitShowData>()
    var fruitData: List<FruitShowData>
        get() = _fruitData
        set(value) {
            _fruitData = value
            filteredData = _fruitData
            notifyDataSetChanged()
        }

    private var filteredData = listOf<FruitShowData>()

    // 검색 기능 함수
    fun search(searchName: String) {
        // 검색어가 포함된 데이터만 filteredData에 추가
        filteredData = if (searchName.isEmpty()) {
            _fruitData
        } else {
            _fruitData.filter { it.nickname?.contains(searchName) == true }
        }
        notifyDataSetChanged()
    }

    // ViewHolder 클래스
    class Holder(val binding: FruitshowListBinding) : RecyclerView.ViewHolder(binding.root) {
        fun dataBind(data: FruitShowData, position: Int) {
            binding.apply {
                userName.text = data.nickname
                userFruit.text = data.fruitnum.toString()
                userRank.text = (position+1).toString()
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        // ViewHolder를 생성하는 메소드
        val binding = FruitshowListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        // ViewHolder에 데이터를 바인딩하는 메소드
        holder.dataBind(filteredData[position], position)
    }

    override fun getItemCount() = filteredData.size
    // 아이템 개수를 반환하는 메소드
}

 

  1. 데이터 선언: _fruitData filteredData 두 개의 데이터 리스트를 선언한다. _fruitData는 원본 데이터를 저장하고, filteredData는 검색 결과를 저장한다. fruitData 프로퍼티를 통해 외부에서 _fruitData에 접근할 수 있게 하였다.

  2. 검색 메소드: search() 메소드에서는 검색어를 인자로 받아, 검색어가 포함된 데이터만 filteredData에 저장한다. 검색어가 비어있는 경우에는 원본 데이터를 filteredData에 저장한다.

  3. ViewHolder 클래스: Holder 클래스는 RecyclerView.ViewHolder를 상속받아 생성되었다. 이 클래스에서는 아이템 뷰에 데이터를 바인딩하는 dataBind() 메소드를 정의하고 있다.

  4. onCreateViewHolder 메소드: 이 메소드에서는 ViewHolder를 생성한다. FruitshowListBinding.inflate() 메소드를 통해 바인딩 객체를 생성하고, 이를 Holder의 인자로 전달하여 Holder를 생성한다.

  5. onBindViewHolder 메소드: 이 메소드에서는 ViewHolder에 데이터를 바인딩한다. dataBind() 메소드를 호출하여 ViewHolder의 아이템 뷰에 데이터를 설정한다.

  6. getItemCount 메소드: 이 메소드에서는 아이템의 개수를 반환한다. filteredData의 크기를 반환하여 현재 보여줄 데이터의 개수를 알려준다.

  7. notifyDataSetChaged() 메소드: RecyclerView.Adapter에서 데이터 세트가 변경되었음을 알리는 데 사용된다. 이 메소드를 호출하면 RecyclerView는 전체 데이터 세트에 대해 뷰를 다시 바인딩하고 그리는 작업을 실행한다. 여기서 notifyDataSetChanged()는 두 가지 상황에서 사용된다.
    1. fruitData의 setter에서: fruitData가 새로 설정될 때마다 원본 데이터 _fruitData와 필터링된 데이터 filteredData를 업데이트하고, notifyDataSetChanged()를 호출하여 RecyclerView에 데이터 변경을 알린다.
    2. search() 메소드에서: 검색어에 따라 filteredData가 업데이트되면, notifyDataSetChanged()를 호출하여 RecyclerView에 데이터 변경을 알린다.
    이렇게 함으로써 RecyclerView는 새로운 데이터에 따라 UI를 업데이트할 수 있다. 그러나 notifyDataSetChanged()는 전체 데이터 세트에 대한 업데이트를 수행하기 때문에 성능에 부담이 될 수 있다. 따라서 가능한 한 notifyItemInserted(), notifyItemRemoved(), notifyItemChanged() 등의 메소드를 사용하여 변경된 항목만 업데이트하는 것이 좋다.