다음주부터 본격적으로 최종 프로젝트 개발에 들어간다. 이번 프로젝트에는 Firebase의 realtime database를 사용하는데, Firebase를 오랜만에 사용하려다 보니 헷갈리는 부분이 있어서 샘플 코드를 정리하며 복습해보려고 한다. 🤓 자세한 내용은 프로젝트를 진행하며 정리해보겠다 💪🏻
@Singleton
class ContentRepositoryImpl @Inject constructor(
private val databaseReference: DatabaseReference
) : ContentRepository {
/**
* 새로운 entity를 추가한다.
*
* @param title 제목
* @param content 내용
* @param description 설명
* @param datetime 날짜 및 시간
* @return entity 추가를 성공했을 경우 true, 실패했을 경우 false 반환
*/
override suspend fun addPostEntity(
title: String,
content: String,
description: String,
datetime: String
): Boolean {
val key = databaseReference.push().key
return key?.let { nonNullKey ->
val entity = PostEntity(nonNullKey, title, content, description, datetime)
try {
databaseReference.child(nonNullKey).setValue(entity).await()
true
} catch (e: Exception) {
false
}
} ?: false
}
/**
* key에 해당하는 entity를 업데이트한다.
*
* @param key 업데이트 할 entity의 고유 키
* @param title 새로운 제목
* @param content 새로운 내용
* @param description 새로운 설명
* @return 업데이트를 성공했을 경우 true, 실패했을 경우 false 반환
*/
override suspend fun updatePostEntity(
key: String,
title: String,
content: String,
description: String
): Boolean {
val data = mapOf("title" to title, "content" to content, "description" to description)
return try {
databaseReference.child(key).updateChildren(data).await()
true
} catch (e: Exception) {
false
}
}
/**
* key에 해당하는 entity를 삭제한다.
*
* @param key 삭제할 entity의 고유 키
* @return 삭제를 성공했을 경우 true, 실패했을 경우 false 반환
*/
override suspend fun deletePostEntity(key: String): Boolean {
return try {
databaseReference.child(key).removeValue().await()
true
} catch (e: Exception) {
false
}
}
/**
* 전달받은 key에 해당하는 entity를 읽어온다.
*
* @param key 읽어올 entity의 고유 키
*/
override suspend fun readEntityByKey(key: String) {
databaseReference.child(key).get().addOnSuccessListener {
if (it.exists()) {
val title = it.child("title").value
val content = it.child("content").value
val description = it.child("description").value
val datetime = it.child("datetime").value
} else {
// TODO key does not exists
}
}.addOnFailureListener {
// TODO failed
}
}
/**
* 모든 entity를 읽어온다.
*
* @return entity 목록
*/
override suspend fun readEntities(): List<PostEntity> = suspendCoroutine { continuation ->
val entities = mutableListOf<PostEntity>()
var isResumed = false
databaseReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for (snapshot in dataSnapshot.children) {
val entity = snapshot.getValue(PostEntity::class.java)
entity?.let {
entities.add(it)
}
}
if (!isResumed) {
isResumed = true
continuation.resume(entities)
}
}
override fun onCancelled(error: DatabaseError) {
if (!isResumed) {
isResumed = true
continuation.resumeWithException(error.toException())
}
}
})
}
}
@HiltViewModel
class PostContentViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val contentRepository: ContentRepository
) : ViewModel() {
// SavedStateHandle를 통해 전달받은 EntryType을 가져오는 프로퍼티
private val entryType
get() = savedStateHandle.get<PostContentEntryType>(
EXTRA_ENTRY_TYPE
)
// SavedStateHandle를 통해 전달받은 PostEntity를 가져오는 프로퍼티
private val entity get() = savedStateHandle.get<PostEntity>(EXTRA_POST_ENTITY)
// UI 상태를 관리하는 MutableLiveData
private val _uiState: MutableLiveData<PostContentUiState> = MutableLiveData(
PostContentUiState.init()
)
val uiState: LiveData<PostContentUiState> get() = _uiState
init {
// ui 상태 초기화
_uiState.value = uiState.value?.copy(
title = entity?.title,
content = entity?.content,
description = entity?.description,
button = if (entryType == PostContentEntryType.UPDATE) {
PostContentButtonUiState.Update
} else {
PostContentButtonUiState.Create
}
)
}
/**
* 엔터티를 업데이트하는 메서드
*
* @param title 새로운 제목
* @param content 새로운 내용
* @param description 새로운 설명
* @return 업데이트 성공 여부를 반환
*/
private suspend fun updateEntity(
title: String,
content: String,
description: String
): Boolean {
return entity?.let {
contentRepository.updatePostEntity(it.key, title, content, description)
} ?: false
}
/**
* 엔터티를 추가하는 메서드
*
* @param title 제목
* @param content 내용
* @param description 설명
* @return 추가 성공 여부를 반환
*/
private suspend fun addEntity(
title: String,
content: String,
description: String
): Boolean {
return contentRepository.addPostEntity(title, content, description, getCurrentTime())
}
/**
* 버튼 클릭 이벤트 처리 메서드
*
* @param title 제목
* @param content 내용
* @param description 설명
* @param onSuccess 성공 시 실행할 콜백
* @param onFailure 실패 시 실행할 콜백
*/
fun onClickEvent(
title: String,
content: String,
description: String,
onSuccess: () -> Unit,
onFailure: () -> Unit
) = viewModelScope.launch {
val success = if (entryType == PostContentEntryType.UPDATE) {
updateEntity(title, content, description)
} else {
addEntity(title, content, description)
}
// 성공 여부에 따라 콜백 실행
if (success) {
onSuccess.invoke()
} else {
onFailure.invoke()
}
}
}
@AndroidEntryPoint
class PostContentActivity : AppCompatActivity() {
companion object {
/**
* 새로운 포스트를 생성하기 위한 인텐트를 생성한다.
*
* @param context 컨텍스트
* @return 새로운 포스트 생성을 위한 인텐트
*/
fun newIntentForCreate(
context: Context
) = Intent(context, PostContentActivity::class.java).apply {
putExtra(EXTRA_ENTRY_TYPE, PostContentEntryType.CREATE)
}
/**
* 포스트를 업데이트하기 위한 인텐트를 생성한다.
*
* @param context 컨텍스트
* @param position 포스트의 위치
* @param entity 업데이트할 포스트 엔터티
* @return 포스트 업데이트를 위한 인텐트
*/
fun newIntentForUpdate(
context: Context,
position: Int,
entity: PostEntity
) = Intent(context, PostContentActivity::class.java).apply {
putExtra(EXTRA_ENTRY_TYPE, PostContentEntryType.UPDATE)
putExtra(EXTRA_POSITION_ENTITY, position)
putExtra(EXTRA_POST_ENTITY, entity)
}
}
// PostContentViewModel을 주입받아 사용
private val viewModel: PostContentViewModel by viewModels()
private val binding: ActivityPostContentBinding by lazy {
ActivityPostContentBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initView()
initViewModel()
}
private fun initView() {
binding.btSubmit.setOnClickListener {
viewModel.onClickEvent(
binding.tvTitle.text.toString(),
binding.tvContent.text.toString(),
binding.tvDescription.text.toString(),
onSuccess = {
finish()
},
onFailure = {
Log.d("TAG", "failed")
}
)
}
}
/**
* ViewModel과 관련된 초기화 작업을 수행한다.
*/
private fun initViewModel() = with(viewModel) {
// UI 상태를 관찰하고 UI 갱신
uiState.observe(this@PostContentActivity) { entity ->
binding.tvTitle.setText(entity.title)
binding.tvContent.setText(entity.content)
binding.tvDescription.setText(entity.description)
// 버튼 상태에 따라 버튼 텍스트 설정
when (entity.button) {
PostContentButtonUiState.Create -> {
binding.btSubmit.text = getString(R.string.create)
}
PostContentButtonUiState.Update -> {
binding.btSubmit.text = getString(R.string.update)
}
else -> Unit
}
}
}
}