Skip to content

Commit b3622cc

Browse files
authored
Merge pull request #31 from everymeals/feature/base
[feature/base] BaseViewModel 및 State Hoisting 패턴 적용
2 parents bd84b1a + e6480f3 commit b3622cc

File tree

10 files changed

+189
-16
lines changed

10 files changed

+189
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.everymeal.presentation.base
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import kotlinx.coroutines.channels.Channel
6+
import kotlinx.coroutines.flow.*
7+
import kotlinx.coroutines.launch
8+
9+
abstract class BaseViewModel<S : ViewState, A : ViewSideEffect, E : ViewEvent>(
10+
initialState: S
11+
) : ViewModel() {
12+
13+
abstract fun handleEvents(event: E)
14+
15+
private val _viewState: MutableStateFlow<S> = MutableStateFlow<S>(initialState)
16+
val viewState = _viewState.asStateFlow()
17+
18+
private val currentState: S
19+
get() = _viewState.value
20+
21+
private val _effect: Channel<A> = Channel()
22+
val effect = _effect.receiveAsFlow()
23+
24+
private val _event: MutableSharedFlow<E> = MutableSharedFlow()
25+
26+
protected fun updateState(reducer: S.() -> S) {
27+
val newState = currentState.reducer()
28+
_viewState.value = newState
29+
}
30+
31+
protected fun sendEffect(vararg builder: () -> A) {
32+
for (effectValue in builder) {
33+
viewModelScope.launch { _effect.send(effectValue()) }
34+
}
35+
}
36+
37+
open fun setEvent(event : E) {
38+
deliverEvent(event)
39+
}
40+
41+
private fun deliverEvent(event : E) = viewModelScope.launch {
42+
handleEvents(event)
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.everymeal.presentation.base
2+
3+
enum class LoadState {
4+
SUCCESS,
5+
LOADING,
6+
ERROR
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.everymeal.presentation.base
2+
3+
interface ViewEvent {
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.everymeal.presentation.base
2+
3+
interface ViewSideEffect {
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.everymeal.presentation.base
2+
3+
interface ViewState {
4+
}

presentation/src/main/java/com/everymeal/presentation/components/EveryMealButton.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.compose.ui.unit.sp
1818
import com.everymeal.presentation.R
1919
import com.everymeal.presentation.ui.theme.Gray200
2020
import com.everymeal.presentation.ui.theme.Main100
21+
import com.everymeal.presentation.ui.theme.Main800
2122

2223
@Composable
2324
fun EveryMealMainButton(
@@ -30,7 +31,10 @@ fun EveryMealMainButton(
3031
.fillMaxWidth()
3132
.padding(vertical = 16.dp),
3233
shape = RoundedCornerShape(12.dp),
33-
colors = ButtonDefaults.buttonColors(containerColor = if(enabled) Main100 else Gray200),
34+
colors = ButtonDefaults.buttonColors(
35+
containerColor = if(enabled) Main100 else Gray200,
36+
contentColor = Main800
37+
),
3438
enabled = enabled,
3539
onClick = onClick,
3640
) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.everymeal.presentation.ui.signup
2+
3+
import com.everymeal.presentation.base.LoadState
4+
import com.everymeal.presentation.base.ViewEvent
5+
import com.everymeal.presentation.base.ViewSideEffect
6+
import com.everymeal.presentation.base.ViewState
7+
8+
class UnivSelectContract {
9+
10+
/*
11+
대학교 불러오기 LoadState
12+
대학교 선택하기 State Hoisting
13+
*/
14+
data class UnivSelectState(
15+
val univSelectLoadState: LoadState = LoadState.SUCCESS,
16+
val selectedUniv: String = ""
17+
) : ViewState
18+
19+
/*
20+
선택하기 버튼 ViewEvent
21+
대학교 선택하기 ViewEvent
22+
*/
23+
sealed class UnivSelectEvent : ViewEvent {
24+
object SelectButtonClicked : UnivSelectEvent()
25+
data class SelectedUniv(
26+
val selectedUniv: String
27+
) : UnivSelectEvent()
28+
}
29+
30+
/*
31+
메인 화면으로 이동 ViewSideEffect
32+
*/
33+
sealed class UnivSelectEffect : ViewSideEffect {
34+
object MoveToMain: UnivSelectEffect()
35+
}
36+
}

presentation/src/main/java/com/everymeal/presentation/ui/signup/UnivSelectScreen.kt

+54-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.everymeal.presentation.ui.signup
22

3+
import android.annotation.SuppressLint
34
import androidx.compose.foundation.Image
45
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.clickable
7+
import androidx.compose.foundation.interaction.MutableInteractionSource
58
import androidx.compose.foundation.layout.Arrangement
69
import androidx.compose.foundation.layout.Box
710
import androidx.compose.foundation.layout.Column
@@ -17,6 +20,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
1720
import androidx.compose.material3.Icon
1821
import androidx.compose.material3.Text
1922
import androidx.compose.runtime.Composable
23+
import androidx.compose.runtime.LaunchedEffect
24+
import androidx.compose.runtime.collectAsState
25+
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.remember
2027
import androidx.compose.ui.Alignment
2128
import androidx.compose.ui.Modifier
2229
import androidx.compose.ui.draw.clip
@@ -30,13 +37,15 @@ import androidx.compose.ui.text.font.FontWeight
3037
import androidx.compose.ui.tooling.preview.Preview
3138
import androidx.compose.ui.unit.dp
3239
import androidx.compose.ui.unit.sp
40+
import androidx.hilt.navigation.compose.hiltViewModel
3341
import com.everymeal.presentation.R
3442
import com.everymeal.presentation.components.EveryMealMainButton
3543
import com.everymeal.presentation.ui.theme.EveryMeal_AndroidTheme
3644
import com.everymeal.presentation.ui.theme.Gray100
3745
import com.everymeal.presentation.ui.theme.Gray300
3846
import com.everymeal.presentation.ui.theme.Gray500
3947
import com.everymeal.presentation.ui.theme.Gray800
48+
import com.everymeal.presentation.ui.theme.Paddings
4049

4150
data class Item(
4251
val Image: Int,
@@ -45,8 +54,11 @@ data class Item(
4554

4655
@Composable
4756
fun UnivSelectScreen(
48-
onSelectClick : () -> Unit
57+
viewModel: UnivSelectViewModel = hiltViewModel(),
58+
onUnivSelectClick : () -> Unit,
4959
) {
60+
val viewState by viewModel.viewState.collectAsState()
61+
5062
val items = listOf(
5163
Item(Image = R.drawable.image_myongji, name = "명지대"),
5264
Item(Image = R.drawable.image_sungsin, name = "성신여대"),
@@ -62,7 +74,7 @@ fun UnivSelectScreen(
6274
Column(
6375
modifier = Modifier
6476
.fillMaxSize()
65-
.padding(horizontal = 24.dp)
77+
.padding(horizontal = Paddings.extra)
6678
) {
6779
Spacer(modifier = Modifier.padding(58.dp))
6880
Text(
@@ -78,14 +90,21 @@ fun UnivSelectScreen(
7890
modifier = Modifier.weight(1f),
7991
) {
8092
items(items.size) { index ->
81-
UnivSelectItem(item = items[index])
93+
val item = items[index]
94+
val isSelected = viewState.selectedUniv == item.name
95+
UnivSelectItem(
96+
item = item,
97+
isSelected = isSelected,
98+
) {
99+
viewModel.setEvent(UnivSelectContract.UnivSelectEvent.SelectedUniv(item.name))
100+
}
82101
}
83102
}
84103
Row(
85104
modifier = Modifier
86105
.fillMaxWidth()
87106
.background(Gray300, RoundedCornerShape(100.dp))
88-
.padding(horizontal = 24.dp, vertical = 14.dp),
107+
.padding(horizontal = Paddings.extra, vertical = 14.dp),
89108
verticalAlignment = Alignment.CenterVertically,
90109
) {
91110
Icon(
@@ -115,21 +134,36 @@ fun UnivSelectScreen(
115134
}
116135
EveryMealMainButton(
117136
text = stringResource(R.string.select),
118-
enabled = false,
137+
enabled = viewState.selectedUniv.isNotEmpty(),
119138
) {
120-
onSelectClick()
139+
viewModel.setEvent(UnivSelectContract.UnivSelectEvent.SelectButtonClicked)
140+
}
141+
}
142+
}
143+
144+
LaunchedEffect(key1 = viewModel.effect) {
145+
viewModel.effect.collect { effect ->
146+
when(effect) {
147+
UnivSelectContract.UnivSelectEffect.MoveToMain -> {
148+
onUnivSelectClick()
149+
}
121150
}
122151
}
123152
}
124153
}
125154

155+
@SuppressLint("RememberReturnType")
126156
@Composable
127-
fun UnivSelectItem(item: Item) {
157+
fun UnivSelectItem(item: Item, isSelected: Boolean, onSelectClick: (Item) -> Unit) {
128158
Column(
129159
modifier = Modifier
130-
.padding(8.dp)
131-
.clip(RoundedCornerShape(8.dp))
132-
.background(Gray100)
160+
.clickable(
161+
indication = null,
162+
interactionSource = remember { MutableInteractionSource() }
163+
) { onSelectClick(item) }
164+
.padding(Paddings.medium)
165+
.clip(RoundedCornerShape(Paddings.medium))
166+
.background(if (isSelected) Gray500 else Gray100)
133167
.fillMaxSize(),
134168
horizontalAlignment = Alignment.CenterHorizontally,
135169
verticalArrangement = Arrangement.Center
@@ -152,7 +186,9 @@ fun UnivSelectItem(item: Item) {
152186
@Composable
153187
fun UnivSelectScreenPreview() {
154188
EveryMeal_AndroidTheme {
155-
UnivSelectScreen{ }
189+
UnivSelectScreen {
190+
191+
}
156192
}
157193
}
158194

@@ -161,8 +197,12 @@ fun UnivSelectScreenPreview() {
161197
fun UnivSelectScreenItemPreview() {
162198
EveryMeal_AndroidTheme {
163199
UnivSelectItem(item = Item(
164-
Image = R.drawable.image_myongji,
165-
name = "명지대학교"
166-
))
200+
Image = R.drawable.image_myongji,
201+
name = "명지대학교"
202+
),
203+
false
204+
) {
205+
206+
}
167207
}
168208
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.everymeal.presentation.ui.signup
2+
3+
import com.everymeal.presentation.base.BaseViewModel
4+
import com.everymeal.presentation.ui.signup.UnivSelectContract.UnivSelectEvent
5+
import com.everymeal.presentation.ui.signup.UnivSelectContract.UnivSelectEffect
6+
import com.everymeal.presentation.ui.signup.UnivSelectContract.UnivSelectState
7+
import dagger.hilt.android.lifecycle.HiltViewModel
8+
import javax.inject.Inject
9+
10+
@HiltViewModel
11+
class UnivSelectViewModel @Inject constructor(
12+
13+
): BaseViewModel<UnivSelectState, UnivSelectEffect, UnivSelectEvent>(
14+
UnivSelectState()
15+
) {
16+
17+
override fun handleEvents(event: UnivSelectEvent) {
18+
when(event) {
19+
is UnivSelectEvent.SelectButtonClicked -> {
20+
sendEffect({ UnivSelectEffect.MoveToMain })
21+
}
22+
is UnivSelectEvent.SelectedUniv -> {
23+
updateState { copy(
24+
selectedUniv = event.selectedUniv
25+
) }
26+
}
27+
}
28+
}
29+
}

presentation/src/main/java/com/everymeal/presentation/ui/theme/Color.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ val Gray300 = Color(0xFFF2F4F6)
1616
val Gray500 = Color(0xFFB0B8C1)
1717
var Gray800 = Color(0xFF4E5968)
1818

19-
val Main100 = Color(0xFFFF4848)
19+
val Main100 = Color(0xFFFF4848)
20+
val Main800 = Color(0xFFCC3939)

0 commit comments

Comments
 (0)