이번 편에서는 진행 예정인 작업은 다음과 같습니다.
그럼 작업을 진행해보겠습니다.
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) (0) | 2023.12.21 |
---|---|
나만의 안드로이드 앱 만들기(초보자 편) - 페이지 구성 (7) (1) | 2023.12.10 |
나만의 안드로이드 앱 만들기(초보자 편) - UI (6-6) (0) | 2023.11.11 |
나만의 안드로이드 앱 만들기(초보자 편) - DI (6-5) (0) | 2023.11.11 |
나만의 안드로이드 앱 만들기(초보자 편) - core (6-4) (0) | 2023.11.11 |