Created
August 4, 2025 13:50
-
-
Save mahdiPourkazemi/d25fe155485056ba226f74ad9dce6557 to your computer and use it in GitHub Desktop.
example of handling UI state with data request
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// UiState بهبود یافته با generic type | |
sealed class UiState<out T> { | |
object Loading : UiState<Nothing>() | |
data class Success<T>(val data: T) : UiState<T>() | |
data class Error( | |
val throwable: Throwable, | |
val userMessage: String, | |
val isRetryable: Boolean = true | |
) : UiState<Nothing>() | |
} | |
// Domain-specific UiState | |
typealias NewsUiState = UiState<List<News>> | |
class ImprovedNewsViewModel( | |
private val newsRepository: NewsRepository | |
) : ViewModel() { | |
private val _refreshTrigger = MutableSharedFlow<Unit>( | |
replay = 0, // بدون replay | |
extraBufferCapacity = Channel.UNLIMITED // جلوگیری از back-pressure | |
) | |
val uiState: StateFlow<NewsUiState> = _refreshTrigger | |
.onStart { emit(Unit) } | |
.flatMapLatest { | |
loadNews() | |
.onStart { emit(UiState.Loading) } | |
} | |
.distinctUntilChanged() // جلوگیری از emit های تکراری | |
.stateIn( | |
scope = viewModelScope, | |
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), | |
initialValue = UiState.Loading | |
) | |
private fun loadNews(): Flow<NewsUiState> = flow { | |
try { | |
val news = newsRepository.getLatestNews() | |
if (news.isEmpty()) { | |
emit(UiState.Error( | |
throwable = NoDataException("هیچ خبری یافت نشد"), | |
userMessage = "در حال حاضر خبری موجود نیست", | |
isRetryable = true | |
)) | |
} else { | |
emit(UiState.Success(news)) | |
} | |
} catch (e: Exception) { | |
emit(handleError(e)) | |
} | |
} // Repository خودش flowOn(Dispatchers.IO) داره | |
// جداسازی منطق handle کردن error | |
private fun handleError(throwable: Throwable): UiState.Error { | |
val (userMessage, isRetryable) = when (throwable) { | |
is IOException -> "اتصال اینترنت برقرار نیست" to true | |
is HttpException -> { | |
val message = when (throwable.code()) { | |
in 400..499 -> "درخواست نامعتبر" | |
in 500..599 -> "خطای سرور (${throwable.code()})" | |
else -> "خطای شبکه" | |
} | |
message to true | |
} | |
is TimeoutException -> "زمان اتصال به پایان رسید" to true | |
is NoDataException -> throwable.message ?: "دادهای یافت نشد" to true | |
else -> "خطای ناشناخته" to true | |
} | |
return UiState.Error(throwable, userMessage, isRetryable) | |
} | |
// متد برای refresh کردن | |
fun refresh() { | |
_refreshTrigger.tryEmit(Unit) | |
} | |
// متد برای retry در صورت خطا | |
fun retry() { | |
refresh() | |
} | |
} | |
// Exception های سفارشی | |
class NoDataException(message: String) : Exception(message) | |
// Repository بهبود یافته با proper threading | |
class NewsRepository( | |
private val apiService: NewsApiService, | |
private val newsDao: NewsDao | |
) { | |
suspend fun getLatestNews(): List<News> = withContext(Dispatchers.IO) { | |
try { | |
// ابتدا از cache بخوان | |
val cachedNews = newsDao.getAllNews() | |
if (cachedNews.isNotEmpty() && isCacheValid()) { | |
return@withContext cachedNews | |
} | |
// سپس از API | |
val apiNews = apiService.getLatestNews() | |
// cache کن | |
newsDao.insertAll(apiNews) | |
apiNews | |
} catch (e: Exception) { | |
// در صورت خطای API، از cache استفاده کن | |
val cachedNews = newsDao.getAllNews() | |
if (cachedNews.isNotEmpty()) { | |
cachedNews | |
} else { | |
throw e | |
} | |
} | |
} | |
private suspend fun isCacheValid(): Boolean { | |
// منطق validation cache (مثلاً بررسی timestamp) | |
return false // پیادهسازی براساس نیاز | |
} | |
} | |
// استفاده در Composable | |
@Composable | |
fun NewsScreen( | |
viewModel: NewsViewModel = hiltViewModel() | |
) { | |
val uiState by viewModel.uiState.collectAsState() | |
// Pull-to-refresh support | |
val pullRefreshState = rememberPullRefreshState( | |
refreshing = uiState is UiState.Loading, | |
onRefresh = { viewModel.refresh() } | |
) | |
Box( | |
modifier = Modifier | |
.fillMaxSize() | |
.pullRefresh(pullRefreshState) | |
) { | |
when (uiState) { | |
is UiState.Loading -> { | |
LoadingContent() | |
} | |
is UiState.Success -> { | |
LazyColumn( | |
modifier = Modifier.fillMaxSize(), | |
contentPadding = PaddingValues(16.dp), | |
verticalArrangement = Arrangement.spacedBy(8.dp) | |
) { | |
items( | |
items = uiState.data, | |
key = { it.id } // برای بهتر شدن performance | |
) { news -> | |
NewsItem(news = news) | |
} | |
} | |
} | |
is UiState.Error -> { | |
ErrorContent( | |
message = uiState.userMessage, | |
isRetryable = uiState.isRetryable, | |
onRetry = { viewModel.retry() } | |
) | |
} | |
} | |
PullRefreshIndicator( | |
refreshing = uiState is UiState.Loading, | |
state = pullRefreshState, | |
modifier = Modifier.align(Alignment.TopCenter) | |
) | |
} | |
} | |
@Composable | |
private fun LoadingContent() { | |
Box( | |
modifier = Modifier.fillMaxSize(), | |
contentAlignment = Alignment.Center | |
) { | |
Column( | |
horizontalAlignment = Alignment.CenterHorizontally, | |
verticalArrangement = Arrangement.spacedBy(16.dp) | |
) { | |
CircularProgressIndicator() | |
Text( | |
text = "در حال بارگذاری...", | |
style = MaterialTheme.typography.bodyMedium, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
} | |
} | |
} | |
@Composable | |
private fun ErrorContent( | |
message: String, | |
isRetryable: Boolean, | |
onRetry: () -> Unit | |
) { | |
Column( | |
modifier = Modifier | |
.fillMaxSize() | |
.padding(32.dp), | |
horizontalAlignment = Alignment.CenterHorizontally, | |
verticalArrangement = Arrangement.Center | |
) { | |
Icon( | |
imageVector = Icons.Default.Error, | |
contentDescription = null, | |
tint = MaterialTheme.colorScheme.error, | |
modifier = Modifier.size(64.dp) | |
) | |
Spacer(modifier = Modifier.height(16.dp)) | |
Text( | |
text = message, | |
style = MaterialTheme.typography.bodyLarge, | |
textAlign = TextAlign.Center, | |
color = MaterialTheme.colorScheme.onSurface | |
) | |
if (isRetryable) { | |
Spacer(modifier = Modifier.height(24.dp)) | |
Button( | |
onClick = onRetry, | |
modifier = Modifier.fillMaxWidth() | |
) { | |
Icon( | |
imageVector = Icons.Default.Refresh, | |
contentDescription = null, | |
modifier = Modifier.size(18.dp) | |
) | |
Spacer(modifier = Modifier.width(8.dp)) | |
Text("تلاش مجدد") | |
} | |
} | |
} | |
} | |
// Extension function برای بهتر شدن error handling | |
inline fun <T> Flow<T>.catchAndHandle( | |
crossinline handler: suspend (Throwable) -> T | |
): Flow<T> = catch { throwable -> | |
emit(handler(throwable)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment