티스토리 뷰

1. 안드로이드 틴더 앱 만들어보기

2012년 개발된 틴더라는 어플을 모티브로 하여 간단한 소셜 디스커버리 앱을 만들어 보도록 하겠습니다. 틴더는 2014년 부터 하루 당 10억건의 스와이프를 기록했다고 하는데 그정도 까진 아니어도 기본 동작이 실시간 데이터베이스를 바탕으로 가능하도록 만들어 봅시다.

메일로 회원가입이 가능하도록 firebase 인증 기능과 페이스북 로그인 기능까지 추가해보고 이를 통해 가입된 회원들 간에 좋아요와 싫어요를 바탕으로 서로 좋아요를 누른 회원들 끼리 매칭시켜 주는 어플리케이션을 최종 목표로 간단하게 진행해보았습니다.

주요 기능

  • 이메일 or Facebook 회원 가입 기능
  • 가입된 회원 간에 좋아요 싫어요
  • 서로 좋아요한 회원 끼리 매칭 확인
  • 카드 스택 뷰를 기반으로 한 스와이핑

사용 기술

  • Firebase Authenetication (email, facebook login)
    • facebook developer
  • Firebase Realtime Database (실시간 데이터베이스)
  • yuyakaido/CardStackView
  • recyclerView

결과 화면

1.1. 기본 레이아웃 구성

메인 엑티비티에 처음 입장하면 로그인 부터 시키게 되는데, firebase Authenetication 을 사용해서 로그인을 통과해야 스와이핑 엑티비티로 넘어가게 됩니다. 대략 적인구성은 위 파일 트리 구조를 보시면 되겠습니다.

activity_like 는 카드 스택 뷰를 가지며 기본 리사클러뷰를 상속하는 이 뷰는 CardItem 이라는 모델 클래스, CardItemAdpter 어댑터 그리고 item_card 라는 아이템을 가지도록 해서 사용하도록 구성되어 있습니다.

매치된 유저의 경우에는 기본적인 리사클러뷰를 사용하여 이전 프로젝트와 동일한 방식으로 구현하여 사용하였습니다.

2. Firebase Authenetication

파이어 베이스 프로젝트를 하나 생성해주시고 그중 인증 서비스를 활성화 시켜줍니다. 실제로 로그인 진행시 식별자로 메일과 제공업체, 사용자 UID를 받아온 것을 아래 화면에서처럼 확인이 가능합니다.

로그인 제공업체는 기본적으로 이메일/비밀번호 형식을 사용하지만 제공하는 업체가 구글, 페이스북, 트위터, 깃허브 등과 같이 다양하기 때문에 이 중에서 간단히 페이스북 로그인 기능까지 추가하여  사용해보도록 하겠습니다.

2.1. developers.facebook

페이스북 로그인 같은 경우에는 firebase 계정 외에 페이스북 계정이 따로 필요하며 일반적인 페이스북 홈페이지가 아닌 개발자 전용 홈페이지로 접속해서 프로젝트(앱)를 추가한뒤 빠른시작에서 제시하고 있는 절차를 따라주면 간단하게 추가할 수 있습니다.

SDK를 직접 다운로드 하여 추가해주어도 되지만 저는 gradle 에 추가해서 원격으로 가져오는 방식을 사용했습니다. 인증을 위해 추가한 라이브러리 종속성은 아래와 같습니다. (앱 수준 gradle 입니다.)

    implementation platform('com.google.firebase:firebase-bom:28.1.0')
    implementation 'com.google.firebase:firebase-analytics-ktx'
    implementation 'com.google.firebase:firebase-auth-ktx'
    implementation 'com.google.firebase:firebase-database-ktx'

    implementation 'com.facebook.android:facebook-login:8.2.0'

다음으로 프로젝트 수준 gradle 입니다. mavenCentral() 를 추가해주세요.

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    ...

2.2. 페이스북 로그인 버튼 사용하기

이제 페이스북 로그인 버튼을 추가해서 사용할 수 있습니다. (파이어 베이스와 페이스북 디벨로퍼 설정을 마치고, 라이브러리 종속성 옳바르게 추가 후 sync 까지 하셔야 합니다.)

 <com.facebook.login.widget.LoginButton
        android:id="@+id/facebookLoginButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/loginButton" />

2.3. 페이스북 로그인 기능 사용 코드 구현

