나만의 안드로이드 앱 만들기/초급자

나만의 안드로이드 앱 만들기(초보자 편) - 데이터 연동 (8)

Victorywskim 2023. 12. 13. 02:36
반응형
이번 편에서는 진행 예정인 작업은 다음과 같습니다.

 

  1. UseCase 를 작성합니다.
  2. 데이터 접근을 위해 Repository, DataSource 를 작성합니다.
  3. 실질적인 데이터 관리를 위해 SharedPreferences 를 작성합니다.
  4. ViewModel 에 UseCase 를 연동합니다.
  5. Screen 에 데이터를 연동합니다.

그럼 작업을 진행해보겠습니다.

 

1. UseCase 를 작성합니다.
class DutchInfoUseCase @Inject constructor(
    private val dutchInfoRepository: DutchInfoRepository,
) {
    // 더치 페이 글쓰기 전 유효성 체크
    fun processedDutchWriteValidation(
        title: String,
        amount: String,
        enterPersonList: List<String>
    ): Boolean {
        return title.isNotEmpty() && amount.isNotEmpty() && enterPersonList.isNotEmpty()
    }

    // 더치 페이 글쓰기
    fun saveDutchInfo(homeDutchListItemVO: HomeDutchListItemVO) {
        dutchInfoRepository.insertDutchInfo(homeDutchListItemVO)
    }

    // 더치 페이 목록 조회
    fun findDutchInfoList() : ArrayList<HomeDutchListItemVO> {
        return dutchInfoRepository.selectDutchInfoList()
    }
}

 

기존의 UserInfoUseCase 이 사용될 일이 없어서  DutchInfoUseCase 으로 이름을 변경하였습니다.

그 외 UserInfoRepository 및 UserInfoDataSource 도 이름을 변경하였습니다.

 

그리고 UseCase 에 HomeDutchListItemVO 라는게 있는데 이건 List 의 아이템으로 쓰일 예정이며, 다음과 같습니다.

data class HomeDutchListItemVO(val title: String, val amount: String, val enterPersonList: List<String>)

 

2. 데이터 접근을 위해 Repository, DataSource 를 작성합니다.

 

interface DutchInfoRepository {
    fun insertDutchInfo(homeDutchListItemVO: HomeDutchListItemVO)
    fun selectDutchInfoList(): ArrayList<HomeDutchListItemVO>
}

 

class DutchInfoRepositoryImpl @Inject constructor(
    private val dutchInfoDataSource: DutchInfoDataSource
) : DutchInfoRepository  {
    override fun insertDutchInfo(homeDutchListItemVO: HomeDutchListItemVO) {
        dutchInfoDataSource.insertDutchInfo(homeDutchListItemVO)
    }

    override fun selectDutchInfoList(): ArrayList<HomeDutchListItemVO> {
        return dutchInfoDataSource.selectDutchInfoList()
    }
}

 

interface DutchInfoDataSource {
    fun insertDutchInfo(homeDutchListItemVO: HomeDutchListItemVO)
    fun selectDutchInfoList(): ArrayList<HomeDutchListItemVO>
}

 

class DutchInfoDataSourceImpl @Inject constructor(
    private val sharedPreferencesManager: SharedPreferencesManager
) : DutchInfoDataSource {
    override fun insertDutchInfo(homeDutchListItemVO: HomeDutchListItemVO) {
        sharedPreferencesManager.saveDutchInfo(homeDutchListItemVO)
    }

    override fun selectDutchInfoList(): ArrayList<HomeDutchListItemVO> {
        return sharedPreferencesManager.getDutchInfoList()
    }
}

 

3. 실질적인 데이터 관리를 위해 SharedPreferences 를 작성합니다.

 

open class BaseSharedPreferencesManager(context: Context) {
    protected val privateAppPrefs: SharedPreferences by lazy {
        context.getSharedPreferences(PREF_DEFAULT, Context.MODE_PRIVATE)
    }

    private val privateAppPrefsEditor: SharedPreferences.Editor by lazy {
        privateAppPrefs.edit()
    }

    protected fun putValueToPrivateAppPrefs(value: Any?, key: String) = putValueToPref(privateAppPrefsEditor, value, key)
    private fun putValueToPref(prefEditor: SharedPreferences.Editor, value: Any?, key: String) {
        when (value) {
            is String -> prefEditor.putString(key, value).apply()
            is Boolean -> prefEditor.putBoolean(key, value).apply()
            is Int -> prefEditor.putInt(key, value).apply()
            is Long -> prefEditor.putLong(key, value).apply()
            is Float -> prefEditor.putFloat(key, value).apply()
        }
    }

    companion object {
        const val PREF_DEFAULT = "PREF_DEFAULT"
        const val SP_HOME_DUTCH_LIST_ITEM = "SP_HOME_DUTCH_LIST_ITEM"
    }
}

 

