틴더 앱 (Firebase, Swipe Animation 라이브러리)
Firebase Authentication 을 사용해 이메일로 회원가입하고 로그인할 수 있고, 가입된 회원들 간에 좋아요와 싫어요를 바탕으로 서로 좋아요를 누른 회원들끼리 매칭시켜주도록 구현하였다.
주요 기능
- Firebase Authentication 을 통해 이메일 회원가입 / 로그인
- Firebase Realtime Database 를 이용해 기록을 저장하고, 불러오기
- 가입된 유저 간 like / dislike 기능
- 서로 like를 한 유저끼리 매칭
- 오픈소스 라이브러리 CardStackView 를 이용해 스와이프 기능 추가
사용 기술
- Firebase Authentication
- Firebase Realtime Database
- yuyakaido/CardStackView
- RecyclerView
Firebase 환경설정
프로젝트 추가 > 프로젝트 이름 지정 > 프로젝트 마들기 > 안드로이드 앱 버튼 > 패키지 이름 > 앱 등록 > SDK 추가
설정이 끝나면 콘솔로 이동해주고 Authentication 과 Realtime Database 를 '시작하기' 눌러준다.
Realtime Database 는 데이터베이스 만들기 > 실시간 데이터베이스 위치 : 미국 > 보안규칙 : 테스트모드 > 사용설정
사용해줄 Authentication 과 Realtime Database 를 설정해줬으면 google·service.json 을 다운받아 프로젝트 수준의 app 폴더에 담아준다.
firebase 에서 사용할 authentication 과 realtime database 을 추가해준다.
implementation platform('com.google.firebase:firebase-bom:29.0.3')
implementation 'com.google.firebase:firebase-auth-ktx'
implementation 'com.google.firebase:firebase-database-ktx'
Firebase Authentication 이용해 이메일 로그인
공식 문서 https://firebase.google.com/docs/auth/android/password-auth?hl=ko#kotlin+ktx_2
layout/activity_login.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="24dp">
<EditText
android:id="@+id/emailEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/passwordEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/emailEditText" />
<Button
android:id="@+id/loginButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />
<Button
android:id="@+id/signUpButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/signup"
app:layout_constraintEnd_toStartOf="@id/loginButton"
app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />
</androidx.constraintlayout.widget.ConstraintLayout>
LoginActivity.kt
package com.example.tinder
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
class LoginActivity : AppCompatActivity() {
private lateinit var auth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
auth = Firebase.auth
val emailEditText = findViewById<EditText>(R.id.emailEditText)
val passwordEditText = findViewById<EditText>(R.id.passwordEditText)
initLoginButton()
initSignUpButton()
}
// 로그인 버튼
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) {
finish() // LoginActivity 종료시킴
} else {
Toast.makeText(
this,
"로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.",
Toast.LENGTH_SHORT
).show()
}
}
}
}
// 회원가입 버튼
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 {
Toast.makeText(this, "이미 가입한 이메일이거나, 회원가입에 실패했습니다.", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun getInputEmail(): String {
return findViewById<EditText>(R.id.emailEditText).text.toString()
}
private fun getInputPassword(): String {
return findViewById<EditText>(R.id.passwordEditText).text.toString()
}
}
이메일과 비밀번호 EditTextView 의 경우 각 텍스트를 String 형식으로 가져오도록 하는 기능을 getInputEamil() 과 getInputPassword() 함수로 따로 빼주었다.
로그인 버튼의 경우 signInWithEmailAndPassword 로 로그인할 수 있고, addOnCompleteListener 를 달아 로그인에 성공한 경우 로그인 성공처리를 해주고 실패시 토스트 메시지를 띄워주었다.
회원가입 버튼의 경우 createUserWithEmailAndPassword 를 사용해 회원가입할 수 있고, addOnCompleteListener 를 달아 회원가입에 성공한 경우와 실패한 경우에 대한 토스트 메시지를 띄워주었다.
(비밀번호의 경우 6자리 이상 사용해 주어야 한다.)
// 이메일과 비밀번호가 공백이라면 로그인과 회원가입 버튼 비활성화 시키는 예외처리 하려고 하는데
// addTextChangedListener 오류 뜸 해결하기!!!!!!!!!!!!
private fun initEmailAndPasswordEditText() {
val emailEditText = findViewById<EditText>(R.id.emailEditText).text.toString()
val passwordEditText = findViewById<EditText>(R.id.passwordEditText).text.toString()
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
}
}
manifest 에 activity 추가해주는 거 잊지말기!!
<activity android:name=".LoginActivity"/>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val auth: FirebaseAuth = FirebaseAuth.getInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onStart() {
super.onStart()
// 로그인 되지 않은 경우 Intent 통해 LoginActivity 로 이동
if (auth.currentUser == null) {
startActivity(Intent(this, LoginActivity::class.java))
}
}
}
firebase console 로 들어가서 확인해보면 회원가입한 계정과 로그인한 날짜가 뜬다.
Firebase Realtime Database 연동해 유저 정보 추가
LoginActivity.kt
private fun handleSuccessLogin() {
// 값이 없는 경우 return 하도록 예외처리
if (auth.currentUser == null) {
Toast.makeText(this, "로그인에 실패했습니다.", Toast.LENGTH_SHORT).show()
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()
}
로그인에 성공한 경우 Firebase Realtime database 의 유저 정보를 추가해주는 함수를 하나 만들고, 로그인 isSuccessful 부분에서 임시로 finish() 해주었던 것을 handleSuccessLogin 함수로 바꿔준다.
currentUser 은 nullable 하여 null 로 반환될 수 있기 때문에 nullable 처리를 해줌과 동시에 userId 가 null 이 될 수 있기 때문에 orEmpty() 를 통해 null 일 경우 userId 를 empty 로 바꿔주었다.
로그인에 성공한 경우 Users 안에 있는 userId 안에 userId 라는 이름으로 저장이 된다.
LikeActivity.kt
package com.example.tinder
import android.os.Bundle
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
class LikeActivity : AppCompatActivity() {
private var auth: FirebaseAuth = FirebaseAuth.getInstance()
private lateinit var userDB: DatabaseReference
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_like)
userDB = Firebase.database.reference.child("Users")
val currentUserDB = userDB.child(getCurrentUserID())
currentUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.child("name").value == null) {
showNameInputPopup()
return
}
// TODO 유저정보를 갱신
}
override fun onCancelled(error: DatabaseError) {
}
})
}
private fun showNameInputPopup() {
val editText = EditText(this)
AlertDialog.Builder(this)
.setTitle("이름을 입력해주세요")
.setView(editText)
.setPositiveButton("저장") { _, _ ->
if (editText.text.isEmpty()) {
showNameInputPopup()
} else {
saveUserName(editText.text.toString())
}
}
.setCancelable(false) // 뒤로가기 불가능하게 설정
.show()
}
private fun saveUserName(name: String) {
// 유저아이디 가져옴
val userId = getCurrentUserID()
val currentUserDB = userDB.child(userId)
val user = mutableMapOf<String, Any>()
user["userId"] = userId
user["name"] = name
currentUserDB.updateChildren(user)
}
private fun getCurrentUserID(): String {
if (auth.currentUser == null) {
Toast.makeText(this, "로그인이 되어있지 않습니다.", Toast.LENGTH_SHORT).show()
finish()
}
return auth.currentUser?.uid.orEmpty()
}
}
database.reference 의 child 로 Users 생성해준다.
userDB 를 가져와서 이름 칸이 비어 있다면 이름을 입력 받도록 AlertDialog 팝업을 띄우기 위해 addListenerForSingleValueEvent 함수를 이용해 databaseReference 에 ValueEventListener 를 추가해주었다.
AlertDialog 팝업에 EditText 를 추가해 이름을 받아오도록 해주었다. 빈 텍스트일 경우 계속해서 팝업을 띄우도록 해주고, 이름이 입력된 경우 유저 정보를 갱신하여 가져오도록 하였다.
MainActivity.kt
if (auth.currentUser == null) {
// 로그인 되지 않은 경우 Intent 통해 LoginActivity 로 이동
startActivity(Intent(this, LoginActivity::class.java))
} else {
// 로그인이 된 경우 LikeActivity 로 이동
startActivity(Intent(this, LikeActivity::class.java))
}
Swipe Animation 라이브러리
틴더 앱에서 쓸 스와이프 라이브러리 받아서 사용해준다.
https://github.com/yuyakaido/CardStackView
GitHub - yuyakaido/CardStackView: 📱Tinder like swipeable card view for Android
📱Tinder like swipeable card view for Android. Contribute to yuyakaido/CardStackView development by creating an account on GitHub.
github.com
라이브러리
implementation "com.yuyakaido.android:card-stack-view:2.3.4"
CardItem.kt
data class CardItem(
val userId: String,
var name: String
)
activity_like.xml
<com.yuyakaido.android.cardstackview.CardStackView
android:id="@+id/cardStackView"
android:layout_width="0dp"
android:layout_height="30dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
LikeActivity.kt
class LikeActivity : AppCompatActivity(), CardStackListener {
private val adapter = CardItemAdapter()
private val cardItems = mutableListOf<CardItem>() // item에 대한 것을 저장하기 위해 선언
private val manager by lazy {
CardStackLayoutManager(this, this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_like)
initCardStackView()
}
private fun initCardStackView() {
val stackView = findViewById<CardStackView>(R.id.cardStackView)
stackView.layoutManager = manager
stackView.adapter = adapter
}
모델클래스를 만들어 주고, activity_like.xml 레이아웃에 CardStackView 를 추가해주고 findViewById 를 통해 가져온다.
layoutManager 에 CardStackLayoutManager 를 연결해준다.
CardStackListener 를 Activity 에 implement 시켜 추가해주고 메서드들을 추가해준다.
override fun onCardDragging(direction: Direction?, ratio: Float) {}
override fun onCardSwiped(direction: Direction?) {
}
override fun onCardRewound() {}
override fun onCardCanceled() {}
override fun onCardAppeared(view: View?, position: Int) {}
override fun onCardDisappeared(view: View?, position: Int) {}
item_card.xml
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="24dp"
app:cardCornerRadius="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFC107">
<TextView
android:id="@+id/nameTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="name"
android:textColor="@color/black"
android:textSize="40sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
CardItemAdapter.kt
package com.example.tinder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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
}
}
}
}
DB 연동
DB에서 가져와 어댑터에 넘겨주면 like 하는 과정이 완료된다.
LikeActivity.kt
private fun getUnSelectedUsers() {
userDB.addChildEventListener(object : ChildEventListener {
@SuppressLint("NotifyDataSetChanged")
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
// 내가 한번도 선택한 적 없는 User
if (snapshot.child("userId").value != getCurrentUserID()
&& snapshot.child("likedBy").child("like").hasChild(getCurrentUserID()).not()
&& snapshot.child("likedBy").child("dislike").hasChild(getCurrentUserID()).not()) {
// userId 와 name 가져옴
val userId = snapshot.child("userId").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() // recyclerView 를 최신버전으로 갱신
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
// 상대방의 data가 변경됐을 때도 리스트를 갱신
cardItems.find { it.userId == snapshot.key }?.let {
it.name = snapshot.child("name").value.toString()
}
// 수정이 되었다면 cardItems 로 갱신해주고, 데이터가 최신버전으로 갱신되도록 해줌
adapter.submitList(cardItems)
adapter.notifyDataSetChanged()
}
override fun onChildRemoved(snapshot: DataSnapshot) {}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onCancelled(error: DatabaseError) {}
})
}
DB에 연동할 때 유저 정보를 갱신하고, 유저 정보를 저장할 때 유저 정보를 가져오기 위해 getUnSelectedUsers() 함수를 하나 생성해주고, currentUserDB 안의 onDataChange 함수 안과 saveUserName 함수 안에추가해준다.
유저가 처음 로그인 했을 때 아직 이름이 정의되지 않을 수 있기 때문에 초기값을 undecide 로 설정해주었다.
스와이프를 통해 like 와 dislike 저장
LikeActivity.kt
override fun onCardSwiped(direction: Direction?) {
when (direction) {
Direction.Right -> like()
Direction.Left -> dislike()
else -> { }
}
}
private fun like() {
val card = cardItems[manager.topPosition -1] // 1부터 시작하지만 index 값은 0부터 시작하기 때문
cardItems.removeFirst()
// 나의 currentUserId를 상대방의 like에 저장
userDB.child(card.userId) // 상대방의 userId에
.child("likedBy")
.child("like")
.child(getCurrentUserID()) // getCurrentUserId를 저장
.setValue(true)
// TODO 상대방이 나를 like를 했다면 매칭이 된 시점에 알려야 함
Toast.makeText(this, "${card.name}님을 Like 하셨습니다.", Toast.LENGTH_SHORT).show()
}
private fun dislike() {
val card = cardItems[manager.topPosition -1] // 1부터 시작하지만 index 값은 0부터 시작하기 때문
cardItems.removeFirst()
// 나의 currentUserId를 상대방의 dislike에 저장
userDB.child(card.userId) // 상대방의 userId에
.child("likedBy")
.child("dislike")
.child(getCurrentUserID()) // getCurrentUserId를 저장
.setValue(true)
Toast.makeText(this, "${card.name}님을 dislike 하셨습니다.", Toast.LENGTH_SHORT).show()
}
오른쪽으로 스와이프 하면 like를, 왼쪽으로 스와이프 하면 dislike를 선택하도록 하였다.
매칭이 된 유저의 목록 보여주기
LikeActivity.kt
private fun saveMatchIfOtherUserLikedMe(otherUserId: String) {
val otherUserDB = userDB.child(getCurrentUserID()).child("likedBy").child("like").child(otherUserId)
otherUserDB.addListenerForSingleValueEvent(object : ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.value == true) {
userDB.child(getCurrentUserID())
.child("likedBy")
.child("match")
.child(otherUserId)
.setValue(true)
userDB.child(otherUserId)
.child("likedBy")
.child("match")
.child(getCurrentUserID())
.setValue(true)
}
}
override fun onCancelled(error: DatabaseError) {}
})
}
saveMatchIfOtherUserLikedMe(card.userId) // 상대방의 이름은 card.usrId로 가져옴
상대방이 나를 like 했다면 매칭이 된 시점에 알려주는 것을 구현하기 위해 saveMatchIfOtherUserLikedMe() 함수를 생성해주고, like() 함수 안에 추가해준다.
매칭이 된 것을 확인할 수 있는 뷰 생성
매칭이 된 것을 확인할 수 있는 뷰를 만들기 위해 MatchedUserActivity 를 생성하고 auth 와 userDB 변수를 선언해주고 getCurrentUserId 메서드를 생성해주었다.
activity_matched_user.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/matchedUserRecyclerView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
MatchedUserActivity.kt
package com.example.tinder
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
class MatchedUserActivity : AppCompatActivity() {
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)
setContentView(R.layout.activity_matched_user)
userDB = Firebase.database.reference.child("Users")
initMatchedUserRecyclerView()
getMatchUsers()
}
private fun initMatchedUserRecyclerView() {
val recyclerView = findViewById<RecyclerView>(R.id.matchedUserRecyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.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() == true) {
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()
}
}
MatchedUserAdapter.kt
package com.example.tinder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
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
}
}
}
}
item_matched_user.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/userNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp" />
</LinearLayout>
activity_matched_user.xml 레이아웃을 생성하여 유저의 이름정도를 볼 수 있는 간단한 ListView 를 만들었다.
MatchedUserActivity 로 들어갈 수 있는 진입로를 만들기 위해 activity_like.xml 레이아웃에서 버튼을 두개 생성해줬다.
activity_like.xml
<Button
android:id="@+id/matchListButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="매치 리스트 보기"
app:layout_constraintBottom_toTopOf="@id/sighOutButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/sighOutButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="로그아웃 하기"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
LikeActivity.kt
private fun initSignOutButton() {
val signOutButton = findViewById<Button>(R.id.sighOutButton)
signOutButton.setOnClickListener {
auth.signOut()
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
private fun initMatchedListButton() {
val matchedListButton = findViewById<Button>(R.id.matchListButton)
matchedListButton.setOnClickListener {
startActivity(Intent(this, MainActivity::class.java))
}
}
보완해준 것
activity_like.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="매칭할 카드가 없습니다."/>
<TextView
android:text="LIKE"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="DISLIKE"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.yuyakaido.android.cardstackview.CardStackView
android:id="@+id/cardStackView"
android:layout_width="0dp"
android:layout_height="30dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<Button
android:id="@+id/matchListButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="매치 리스트 보기"
app:layout_constraintBottom_toTopOf="@id/sighOutButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/sighOutButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="로그아웃 하기"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
카드가 없을 때 "매칭할 카드가 없습니다" 메시지와 어디로 스와이프 해야 like 이고 dislike 인지 보기 쉽게 TextView 를 3개 추가하여 UI를 구성해주었다.
DBKey.kt
class DBKey {
companion object {
const val USERS = "Users"
const val LIKED_BY = "LikedBy"
const val LIKE = "like"
const val DIS_LIKE = "dislike"
const val USER_ID = "userId"
const val NAME = "name"
const val MATCH = "match"
}
}
value 값을 상수로 지정해서 오타가 날 염려가 없도록 해주었다.