이번 포스팅에서는 크게 두 가지 기능을 추가해보려 합니다. 첫번째는 내가 좋아요 표시한 유저가 나를 똑같이 좋아요 표시했을 때, 매칭 알림을 전송해주는 기능이고 두번째는 내가 좋아요 표시한 유저를 List View로 나타내는 기능입니다.
① 좋아요한 사람들의 목록을 저장할 경로를 FirebaseRef 클래스에 추가한다.
val userLike = database.getReference("userLike")
② 좋아요 표시한 사람의 UID는 카드뷰를 오른쪽으로 넘겼을 때 받아오면 된다. MainActivity의 onCardSwiped 메서드를 아래와 같이 수정하자.
override fun onCardSwiped(direction: Direction?) {
if(direction == Direction.Right) {
saveUserLike(uid, userInfoList[userCount].uid.toString())
}
userCount++
if(userCount == userInfoList.count()) {
getUserInfoList(currentUserGender)
Toast.makeText(baseContext, "유저 정보를 다시 가져옵니다.", Toast.LENGTH_SHORT).show()
}
}
③ 이제 좋아요 표시한 유저의 목록을 저장하는 메서드를 정의하자.
private fun saveUserLike(uid : String, otherUid : String) {
FirebaseRef.userLike.child(uid).child(otherUid).setValue("like")
}
④ 코드를 실행시켜보면, 파이어베이스의 본인의 UID와 본인이 좋아요 표시한 사람의 UID가 잘 저장되고 있음을 확인할 수 있을 것이다.
① 매칭을 구현하기 위해서는 내가 좋아요를 표시한 사람의 좋아요 목록에 내가 있는지를 확인하는 로직이 필요하다. MainActivity에 아래의 메서드를 추가하자.
private fun getOtherUserLike(uid : String, otherUid : String) {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for(dataModel in dataSnapshot.children) {
if(dataModel.key.toString() == uid) {
Toast.makeText(this@MainActivity, "매칭되었습니다", Toast.LENGTH_SHORT).show()
}
}
}
override fun onCancelled(databseError: DatabaseError) {
Log.w("MainActivity", "onCancelled", databseError.toException())
}
}
FirebaseRef.userLike.child(otherUid).addValueEventListener(postListener)
}
② 방금 만든 saveUserLike 메서드 하단에 위 메서드를 추가하면 된다.
private fun saveUserLike(uid : String, otherUid : String) {
FirebaseRef.userLike.child(uid).child(otherUid).setValue("like")
getOtherUserLike(uid, otherUid)
}
③ 매칭이 잘 되는지 확인해보자. 두 개의 디바이스를 선택한 후 코드를 실행시켜보면, 서로 좋아요를 표시했을 때 Toast 메시지가 나와야한다.
① 안드로이드 알림과 관련한 자세한 설명은 아래의 공식문서를 참조하기 바란다.
>> 안드로이드 알림 관련 공식문서
② MainActivity에 아래의 메서드를 추가한다.
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "name"
val descriptionText = "description"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel("test", name, importance).apply {
description = descriptionText
}
// Register the channel with the system
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
③ 알림을 보내는 메서드도 추가한다.
private fun sendNotification() {
if(NotificationManagerCompat.from(this).areNotificationsEnabled()) {
var builder = NotificationCompat.Builder(this, "test")
.setSmallIcon(R.drawable.icon)
.setContentTitle("매칭 완료")
.setContentText("내가 좋아요 표시한 사람이 나를 좋아요 표시하였습니다.")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
with(NotificationManagerCompat.from(this)) {
if (ActivityCompat.checkSelfPermission(
this@MainActivity,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notify(1, builder.build())
}
}
else {
Log.w("notification", "알림 수신이 차단된 상태입니다.")
}
}
④ AndroidManifest.xml 파일에는 아래의 권한이 허용되어있어야 한다.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
⑤ getOtherUserLike의 onDataChange 메서드를 아래와 같이 수정한다.
for(datamodel in dataSnapshot.children) {
if(datamodel.key.toString() == uid) {
Toast.makeText(this@MainActivity, "매칭되었습니다", Toast.LENGTH_SHORT).show()
createNotificationChannel()
sendNotification()
}
}
⑥ 이제 코드를 실행시켜보면, 매칭 알림이 잘 전송되는 것을 확인할 수 있을 것이다.
① default 패키지 하위로, chat이라는 이름의 패키지를 추가하고, chat 패키지 하위로 MatchingListActivity를 추가한다.
② activity_setting.xml 파일에 아래의 Button 태그를 추가한다.
<Button
android:id="@+id/myMatchingList"
android:text="매칭 리스트"
android:textStyle="bold"
android:layout_margin="10dp"
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="60dp"/>
③ 버튼에 대한 클릭 이벤트 리스너를 SettingActivity 파일에 작성한다.
val matchingListBtn = findViewById<Button>(R.id.myMatchingList)
likeListBtn.setOnClickListener {
startActivity(Intent(this, LikeListActivity::class.java))
}
④ MatchingListActivity 파일에 내가 좋아요 표시한 유저의 UID를 담는 List를 생성한다.
private val myLikeList = mutableListOf<String>()
⑤ 내가 좋아요 표시한 유저의 UID를 방금 생성한 list에 넣는 메서드를 정의한다.
private fun getMyLikeList() {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for(dataModel in dataSnapshot.children) {
myLikeList.add(dataModel.key.toString())
}
}
override fun onCancelled(databseError: DatabaseError) {
Log.w("MatchingListActivity", "onCancelled", databseError.toException())
}
}
FirebaseRef.userLike.child(uid).addValueEventListener(postListener)
}
⑥ 이번에는 좋아요 표시한 유저의 정보를 담는 List를 생성한다.
private val myLikeUserInfo = mutableListOf<UserInfo>()
⑦ 내가 좋아요 표시한 유저의 정보만 List에 추가하기 위해 아래의 메서드를 정의한다.
private fun getUserInfoList() {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for(dataModel in dataSnapshot.children) {
val userInfo = dataModel.getValue(UserInfo::class.java)
}
}
override fun onCancelled(databseError: DatabaseError) {
Log.w("MatchingListActivity", "onCancelled", databseError.toException())
}
}
FirebaseRef.userInfo.addValueEventListener(postListener)
}
⑧ onCreate 메서드에서 위에서 작성한 메서드를 호출한다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_matching_list)
getUserInfoList()
getMyLikeList()
}
① layout 디렉토리 하위로 list_view_item.xml 파일을 추가한다.
② 아래와 같이 TextView 태그를 추가한다.
<TextView
android:id="@+id/lvNickname"
android:text="닉네임 :"
android:textSize="20sp"
android:layout_margin="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/lvNicknameArea"
android:text="크롬"
android:textSize="20sp"
android:textStyle="bold"
android:layout_margin="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
③ 이제 chat 디렉토리 하위로, LVAdapter 클래스를 생성하자.
class LVAdapter(val context : Context, val dataList : MutableList<UserInfo>) : BaseAdapter() {
override fun getCount(): Int {
return dataList.size
}
override fun getItem(position: Int): Any {
return dataList[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var convertView = convertView
if(convertView == null) {
convertView = LayoutInflater.from(parent?.context).inflate(R.layout.list_view_item, parent, false)
}
val nickname = convertView!!.findViewById<TextView>(R.id.lvNicknameArea)
nickname!!.text = dataList[position].nickname
return convertView!!
}
}
④ activity_matching_list.xml 파일에 ListView 태그를 추가한다.
<ListView
android:id="@+id/listViewItems"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
⑤ MatchingListActivity 파일에서 lateinit으로 어댑터를 선언한다.
lateinit var listViewAdapter : LVAdapter
⑥ onCreate 메서드를 아래와 같이 수정한다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_matching_list)
val listview = findViewById<ListView>(R.id.listViewItems)
listViewAdapter = LVAdapter(this, myLikeUserInfo)
listview.adapter = listViewAdapter
getUserInfoList()
getMyLikeList()
}
⑦ getUserInfoList 메서드의 for문이 끝나는 시점에 Adapter를 동기화 시켜야한다.
for(dataModel in dataSnapshot.children) {
val userInfo = dataModel.getValue(UserInfo::class.java)
if(myLikeList.contains(userInfo?.uid)) {
myLikeUserInfo.add(userInfo!!)
}
}
listViewAdapter.notifyDataSetChanged()
아마 코드를 실행시킨 후 매칭 리스트에 들어가보면, 결과가 제대로 나타나지 않을 것이다. 이는 비동기처리에 인한 결과로, myLikeList가 다 채워지기도 전에 if(myLikeList.contains(userInfo?.uid)) 조건을 확인하면서 발생하는 문제이다. 그래서 if(myLikeList.contains(userInfo?.uid)) 직전에 Log.d로 myLikeList.toString()의 결과를 확인해보면 빈 리스트가 나올 것이다. Android Kotlin에서 비동기로 처리되는 작업은 아래와 같다.
나중에 Api 연동을 다룰 때에는 Coroutine을 사용하여 이 문제를 해결하겠지만, 여기서는 getMyLikeList()와 getUserInfoList() 메서드를 순차적으로(Blocking 방식으로) 동작시키는 방식으로 해결해보자. 메서드를 Blocking 방식으로 실행하는 방법에는 크게 두 가지가 있다.
이 아이디어는 "코드는 한줄씩 순차적으로 수행된다"는 점에서 착안한다. 위 경우에서도 당연히 첫번째 메서드가 먼저 실행되긴 하지만, 두번째 메서드가 Non-Blocking 방식으로 실행됨에 따라 두 메서드는 거의 동시에 실행되는 것처럼 보일 것이다. 따라서 두 메서드 사이에 delay 타임을 주어 첫번째 메서드가 완료될 때까지 충분히 기다렸다가, 두번째 메서드를 실행해야 한다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_matching_list)
val listview = findViewById<ListView>(R.id.listViewItems)
listViewAdapter = LVAdapter(this, myLikeUserInfo)
listview.adapter = listViewAdapter
runBlocking {
launch {
getMyLikeList()
}
delay(200) // 0.2초
launch {
getUserInfoList()
}
}
}
delay를 발생시키는 방법에는 명확한 한계가 있다. delay가 너무 짧아도, 너무 길어도 문제가 된다는 점에서 적절한 delay 시간을 찾기도 어렵고 심지어는 List의 사이즈에 따라 delay를 변경해야 할 수도 있다.
사실 더 좋은 방법은 메서드를 내부 호출하는 방식이다. 다시말해 onCreate 메서드에서 getMyLikeList()만 호출하고, getUserInfoList()를 아래와 같이 getMyLikeList()에서 내부 호출하는 것이다.
override fun onCreate(savedInstanceState: Bundle?) {
...
getMyLikeList()
}
private fun getMyLikeList() {
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for(dataModel in dataSnapshot.children) {
myLikeList.add(dataModel.key.toString())
}
getUserInfoList()
}
override fun onCancelled(databseError: DatabaseError) {
Log.w("MatchingListActivity", "onCancelled", databseError.toException())
}
}
FirebaseRef.userLike.child(uid).addValueEventListener(postListener)
}
우리가 여기서 알아야 할 것은, 순서가 보장되어야하는 작업을 처리할 때에는 항상 비동기처리에 대한 부분을 염두에 두어야 한다는 것이다. 이제 코드를 실행시켜보면, 매칭 리스트에 닉네임이 잘 출력될 것이다.