class SharedPreferencesManager(context: Context) : BaseSharedPreferencesManager(context) {

    fun clearDefaultPrefs() = privateAppPrefs.edit().clear().apply()

    fun saveDutchInfo(homeDutchListItemVO: HomeDutchListItemVO) {
        putValueToPrivateAppPrefs(
            Gson().toJson(
                getDutchInfoList().apply {
                    add(homeDutchListItemVO)
                }
            ),
            SP_HOME_DUTCH_LIST_ITEM
        )
    }

    fun getDutchInfoList(): ArrayList<HomeDutchListItemVO> {
        val type = object : TypeToken<ArrayList<HomeDutchListItemVO>>() {}.type
        return privateAppPrefs.getString(SP_HOME_DUTCH_LIST_ITEM, "")?.let {
            Gson().fromJson(it, type)
        }?: arrayListOf()
    }
}

 

4. ViewModel 에 UseCase 를 연동합니다.

 

기존 HomeWriteViewModel 의 코드를 정리 및 usecase 를 연동하였습니다.

@HiltViewModel
class HomeWriteViewModel @Inject constructor(
    private val dutchInfoUseCase: DutchInfoUseCase,
) : SdV1ViewModel<HomeWriteContract.Event, HomeWriteContract.State, HomeWriteContract.Effect>() {

    init {
        super.onCreate(
            initialData = {
                setInitialData()
            },
            initialHandleUiMonitoring = {
                handleUiMonitoring()
            }
        )
    }

    override fun setInitialState(): HomeWriteContract.State = HomeWriteContract.State(
        screenState = mutableStateOf(SdV1ScreenStateEnum.SUCCESS),
        title = MutableStateFlow(""),
        amount = MutableStateFlow(""),
        enterPersonName = mutableStateOf(""),
        enterPersonList = mutableStateListOf(),
        completeButtonEnabled = mutableStateOf(false)
    )

    override suspend fun setInitialData() {
        // 데이터 추가
    }

    override fun handleUiMonitoring() {
        viewState.value.title.onEach { processedDutchWriteValidation() }.launchIn(viewModelScope)
        viewState.value.amount.onEach { processedDutchWriteValidation() }.launchIn(viewModelScope)
    }

    override fun handleEvents(event: HomeWriteContract.Event) {
        when (event) {
            is HomeWriteContract.Event.BackButtonClicked -> backButtonClicked()
            is HomeWriteContract.Event.SaveEnterPersonClicked -> saveEnterPersonClicked()
            is HomeWriteContract.Event.RemoveEnterPersonClicked -> removeEnterPersonClicked(event.position)
            is HomeWriteContract.Event.CompleteButtonClicked -> completeButtonClicked()
        }
    }

    // 뒤로가기 버튼 클릭
    private fun backButtonClicked() {
        setEffect { HomeWriteContract.Effect.Navigation.GoToBack }
    }

    // 더치 페이 글쓰기 전 유효성 체크
    private fun processedDutchWriteValidation() {
        setState {
            copy(
                completeButtonEnabled = mutableStateOf(
                    dutchInfoUseCase.processedDutchWriteValidation(
                        title = title.value,
                        amount = amount.value,
                        enterPersonList = enterPersonList
                    )
                )
            )
        }
    }

    // 참여자 추가 버튼 클릭
    private fun saveEnterPersonClicked() {
        viewState.value.enterPersonList.add(viewState.value.enterPersonName.value)
        setState { copy(enterPersonName = mutableStateOf("")) }
        processedDutchWriteValidation()
    }

    // 참여자 삭제 버튼 클릭
    private fun removeEnterPersonClicked(position: Int) {
        viewState.value.enterPersonList.removeAt(position)
        processedDutchWriteValidation()
    }

    // 더치 페이 추가 버튼 클릭
    private fun completeButtonClicked() {
        // 저장 전 참여자 목록이 빈값이면 진행 불가
        if (!viewState.value.completeButtonEnabled.value) return

        // 저장 하기
        viewState.value.also {
            dutchInfoUseCase.saveDutchInfo(
                HomeDutchListItemVO(
                    title = it.title.value,
                    amount = it.amount.value,
                    enterPersonList = it.enterPersonList
                )
            )
        }

        // 저장 완료 토스트 노출 및 뒤로가기
        setEffect { HomeWriteContract.Effect.Toast.ShowComplete }
        setEffect { HomeWriteContract.Effect.Navigation.GoToBack }
    }
}

 

class HomeContract {
    sealed class Event : BaseViewEvent {
        object OnResume : Event()
        object HomeWriteButtonClicked : Event()
    }

    data class State(
        override val screenState: MutableState<SdV1ScreenStateEnum>,
        val list: SnapshotStateList<HomeDutchListItemVO>
    ) : BaseViewState