로그인 엑티비티의 onCreate 함수 부분에 버튼의 기능 및 기본 초기화를 진행하도록 해줍니다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        auth = Firebase.auth // 코틀린 스러운 가져오기;

        callbackManager = CallbackManager.Factory.create() // 콜백 매니저 초기화;

        initLoginButton()

        initSignupButton()

        initEmailAndPasswordEditText()

        initFacebookLoginButton()

    }

initFacebookLoginButotn() 함수에서 페이스북 로그인 버튼에 대한 초기화를 진행합니다. 아래 코드와 같이 setPermissions 로 가져올 정보를 설정하고 registerCallback 을 통해 콜백 함수를 설정해줍니다. 콜백 매니저의 경우에는 onCreate 에서 초기화 해주었습니다.

    private fun initFacebookLoginButton() {

        val facebookLoginButton = findViewById<LoginButton>(R.id.facebookLoginButton)

        // 가져올 정보;
        facebookLoginButton.setPermissions("email", "public_profile")
        facebookLoginButton.registerCallback(
            callbackManager,
            object : FacebookCallback<LoginResult> {
                override fun onSuccess(result: LoginResult) {
                    // 로그인 성공;

                    // 로그인 결과에서 엑세스 토큰을 가져옴
                    val credential = FacebookAuthProvider.getCredential(result.accessToken.token)

                    // 페이스북 로그인 엑세스 토큰을 넘겨주어 로그
                    auth.signInWithCredential(credential)
                        .addOnCompleteListener(this@LoginActivity) { task ->
                            if (task.isSuccessful) {
                                handleSuccessLogin() //finish()
                            } else {
                                Toast.makeText(
                                    this@LoginActivity,
                                    "페이스북 로그인이 실패했습니다.",
                                    Toast.LENGTH_SHORT
                                )
                                    .show()
                            }
                        }
                }

                override fun onCancel() {
                    // 로그인 취소;
                }

                override fun onError(error: FacebookException?) {
                    Toast.makeText(this@LoginActivity, "페이스북 로그인이 실패했습니다.", Toast.LENGTH_SHORT)
                        .show()
                }

            })
    }

로그인에 성공했다면, 엑세스 토큰을 가져와서 넘겨 로그인을 해주며 성공한 경우 따로 처리해줍니다. 실패한 경우나 에러가 발생한 경우 해당 프로젝트에서는 따로 처리하지는 않으며 간단하게 토스트 메세지를 띄워서 나타내도록 해주었습니다. auth 변수는 파이어베이스 인증 관련 변수입니다. (프로젝트 app 하위 구글-서비스.json 파일을 추가해야 정상 사용 가능)

    private fun handleSuccessLogin() {
        if (auth.currentUser == null) {
            Toast.makeText(this, "Login fail.", Toast.LENGTH_SHORT)
            return
        }

        val userId = auth.currentUser?.uid.orEmpty()

        val currentUserDB = Firebase.database.reference.child("Users").child(userId)
        val user = mutableMapOf<String, Any>()
        user["userId"] = userId
        currentUserDB.updateChildren(user)

        finish()
    }

로그인에 성공한 경우에는 이메일로 로그인 한 경우도 동일할거라 따로 handleSuccessLogin 함수로 빼서 처리해줍니다. 해당 유저의 유저아이디를 가져와서 데이터 베이스에 키(유저 아이디)와 벨류(유저 아이디) 형식으로 DB를 업데이트 해주도록 하고 로그인 엑티비티를 finish() 합니다.

3. 이메일 및 비밀번호로 회원가입하기

이메일 및 비밀번호 텍스트뷰의 내용을 바탕으로 회원가입을 진행합니다. 각 텍스트를 스트링 형식으로 가져오도록 하는 기능은 함수로 따로 빼주었고 auth.createUserWithEmailAndPassword 함수를 사용하여 회원 가입을 진행할 수 있습니다.

여기에 완전한 이벤트 리스너를 달아서 회원 가입에 성공한 경우와 실패한 경우에 대해 간단한 토스트 메세지를 띄워주도록 했습니다.

