https://velog.io/@dldmswo1209/Firebase-Facebook-로그인-구현-RealTime-DB-사용하기
https://github.com/yuyakaido/CardStackView#installation
깃허브 yuyakaido 라는 사람이 만든 CardStackView를 사용 했다.
그런데 implementation 하는 과정에서 아래와 같은 오류가 발생했다.
1시간 동안 삽질해가며 해결 방법을 찾았지만, 해결 방법은 의외로 간단했다.
setting.graddle 에 jcenter()를 추가하면 된다.
dependencyResolutionManagement {
repositories {
...
jcenter()
}
}
https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter
https://velog.io/@l2hyunwoo/Android-RecyclerView-DiffUtil-ListAdapter
요약 하자면 기존에 사용하던 RecyclerView의 notifyDataSetChanged()는 데이터가 변경 될 때 모든 데이터가 통째로 업데이트 되면서 지연시간이 발생하는 이슈가 있다.
DiffUtill 은 이러한 문제를 해결하기 위해서 현재 리스트와 교체 해야할 리스트를 비교하여 실제로 바꿔야 하는 데이터만 업데이트 해서 훨씬 빠른 속도로 업데이트가 가능하다!
리스트에 CardItem객체 형태로 저장
data class CardItem(
val userId: String,
var name: String
)
ListAdapter 사용
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListAdapter
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
class CardItemAdapter: androidx.recyclerview.widget.ListAdapter<CardItem, CardItemAdapter.ViewHolder>(diffUtil) {
inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view){
fun bind(cardItem: CardItem){
view.findViewById<TextView>(R.id.nameTextView).text = cardItem.name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(inflater.inflate(R.layout.item_card, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(currentList[position])
}
companion object{
val diffUtil = object: DiffUtil.ItemCallback<CardItem>(){
override fun areItemsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
return oldItem.userId == newItem.userId
}
override fun areContentsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
return oldItem == newItem
}
}
}
}
class MatchedUserAdapter: androidx.recyclerview.widget.ListAdapter<CardItem, MatchedUserAdapter.ViewHolder>(diffUtil) {
inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view){
fun bind(cardItem: CardItem){
view.findViewById<TextView>(R.id.userNameTextView).text = cardItem.name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ViewHolder(inflater.inflate(R.layout.item_matched_user, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(currentList[position])
}
companion object{
val diffUtil = object: DiffUtil.ItemCallback<CardItem>(){
override fun areItemsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
return oldItem.userId == newItem.userId
}
override fun areContentsTheSame(oldItem: CardItem, newItem: CardItem): Boolean {
return oldItem == newItem
}
}
}
}
package com.dldmswo1209.tinder
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.dldmswo1209.tinder.DBKey.Companion.DIS_LIKE
import com.dldmswo1209.tinder.DBKey.Companion.LIKE
import com.dldmswo1209.tinder.DBKey.Companion.LIKED_BY
import com.dldmswo1209.tinder.DBKey.Companion.MATCH
import com.dldmswo1209.tinder.DBKey.Companion.NAME
import com.dldmswo1209.tinder.DBKey.Companion.USERS
import com.dldmswo1209.tinder.DBKey.Companion.USER_ID
import com.dldmswo1209.tinder.databinding.ActivityLikeBinding
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import com.yuyakaido.android.cardstackview.CardStackLayoutManager
import com.yuyakaido.android.cardstackview.CardStackListener
import com.yuyakaido.android.cardstackview.Direction
import java.util.jar.Attributes
class LikeActivity : AppCompatActivity(), CardStackListener {
var mBinding : ActivityLikeBinding? = null
val binding get() = mBinding!!
private var auth : FirebaseAuth = FirebaseAuth.getInstance()
private lateinit var userDB: DatabaseReference
private val adapter = CardItemAdapter()
private val cardItems = mutableListOf<CardItem>()
private val manager by lazy {
CardStackLayoutManager(this,this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityLikeBinding.inflate(layoutInflater)
setContentView(binding.root)
userDB = Firebase.database.reference.child(USERS)
val currentUserDB = userDB.child(auth.currentUser!!.uid)
currentUserDB.addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
// 처음에는 데이터가 존재하면 onDataChange 에 들어오게 된다
// 데이터가 수정 되었을 때도 들어온다.
if(snapshot.child(NAME).value == null){ // name 정보가 없으면
showNameInputPopup() // 팝업을 띄움(name 을 설정)
return
}
// 유저정보를 갱신
getUnSelectedUsers()
}
override fun onCancelled(error: DatabaseError) {}
})
initCardStackView()
initSignOutButton()
initMatchedListButton()
}
private fun initCardStackView(){
binding.cardStackView.layoutManager = manager
binding.cardStackView.adapter = adapter
}
private fun initSignOutButton(){ // 로그아웃 버튼
binding.signOutButton.setOnClickListener {
auth.signOut()
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
private fun initMatchedListButton(){ // 매치 리스트 버튼
binding.matchListButton.setOnClickListener {
startActivity(Intent(this, MatchedUserActivity::class.java))
}
}
private fun getUnSelectedUsers(){
// 싫어요나 좋아요를 하지 않은 유저 리스트를 가져와서 cardItems 에 추가하고 adapter 에 데이터가 변경되었다고 notify 를 해줌
userDB.addChildEventListener(object: ChildEventListener {
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
if(snapshot.child(USER_ID).value != getCurrentUserID()
&& !snapshot.child(LIKED_BY).child(LIKE).hasChild(getCurrentUserID())
&& !snapshot.child(LIKED_BY).child(DIS_LIKE).hasChild(getCurrentUserID())) {
// 현재 snapshot 정보가 내가 아니고(내 카드는 보일 필요가 없음), 좋아요나 싫어요를 누르지 않은 경우
val userId = snapshot.child(USER_ID).value.toString()
var name = "undecided"
if(snapshot.child(NAME).value != null){
name = snapshot.child(NAME).value.toString()
}
cardItems.add(CardItem(userId, name))
adapter.submitList(cardItems)
adapter.notifyDataSetChanged()
}
}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
cardItems.find{ it.userId == snapshot.key }?.let{
it.name = snapshot.child(NAME).value.toString()
}
adapter.submitList(cardItems)
adapter.notifyDataSetChanged()
}
override fun onChildRemoved(snapshot: DataSnapshot) {}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onCancelled(error: DatabaseError) {}
})
}
private fun showNameInputPopup(){
// 로그인 이후에 이름 정보를 저장하기 위해 팝업을 띄워주는 메소드
val editText = EditText(this) // EditText 생성
AlertDialog.Builder(this) // AlertDialog 생성
.setTitle(getString(R.string.write_name)) // title 설정
.setView(editText) // View 설정
.setPositiveButton("저장"){_,_ -> // 저장 버튼
if(editText.text.isEmpty()){ // editText 가 비어있으면
showNameInputPopup() // 다시 팝업을 띄움
}else{
saveUserName(editText.text.toString()) // 이름 저장 메소드 호출
}
}
.setCancelable(false) // 뒤로가기 비활성화
.show()
}
private fun saveUserName(name: String){
// 이름 정보를 DB에 저장하는 메소드
val userId = getCurrentUserID() // 현재 유저의 uid 불러오기
val currentUserDB = userDB.child(userId)
val user = mutableMapOf<String, Any>()
user[USER_ID] = userId
user[NAME] = name
currentUserDB.updateChildren(user) // 이름 정보가 추가된 user 를 DB에 업데이트 -> Realtime Database 에 추가
// 유저 정보를 가져옴
getUnSelectedUsers()
}
private fun getCurrentUserID(): String{
// 현재 유저의 uid 를 리턴
if(auth.currentUser == null){
Toast.makeText(this, "로그인이 되어있지 않습니다.", Toast.LENGTH_SHORT).show()
finish()
}
return auth.currentUser?.uid.orEmpty()
}
private fun like(){
// 좋아요
val card = cardItems[manager.topPosition-1] // 좋아요 한 카드를 가져옴
cardItems.removeFirst() // 리스트에서 삭제
// DB -> 좋아요를 받은 사람의 uid -> likedBy -> like -> 좋아요를 누른 사람의 uid -> true
userDB.child(card.userId)
.child(LIKED_BY)
.child(LIKE)
.child(getCurrentUserID())
.setValue(true)
Toast.makeText(this, "${card.name}님을 Like 하셨습니다.", Toast.LENGTH_SHORT).show()
// 매칭이 되었으면(서로 좋아요를 누른 상태) DB에 매치 유무를 저장
saveMatchIfOtherUserLikedMe(card.userId)
}
private fun disLike() {
// 싫어요
val card = cardItems[manager.topPosition-1]
cardItems.removeFirst()
userDB.child(card.userId)
.child(LIKED_BY)
.child(DIS_LIKE)
.child(getCurrentUserID())
.setValue(true)
Toast.makeText(this, "${card.name}님을 disLike 하셨습니다.", Toast.LENGTH_SHORT).show()
}
private fun saveMatchIfOtherUserLikedMe(otherUserId: String) {
// 서로 매치가 되었는지 확인하고 매치가 되었으면 DB에 match 유무를 저장
val otherUserDB = userDB.child(getCurrentUserID()).child(LIKED_BY).child(LIKE).child(otherUserId)
otherUserDB.addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
if(snapshot.value == true){
userDB.child(getCurrentUserID())
.child(LIKED_BY)
.child(MATCH)
.child(otherUserId)
.setValue(true)
userDB.child(otherUserId)
.child(LIKED_BY)
.child(MATCH)
.child(getCurrentUserID())
.setValue(true)
}
}
override fun onCancelled(error: DatabaseError) {}
})
}
override fun onCardDragging(direction: Direction?, ratio: Float) {}
override fun onCardSwiped(direction: Direction?) {
// 카드 스와이프 이벤트
when(direction){
Direction.Right-> like() // 오른쪽은 좋아요
Direction.Left -> disLike() // 왼쪽은 싫어요
else->{
}
}
}
override fun onCardRewound() {}
override fun onCardCanceled() {}
override fun onCardAppeared(view: View?, position: Int) {}
override fun onCardDisappeared(view: View?, position: Int) {}
}
userDB.addChildEventListener() : Realtime Database에 저장되어 있는 정보를 가져온다(이벤트가 발생할 때마다 정보를 가져옴)
userDB.addListenerForSingleValueEvent() : 즉시성 1회만 호출
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import com.dldmswo1209.tinder.databinding.ActivityMatchedUserBinding
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
class MatchedUserActivity : AppCompatActivity() {
var mBinding : ActivityMatchedUserBinding? = null
val binding get() = mBinding!!
private var auth : FirebaseAuth = FirebaseAuth.getInstance()
private lateinit var userDB: DatabaseReference
private val adapter = MatchedUserAdapter()
private val cardItems = mutableListOf<CardItem>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMatchedUserBinding.inflate(layoutInflater)
setContentView(binding.root)
auth = Firebase.auth
userDB = Firebase.database.reference.child("Users")
initMatchedUserRecyclerView()
getMatchUsers()
}
private fun initMatchedUserRecyclerView(){
binding.matchedUserRecyclerView.layoutManager = LinearLayoutManager(this)
binding.matchedUserRecyclerView.adapter = adapter
}
private fun getMatchUsers(){
val matchedDB = userDB.child(getCurrentUserID()).child("likedBy").child("match")
matchedDB.addChildEventListener(object: ChildEventListener{
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
if(snapshot.key!!.isNotEmpty()){
getUserByKey(snapshot.key.orEmpty())
}
}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onChildRemoved(snapshot: DataSnapshot) {}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onCancelled(error: DatabaseError) {}
})
}
private fun getUserByKey(userId: String){
userDB.child(userId).addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
cardItems.add(CardItem(userId, snapshot.child("name").value.toString()))
adapter.submitList(cardItems)
}
override fun onCancelled(error: DatabaseError) {
}
})
}
private fun getCurrentUserID(): String{
if(auth.currentUser == null){
Toast.makeText(this, "로그인이 되어있지 않습니다.", Toast.LENGTH_SHORT).show()
finish()
}
return auth.currentUser?.uid.orEmpty()
}
}
❗️ 해당 프로젝트는 FastCampus의 "30개 프로젝트로 배우는 Android 앱 개발 with Kotlin 초격차 패키지 Online" 강의를 수강하면서 만든 프로젝트 입니다.