저번시간까지는 깃허브에서 카드스택뷰 라이브러리를 사용해서 리사이클러 어댑터를 연결시키는 과정까지 완료했다. 이번에는 데이터를 리얼타임 데이터베이스에 저장하고 또 불러오면서 카드스택뷰에 실시간으로 처리했다.
# 결과물 미리보기
# TODO 리스트 - 구현명세
- 새로운 카드(유저) 추가
- 스와이프하여 like, dislike 하기
# 신규 유저목록 불러와 카드뷰로 만들어주기
전 시간에 onCreate에서 addListenerForSingleValueEvent을 달았다. 해당 이벤트는 실행시 최초 한번 실행이 된다.이 안에다가 getUnSelectedUser()라는 메서드를 만들어 줬다. 해당 함수가 실행되면서 addChildEventListener를 userDB에 등록시켜준다. 이렇게 함으로써 변경되는 Child만을 snapshot으로 받아올수 있는 초기화(init)을 끝냈다.
private fun getUnSelectedUser() {
userDB.addChildEventListener(object : ChildEventListener {
// 여기서 snapshot은 추가된 데이터의 snapshot인가 전체의 snapshot인가?
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
// userDB에 새로운 유저가 들어왔을때
// user목록을 최신화 시키고 어댑터에 아이템을 넘겨줘야함
// 새로운 유저가 회원가입을 하게 된다면, 현재 이벤트 리스너가 리스닝하게 됨
// 나 자신과, liked 또는 unliked한 유저 정보는 가져오면 안됨
if (snapshot.child("userId").value != getCurrentUserID()
&& snapshot.child("likeBy").child("like").hasChild(getCurrentUserID()).not()
&& snapshot.child("likeBy").child("dislike").hasChild(getCurrentUserID()).not()
) {
val userId = snapshot.child("userId").value.toString()
val name = snapshot.child("name").value.toString() ?: "initName"
val cardItem = CardItem(userId, name)
cardItemList.add(cardItem)
adpater.submitList(cardItemList)
adpater.notifyDataSetChanged()
}
}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
// 이름이 변경되면 다시 만들어 줘야함
cardItemList.find { it.userId == snapshot.child("userId").value.toString() }?.let {
// 이게 되는 이유는 객체는 레퍼런스여서 그런거겠지?
it.name = snapshot.child("name").value.toString()
}
adpater.submitList(cardItemList)
adpater.notifyDataSetChanged()
}
override fun onChildRemoved(snapshot: DataSnapshot) {}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onCancelled(error: DatabaseError) {}
})
}
썩을 오타때문에.. likedBy -> likeBy 때문에 게속 새롭게 추가됐네 ㅠ 에러 잡았다!
## onChildAdded()
해당 리스너가 실행되는 시점을 생각해보자, 나는 현재 정상 로그인된 상태이다. 즉 아래 화면일 것이다.
이제 새로운 신규유저가 회원가입을 했다. 그러면 DB에 변화가 생길 것이다. 이때 onChildAdded리스너가 동작한다. 그리고 SnapShot으로 추가된 유저의 정보를 가져오게 될 것이다. 그리고 조건을 검사하고 새로운 cardlist에 추가한다음에, adapter에 바뀐 정보를 전달 시켜준다.
이름이 변경되는 시점도 처리할 수 있어야 했기 때문에 onChildChange를 통해서 처리 할 수 있도록 한다.
# 스와이프 동작 구현하기
스와이프는 CardStackListener 인터페이스를 상속받아 구현해주면 됐다. 저번에는 익명객체로 만들어 사용했는데, 해당 매니저는 자주 사용되기 때문에 전역으로 바꿔줬다.
private val cardStackLayoutManager by lazy {
CardStackLayoutManager(this, this)
}
저번 시간에 정리했던거와 같이 액티비티에서 CardStackListener를 상속받아서 사용하면 된다.
override fun onCardSwiped(direction: Direction?) {
when (direction) {
Direction.Right -> like()
Direction.Left -> dislike()
else -> {
}
}
}
그중 Swiped만 사용하면 된다. Direction은 Left,Right,Top,Bottom을 가지고 있는 객체인데, 오른쪽 왼쪽 스와이프로 like와 dislike를 구현하면 된다.
private fun like() {
// 카트스택의 최 상위를 찾은다음에 뺴줘야함
val card = cardItemList[cardStackLayoutManager.topPosition - 1]
cardItemList.removeFirst()
// like한 상태를 저장시켜줘야 함
userDB.child(card.userId).child("likeBy").child("like").child(getCurrentUserID())
.setValue(true)
adpater.submitList(cardItemList)
adpater.notifyDataSetChanged()
Toast.makeText(this, "${card.name}님을 like 했습니다.", Toast.LENGTH_SHORT).show()
}
private fun dislike() {
// 카트스택의 최 상위를 찾은다음에 뺴줘야함
val card = cardItemList[cardStackLayoutManager.topPosition - 1]
cardItemList.removeFirst()
// like한 상태를 저장시켜줘야 함
userDB.child(card.userId).child("likeBy").child("dislike").child(getCurrentUserID())
.setValue(true)
adpater.submitList(cardItemList)
adpater.notifyDataSetChanged()
Toast.makeText(this, "${card.name}님을 dislike 했습니다.", Toast.LENGTH_SHORT).show()
}
구현방법은 내가 방금 스와이프 한 카드를 cardItemList에서 제거시켜주면 된다.
# 파이에 베이스 리스너 이해하기
구현하기 앞서, Firebase에서 db를 리스닝하는 많은 요소들이 있는데, 이거 이해하려고 꽤 시간을 많이 썼다.
private lateinit var userDB: DatabaseReference
userDB = Firebase.database.reference.child("Users")
val currentUserDB = userDB.child(getCurrentUserID())
위처럼 DB는 특정 레퍼런스가 생기게 되는데, 리스너들을 이 레퍼런스에 등록하는 구조인것이었다.
그래서 아래처럼 각각 두개의 레퍼런스 지점에 아래와 같이 리스너을 달아주게 되는 것이다.
currentUserDB.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {}
override fun onCancelled(error: DatabaseError) {}
}
)
userDB.addChildEventListener(object : ChildEventListener {
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onChildRemoved(snapshot: DataSnapshot) {}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onCancelled(error: DatabaseError) {}
})
이제 해당 레퍼런스부터 디비에서의 어떤 변화를 감지하게 되는데, addListenerForSingleValueEvent는 파이어베이스로부터 한번 콜백을 받으면 currentUserDB에서 해당 리스너는 없어지고 더이상 리스닝을 하지 못한다.
userDB에 달린 addChildEventListener는 userDB의 child들의 변화를 감지하게 되고, 사라지지 않고 계속해서 리스닝한다. 여기서 snapshot은 변화된 부분만 가져온다.
addListenerForSingleValueEvent가 최초 실행되면서 getUnSelectedUser()함수가 실행되면 addChildEventListener가 userDB에 등록되기 때문에 계속해서 리스닝이 가능했던 것 이었다.
그리고 리스너들은 일단 호출되는 순간에 최초로 한번은 무조건 실행되는거 같다. 그렇기 때문에 어떤사람은 addListenerForSingleValueEvent을 HTTP통신과 같이 사용하라고 했다. request를 보내는거처럼~
# 느낀점
이제 코드를 짜기 전에, 주석으로 구현해야할 것을 적어가며 하나씩 코딩해나가고 있는데 이렇게 하니까 딱딱 정리되면서 뭔가 빠르게 코드를 짤 수 있었다. 기존에는 코드를 다 적고나서 주석을 달았는데, 해야할것(TODO)를 적고 코드를 적는 방식으로 코드를 짜고 있는 중이다.
파이어베이스 헷갈린다,,
오늘 포스팅 양은 없는거 같은데 한 세시간은 찾아보고 코드 계속 읽어봤다.. 물론 틀린정보도 수두룩하겠지만 나름 성과는 있었다.
# 참고