여기서 당황했던 점은 회원가입이 아무리 해도 안됬던 상황이었는데 디버깅해서 잡아보니 비밀번호가 최소 6글자 이어야 하더군요. 계속 111이런식으로 비밀번호를 짧게 주어 회원가입이 안되었습니다. 아무래도 보안상 최소 6글자를 사용하도록 정해놓은 것 같아요.

    private fun initSignupButton() {
        val signUpButton = findViewById<Button>(R.id.signupButton)
        signUpButton.setOnClickListener {
            val email = getInputEmail()
            val password = getInputPassword()

            auth.createUserWithEmailAndPassword(email, password)
                .addOnCompleteListener(this) { task ->
                    if (task.isSuccessful) {
                        Toast.makeText(
                            this,
                            "회원가입에 성공했습니다. 로그인 버튼을 눌러 로그인 해주세요.",
                            Toast.LENGTH_SHORT
                        )
                            .show()
                    } else {
                        // 2021-06-12 18:26:30.011 19535-19535/com.lilcode.aop.p3.c05.tinder D/debug: com.google.firebase.auth.FirebaseAuthWeakPasswordException: The given password is invalid. [ Password should be at least 6 characters ]
                        Log.d("debug", task.exception.toString())
                        Toast.makeText(this, "이미 가입된 이메일 이거나 회원가입에 실패했습니다.", Toast.LENGTH_SHORT)
                            .show()
                    }
                }

        }
    }

아무튼 회원 가입에 성공하면 파이어베이스 인증 페이지 내부에서 아래와 같이 가입된 이메일과 제공업체, 생성한 날짜, 로그인한 날짜, 사용자 UID 등을 확인할 수 있습니다. 아래 처럼 가입 내역이 추가되어야 제대로 회원가입이 된 것입니다.

 

3.1. 이메일 + 비밀번호로 로그인 하기

로그인 버튼입니다.  마찬가지로 각각 값을 받아서 signInWithEmailAndPassword 로 로그인할 수 있고 여기도 마찬가지로 완전 리스너를 달아서 성공한 경우에는 로그인 성공처리를 해주고(finish 처리 등) 실패시에 토스트 메세지를 띄워주도록 해주었습니다.

    private fun initLoginButton() {
        val loginButton = findViewById<Button>(R.id.loginButton)
        loginButton.setOnClickListener {

            val email = getInputEmail()
            val password = getInputPassword()

            auth.signInWithEmailAndPassword(email, password)
                .addOnCompleteListener(this) { task ->
                    if (task.isSuccessful) { // 로그인 성공;

                        handleSuccessLogin() //finish() // 엑티비티 종료;
                    } else {
                        Toast.makeText(
                            this,
                            "로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.",
                            Toast.LENGTH_SHORT
                        )
                            .show()

                    }
                }

        }
    }

3.2. 예외 처리

메일 주소 또는 비밀번호가 공란으로 되어있는 경우에는 로그인과 회원가입이 진행되어서는 안됩니다. 이경우에 로그인 버튼과 회원가입 버튼을 비활성화 처리해주어 예외처리를 해주었습니다.

    private fun initEmailAndPasswordEditText() {
        val emailEditText = findViewById<EditText>(R.id.emailEditText)
        val passwordEditText = findViewById<EditText>(R.id.passwordEditText)
        val loginButton = findViewById<Button>(R.id.loginButton)
        val signUpButton = findViewById<Button>(R.id.signupButton)

        // 비어있는 경우 처리;
        emailEditText.addTextChangedListener {
            // 입력 될 때마다 체크
            val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
            loginButton.isEnabled = enable
            signUpButton.isEnabled = enable
        }

        passwordEditText.addTextChangedListener {
            val enable = emailEditText.text.isNotEmpty() && passwordEditText.text.isNotEmpty()
            loginButton.isEnabled = enable
            signUpButton.isEnabled = enable
        }
    }

4. MainActivity

앱 처음 실행 시 로그인 여부를 확인하여 로그인 되어있지 않다면 로그인 화면을 띄워줍니다. 로그인 화면에서 뒤로가기 한 경우에도 메인 엑티비티의 onStart() 가 실행될 것이기 때문에 다시 로그인 화면이 뜨게됩니다.

이후 로그인이 되었다면 좋아요 싫어요 가능한 엑티비티로 전환해주도록 합니다.

class MainActivity : AppCompatActivity() {

    private var auth: FirebaseAuth = FirebaseAuth.getInstance() // 이렇게 가져오는 방법도 있음;

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


    }

    override fun onStart() {
        super.onStart()

        if (auth.currentUser == null) {
            startActivity(Intent(this, LoginActivity::class.java))
        } else {
            startActivity(Intent(this, LikeActivity::class.java))
            finish()
        }
    }
}

4.1. LikeActivity

