این چند روز مشغول یه پروژهی kmp هستم که برای decstop ،ios و android خروجی میده. تو این پروژهها خیلی مهمه که تا حد ممکن کدها رو تو ماژول shared (commonMain) قرار داد تا هی کد تکراری ننویسیم تو هر ماژول(پلتفرم) یا بهاصطلاح باید اصل DRY پیروی کنیم. یه موضوع مهمی که برخوردم بهش بحث exception handling بود. معمولا وقتی که از compose و برای بحث async از kotlin flow استفاده میکنین، میایم برای responseمون یه دیتا کلاس wrapper ایجاد میکنیم و توش error ها و loading و ... رو هندل میکنیم. اینجوری:
interface HomeRepository { fun getPhotos( page: Int?, perPage: Int?, orderBy: String? ): Flow<DataState<List<Photo>>> }
اینجا wrapper مون کلاس DataState هستش:
data class DataState<T>( val message: String? = null, val data: T? = null, val isLoading: Boolean = false, val type: Type? = Type.SIMPLE ) { enum class Type { SIMPLE, AUTH } companion object { fun <T> error( message: String, type: Type? ): DataState<T> { return DataState( message = message, type = type ) } fun <T> data ( data : T? = null ) : DataState<T>{ return DataState( data = data ) } fun <T> loading() = DataState<T>(isLoading = true) } }
اینم repository impl:
override fun getPhotos( page: Int?, perPage: Int?, orderBy: String? ): Flow<DataState<List<Photo>>> = flow { emit(DataState.loading()) try { val photos = photoService.getPhotos( page = page, per_page = perPage, order_by = orderBy ) emit(DataState.data(photos)) } catch (e: Exception) { emit( DataState.error( message = e.message ?: "Unknown Error", type = DataState.Type.SIMPLE ) ) } }
و داخل viewModel:
@HiltViewModel class HomeViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val homeRepository: HomeRepository ) : FrameViewModel() { val state: MutableState<HomeState> = mutableStateOf(HomeState()) init { homeRepository.getPhotos( page = state.value.page, perPage = 25, orderBy = state.value.orderBy, ) . { state.value = state.value.copy(isLoading = true) } .onCompletion { if (it == null) { state.value = state.value.copy(isLoading = false) } else { Log.e("dlfjask", "there is a problem") } } .onEach { dataState -> // state.value = state.value.copy(isLoading = dataState.isLoading) Log.i(TAG, "photos: $dataState.isLoading") dataState.data?.let { photos -> state.value = state.value.copy(photos = photos) Log.i(TAG, "photos: $photos") } }.catch { Log.e(TAG, "error: $it") }.launchIn(viewModelScope)
خب این کد الان کار میکنه ولی چندین جا کدهای تکراری داریم. خب یه روش دیگه که بنظرم بهتره اینه که: اول یه extension function برای اینترفیس Flow مینویسیم تا همه error ها و loading ها رو یکجا تو یه مکان داشته باشیم و بتونیم بعدا هندل کنیم:
sealed interface Result<out T> { data class Success<T>(val data: T) : Result<T> data class Error(val exception: Throwable? = null) : Result<Nothing> data object Loading : Result<Nothing> } fun <T> Flow<T>.asResult(): Flow<Result<T>> { return this .map<T, Result<T>> { Result.Success(it) } . { emit(Result.Loading) } .catch { emit(Result.Error(it)) } }
خب دیگه نمیخواد wrap کنیم response مون رو. این میشه repository:
interface PhotoRepository { fun getPhotos() : Flow<List<Photo>> }
این هم repository impl:
fun getPhotos(sortBy: PhotoSortField = NONE): Flow<List<Photo>> { return combine( userDataRepository.userData, photoRepository.getPhoto(), ) { userData, photos -> val followedPhotos = photos .map { pohto -> FollowablePhoto( photo = pohto, isFollowed = pohto.id in userData.followedPhoto, ) } when (sortBy) { NAME -> followedPhotos.sortedBy { it.photo.name } else -> followedPhotos } } } }
خب تا اینجا که کاری به هندل کردن ارور نداشتیم. هندل کردن ارورها و لودینگها رو تو viewmodel انجام میدیم. خب یه فانکشن مینویسیم برای state های اون صفحهمون:
private fun photoUiState( photoId: String, userDataRepository: UserDataRepository, photosRepository: photosRepository, ): Flow<photoUiState> { // Observe the followed photos, as they could change over time. val followedphotoIds: Flow<Set<String>> = userDataRepository.userData .map { it.followedPhoto } // Observe photo information val photoStream: Flow<photo> = photosRepository.getphoto( id = photoId, ) return combine( followedphotoIds, photoStream, ::Pair, ) .asResult() .map { followedphotoToPhotoResult -> when (followedPhotoTophotoResult) { is Result.Success -> { val (followedphotos, photo) = followedphotoTophotoResult.data val followed = followedphotos.contains(photoId) photoUiState.Success( followablephoto = Followablephoto( photo = photo, isFollowed = followed, ), ) } is Result.Loading -> { photoUiState.Loading } is Result.Error -> { photoUiState.Error } } } }
اینم از viewmodel مون:
val photoUiState: StateFlow<photoUiState> = photoUiState( photoId = photoArgs.photoId, userDataRepository = userDataRepository, photosRepository = photosRepository, ) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = photoUiState.Loading, )
خب این هم از این.
نکته: روشهای دیگهای هم هستن مثل RxBus یا استفاده از shared flow برای پیادهسازی مکانیزمی شبیه Event Bus.
خب امیدوارم خوشتون اومده باشه. اگه نظری داشتین درباره کد میتونین بپرسین.