Skip to content

Commit 3394e5a

Browse files
[Feat/#1566] SP1 알림 화면 Pull to refresh 추가 (#1567)
* [FEAT/#1566] 알림 화면 Pull to refresh 추가 - `LoadState` 상태에 따른 UI 분기 - 알림 전체 읽기 상태를 업데이트할 때, UI와 상태의 동기화 보장하기 위해 `NotificationViewModel` onSuccess 콜백을 받도록 수정 - LazyColumn의 각 아이템에 notificationId를 Key로 할당하여 리스트 렌더링 성능 개선 * [FIX/#1566] Pull to refresh 로딩 조건 수정 * [EDIT/#1566] updateEntireNotificationReadingState 함수 호출 로직 수정
1 parent d8a223c commit 3394e5a

2 files changed

Lines changed: 104 additions & 43 deletions

File tree

feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationActivity.kt

Lines changed: 98 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,36 +33,49 @@ import androidx.appcompat.app.AppCompatActivity
3333
import androidx.compose.foundation.Image
3434
import androidx.compose.foundation.clickable
3535
import androidx.compose.foundation.layout.Arrangement
36+
import androidx.compose.foundation.layout.Box
3637
import androidx.compose.foundation.layout.Column
3738
import androidx.compose.foundation.layout.Row
3839
import androidx.compose.foundation.layout.Spacer
3940
import androidx.compose.foundation.layout.fillMaxSize
4041
import androidx.compose.foundation.layout.height
4142
import androidx.compose.foundation.layout.padding
4243
import androidx.compose.foundation.lazy.LazyColumn
44+
import androidx.compose.material.ExperimentalMaterialApi
45+
import androidx.compose.material.pullrefresh.PullRefreshIndicator
46+
import androidx.compose.material.pullrefresh.pullRefresh
47+
import androidx.compose.material.pullrefresh.rememberPullRefreshState
4348
import androidx.compose.material3.CenterAlignedTopAppBar
4449
import androidx.compose.material3.ExperimentalMaterial3Api
4550
import androidx.compose.material3.Icon
4651
import androidx.compose.material3.IconButton
4752
import androidx.compose.material3.Scaffold
4853
import androidx.compose.material3.Text
4954
import androidx.compose.material3.TopAppBarDefaults
55+
import androidx.compose.runtime.LaunchedEffect
5056
import androidx.compose.runtime.getValue
57+
import androidx.compose.runtime.mutableStateOf
5158
import androidx.compose.runtime.remember
59+
import androidx.compose.runtime.rememberCoroutineScope
60+
import androidx.compose.runtime.setValue
5261
import androidx.compose.ui.Alignment
5362
import androidx.compose.ui.Modifier
5463
import androidx.compose.ui.graphics.vector.ImageVector
5564
import androidx.compose.ui.platform.LocalContext
5665
import androidx.compose.ui.res.vectorResource
5766
import androidx.compose.ui.unit.dp
5867
import androidx.lifecycle.compose.collectAsStateWithLifecycle
68+
import androidx.paging.LoadState
5969
import androidx.paging.compose.collectAsLazyPagingItems
6070
import dagger.hilt.android.AndroidEntryPoint
6171
import dagger.hilt.android.EntryPointAccessors
72+
import kotlinx.coroutines.launch
6273
import org.sopt.official.common.navigator.NavigatorEntryPoint
6374
import org.sopt.official.common.view.toast
6475
import org.sopt.official.designsystem.Orange400
6576
import org.sopt.official.designsystem.SoptTheme
77+
import org.sopt.official.designsystem.component.dialog.NetworkErrorDialog
78+
import org.sopt.official.designsystem.component.indicator.LoadingIndicator
6679
import org.sopt.official.feature.notification.R
6780
import org.sopt.official.feature.notification.all.component.NotificationCategoryChip
6881
import org.sopt.official.feature.notification.all.component.NotificationInfoItem
@@ -71,18 +84,38 @@ import org.sopt.official.feature.notification.all.component.NotificationInfoItem
7184
class NotificationActivity : AppCompatActivity() {
7285
private val viewModel by viewModels<NotificationViewModel>()
7386

74-
@OptIn(ExperimentalMaterial3Api::class)
87+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
7588
override fun onCreate(savedInstanceState: Bundle?) {
7689
super.onCreate(savedInstanceState)
7790
setContent {
7891
val notifications = viewModel.notifications.collectAsLazyPagingItems()
92+
var isPullRefreshing by remember { mutableStateOf(false) }
93+
94+
val refreshLoadState = notifications.loadState.refresh
95+
val refreshState = rememberPullRefreshState(
96+
refreshing = isPullRefreshing,
97+
onRefresh = {
98+
isPullRefreshing = true
99+
notifications.refresh()
100+
}
101+
)
102+
103+
val coroutineScope = rememberCoroutineScope()
104+
79105
val context = LocalContext.current
80106
val navigator = remember {
81107
EntryPointAccessors.fromApplication(
82108
context,
83109
NavigatorEntryPoint::class.java
84110
).navigatorProvider()
85111
}
112+
113+
LaunchedEffect(refreshLoadState) {
114+
if (refreshLoadState is LoadState.NotLoading || refreshLoadState is LoadState.Error) {
115+
isPullRefreshing = false
116+
}
117+
}
118+
86119
SoptTheme {
87120
val state by viewModel.state.collectAsStateWithLifecycle()
88121

@@ -115,8 +148,11 @@ class NotificationActivity : AppCompatActivity() {
115148
modifier = Modifier
116149
.padding(end = 20.dp)
117150
.clickable{
118-
viewModel.updateEntireNotificationReadingState()
119-
notifications.refresh()
151+
coroutineScope.launch {
152+
viewModel.updateEntireNotificationReadingState().onSuccess {
153+
notifications.refresh()
154+
}
155+
}
120156
}
121157
.padding(vertical = 8.dp, horizontal = 4.dp)
122158
)
@@ -152,41 +188,70 @@ class NotificationActivity : AppCompatActivity() {
152188
}
153189
}
154190

155-
if (notifications.itemCount > 0) {
156-
LazyColumn {
157-
items(notifications.itemCount) {
158-
val notification = notifications[it]
159-
160-
NotificationInfoItem(
161-
notification = notification,
162-
onCLick = {
163-
if (notification?.notificationId == null) {
164-
toast("문제가 발생했습니다.")
165-
} else {
166-
context.startActivity(navigator.getNotificationDetailActivityIntent(notification.notificationId))
191+
Box(
192+
modifier = Modifier
193+
.fillMaxSize()
194+
.pullRefresh(refreshState)
195+
) {
196+
when (refreshLoadState) {
197+
is LoadState.Loading -> {
198+
if (notifications.itemCount == 0 && !isPullRefreshing) {
199+
LoadingIndicator()
200+
}
201+
}
202+
is LoadState.NotLoading -> {
203+
if (notifications.itemCount > 0) {
204+
LazyColumn {
205+
items(
206+
count = notifications.itemCount,
207+
key = { index -> notifications[index]?.notificationId ?: index }
208+
) { index ->
209+
val notification = notifications[index]
210+
NotificationInfoItem(
211+
notification = notification,
212+
onCLick = {
213+
if (notification?.notificationId == null) {
214+
context.toast("문제가 발생했습니다.")
215+
} else {
216+
context.startActivity(navigator.getNotificationDetailActivityIntent(notification.notificationId))
217+
}
218+
}
219+
)
167220
}
168221
}
222+
} else {
223+
Column(
224+
modifier = Modifier.fillMaxSize(),
225+
verticalArrangement = Arrangement.Center,
226+
horizontalAlignment = Alignment.CenterHorizontally
227+
) {
228+
Image(
229+
imageVector = ImageVector.vectorResource(R.drawable.icon_notification_empty),
230+
contentDescription = "알림이 없습니다."
231+
)
232+
Spacer(modifier = Modifier.height(24.dp))
233+
Text(
234+
text = "아직 도착한 알림이 없어요.",
235+
style = SoptTheme.typography.heading18B,
236+
color = SoptTheme.colors.onSurface400
237+
)
238+
}
239+
}
240+
}
241+
is LoadState.Error -> {
242+
NetworkErrorDialog(
243+
onConfirm = {
244+
notifications.refresh()
245+
}
169246
)
170247
}
171248
}
172-
} else {
173-
Column(
174-
modifier = Modifier
175-
.fillMaxSize(),
176-
verticalArrangement = Arrangement.Center,
177-
horizontalAlignment = Alignment.CenterHorizontally
178-
) {
179-
Image(
180-
imageVector = ImageVector.vectorResource(R.drawable.icon_notification_empty),
181-
contentDescription = "알림이 없습니다."
182-
)
183-
Spacer(modifier = Modifier.height(24.dp))
184-
Text(
185-
text = "아직 도착한 알림이 없어요.",
186-
style = SoptTheme.typography.heading18B,
187-
color = SoptTheme.colors.onSurface400
188-
)
189-
}
249+
250+
PullRefreshIndicator(
251+
refreshing = isPullRefreshing,
252+
state = refreshState,
253+
modifier = Modifier.align(Alignment.TopCenter)
254+
)
190255
}
191256
}
192257
}

feature/notification/src/main/java/org/sopt/official/feature/notification/all/NotificationViewModel.kt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,14 @@ import androidx.paging.Pager
3030
import androidx.paging.PagingConfig
3131
import androidx.paging.cachedIn
3232
import dagger.hilt.android.lifecycle.HiltViewModel
33-
import javax.inject.Inject
3433
import kotlinx.coroutines.ExperimentalCoroutinesApi
3534
import kotlinx.coroutines.flow.MutableStateFlow
3635
import kotlinx.coroutines.flow.asStateFlow
3736
import kotlinx.coroutines.flow.flatMapLatest
3837
import kotlinx.coroutines.flow.update
39-
import kotlinx.coroutines.launch
4038
import org.sopt.official.domain.notification.repository.NotificationRepository
4139
import timber.log.Timber
40+
import javax.inject.Inject
4241

4342
@HiltViewModel
4443
class NotificationViewModel @Inject constructor(
@@ -56,15 +55,12 @@ class NotificationViewModel @Inject constructor(
5655
}.flow
5756
}.cachedIn(viewModelScope)
5857

59-
fun updateEntireNotificationReadingState() {
60-
viewModelScope.launch {
61-
runCatching {
62-
repository.updateEntireNotificationReadingState()
63-
}.onFailure {
64-
Timber.e(it)
65-
}
58+
suspend fun updateEntireNotificationReadingState(): Result<Unit> =
59+
runCatching {
60+
repository.updateEntireNotificationReadingState()
61+
}.onFailure {
62+
Timber.e(it)
6663
}
67-
}
6864

6965
fun updateNotificationCategory(category: NotificationCategory) {
7066
_state.update {

0 commit comments

Comments
 (0)