틴더

LeeEunJae·2022년 7월 21일
2

Kotlin Project

목록 보기
3/10

📌 실행화면

📌 요구 사항

  • Firebase Authenetication 사용하기
    • Email Login
    • Facebook Login
  • Firebase Realtime Database 사용하기
  • yuyakaido/CardStackView 사용하기

📌 틴더

  • Firebase Authentication 을 통해 이메일 로그인과 페이스북 로그인을 할 수 있음
  • Firebase Realtime Database 를 이용하여 기록을 저장하고, 불러올 수 있음
  • GitHub에서 Opensource Library를 찾아 사용할 수 있음

✅ Facebook 로그인 & Realtime DB 사용법

https://velog.io/@dldmswo1209/Firebase-Facebook-로그인-구현-RealTime-DB-사용하기

✅ CardStackView 사용하기(yuyakaido)

https://github.com/yuyakaido/CardStackView#installation
깃허브 yuyakaido 라는 사람이 만든 CardStackView를 사용 했다.
그런데 implementation 하는 과정에서 아래와 같은 오류가 발생했다.

✅ 해결 방법

1시간 동안 삽질해가며 해결 방법을 찾았지만, 해결 방법은 의외로 간단했다.
setting.graddle 에 jcenter()를 추가하면 된다.

dependencyResolutionManagement {
  repositories {
      ...
      jcenter()
  }
}

📌 ListAdapter & DiffUtil 개념

참고자료 및 출처

https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter

https://velog.io/@l2hyunwoo/Android-RecyclerView-DiffUtil-ListAdapter

요약 하자면 기존에 사용하던 RecyclerView의 notifyDataSetChanged()는 데이터가 변경 될 때 모든 데이터가 통째로 업데이트 되면서 지연시간이 발생하는 이슈가 있다.
DiffUtill 은 이러한 문제를 해결하기 위해서 현재 리스트와 교체 해야할 리스트를 비교하여 실제로 바꿔야 하는 데이터만 업데이트 해서 훨씬 빠른 속도로 업데이트가 가능하다!

📌 CardItem.kt(data class)

리스트에 CardItem객체 형태로 저장

data class CardItem(
    val userId: String,
    var name: String
)

📌 CardItemAdapter.kt

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
            }

        }
    }
}

📌 MatchedUserAdapter.kt

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
            }

        }
    }
}

📌 LikeActivity.kt

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회만 호출

📌 MatchedUserActivity.kt

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" 강의를 수강하면서 만든 프로젝트 입니다.

profile
매일 조금씩이라도 성장하자

0개의 댓글