카드 스택 뷰를 사용하여 좋아요 싫어요 스와이프를 주로 하는 엑티비티입니다. 초기에 유저 DB 를 가져와서 이름 항목이 없는 경우라면 이름을 입력 받도록 경고 팝업을 하나 띄우도록 해당 값에 대한 싱글 이벤트 리스너를 달아주도록 합니다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_like)

        userDB = Firebase.database.reference.child(USERS)

        val currentUserDB = userDB.child(getCurrentLoginedUserId())

        currentUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                if (snapshot.child(NAME).value == null) {
                    showNameInputPopup()
                    return
                }

                // 유저 정보 갱신;
                getUnSelectedUsers()
            }

            override fun onCancelled(error: DatabaseError) {

            }

        })

        initCardStacView()
        initSignOutButton()
        initMatchedListButton()
    }

4.2. AlertDialog 로 이름 입력 받기 (AlertDialog.Builder)

경고 대화상자에 EditText 를 하나 추가해서 이름을 받아오도록 하고 빈 텍스트를 입력한 경우면 계속해서 팝업을 띄우도록 해주며 이름이 입력된 경우에는 원격 데이터 베이스에 저장하여 위에서 달아 놓은 싱글 데이터 리스너가 발동하도록 해주면 이후 유저 정보를 갱신하여 가져오게 되겠죠.

setCancelable(false) 를 설정하여 취소 (뒤로가기)가 불가능하도록 설정해주고 마지막으로 show()로 대화상자를 띄워주시면 되겠습니다.

    private fun showNameInputPopup() {

        val editText = EditText(this)

        AlertDialog.Builder(this)
            .setTitle(getString(R.string.write_name))
            .setView(editText)
            .setPositiveButton(getString(R.string.save)) { _, _ ->
                if (editText.text.isEmpty()) { // 빈 텐스트면 다시 띄우기 (무한 띄우기)
                    showNameInputPopup()
                } else {
                    // 입력 되었다면 원격 db에 저장;
                    saveUserName(editText.text.toString())
                }
            }
            .setCancelable(false) // 취소 불가;
            .show()
    }

4.3. getUnSelectedUsers()

이름 까지 입력이 되었다면, 다음으로는 내가 좋아요나 싫어요 표시를 하지 않은 유저를 가져오도록 합니다. addChildEventListener 를 하나 달아서 새로운 유저가 추가되는 경우 변경되는 경우 각각을 처리해주도록 하면됩니다.

onChildAdded 에서 조건으로 추가된 유저가 현재 로그인한 본인이 아니고 상대방의 좋아요 목록에 내가 없다면 내가 아직 선택하지 않은 것이기 때문에(좋아요 싫어요 한 경우 상대방의 likedBy에 내 아이디가 남도록 구현했기 때문) 해당 유저를 띄우면 되는 것이다.