    sealed class Effect : BaseViewSideEffect {
        sealed class Navigation : Effect() {
            object GoToHomeWrite : Navigation()
        }
    }
}

 

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val dutchInfoUseCase: DutchInfoUseCase,
) : SdV1ViewModel<HomeContract.Event, HomeContract.State, HomeContract.Effect>() {

    init {
        super.onCreate(
            initialData = {
                setInitialData()
            },
            initialHandleUiMonitoring = {
                handleUiMonitoring()
            }
        )
    }

    override fun setInitialState(): HomeContract.State = HomeContract.State(
        screenState = mutableStateOf(SdV1ScreenStateEnum.SUCCESS),
        list = mutableStateListOf()
    )

    override suspend fun setInitialData() {
        // 데이터 추가
    }

    override fun handleUiMonitoring() {}

    override fun handleEvents(event: HomeContract.Event) {
        when (event) {
            is HomeContract.Event.HomeWriteButtonClicked -> setEffect {
                HomeContract.Effect.Navigation.GoToHomeWrite
            }
            is HomeContract.Event.OnResume -> onResume()
        }
    }

    private fun onResume() {
        setState { copy(list = dutchInfoUseCase.findDutchInfoList().toMutableStateList()) }
    }
}

 

HomeViewModel 에 HomeContract.Event.OnResume 라는 부분이 있는데 이는 기존 안드로이드 생명주기를 Compose 측면에서 똑같이 만든 부분이라고 생각하시면 되겠습니다.

@Composable
fun HomeScreen(
    state: HomeContract.State,
    effectFlow: Flow<HomeContract.Effect>?,
    onEventSent: (event: HomeContract.Event) -> Unit,
    onNavigationRequested: (HomeContract.Effect.Navigation) -> Unit
) {
    LaunchedEffect(SIDE_EFFECTS_KEY) {
        onEventSent(HomeContract.Event.OnResume)

        effectFlow?.collect { effect ->
            when (effect) {
                is HomeContract.Effect.Navigation -> {
                    onNavigationRequested(effect)
                }
            }
        }
    }

    BaseScreen(
        screenState = state.screenState,
        body = {
            // 총 가격
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(150.dp),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "0원", fontSize = 30.sp)
            }

            Button(
                onClick = { onEventSent(HomeContract.Event.HomeWriteButtonClicked) },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 40.dp),
                colors = ButtonDefaults.buttonColors(containerColor = Blue)
            ) {
                Box(
                    modifier = Modifier.height(30.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(text = "추가하기", fontSize = 16.sp, fontWeight = FontWeight.Bold)
                }
            }

            HomeList(state.list)
        }
    )
}

 

LaunchedEffect 를 이용해서 onEventSent(HomeContract.Event.OnResume) 를 통해 유저와 상호작용이 가능한 것을 뷰모델에 알리고 list 를 재조회 하게 됩니다.

@Composable
fun HomeListItem(item: HomeDutchListItemVO) {

    Column {
        ConstraintLayout(
            modifier = Modifier
                .fillMaxWidth()
        ) {
            val (name, price, enterCount) = createRefs()

            Text(
                text = item.title,
                modifier = Modifier.constrainAs(name) {
                    start.linkTo(parent.start)
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                }
            )

            Text(
                text = "${item.amount}원",
                modifier = Modifier.constrainAs(price) {
                    end.linkTo(parent.end)
                    top.linkTo(parent.top)
                    bottom.linkTo(enterCount.top)
                }
            )

            Text(
                text = "${item.enterPersonList.size}명",
                modifier = Modifier.constrainAs(enterCount) {
                    end.linkTo(parent.end)
                    top.linkTo(price.bottom)
                    bottom.linkTo(parent.bottom)
                }
            )
        }

        Divider(
            modifier = Modifier.padding(top = 5.dp, bottom = 10.dp)
        )
    }
}

 

마지막으로 HomeListItem 에서 변경된 아이템으로 바인딩을 수정합니다.

 

이렇게 데이터 연동까지 진행해보았으며, 다음편에서는 정산 기능을 구현해보려 합니다.

 

정산 및 저장소 2가지 기능을 추가로 더 개발해보려하는데 각각의 기능들은 디자인과 기능을 구분하지 않고 한번에 진행 해볼까 합니다.

 

그럼 정산 편에서 다시 뵙겠습니다.

 

나만의 안드로이드 앱 만들기(초보자 편) - 풀 사이클 및 마무리(9)

이번 편에서는 지금까지 해왔던 UI 구성부터 데이터 연동까지 전반적으로 작업을 진행해보려고 합니다. 지금까지 진행했던 작업을 요약하면 다음과 같습니다. 더치 페이 글쓰기 기능 추가 더치

victorywskim.tistory.com

 

전체 소스는 다음과 같습니다.

 

GitHub - tmvlke/SimpleDutch: 심플더치 프로젝트 입니다.

심플더치 프로젝트 입니다. Contribute to tmvlke/SimpleDutch development by creating an account on GitHub.

github.com

 

감사합니다.

반응형