[Androd / Kotlin] Firebase CRUD

Subeen·2024년 2월 22일
0

Android

목록 보기
66/71

다음주부터 본격적으로 최종 프로젝트 개발에 들어간다. 이번 프로젝트에는 Firebase의 realtime database를 사용하는데, Firebase를 오랜만에 사용하려다 보니 헷갈리는 부분이 있어서 샘플 코드를 정리하며 복습해보려고 한다. 🤓 자세한 내용은 프로젝트를 진행하며 정리해보겠다 💪🏻

Repository

@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())
                }
            }
        })
    }

}

ViewModel

@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()
        }
    }

}

Activity

@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

            }
        }
    }

}
profile
개발 공부 기록 🌱

0개의 댓글