Skip to content

Instantly share code, notes, and snippets.

@mahdiPourkazemi
Created August 4, 2025 13:50
Show Gist options
  • Save mahdiPourkazemi/d25fe155485056ba226f74ad9dce6557 to your computer and use it in GitHub Desktop.
Save mahdiPourkazemi/d25fe155485056ba226f74ad9dce6557 to your computer and use it in GitHub Desktop.
example of handling UI state with data request
// 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