아무런 호감 표시를 하지 않는 사람은 cardItems 에 추가해서 어뎁터에 submitList 한 뒤 notifyDataSetChanged() 를 통해 변경되었음을 알려 업데이트한다.

    private fun getUnSelectedUsers() {

        userDB.addChildEventListener(object: ChildEventListener{
            override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
                if (snapshot.child(USER_ID).value != getCurrentLoginedUserId() // 현재 유저가 내가 아니고;
                    && snapshot.child(LIKED_BY).child(LIKE).hasChild(getCurrentLoginedUserId()).not() // 상대 방의 likedBy에 Like에 내가 없고;
                    && snapshot.child(LIKED_BY).child(DIS_LIKE).hasChild(getCurrentLoginedUserId()).not()) // 상대 방의 likeBy에 disLike에 내가 없다;
                        // 즉 내가 선택한 적이 한번도 없는 유저
                {
                    val userId = snapshot.child(USER_ID).value.toString()
                    var name = getString(R.string.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) {
            }

        })
    }

다음으로 onChildChanged 에서 데이터가 변경되었을 때 해당 아이디를 현재 cardItems 에서 일치하는 데이터를 찾고 이름을 갱신해주도록 한다. (최초 이름 설정 시) 그리고 카드 스택 뷰를 업데이트 해주도록 마찬가지로 작성해준다. (이 부분은 중복되니 함수로 따로 빼도 되겠네요.)

4.4. 기타

유저 이름 저장하기

현재 유저 아이디에 대한 데이터베이스를 가져와서 유저 아이디와 이름을 키-값 형식으로 받아서 업데이터 해주는 코드입니다.

    private fun saveUserName(name: String) {
        val userId = getCurrentLoginedUserId()

        val currentUserDB = userDB.child(userId)
        val user = mutableMapOf<String, Any>()
        user["userId"] = userId
        user["name"] = name
        currentUserDB.updateChildren(user)

    }

현재 로그인된 유저 아이디 가져오기

로그인이 되었는지 확인하고 로그인 되어있다면 현재 유저의 uid 를 가져와 반환해줍니다.

    private fun getCurrentLoginedUserId(): String {
        if (auth.currentUser == null) {
            Toast.makeText(this, getString(R.string.not_logined), Toast.LENGTH_SHORT)
                .show()
            finish()
        }
        return auth.currentUser?.uid.orEmpty()
    }

5. yuyakaido/CardStackView 사용 하기

앱 구현 시 모든 기능을 직접 구현하면 좋긴 하지만 그에 따른 많은 비용과 시간이 소모될 수 있습니다. 웬만한 것들은 보통 공개 라이브러리에서 찾을 수 있고 무료로 사용이 가능합니다. 물론 발견되지 않은 버그가 있을 수 있지만 바로 사용가능하다는 점이 큰 장점입니다.

매칭앱을 구현하면서 쌓인 카드를 넘겨버리는 형식을 사용하려 했고 이를 검색해보니 우리가 원하던 기능을 제공하는 yuyakaido/CardStackView 라는 라이브러리를 발견하여 사용해보도록 했습니다.

먼저, 최신 버전의 card statck view 를 앱 수준 그레이들에 추가하여 sync 해줍니다.

    implementation "com.yuyakaido.android:card-stack-view:2.3.4"

이후 사용할 레이아웃에 카드 스택 뷰를 추가해줍니다.

<com.yuyakaido.android.cardstackview.CardStackView
        android:id="@+id/cardStackView"
        android:layout_width="0dp"
        android:layout_height="300dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

이후 findViewById 로 해당 뷰를 가져와 레이아웃 매니저 및 어댑터를 설정해주면 됩니다.

private val adapter = CardItemAdapter()

private val manager by lazy {
        CardStackLayoutManager(this, this)
    }

...

private fun initCardStacView() {
        val cardStackView = findViewById<CardStackView>(R.id.cardStackView)
        cardStackView.layoutManager = manager // 여기서 초기화;
        cardStackView.adapter = adapter //CardStackAdapter()
    }

5.1. 어댑터 구현하기

어뎁터의 경우 리사이클러뷰에서 사용했던 것과 같이 구현해주면 되겠습니다. 먼저 데이터 클래스를 하나 정의해주고 해당 아이템을 바탕으로 뷰홀더에서 정의해준 아이템 레이아웃에 바인딩해서 보여주도록 하였습니다.

data class CardItem (
    val userId: String,
    var name: String
)
class CardItemAdapter: 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
            }
        }
    }
}

5.2. item xml

아이템의 경우 간단하게 카드뷰 내부에 텍스트뷰를 넣어 해당 텍스트뷰에 유저 이름이 표시되도록 했습니다.

5.3. CardStackListener

카드 스택 이벤트 리스너의 경우에는 LikeActivity 에 상속하여 구현 해주도록 하여 이를 통해 메니저를 초기화 할 때 리스너 부분에 this 를 전달하여 리스너를 설정해줄 수 있었습니다.

class LikeActivity : AppCompatActivity(), CardStackListener {
...

	    private val manager by lazy {
        CardStackLayoutManager(this, this)
    }
}

그리고 내부에 아래와 같이 인터페이스를 구현하여 필요한 이벤트를 처리해주도록 했습니다. 해당 프로젝트에서는 카드가 스와이프 될 때만 처리해서 사용해주도록 하였고 나머지는 미구현 상태로 두었습니다.

    // <CardStackListener>
    override fun onCardDragging(direction: Direction?, ratio: Float) {

    }

    override fun onCardSwiped(direction: Direction?) {
        when(direction){
            Direction.Right ->{
                like()
            }
            Direction.Left ->{
                disLike()
            }
            else -> Unit
        }
    }

    override fun onCardRewound() {
    }

    override fun onCardCanceled() {
    }

    override fun onCardAppeared(view: View?, position: Int) {
    }

    override fun onCardDisappeared(view: View?, position: Int) {
    }
    // </CardStackListener>

카드가 스와이프된 경우는 방향을 얻을 수 있는데 상하좌우 중 어디로 스와이프 되었는 지를 받아와 처리해줄 수 있습니다. 좌, 우에 대해서만 처리할 것이기 때문에 좌, 우 에 대해서만 when 문으로 처리해주었습니다.

다음으로 각각 좋아요 싫어요를 했을 때 처리를 해주겠습니다. 먼저 카드 아이템 중에서 가장 위에 있는 것을 가져와 지우는 것으로 처리해주고 좋아요 싫어요한 부분에 대해서 상대의 아이디에 해당하는 디비 하위에 저장하도록 해줍니다.

이때 좋아요 한 경우에는 서로 좋아요한 경우에는 매칭된 것이기 때문에 따로 매칭 처리가 필요한데 한 사람이 좋아요 한 경우는 다른 사람이 아직 좋아요 하지 않았을 수 있기 때문에 둘 다 좋아요한 시점에 매칭된 것을 처리해주도록 해야합니다.

    private fun like(){
        val card = cardItems[manager.topPosition - 1]
        cardItems.removeFirst() // 뷰 데이터를 실제로 지울 것임;

        // 상대방의 likedBy에 저장;
        userDB.child(card.userId)
            .child(LIKED_BY)
            .child(LIKE)
            .child(getCurrentLoginedUserId())
            .setValue(true)

        // 매칭이 된 시점을 봐야한다.
        saveMatchIfOtherUserLikedMe(card.userId)


        Toast.makeText(this, "${card.name}님을 Like 하셨습니다.", Toast.LENGTH_SHORT)
            .show()
    }

    private fun disLike(){
        val card = cardItems[manager.topPosition - 1]
        cardItems.removeFirst() // 뷰 데이터를 실제로 지울 것임;

        // 상대방의 likedBy에 저장;
        userDB.child(card.userId)
            .child(LIKED_BY)
            .child(DIS_LIKE)
            .child(getCurrentLoginedUserId())
            .setValue(true)

        Toast.makeText(this, "${card.name}님을 disLike 하셨습니다.", Toast.LENGTH_SHORT)
            .show()
    }

매치 처리 해주기

나에 아이디에 해당하는 데이터를 가져와서 현재 상대방 또한 나를 좋아요 한 경우에 대해서 이벤트 처리기를 하나 달아주어 상대방 또한 좋아요를 한경우에 서로 매치된 데이터를 true 로 저장해서 누구와 매치되었는지 확인할 수 있도록 합니다.

    private fun saveMatchIfOtherUserLikedMe(otherUserId: String){
        val otherUserDB = userDB.child(getCurrentLoginedUserId()).child(LIKED_BY)
            .child(LIKE).child(otherUserId) // 상대방이 나를 좋아요 할 때 변경될 디비값.

        // 해당 값에 대한 이벤트 처리기.
        otherUserDB.addListenerForSingleValueEvent(object: ValueEventListener{
            override fun onDataChange(snapshot: DataSnapshot) {
                if (snapshot.value == true){ // 상대방도 나를 좋아요한 경우.
                    userDB.child(getCurrentLoginedUserId())
                        .child(LIKED_BY)
                        .child(MATCH)
                        .child(otherUserId)
                        .setValue(true)

                    userDB.child(otherUserId)
                        .child(LIKED_BY)
                        .child(MATCH)
                        .child(getCurrentLoginedUserId())
                        .setValue(true)
                }
            }
            override fun onCancelled(error: DatabaseError) {
            }
        })
    }

이후 match 에 대한 키가 존재하면 이를 참고하여 매치된 유저를 표시해주도록 해주면 되겠습니다. 매치 레이아웃의 경우 리사이클러뷰를 사용해 간단히 유저 네임 텍스트를 보여주는 형식으로 구현해주었고 이는 이미 다뤄본 적이 있어 글에서 생략하도록 하겠습니다.

해당 프로젝트의 전체 코드는 저의 깃허브 저장소에서 확인할 수 있습니다.

2021.06.10 - [Android/App] - [Android] 도서 검색기 + 리뷰 기능 어플 만들기 (OpenAPI, Retrofit)

2021.06.08 - [Android/App] - [Android] 알람 어플 구현 (Kotlin, AlarmManager, Broadcast)

2021.06.07 - [Android/App] - [Android] 오늘의 명언 어플 만들기 (Firebase remote config)

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함