여기까지만 보면 enum과 다른게 없어 보이지만 큰 차이는 child의 param을 다르게 설정 할수 있으므로 유용하다.
아래 코드를 살펴보자.
sealed class ApiResult<out T> {
//로딩시 (최초값으로 사용하기)
object Loading : ApiResult<Nothing>() // 상태값이 바뀌지 않는 서브 클래스의 경우 object 를 사용하는 것을 권장
// 성공적으로 수신할 경우 body 데이터를 반환
data class Success<out T>(val data: T) : ApiResult<T>()
// 오류 메시지가 포함된 응답을 성공적으로 수신한 경우
data class Error(val code: Int, val message: String?) : ApiResult<Nothing>()
//예외 발생시
data class Exception(val e: Throwable) : ApiResult<Nothing>()
}
이런식으로 ApitResult라는 SealdClass의 안에 Loading,Success,Error,Exception 이라는 4가지 child를 구현해보았다.
Sealed Class를 사용할때는 서브클래스들은 반드시 같은 파일내에 선언해주어야한다.
Sealed Class는 자식으로 Sealed Class를 자식으로 받는것이 가능하다.
다음과 같이 ApiResult라는 SealedClass안에 Fail이라는 Sealed Class를 만들었다.
sealed class ApiResult<out T> {
//로딩시 (최초값으로 사용하기)
object Loading : ApiResult<Nothing>() // 상태값이 바뀌지 않는 서브 클래스의 경우 object 를 사용하는 것을 권장
// 성공적으로 수신할 경우 body 데이터를 반환
data class Success<out T>(val data: T) : ApiResult<T>()
sealed class Fail : ApiResult<Nothing>() {
data class Error(val code: Int,val message: String?) : Fail()
data class Exception(val e:Throwable) : Fail()
}
}
이제 Retrofit 통신을 하기 위해 interface와 Client를 구성하자.
Hilt를 사용하여 다음과 같이 구성하여 ApiModule을 만들었다,
@Module
@InstallIn(SingletonComponent::class)
class ApiModule {
@Provides
fun provideBaseUrl() = "https://jsonplaceholder.typicode.com/"
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
val connectionTimeOut = (1000 * 30).toLong()
val readTimeOut = (1000 * 5).toLong()
val interceptor = HttpLoggingInterceptor()
HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
if (!message.startsWith("{") && !message.startsWith("[")) {
Timber.tag("OkHttp").d(message)
return
}
try {
// Timber 와 Gson setPrettyPrinting 를 이용해 json 을 보기 편하게 표시해준다.
Timber.tag("OkHttp").d(GsonBuilder().setPrettyPrinting().create().toJson(
JsonParser().parse(message)))
} catch (m: JsonSyntaxException) {
Timber.tag("OkHttp").d(message)
}
}
})
interceptor.level = HttpLoggingInterceptor.Level.NONE
if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY
}
return OkHttpClient.Builder()
.readTimeout(readTimeOut, TimeUnit.MILLISECONDS)
.connectTimeout(connectionTimeOut, TimeUnit.MILLISECONDS)
.addInterceptor(interceptor)
.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient, baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Singleton
@Provides
fun providePostsService(retrofit: Retrofit): ApiInterface {
return retrofit.create(ApiInterface::class.java)
}
}
그리고 다음과 같이 interface를 만들었다.
interface ApiInterface {
@GET("posts")
suspend fun getAllPosts(
@Query("userId") userId: Int,
): List<PostItem>
@GET("photos")
suspend fun getAllPhotos(
@Query("albumId") albumId: Int,
): List<PhotoItem>
}
여기서 이상함을 느낀사람이 있을것이다.
대부분 interface에서 Response<> 형태로 받아와서 successful일때 분기처리하여 넘겨주는 것이 일반적이다.
하지만 flow를 이용하여 분기를 처리할 예정이므로 위와같이 처리를 해주고 Repository를 만들었다.
class PostRepository @Inject constructor(
private val service: ApiInterface,
) {
fun getAllPost(userId:Int): Flow<ApiResult<List<PostItem>>> =
handleFlowApi {
service.getAllPosts(userId)
}
suspend fun getAllPhotos(albumId: Int): Flow<ApiResult<List<PhotoItem>>> =
handleFlowApi {
service.getAllPhotos(albumId)
}
}
handleFlowApi 넘어온값을 핸들링해주는 함수이다. 같은 형식으로 넘어가기때문에 편하게 사용하기 위해 사용했다.
fun <T : Any> handleFlowApi(
execute: suspend () -> T,
): Flow<ApiResult<T>> = flow {
emit(ApiResult.Loading) //값 갱신전 로딩을 emit
delay(1000) // (1초대기)
try {
emit(ApiResult.Success(execute())) // execute 성공시 해당값을 Success에 담아서 반환
} catch (e: HttpException) {
emit(ApiResult.Error(code = e.code(), message = e.message())) // 네트워크 오류시 code와 메세지를 반환
} catch (e: Exception) {
emit(ApiResult.Exception(e = e)) // 예외 발생시 해당 에러를 반환
}
}
이렇게 구성하면 Repository에서 viewmodel로 ApiResult<>형태로 값을 넘겨줄것이다.
@HiltViewModel
class PostsViewModel
@Inject constructor(
private val postRepository: PostRepository,
) : ViewModel() {
val userId: StateFlow<Int> get() = _userId
private var _userId = MutableStateFlow<Int>(1)
val albumId: StateFlow<Int> get() = _albumId
private var _albumId = MutableStateFlow<Int>(1)
@OptIn(ExperimentalCoroutinesApi::class)
val postList: StateFlow<ApiResult<List<PostItem>>> =
userId.flatMapLatest { id -> //마지막 데이터 반환
postRepository.getAllPost(id) // post 요청
}.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3000L),
initialValue = ApiResult.Loading
)
@OptIn(ExperimentalCoroutinesApi::class)
val photoList: StateFlow<ApiResult<List<PhotoItem>>> =
albumId.flatMapLatest { id ->
postRepository.getAllPhotos(id)
}.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3000L),
initialValue = ApiResult.Loading
)
fun insertUserId(id: Int) {
_userId.value = id
}
fun insertPhotoUserId(id: Int) {
_albumId.value = id
}
}
이와같이 viewmodel이 구성되어있다.
넘어온 list를 ApiResult 값에 따라 분기처리 해주면 될것이다.
databinding을 사용중이라 다음과 같이 BindingAdpater를 만들어 사용했다. 그중 일부를 보여드리면
object ApiResultBinding {
@BindingAdapter("bindProgressShow")
@JvmStatic
fun View.bindShowProgress(
data: StateFlow<ApiResult<List<Any>>>,
) {
data.value.let {result ->
this.visibility = View.GONE
result.onLoading {
this.visibility = View.VISIBLE
}
}
}
@Suppress("UNCHECKED_CAST")
@BindingAdapter("bindItems")
@JvmStatic
fun RecyclerView.bindItems(
data: StateFlow<ApiResult<List<Any>>>,
) {
data.value.let { result ->
result.onSuccess {
when (val mAdapter = this.adapter) {
is PhotoAdapter -> {
(mAdapter).insertList(it as List<PhotoItem>)
}
is PostAdapter -> {
mAdapter.insertList(it as List<PostItem>)
}
else -> {
}
}
}
}
}
}
Loading일때 Progress 처리
success일때 data 처리 역할을 해준다.
추가로 onSuccess같은 함수는 편하게 쓰기위해 inline함수로 구현하여 사용했다.
// inline function .. 반복 개체생성이 안됨
// reified : 인라인(inline) 함수와 reified 키워드를 함께 사용하면 T type에 대해서 런타임에 접근할 수 있게 해줌.
inline fun <reified T : Any> ApiResult<T>.onLoading(action: () -> Unit) {
if (this is ApiResult.Loading) action()
}
inline fun <reified T : Any> ApiResult<T>.onSuccess(action: (data: T) -> Unit) {
if (this is ApiResult.Success) action(data)
}
inline fun <reified T : Any> ApiResult<T>.onError(action: (code: Int, message: String?) -> Unit) {
if (this is ApiResult.Error) action(code, message)
}
inline fun <reified T : Any> ApiResult<T>.onException(action: (e: Throwable) -> Unit) {
if (this is ApiResult.Exception) action(e)
}
이상입니다.