Skip to content

Commit

Permalink
BREAKING CHANGE:
Browse files Browse the repository at this point in the history
ViewState has a ViewStatefulEvent.kt as a state descriptor and payload as something that holds the value

ViewStateContract.kt data is now renamed to viewState

ViewEventContract now doesn't force you to integrate

val viewEvent: Flow<ViewEvent>

and is also a functional contract

Check MVIFragment.kt for sample
  • Loading branch information
FunkyMuse committed May 2, 2022
1 parent 59f36c1 commit 39d080f
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import com.crazylegend.retrofit.adapter.ApiResultAdapterFactory
import com.crazylegend.retrofit.apiresult.ApiResult
import com.crazylegend.retrofit.interceptors.ConnectivityInterceptor
import com.crazylegend.retrofit.randomPhotoIndex
import com.crazylegend.retrofit.viewstate.event.ViewStatefulEvent
import com.crazylegend.retrofit.viewstate.state.ViewState
import com.crazylegend.retrofit.viewstate.state.ViewStateContract
import com.crazylegend.retrofit.viewstate.state.asViewStatePayloadWithEvents
import com.crazylegend.setofusefulkotlinextensions.adapter.TestModel
import kotlinx.coroutines.delay
Expand All @@ -33,13 +33,10 @@ class TestAVM(
) : AndroidViewModel(application) {

private val viewEventProvider = ViewEventProvider()
private val viewState = ViewState<List<TestModel>>(viewEventProvider)
val viewState = ViewState<List<TestModel>>(viewEventProvider)

//use delegation to avoid having these properties since the view event provider can be injected easily
val payload: List<TestModel>? get() = viewState.payload
val isDataNotLoaded get() = viewState.isDataNotLoaded
val isDataLoaded: Boolean get() = viewState.isDataLoaded
val viewEvent = viewEventProvider.viewEvent
val viewEvent = viewEventProvider.viewStatefulEvent
//

sealed class TestAVMIntent {
Expand All @@ -52,7 +49,7 @@ class TestAVM(
}

private val intents = MutableSharedFlow<TestAVMIntent>()
val posts: StateFlow<ApiResult<List<TestModel>>> = viewState.data
val posts: StateFlow<ViewStatefulEvent> = viewState.viewState

private fun getPosts() {
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.crazylegend.setofusefulkotlinextensions

import com.crazylegend.retrofit.viewstate.event.ViewEvent
import com.crazylegend.retrofit.viewstate.event.ViewStatefulEvent
import com.crazylegend.retrofit.viewstate.event.ViewEventContract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
Expand All @@ -11,10 +11,10 @@ import kotlinx.coroutines.withContext
//should be injected
class ViewEventProvider : ViewEventContract {

private val channelEvents: Channel<ViewEvent> = Channel(Channel.BUFFERED)
override val viewEvent: Flow<ViewEvent> = channelEvents.receiveAsFlow()
private val channelEvents: Channel<ViewStatefulEvent> = Channel(Channel.BUFFERED)
val viewStatefulEvent: Flow<ViewStatefulEvent> = channelEvents.receiveAsFlow()

override suspend fun provideEvent(viewEvent: ViewEvent) = withContext(Dispatchers.Main.immediate) {
channelEvents.send(viewEvent)
override suspend fun provideEvent(viewStatefulEvent: ViewStatefulEvent) = withContext(Dispatchers.Main.immediate) {
channelEvents.send(viewStatefulEvent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ import com.crazylegend.common.ifTrue
import com.crazylegend.internetdetector.InternetDetector
import com.crazylegend.lifecycle.repeatingJobOnStarted
import com.crazylegend.lifecycle.viewCoroutineScope
import com.crazylegend.retrofit.apiresult.*
import com.crazylegend.retrofit.throwables.isNoConnectionException
import com.crazylegend.retrofit.viewstate.*
import com.crazylegend.retrofit.viewstate.event.ViewEvent
import com.crazylegend.retrofit.viewstate.event.ViewStatefulEvent
import com.crazylegend.retrofit.viewstate.event.asApiErrorBody
import com.crazylegend.retrofit.viewstate.event.getAsThrowable
import com.crazylegend.retrofit.viewstate.event.isApiError
import com.crazylegend.retrofit.viewstate.event.isError
import com.crazylegend.retrofit.viewstate.state.handleApiError
import com.crazylegend.retrofit.viewstate.state.showEmptyDataOnErrors
import com.crazylegend.retrofit.viewstate.state.showLoadingWhenDataIsLoaded
import com.crazylegend.retrofit.viewstate.state.showLoadingWhenDataNotLoaded
import com.crazylegend.setofusefulkotlinextensions.R
import com.crazylegend.setofusefulkotlinextensions.TestAVM
import com.crazylegend.setofusefulkotlinextensions.adapter.TestModel
import com.crazylegend.setofusefulkotlinextensions.adapter.TestViewBindingAdapter
import com.crazylegend.setofusefulkotlinextensions.databinding.FragmentTestBinding
import com.crazylegend.view.setIsNotRefreshing
Expand Down Expand Up @@ -55,17 +56,18 @@ class MVIFragment : Fragment(R.layout.fragment_test) {
binding.swipeToRefresh.setOnRefreshListener {
binding.swipeToRefresh.setIsRefreshing()
getApiPosts()
binding.swipeToRefresh.setIsNotRefreshing()
}
binding.staticPosts.setOnClickListenerCooldown {
testAVM.sendEvent(TestAVM.TestAVMIntent.GetRandomPosts)
}
}

private fun handleViewEvents(viewEvent: ViewEvent) {
viewEvent.isError.and(testAVM.isDataLoaded).ifTrue(::showErrorSnack)
viewEvent.isApiError.ifTrue {
private fun handleViewEvents(viewStatefulEvent: ViewStatefulEvent) {
viewStatefulEvent.isError.and(testAVM.viewState.isDataLoaded).ifTrue(::showErrorSnack)
viewStatefulEvent.isApiError.ifTrue {
toast?.cancel()
toast = Toast.makeText(requireContext(), handleApiError(testAVM.savedStateHandle, viewEvent.asApiErrorBody), LENGTH_LONG)
toast = Toast.makeText(requireContext(), handleApiError(testAVM.savedStateHandle, viewStatefulEvent.asApiErrorBody), LENGTH_LONG)
toast?.show()
}
}
Expand All @@ -74,14 +76,12 @@ class MVIFragment : Fragment(R.layout.fragment_test) {
testAVM.sendEvent(TestAVM.TestAVMIntent.GetPosts)
}

private fun updateUIState(apiResult: ApiResult<List<TestModel>>) {
private fun updateUIState(apiResult: ViewStatefulEvent) {
apiResult.getAsThrowable?.isNoConnectionException?.ifTrue(::retryOnInternetAvailable)

apiResult.isNotLoading.ifTrue { binding.swipeToRefresh.setIsNotRefreshing() }
binding.error.isVisible = testAVM.isDataNotLoaded and (apiResult.isError || apiResult.isApiError)
binding.centerBigLoading.isVisible = apiResult.isLoading and testAVM.isDataNotLoaded
binding.progress.isVisible = apiResult.isLoading and testAVM.isDataLoaded
adapter.submitList(testAVM.payload)
binding.error.isVisible = testAVM.viewState.showEmptyDataOnErrors
binding.centerBigLoading.isVisible = testAVM.viewState.showLoadingWhenDataNotLoaded
binding.progress.isVisible = testAVM.viewState.showLoadingWhenDataIsLoaded
adapter.submitList(testAVM.viewState.payload)
}

private fun showErrorSnack() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package com.crazylegend.retrofit.viewstate.event

import kotlinx.coroutines.flow.Flow

interface ViewEventContract {
//decouple this as well so that a direct flow is not forced, hmm
val viewEvent: Flow<ViewEvent>

suspend fun provideEvent(viewEvent: ViewEvent)
fun interface ViewEventContract {
suspend fun provideEvent(viewStatefulEvent: ViewStatefulEvent)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.crazylegend.retrofit.viewstate.event

import okhttp3.ResponseBody

/**
* Created by funkymuse on 11/20/21 to long live and prosper !
*/
sealed interface ViewStatefulEvent {
data class Error(val throwable: Throwable) : ViewStatefulEvent
data class ApiError(val errorBody: ResponseBody?, val responseCode: Int) : ViewStatefulEvent

object Success : ViewStatefulEvent
object Loading : ViewStatefulEvent
object Idle : ViewStatefulEvent
}

val ViewStatefulEvent.isLoading get() = this is ViewStatefulEvent.Loading
val ViewStatefulEvent.isNotLoading get() = this !is ViewStatefulEvent.Loading

val ViewStatefulEvent.isIdle get() = this is ViewStatefulEvent.Idle
val ViewStatefulEvent.isNotIdle get() = this !is ViewStatefulEvent.Idle

val ViewStatefulEvent.isError get() = this is ViewStatefulEvent.Error
val ViewStatefulEvent.isNotError get() = this !is ViewStatefulEvent.Error

val ViewStatefulEvent.isApiError get() = this is ViewStatefulEvent.ApiError
val ViewStatefulEvent.isNotApiError get() = this !is ViewStatefulEvent.ApiError

val ViewStatefulEvent.isErrorOrApiError get() = this is ViewStatefulEvent.Error || this is ViewStatefulEvent.ApiError

val ViewStatefulEvent.isSuccess get() = this is ViewStatefulEvent.Success
val ViewStatefulEvent.isNotSuccess get() = this !is ViewStatefulEvent.Success

val ViewStatefulEvent.asError get() = (this as ViewStatefulEvent.Error)
val ViewStatefulEvent.asErrorThrowable get() = (this as ViewStatefulEvent.Error).throwable
val ViewStatefulEvent.asApiError get() = (this as ViewStatefulEvent.ApiError)
val ViewStatefulEvent.asApiErrorBody get() = (this as ViewStatefulEvent.ApiError).errorBody
val ViewStatefulEvent.asApiErrorCode get() = (this as ViewStatefulEvent.ApiError).responseCode


val ViewStatefulEvent.getAsThrowable: Throwable? get() = if (this is ViewStatefulEvent.Error) throwable else null
val ViewStatefulEvent.getAsApiFailureCode: Int? get() = if (this is ViewStatefulEvent.ApiError) responseCode else null
val ViewStatefulEvent.getAsApiResponseBody: ResponseBody? get() = if (this is ViewStatefulEvent.ApiError) errorBody else null

fun ViewStatefulEvent.asSuccess() = this as ViewStatefulEvent.Success
fun ViewStatefulEvent.asLoading() = this as ViewStatefulEvent.Loading
fun ViewStatefulEvent.asError() = this as ViewStatefulEvent.Error
fun ViewStatefulEvent.asApiError() = this as ViewStatefulEvent.ApiError
fun ViewStatefulEvent.asIdle() = this as ViewStatefulEvent.Idle

inline fun ViewStatefulEvent.onError(action: (Throwable) -> Unit): ViewStatefulEvent {
if (this is ViewStatefulEvent.Error) {
action(throwable)
}
return this
}

inline fun ViewStatefulEvent.onSuccess(action: () -> Unit): ViewStatefulEvent {
if (this is ViewStatefulEvent.Success) {
action()
}
return this
}

inline fun ViewStatefulEvent.onIdle(action: () -> Unit): ViewStatefulEvent {
if (this is ViewStatefulEvent.Idle) {
action()
}
return this
}

inline fun ViewStatefulEvent.onLoading(action: () -> Unit): ViewStatefulEvent {
if (this is ViewStatefulEvent.Loading) {
action()
}
return this
}

inline fun ViewStatefulEvent.onApiError(action: (errorBody: ResponseBody?, responseCode: Int) -> Unit): ViewStatefulEvent {
if (this is ViewStatefulEvent.ApiError) {
action(errorBody, responseCode)
}
return this
}

Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package com.crazylegend.retrofit.viewstate.state

import com.crazylegend.retrofit.apiresult.ApiResult
import com.crazylegend.retrofit.apiresult.onApiErrorSuspend
import com.crazylegend.retrofit.apiresult.onErrorSuspend
import com.crazylegend.retrofit.apiresult.onIdleSuspend
import com.crazylegend.retrofit.apiresult.onLoadingSuspend
import com.crazylegend.retrofit.apiresult.onSuccessSuspend
import com.crazylegend.retrofit.viewstate.event.ViewEvent
import com.crazylegend.retrofit.viewstate.event.ViewStatefulEvent
import com.crazylegend.retrofit.viewstate.event.ViewEventContract
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -16,11 +11,11 @@ import kotlinx.coroutines.flow.asStateFlow
*/
class ViewState<T>(
private val viewEventContract: ViewEventContract? = null,
defaultApiState: ApiResult<T> = ApiResult.Idle
defaultViewState: ViewStatefulEvent = ViewStatefulEvent.Idle
) : ViewStateContract<T> {

private val dataState: MutableStateFlow<ApiResult<T>> = MutableStateFlow(defaultApiState)
override val data = dataState.asStateFlow()
private val _viewState: MutableStateFlow<ViewStatefulEvent> = MutableStateFlow(defaultViewState)
override val viewState = _viewState.asStateFlow()

override var payload: T? = null

Expand All @@ -29,7 +24,7 @@ class ViewState<T>(
}

override fun emitState(apiResult: ApiResult<T>) {
dataState.value = apiResult
_viewState.value = apiResult.asViewEvent()
}

override val isDataLoaded get() = payload != null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.crazylegend.retrofit.viewstate.state

import com.crazylegend.retrofit.apiresult.ApiResult
import com.crazylegend.retrofit.viewstate.event.ViewStatefulEvent
import kotlinx.coroutines.flow.StateFlow

/**
* Created by funkymuse on 11/20/21 to long live and prosper !
*/
interface ViewStateContract<T> {
val data : StateFlow<ApiResult<T>>
val viewState : StateFlow<ViewStatefulEvent>

var payload: T?

Expand Down
Loading

0 comments on commit 39d080f

Please sign in to comment.