diff --git a/.gitignore b/.gitignore index 58490dda..eb8080a3 100644 --- a/.gitignore +++ b/.gitignore @@ -239,3 +239,9 @@ fabric.properties app/.config/secrets.properties app/schema.graphql + +.idea/deploymentTargetSelector.xml + +.idea/appInsightsSettings.xml + +app/lint-baseline.xml diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml deleted file mode 100644 index e7629756..00000000 --- a/.idea/appInsightsSettings.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index b268ef36..00000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fe63bb67..148fdd24 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index d18963a7..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 57d6d2dd..4615a58d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,6 +16,9 @@ android { buildFeatures { buildConfig = true } + lint { + baseline = file("lint-baseline.xml") + } } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d7f84553..d84bea7b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + - - \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/SampleApp.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/SampleApp.kt index 0e9c00ca..0bbd829f 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/SampleApp.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/SampleApp.kt @@ -6,11 +6,6 @@ import io.wax911.emojify.EmojiManager abstract class SampleApp : Application() { - /** - * Emoji manager instance - */ - internal abstract val emojiManager: EmojiManager - /** * Uncaught exception handler */ diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/koin/CoreModules.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/koin/CoreModules.kt index 3fb57272..58cd9cec 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/koin/CoreModules.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/koin/CoreModules.kt @@ -1,6 +1,7 @@ package co.anitrend.retrofit.graphql.core.koin -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.extension.dispatchers.SupportDispatcher +import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher import co.anitrend.retrofit.graphql.core.settings.Settings import coil.ImageLoader import coil.ImageLoaderFactory @@ -20,8 +21,8 @@ private val coreModule = module { androidContext() ) } binds(Settings.BINDINGS) - single { - SupportDispatchers() + single { + SupportDispatcher() } } diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/model/FragmentItem.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/model/FragmentItem.kt index 2bc37bab..9a966c39 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/model/FragmentItem.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/model/FragmentItem.kt @@ -10,5 +10,5 @@ data class FragmentItem( val parameter: Bundle? = null, val fragment: Class ) { - fun tag() = fragment.simpleName + fun tag(): String = fragment.simpleName } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/settings/Settings.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/settings/Settings.kt index 20e001e4..c88b1d9f 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/settings/Settings.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/settings/Settings.kt @@ -1,36 +1,39 @@ package co.anitrend.retrofit.graphql.core.settings import android.content.Context -import co.anitrend.arch.extension.preference.BooleanPreference -import co.anitrend.arch.extension.preference.IntPreference -import co.anitrend.arch.extension.preference.StringPreference -import co.anitrend.arch.extension.preference.SupportSettings +import co.anitrend.arch.extension.preference.SupportPreference +import co.anitrend.arch.extension.settings.BooleanSetting +import co.anitrend.arch.extension.settings.IntSetting +import co.anitrend.arch.extension.settings.StringSetting import co.anitrend.retrofit.graphql.data.authentication.settings.IAuthenticationSettings import co.anitrend.retrofit.graphql.sample.R -class Settings(context: Context) : SupportSettings(context), IAuthenticationSettings { +class Settings(context: Context) : SupportPreference(context), IAuthenticationSettings { - override var authenticatedUserId by StringPreference( - R.string.setting_authenticated_user_id, - IAuthenticationSettings.INVALID_USER_ID, - context.resources + override val authenticatedUserId = StringSetting( + key = R.string.setting_authenticated_user_id, + default = IAuthenticationSettings.INVALID_USER_ID, + resources = context.resources, + preference = this, ) - override var isNewInstallation by BooleanPreference( - R.string.setting_is_new_installation, - true, - context.resources + override val isNewInstallation = BooleanSetting( + key = R.string.setting_is_new_installation, + default = true, + resources = context.resources, + preference = this, ) - override var versionCode by IntPreference( - R.string.setting_version_code, - 1, - context.resources + override val versionCode = IntSetting( + key = R.string.setting_version_code, + default = 1, + resources = context.resources, + preference = this, ) companion object { val BINDINGS = arrayOf( - Settings::class, SupportSettings::class, IAuthenticationSettings::class + Settings::class, SupportPreference::class, IAuthenticationSettings::class ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleActivity.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleActivity.kt index ba2f5e47..40d2a119 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleActivity.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleActivity.kt @@ -3,28 +3,39 @@ package co.anitrend.retrofit.graphql.core.view import android.os.Build import android.view.View import androidx.appcompat.app.AppCompatDelegate +import co.anitrend.arch.core.model.ISupportViewModelState import co.anitrend.arch.extension.ext.getCompatColor -import co.anitrend.arch.ui.R import co.anitrend.arch.ui.activity.SupportActivity +import org.koin.android.scope.AndroidScopeComponent import org.koin.androidx.fragment.android.setupKoinFragmentFactory -import org.koin.androidx.scope.lifecycleScope as koinLifecycleScope +import org.koin.androidx.scope.activityRetainedScope +import org.koin.core.component.KoinScopeComponent +import timber.log.Timber -abstract class SampleActivity : SupportActivity() { +abstract class SampleActivity : SupportActivity(), AndroidScopeComponent, KoinScopeComponent { + + override val scope by activityRetainedScope() /** * Can be used to configure custom theme styling as desired */ override fun configureActivity() { - setupKoinFragmentFactory() + runCatching { + Timber.v("Setting up fragment factory using scope: $scope") + setupKoinFragmentFactory(scope) + }.onFailure { + setupKoinFragmentFactory() + Timber.v(it, "Reverting to scope-less based fragment factory") + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val systemUiOptions = window.decorView.systemUiVisibility when (AppCompatDelegate.getDefaultNightMode()) { AppCompatDelegate.MODE_NIGHT_NO -> { - window.navigationBarColor = getCompatColor(R.color.colorPrimary) + window.navigationBarColor = getCompatColor(co.anitrend.arch.theme.R.color.colorPrimary) window.decorView.systemUiVisibility = systemUiOptions or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR } AppCompatDelegate.MODE_NIGHT_YES -> { - window.navigationBarColor = getCompatColor(R.color.colorPrimary) + window.navigationBarColor = getCompatColor(co.anitrend.arch.theme.R.color.colorPrimary) window.decorView.systemUiVisibility = systemUiOptions and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR window.decorView.systemUiVisibility = systemUiOptions and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } @@ -34,4 +45,9 @@ abstract class SampleActivity : SupportActivity() { } } } + + /** + * Proxy for a view model state if one exists + */ + override fun viewModelState(): ISupportViewModelState<*>? = null } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleListFragment.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleListFragment.kt index bdaf28ce..a326585e 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleListFragment.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/core/view/SampleListFragment.kt @@ -1,11 +1,18 @@ package co.anitrend.retrofit.graphql.core.view -import androidx.lifecycle.Observer -import co.anitrend.arch.domain.entities.NetworkState +import co.anitrend.arch.core.model.ISupportViewModelState import co.anitrend.arch.ui.fragment.list.SupportFragmentList +import org.koin.android.scope.AndroidScopeComponent +import org.koin.androidx.scope.fragmentScope +import org.koin.core.component.KoinScopeComponent -abstract class SampleListFragment : SupportFragmentList() { - override val onRefreshObserver = Observer { - // workaround for support-arch:ui on refresh overrides network state - } +abstract class SampleListFragment : SupportFragmentList(), + AndroidScopeComponent, KoinScopeComponent { + + override val scope by fragmentScope() + + /** + * Proxy for a view model state if one exists + */ + override fun viewModelState(): ISupportViewModelState<*>? = null } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/api/provider/RetrofitProvider.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/api/provider/RetrofitProvider.kt index accec74d..751f9c34 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/api/provider/RetrofitProvider.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/api/provider/RetrofitProvider.kt @@ -18,7 +18,6 @@ import timber.log.Timber */ internal object RetrofitProvider { - private val moduleTag = javaClass.simpleName private val retrofitCache = LruCache(3) private fun provideOkHttpClient(endpointType: EndpointType, scope: Scope) : OkHttpClient { diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/common/SampleMapper.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/common/SampleMapper.kt index da147568..29c01c62 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/common/SampleMapper.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/common/SampleMapper.kt @@ -1,6 +1,6 @@ package co.anitrend.retrofit.graphql.data.arch.common -import co.anitrend.arch.data.mapper.contract.ISupportMapperHelper +import co.anitrend.arch.data.mapper.SupportResponseMapper import co.anitrend.retrofit.graphql.domain.common.EntityId /** @@ -8,6 +8,6 @@ import co.anitrend.retrofit.graphql.domain.common.EntityId * @param M model */ internal abstract class SampleMapper { - protected abstract fun from(): ISupportMapperHelper - protected abstract fun to(): ISupportMapperHelper + protected abstract fun from(): SupportResponseMapper + protected abstract fun to(): SupportResponseMapper } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/common/SamplePagedSource.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/common/SamplePagedSource.kt deleted file mode 100644 index ed868716..00000000 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/common/SamplePagedSource.kt +++ /dev/null @@ -1,85 +0,0 @@ -package co.anitrend.retrofit.graphql.data.arch.common - -import androidx.paging.PagedList -import androidx.paging.PagingRequestHelper -import co.anitrend.arch.data.source.paging.SupportPagingDataSource -import co.anitrend.arch.extension.dispatchers.SupportDispatchers -import kotlinx.coroutines.launch - -internal abstract class SamplePagedSource( - supportDispatchers: SupportDispatchers -) : SupportPagingDataSource(supportDispatchers) { - - /** - * Invoked when a request to the network needs to happen - */ - abstract suspend operator fun invoke( - callback: PagingRequestHelper.Request.Callback, - requestType: PagingRequestHelper.RequestType, - model: T? - ) - - /** - * Called when the item at the end of the PagedList has been loaded, and access has - * occurred within [PagedList.Config.prefetchDistance] of it. - * - * No more data will be appended to the [PagedList] after this item. - * - * @param itemAtEnd The first item of [PagedList] - */ - override fun onItemAtEndLoaded(itemAtEnd: T) { - val requestType = PagingRequestHelper.RequestType.AFTER - pagingRequestHelper.runIfNotRunning( - requestType - ) { pagingRequestCallback -> - launch { - invoke( - pagingRequestCallback, - requestType, - itemAtEnd - ) - } - } - } - - /** - * Called when the item at the front of the PagedList has been loaded, and access has - * occurred within [PagedList.Config.prefetchDistance] of it. - * - * No more data will be prepended to the PagedList before this item. - * - * @param itemAtFront The first item of PagedList - */ - override fun onItemAtFrontLoaded(itemAtFront: T) { - val requestType = PagingRequestHelper.RequestType.BEFORE - pagingRequestHelper.runIfNotRunning( - requestType - ) { pagingRequestCallback -> - launch { - invoke( - pagingRequestCallback, - requestType, - itemAtFront - ) - } - } - } - - /** - * Called when zero items are returned from an initial load of the PagedList's data source. - */ - override fun onZeroItemsLoaded() { - val requestType = PagingRequestHelper.RequestType.INITIAL - pagingRequestHelper.runIfNotRunning( - requestType - ) { pagingRequestCallback -> - launch { - invoke( - pagingRequestCallback, - PagingRequestHelper.RequestType.INITIAL, - null - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/SampleController.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/SampleController.kt index de2ede15..da615cfb 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/SampleController.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/SampleController.kt @@ -1,26 +1,22 @@ package co.anitrend.retrofit.graphql.data.arch.controller -import androidx.lifecycle.MutableLiveData -import androidx.paging.PagingRequestHelper -import co.anitrend.arch.data.common.ISupportPagingResponse import co.anitrend.arch.data.common.ISupportResponse import co.anitrend.arch.data.mapper.SupportResponseMapper -import co.anitrend.arch.domain.entities.NetworkState -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.retrofit.graphql.data.arch.controller.strategy.ControllerStrategy import co.anitrend.retrofit.graphql.data.arch.extensions.fetchBodyWithRetry import io.github.wax911.library.model.attribute.GraphError import io.github.wax911.library.model.body.GraphContainer +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Deferred import kotlinx.coroutines.withContext import retrofit2.Response -internal class SampleController private constructor( +internal class SampleController private constructor( private val responseMapper: SupportResponseMapper, private val strategy: ControllerStrategy, - private val dispatchers: SupportDispatchers -) : ISupportResponse>>, D>, - ISupportPagingResponse>>> { + private val dispatcher: CoroutineDispatcher +) : ISupportResponse>>, D> { @Throws private fun handleErrorsIfExist(errors: List) { @@ -31,69 +27,48 @@ internal class SampleController private constructor( ) } - /** - * Response handler for coroutine contexts which need to observe [NetworkState] - * - * @param resource awaiting execution - * @param networkState for the deferred result - * - * @return resource fetched if present - */ - override suspend fun invoke( + suspend operator fun invoke( resource: Deferred>>, - networkState: MutableLiveData - ): D? { - return strategy({ - val responseBody = resource.fetchBodyWithRetry(dispatchers.io) - val mapped = withContext(dispatchers.computation) { - handleErrorsIfExist(responseBody.errors.orEmpty()) - responseBody.data?.let { data -> - responseMapper.onResponseMapFrom(data) - } + requestCallback: RequestCallback, + interceptor: (S) -> S + ) = strategy(requestCallback) { + val responseBody = resource.fetchBodyWithRetry(dispatcher) + val mapped = withContext(dispatcher) { + handleErrorsIfExist(responseBody.errors.orEmpty()) + responseBody.data?.let { + val data = interceptor(it) + responseMapper.onResponseMapFrom(data) } - withContext(dispatchers.io) { - if (mapped != null) - responseMapper.onResponseDatabaseInsert(mapped) - } - mapped - }, networkState) + } + withContext(dispatcher) { + if (mapped != null) + responseMapper.onResponseDatabaseInsert(mapped) + } + mapped } /** - * Response handler for coroutine contexts, mainly for paging + * Response handler for coroutine contexts which need to observe [LoadState] * * @param resource awaiting execution - * @param pagingRequestHelper optional paging request callback + * @param requestCallback for the deferred result + * + * @return resource fetched if present */ override suspend fun invoke( resource: Deferred>>, - pagingRequestHelper: PagingRequestHelper.Request.Callback - ) { - strategy({ - val responseBody = resource.fetchBodyWithRetry(dispatchers.io) - val mapped = withContext(dispatchers.computation) { - handleErrorsIfExist(responseBody.errors.orEmpty()) - responseBody.data?.let { data -> - responseMapper.onResponseMapFrom(data) - } - } - withContext(dispatchers.io) { - if (mapped != null) - responseMapper.onResponseDatabaseInsert(mapped) - } - }, pagingRequestHelper) - } - + requestCallback: RequestCallback + ) = invoke(resource, requestCallback) { it } companion object { fun newInstance( responseMapper: SupportResponseMapper, strategy: ControllerStrategy, - supportDispatchers: SupportDispatchers + dispatcher: CoroutineDispatcher ) = SampleController( responseMapper, strategy, - supportDispatchers + dispatcher ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OfflineControllerPolicy.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OfflineControllerPolicy.kt index c0d76c31..fdede46d 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OfflineControllerPolicy.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OfflineControllerPolicy.kt @@ -16,9 +16,8 @@ package co.anitrend.retrofit.graphql.data.arch.controller.policy -import androidx.lifecycle.MutableLiveData -import androidx.paging.PagingRequestHelper -import co.anitrend.arch.domain.entities.NetworkState +import co.anitrend.arch.domain.entities.RequestError +import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.retrofit.graphql.data.arch.controller.strategy.ControllerStrategy import timber.log.Timber @@ -30,49 +29,23 @@ import timber.log.Timber internal class OfflineControllerPolicy private constructor() : ControllerStrategy() { /** - * Execute a paging task under an implementation strategy + * Execute a task under an implementation strategy * + * @param callback event emitter * @param block what will be executed - * @param pagingRequestHelper paging event emitter */ - override suspend fun invoke( - block: suspend () -> Unit, - pagingRequestHelper: PagingRequestHelper.Request.Callback - ) { + override suspend fun invoke(callback: RequestCallback, block: suspend () -> D?): D? { runCatching { block() - pagingRequestHelper.recordSuccess() + callback.recordSuccess() }.exceptionOrNull()?.also { e -> Timber.e(e) - pagingRequestHelper.recordFailure(e) - } - } - - /** - * Execute a task under an implementation strategy - * - * @param block what will be executed - * @param networkState network state event emitter - */ - override suspend fun invoke( - block: suspend () -> D?, - networkState: MutableLiveData - ): D? { - return runCatching{ - networkState.postValue(NetworkState.Loading) - val result = block() - networkState.postValue(NetworkState.Success) - result - }.getOrElse { - Timber.e(it) - networkState.postValue( - NetworkState.Error( - heading = it.cause?.message ?: "Unexpected error encountered", - message = it.message - ) - ) - null + when (e) { + is RequestError -> callback.recordFailure(e) + else -> callback.recordFailure(RequestError(e)) + } } + return null } companion object { diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OnlineControllerPolicy.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OnlineControllerPolicy.kt index a9bb2e73..f089664c 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OnlineControllerPolicy.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/policy/OnlineControllerPolicy.kt @@ -1,9 +1,8 @@ package co.anitrend.retrofit.graphql.data.arch.controller.policy -import androidx.lifecycle.MutableLiveData -import androidx.paging.PagingRequestHelper -import co.anitrend.arch.domain.entities.NetworkState +import co.anitrend.arch.domain.entities.RequestError import co.anitrend.arch.extension.network.SupportConnectivity +import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.retrofit.graphql.data.arch.controller.strategy.ControllerStrategy import timber.log.Timber @@ -14,67 +13,36 @@ internal class OnlineControllerPolicy private constructor( private val connectivity: SupportConnectivity ) : ControllerStrategy() { - /** - * Execute a paging task under an implementation strategy - * - * @param block what will be executed - * @param pagingRequestHelper paging event emitter - */ - override suspend fun invoke( - block: suspend () -> Unit, - pagingRequestHelper: PagingRequestHelper.Request.Callback - ) { - if (connectivity.isConnected) { - runCatching { - block() - pagingRequestHelper.recordSuccess() - }.exceptionOrNull()?.also { e -> - e.printStackTrace() - Timber.e(e) - pagingRequestHelper.recordFailure(e) - } - } - else { - pagingRequestHelper.recordFailure( - Throwable("Please check your internet connection") - ) - } - } /** * Execute a task under an implementation strategy * + * @param callback event emitter * @param block what will be executed - * @param networkState network state event emitter */ override suspend fun invoke( - block: suspend () -> D?, - networkState: MutableLiveData + callback: RequestCallback, + block: suspend () -> D? ): D? { - if (connectivity.isConnected) { - return runCatching{ - networkState.postValue(NetworkState.Loading) - val result = block() - networkState.postValue(NetworkState.Success) - result - }.getOrElse { - Timber.e(it) - networkState.postValue( - NetworkState.Error( - heading = it.cause?.message ?: "Unexpected error encountered \uD83E\uDD2D", - message = it.message - ) + runCatching { + if (connectivity.isConnected) + block() + else + throw RequestError( + "No internet connection", + "Make sure you have an active internet connection" ) - null + }.onSuccess { result -> + callback.recordSuccess() + return result + }.onFailure { exception -> + Timber.e(exception) + when (exception) { + is RequestError -> callback.recordFailure(exception) + else -> callback.recordFailure(RequestError(exception)) } - } else { - networkState.postValue( - NetworkState.Error( - heading = "No internet connection detected \uD83E\uDD2D", - message = "Please check your internet connection" - ) - ) } + return null } diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/strategy/ControllerStrategy.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/strategy/ControllerStrategy.kt index 80dbac4d..2f2af225 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/strategy/ControllerStrategy.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/controller/strategy/ControllerStrategy.kt @@ -1,32 +1,17 @@ package co.anitrend.retrofit.graphql.data.arch.controller.strategy -import androidx.lifecycle.MutableLiveData -import androidx.paging.PagingRequestHelper -import co.anitrend.arch.domain.entities.NetworkState +import co.anitrend.arch.request.callback.RequestCallback internal abstract class ControllerStrategy { - protected val moduleTag = javaClass.simpleName - - /** - * Execute a paging task under an implementation strategy - * - * @param block what will be executed - * @param pagingRequestHelper paging event emitter - */ - internal abstract suspend operator fun invoke( - block: suspend () -> Unit, - pagingRequestHelper: PagingRequestHelper.Request.Callback - ) - /** * Execute a task under an implementation strategy * + * @param callback event emitter * @param block what will be executed - * @param networkState network state event emitter */ internal abstract suspend operator fun invoke( - block: suspend () -> D?, - networkState: MutableLiveData + callback: RequestCallback, + block: suspend () -> D? ): D? } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/extensions/SampleExtensions.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/extensions/SampleExtensions.kt index 10a23c9e..0a98317e 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/extensions/SampleExtensions.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/extensions/SampleExtensions.kt @@ -1,7 +1,7 @@ package co.anitrend.retrofit.graphql.data.arch.extensions import co.anitrend.arch.data.mapper.SupportResponseMapper -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher import co.anitrend.retrofit.graphql.data.arch.controller.SampleController import co.anitrend.retrofit.graphql.data.arch.controller.policy.OfflineControllerPolicy import co.anitrend.retrofit.graphql.data.arch.controller.policy.OnlineControllerPolicy @@ -42,7 +42,7 @@ private suspend inline fun Deferred>.executeWithRetry( // If we have a HttpException, check whether we have a Retry-After // header to decide how long to delay val retryAfterHeader = e.response()?.headers()?.get("Retry-After") - if (retryAfterHeader != null && retryAfterHeader.isNotEmpty()) { + if (!retryAfterHeader.isNullOrEmpty()) { // Got a Retry-After value, try and parse it to an long try { nextDelay = (retryAfterHeader.toLong() + 10).coerceAtLeast(defaultDelay) @@ -80,11 +80,11 @@ private fun defaultShouldRetry(exception: Exception) = when (exception) { */ internal fun SupportResponseMapper.controller( strategy: ControllerStrategy, - supportDispatchers: SupportDispatchers + supportDispatchers: ISupportDispatcher ) = SampleController.newInstance( responseMapper = this, strategy = strategy, - supportDispatchers = supportDispatchers + dispatcher = supportDispatchers.io ) internal fun Scope.onlineController() = diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/koin/Modules.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/koin/Modules.kt index 8685789f..75716f96 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/koin/Modules.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/arch/koin/Modules.kt @@ -60,21 +60,29 @@ private val networkModule = module { private val interceptorModules = module { factory { (exclusionHeaders: Set) -> - ChuckerInterceptor( - context = androidContext(), + ChuckerInterceptor.Builder( + context = androidContext() // The previously created Collector - collector = ChuckerCollector( - context = androidContext(), - // Toggles visibility of the push notification - showNotification = true, - // Allows to customize the retention period of collected data - retentionPeriod = RetentionManager.Period.ONE_HOUR - ), - // The max body content length in bytes, after this responses will be truncated. - maxContentLength = 10500L, - // List of headers to replace with ** in the Chucker UI - headersToRedact = exclusionHeaders ) + .collector( + collector = ChuckerCollector( + context = androidContext(), + // Toggles visibility of the push notification + showNotification = true, + // Allows to customize the retention period of collected data + retentionPeriod = RetentionManager.Period.ONE_HOUR + ) + // The max body content length in bytes, after this responses will be truncated. + ) + .maxContentLength( + length = 10500L + // List of headers to replace with ** in the Chucker UI + ) + .redactHeaders( + headerNames = exclusionHeaders + ) + .alwaysReadResponseBody(false) + .build() } factory { (interceptorLogLevel: HttpLoggingInterceptor.Level) -> val okHttpClientBuilder = OkHttpClient.Builder() diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/authentication/settings/IAuthenticationSettings.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/authentication/settings/IAuthenticationSettings.kt index 9628368f..e0f28501 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/authentication/settings/IAuthenticationSettings.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/authentication/settings/IAuthenticationSettings.kt @@ -1,7 +1,9 @@ package co.anitrend.retrofit.graphql.data.authentication.settings +import co.anitrend.arch.extension.settings.contract.AbstractSetting + interface IAuthenticationSettings { - var authenticatedUserId: String + val authenticatedUserId: AbstractSetting companion object { const val INVALID_USER_ID: String = "" diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/helper/UploadMutationHelper.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/helper/UploadMutationHelper.kt index 6ad5580a..898a1266 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/helper/UploadMutationHelper.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/helper/UploadMutationHelper.kt @@ -40,7 +40,7 @@ internal object UploadMutationHelper { * by checking if is annotated with [GraphMultiPartUpload] */ fun Array.supportsFileUpload(): Boolean { - return filterIsInstance(GraphMultiPartUpload::class.java).isNotEmpty() + return filterIsInstance().isNotEmpty() } @Throws(Throwable::class) diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/koin/Modules.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/koin/Modules.kt index d485fa73..1542ddf2 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/koin/Modules.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/koin/Modules.kt @@ -24,7 +24,7 @@ private val sourceModule = module { BucketSourceImpl( remoteSource = api(EndpointType.BUCKET), strategy = onlineController(), - dispatchers = get(), + dispatcher = get(), mapper = get() ) } @@ -32,7 +32,7 @@ private val sourceModule = module { BucketUploadSourceImpl( remoteSource = api(EndpointType.BUCKET), strategy = onlineController(), - dispatchers = get(), + dispatcher = get(), mapper = get() ) } diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/BucketRepositoryImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/BucketRepositoryImpl.kt index 6f53efb6..619864c2 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/BucketRepositoryImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/BucketRepositoryImpl.kt @@ -1,8 +1,8 @@ package co.anitrend.retrofit.graphql.data.bucket.repository -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.arch.data.model.UserInterfaceState.Companion.create import co.anitrend.arch.data.repository.SupportRepository +import co.anitrend.arch.data.state.DataState +import co.anitrend.arch.data.state.DataState.Companion.create import co.anitrend.retrofit.graphql.data.bucket.source.browse.contract.BucketSource import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.domain.repositories.BucketRepository @@ -16,4 +16,4 @@ internal class BucketRepositoryImpl( ) } -typealias BucketRepositoryContract = BucketRepository>> \ No newline at end of file +typealias BucketRepositoryContract = BucketRepository>> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/upload/UploadRepositoryImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/upload/UploadRepositoryImpl.kt index 98b4b2a1..ae3670be 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/upload/UploadRepositoryImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/repository/upload/UploadRepositoryImpl.kt @@ -1,8 +1,8 @@ package co.anitrend.retrofit.graphql.data.bucket.repository.upload -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.arch.data.model.UserInterfaceState.Companion.create import co.anitrend.arch.data.repository.SupportRepository +import co.anitrend.arch.data.state.DataState +import co.anitrend.arch.data.state.DataState.Companion.create import co.anitrend.retrofit.graphql.data.bucket.source.upload.contract.BucketUploadSource import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.domain.models.common.IGraphQuery @@ -18,4 +18,4 @@ internal class UploadRepositoryImpl( ) } -typealias UploadRepositoryContract = UploadRepository> \ No newline at end of file +typealias UploadRepositoryContract = UploadRepository> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/BucketSourceImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/BucketSourceImpl.kt index 14bae901..a0939de6 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/BucketSourceImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/BucketSourceImpl.kt @@ -1,6 +1,7 @@ package co.anitrend.retrofit.graphql.data.bucket.source.browse -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher +import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.retrofit.graphql.data.arch.controller.strategy.ControllerStrategy import co.anitrend.retrofit.graphql.data.arch.extensions.controller import co.anitrend.retrofit.graphql.data.bucket.datasource.remote.BucketRemoteSource @@ -8,6 +9,7 @@ import co.anitrend.retrofit.graphql.data.bucket.mapper.BucketResponseMapper import co.anitrend.retrofit.graphql.data.bucket.source.browse.contract.BucketSource import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import io.github.wax911.library.model.request.QueryContainerBuilder +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow @@ -15,14 +17,13 @@ internal class BucketSourceImpl( private val mapper: BucketResponseMapper, private val remoteSource: BucketRemoteSource, private val strategy: ControllerStrategy>, - dispatchers: SupportDispatchers -) : BucketSource(dispatchers) { + override val dispatcher: ISupportDispatcher, +) : BucketSource() { override val observable = MutableStateFlow?>(null) - override suspend fun getStorageBucketFiles() { - super.getStorageBucketFiles() + override suspend fun getStorageBucketFiles(requestCallback: RequestCallback) { val deferred = async { val queryBuilder = QueryContainerBuilder() remoteSource.getStorageBucketFiles( @@ -31,16 +32,18 @@ internal class BucketSourceImpl( } val controller = - mapper.controller(strategy, dispatchers) + mapper.controller(strategy, dispatcher) - val result = controller(deferred, networkState) + val result = controller(deferred, requestCallback) observable.value = result } /** * Clears data sources (databases, preferences, e.t.c) + * + * @param context Dispatcher context to run in */ - override suspend fun clearDataSource() { + override suspend fun clearDataSource(context: CoroutineDispatcher) { observable.value = null } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/contract/BucketSource.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/contract/BucketSource.kt index 3ab17254..264cc42d 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/contract/BucketSource.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/browse/contract/BucketSource.kt @@ -1,28 +1,28 @@ package co.anitrend.retrofit.graphql.data.bucket.source.browse.contract -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData -import androidx.lifecycle.liveData -import co.anitrend.arch.data.source.coroutine.SupportCoroutineDataSource -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.data.source.core.SupportCoreDataSource +import co.anitrend.arch.request.callback.RequestCallback +import co.anitrend.arch.request.model.Request import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch -internal abstract class BucketSource( - dispatchers: SupportDispatchers -) : SupportCoroutineDataSource(dispatchers) { +internal abstract class BucketSource : SupportCoreDataSource() { protected abstract val observable: StateFlow?> - protected open suspend fun getStorageBucketFiles() { - retry = { getStorageBucketFiles() } - } + protected abstract suspend fun getStorageBucketFiles(requestCallback: RequestCallback) - operator fun invoke(): LiveData> = - liveData { - getStorageBucketFiles() - val bucketFlow = observable.mapNotNull { it } - emitSource(bucketFlow.asLiveData()) + operator fun invoke(): Flow> { + scope.launch { + requestHelper.runIfNotRunning( + Request.Default("getStorageBucketFiles", Request.Type.INITIAL) + ) { + getStorageBucketFiles(it) + } } + return observable.mapNotNull { it } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/BucketUploadSourceImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/BucketUploadSourceImpl.kt index 637bb430..e40c260e 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/BucketUploadSourceImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/BucketUploadSourceImpl.kt @@ -1,6 +1,7 @@ package co.anitrend.retrofit.graphql.data.bucket.source.upload -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher +import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.retrofit.graphql.data.arch.controller.strategy.ControllerStrategy import co.anitrend.retrofit.graphql.data.arch.extensions.controller import co.anitrend.retrofit.graphql.data.bucket.datasource.remote.BucketRemoteSource @@ -9,6 +10,7 @@ import co.anitrend.retrofit.graphql.data.bucket.source.upload.contract.BucketUpl import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.domain.models.common.IGraphQuery import io.github.wax911.library.model.request.QueryContainerBuilder +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow @@ -16,14 +18,16 @@ internal class BucketUploadSourceImpl( private val mapper: UploadResponseMapper, private val remoteSource: BucketRemoteSource, private val strategy: ControllerStrategy, - dispatchers: SupportDispatchers -) : BucketUploadSource(dispatchers) { + override val dispatcher: ISupportDispatcher, + ) : BucketUploadSource() { override val observable = MutableStateFlow(null) - override suspend fun uploadToStorageBucket(mutation: IGraphQuery) { - super.uploadToStorageBucket(mutation) + override suspend fun uploadToStorageBucket( + mutation: IGraphQuery, + requestCallback: RequestCallback, + ) { val deferred = async { val queryBuilder = QueryContainerBuilder() .putVariables(mutation.toMap()) @@ -31,16 +35,18 @@ internal class BucketUploadSourceImpl( } val controller = - mapper.controller(strategy, dispatchers) + mapper.controller(strategy, dispatcher) - val result = controller(deferred, networkState) + val result = controller(deferred, requestCallback) observable.value = result } /** * Clears data sources (databases, preferences, e.t.c) + * + * @param context Dispatcher context to run in */ - override suspend fun clearDataSource() { + override suspend fun clearDataSource(context: CoroutineDispatcher) { observable.value = null } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/contract/BucketUploadSource.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/contract/BucketUploadSource.kt index b3f34bd9..aef69418 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/contract/BucketUploadSource.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/source/upload/contract/BucketUploadSource.kt @@ -1,28 +1,32 @@ package co.anitrend.retrofit.graphql.data.bucket.source.upload.contract -import androidx.lifecycle.asLiveData -import androidx.lifecycle.liveData -import co.anitrend.arch.data.source.coroutine.SupportCoroutineDataSource -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.data.source.core.SupportCoreDataSource +import co.anitrend.arch.request.callback.RequestCallback +import co.anitrend.arch.request.model.Request import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.domain.models.common.IGraphQuery +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch -internal abstract class BucketUploadSource( - dispatchers: SupportDispatchers -) : SupportCoroutineDataSource(dispatchers) { +internal abstract class BucketUploadSource: SupportCoreDataSource() { protected abstract val observable: StateFlow - protected open suspend fun uploadToStorageBucket(mutation: IGraphQuery) { - retry = { uploadToStorageBucket(mutation) } - } + protected abstract suspend fun uploadToStorageBucket( + mutation: IGraphQuery, + requestCallback: RequestCallback + ) - operator fun invoke(mutation: IGraphQuery) = - liveData { - uploadToStorageBucket(mutation) - val bucketFlow = observable.mapNotNull { it } - emitSource(bucketFlow.asLiveData()) + operator fun invoke(mutation: IGraphQuery): Flow { + scope.launch { + requestHelper.runIfNotRunning( + Request.Default("uploadToStorageBucket", Request.Type.INITIAL) + ) { + uploadToStorageBucket(mutation, it) + } } + return observable.mapNotNull { it } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/BucketUseCaseImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/BucketUseCaseImpl.kt index 98b761f7..acadf214 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/BucketUseCaseImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/BucketUseCaseImpl.kt @@ -1,7 +1,7 @@ package co.anitrend.retrofit.graphql.data.bucket.usecase -import co.anitrend.arch.data.model.UserInterfaceState import co.anitrend.arch.data.repository.contract.ISupportRepository +import co.anitrend.arch.data.state.DataState import co.anitrend.retrofit.graphql.data.bucket.repository.BucketRepositoryContract import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.domain.usecases.BucketUseCase @@ -18,4 +18,4 @@ internal class BucketUseCaseImpl( } } -typealias BucketUseCaseContract = BucketUseCase>> \ No newline at end of file +typealias BucketUseCaseContract = BucketUseCase>> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/upload/UploadUseCaseImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/upload/UploadUseCaseImpl.kt index b1edcb89..618aff8c 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/upload/UploadUseCaseImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/bucket/usecase/upload/UploadUseCaseImpl.kt @@ -1,7 +1,7 @@ package co.anitrend.retrofit.graphql.data.bucket.usecase.upload -import co.anitrend.arch.data.model.UserInterfaceState import co.anitrend.arch.data.repository.contract.ISupportRepository +import co.anitrend.arch.data.state.DataState import co.anitrend.retrofit.graphql.data.bucket.repository.upload.UploadRepositoryContract import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.domain.usecases.UploadUseCase @@ -18,4 +18,4 @@ internal class UploadUseCaseImpl( } } -typealias UploadUseCaseContract = UploadUseCase> \ No newline at end of file +typealias UploadUseCaseContract = UploadUseCase> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/converters/MarketPlaceConverters.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/converters/MarketPlaceConverters.kt index 28fe018d..96463949 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/converters/MarketPlaceConverters.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/converters/MarketPlaceConverters.kt @@ -1,89 +1,48 @@ package co.anitrend.retrofit.graphql.data.market.converters import co.anitrend.arch.data.converter.SupportConverter -import co.anitrend.arch.data.mapper.contract.ISupportMapperHelper -import co.anitrend.retrofit.graphql.data.arch.common.SampleMapper import co.anitrend.retrofit.graphql.data.market.entity.MarketPlaceEntity import co.anitrend.retrofit.graphql.data.market.model.edge.MarketPlaceListingEdge import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing internal class MarketPlaceEntityConverter( - override val fromType: (MarketPlaceEntity) -> MarketPlaceListing = { from().transform(it) }, - override val toType: (MarketPlaceListing) -> MarketPlaceEntity = { to().transform(it) } -) : SupportConverter() { - companion object : SampleMapper() { - override fun from() = - object : ISupportMapperHelper { - /** - * Transforms the the [source] to the target type - */ - override fun transform(source: MarketPlaceEntity) = - MarketPlaceListing( - id = source.id, - cursorId = source.cursorId, - logoUrl = source.logoUrl, - background = source.logoBackground, - name = source.name, - categories = source.categories, - slug = source.slug, - description = source.description, - isPaid = source.isPaid, - isVerified = source.isVerified - ) - } - - override fun to() = - object : ISupportMapperHelper { - /** - * Transforms the the [source] to the target type - */ - override fun transform(source: MarketPlaceListing): MarketPlaceEntity { - throw Throwable("Not yet implemented") - } - } - } -} + override val fromType: (MarketPlaceEntity) -> MarketPlaceListing = { + MarketPlaceListing( + id = it.id, + cursorId = it.cursorId, + logoUrl = it.logoUrl, + background = it.logoBackground, + name = it.name, + categories = it.categories, + slug = it.slug, + description = it.description, + isPaid = it.isPaid, + isVerified = it.isVerified + ) + }, + override val toType: (MarketPlaceListing) -> MarketPlaceEntity = { throw NotImplementedError() } +) : SupportConverter() internal class MarketPlaceModelConverter( - override val fromType: (MarketPlaceEntity) -> MarketPlaceListingEdge = { from().transform(it) }, - override val toType: (MarketPlaceListingEdge) -> MarketPlaceEntity = { to().transform(it) } -) : SupportConverter() { - companion object : SampleMapper() { - override fun from() = - object : ISupportMapperHelper { - /** - * Transforms the the [source] to the target type - */ - override fun transform(source: MarketPlaceEntity): MarketPlaceListingEdge { - throw Throwable("Not yet implemented") - } - } - - override fun to() = - object : ISupportMapperHelper { - /** - * Transforms the the [source] to the target type - */ - override fun transform(source: MarketPlaceListingEdge): MarketPlaceEntity { - val categories = listOf( - source.node.primaryCategory.name, - source.node.secondaryCategory?.name - ).mapNotNull { it } + override val fromType: (MarketPlaceEntity) -> MarketPlaceListingEdge = { throw NotImplementedError() }, + override val toType: (MarketPlaceListingEdge) -> MarketPlaceEntity = { + val categories = listOf( + it.node.primaryCategory.name, + it.node.secondaryCategory?.name + ).mapNotNull { name -> name } - return MarketPlaceEntity( - id = source.node.id, - cursorId = source.cursor, - logoUrl = source.node.logo, - logoBackground = source.node.background, - name = source.node.name, - categories = categories, - slug = source.node.slug, - description = source.node.description, - isPaid = source.node.isPaid, - isVerified = source.node.isVerified - ) - } - } + MarketPlaceEntity( + id = it.node.id, + cursorId = it.cursor, + logoUrl = it.node.logo, + logoBackground = it.node.background, + name = it.node.name, + categories = categories, + slug = it.node.slug, + description = it.node.description, + isPaid = it.node.isPaid, + isVerified = it.node.isVerified + ) } -} +) : SupportConverter() diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/koin/Modules.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/koin/Modules.kt index 41712259..c08eb7fd 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/koin/Modules.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/koin/Modules.kt @@ -23,7 +23,7 @@ private val sourceModule = module { strategy = onlineController(), mapper = get(), converter = get(), - dispatchers = get() + dispatcher = get() ) } } diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/repository/MarketPlaceRepositoryImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/repository/MarketPlaceRepositoryImpl.kt index 4b4fa23d..3b57316c 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/repository/MarketPlaceRepositoryImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/repository/MarketPlaceRepositoryImpl.kt @@ -1,9 +1,9 @@ package co.anitrend.retrofit.graphql.data.market.repository import androidx.paging.PagedList -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.arch.data.model.UserInterfaceState.Companion.create import co.anitrend.arch.data.repository.SupportRepository +import co.anitrend.arch.data.state.DataState +import co.anitrend.arch.data.state.DataState.Companion.create import co.anitrend.retrofit.graphql.data.market.source.contract.MarketPlaceSource import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing import co.anitrend.retrofit.graphql.domain.repositories.MarketPlaceRepository @@ -18,4 +18,4 @@ internal class MarketPlaceRepositoryImpl( ) } -typealias MarketPlaceRepositoryContract = MarketPlaceRepository>> \ No newline at end of file +typealias MarketPlaceRepositoryContract = MarketPlaceRepository>> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/MarketPlaceSourceImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/MarketPlaceSourceImpl.kt index c9206096..c3d01bb2 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/MarketPlaceSourceImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/MarketPlaceSourceImpl.kt @@ -1,11 +1,9 @@ package co.anitrend.retrofit.graphql.data.market.source -import androidx.lifecycle.liveData -import androidx.paging.PagedList -import androidx.paging.PagingRequestHelper -import androidx.paging.toLiveData -import co.anitrend.arch.data.util.SupportDataKeyStore -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher +import co.anitrend.arch.paging.legacy.FlowPagedListBuilder +import co.anitrend.arch.paging.legacy.util.PAGING_CONFIGURATION +import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.retrofit.graphql.data.arch.controller.strategy.ControllerStrategy import co.anitrend.retrofit.graphql.data.arch.extensions.controller import co.anitrend.retrofit.graphql.data.market.converters.MarketPlaceEntityConverter @@ -15,9 +13,10 @@ import co.anitrend.retrofit.graphql.data.market.entity.MarketPlaceEntity import co.anitrend.retrofit.graphql.data.market.mapper.MarketPlaceResponseMapper import co.anitrend.retrofit.graphql.data.market.model.query.MarketPlaceListingQuery import co.anitrend.retrofit.graphql.data.market.source.contract.MarketPlaceSource -import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing import io.github.wax911.library.model.request.QueryContainerBuilder +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async +import kotlinx.coroutines.withContext internal class MarketPlaceSourceImpl( private val mapper: MarketPlaceResponseMapper, @@ -25,65 +24,46 @@ internal class MarketPlaceSourceImpl( private val remoteSource: MarketPlaceRemoteSource, private val localSource: MarketPlaceLocalSource, private val strategy: ControllerStrategy>, - dispatchers: SupportDispatchers -) : MarketPlaceSource(dispatchers) { + override val dispatcher: ISupportDispatcher +) : MarketPlaceSource() { - private fun buildMarketPlaceListingQuery( - requestType: PagingRequestHelper.RequestType, - model: MarketPlaceListing? - ): MarketPlaceListingQuery { - val query = MarketPlaceListingQuery(first = supportPagingHelper.pageSize) - when (requestType) { - PagingRequestHelper.RequestType.BEFORE -> - query.before = model?.cursorId - PagingRequestHelper.RequestType.AFTER -> - query.after = model?.cursorId - PagingRequestHelper.RequestType.INITIAL -> {} - } - return query - } - - override val observable = liveData { - val factory = localSource.findAllByFactory() - val pagingSource = factory.mapByPage { entities -> - converter.convertFrom(entities) - } - - val callback: PagedList.BoundaryCallback = this@MarketPlaceSourceImpl - - emitSource( - pagingSource.toLiveData( - config = SupportDataKeyStore.PAGING_CONFIGURATION, - boundaryCallback = callback - ) - ) - } + override val observable = FlowPagedListBuilder( + dataSourceFactory = localSource.findAllByFactory() + .mapByPage { entities -> + converter.convertFrom(entities) + }, + config = PAGING_CONFIGURATION, + initialLoadKey = null, + boundaryCallback = this, + ).buildFlow() /** * Invoked when a request to the network needs to happen */ - override suspend fun invoke( - callback: PagingRequestHelper.Request.Callback, - requestType: PagingRequestHelper.RequestType, - model: MarketPlaceListing? + override suspend fun getMarketPlaceListing( + requestCallback: RequestCallback, + marketPlaceListingQuery: MarketPlaceListingQuery, ) { - val marketPlaceQuery = buildMarketPlaceListingQuery(requestType, model) val deferred = async { val queryBuilder = QueryContainerBuilder() - .putVariables(marketPlaceQuery.toMap()) + .putVariables(marketPlaceListingQuery.toMap()) remoteSource.getMarketPlaceApps(queryBuilder) } val controller = - mapper.controller(strategy, dispatchers) + mapper.controller(strategy, dispatcher) - controller(deferred, callback) + controller(deferred, requestCallback) } /** * Clears data sources (databases, preferences, e.t.c) + * + * @param context Dispatcher context to run in */ - override suspend fun clearDataSource() { - localSource.clear() + override suspend fun clearDataSource(context: CoroutineDispatcher) { + withContext(dispatcher.io) { + localSource.clear() + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/contract/MarketPlaceSource.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/contract/MarketPlaceSource.kt index a461e1cb..63d3e130 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/contract/MarketPlaceSource.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/source/contract/MarketPlaceSource.kt @@ -1,16 +1,86 @@ package co.anitrend.retrofit.graphql.data.market.source.contract -import androidx.lifecycle.LiveData import androidx.paging.PagedList -import co.anitrend.arch.extension.dispatchers.SupportDispatchers -import co.anitrend.retrofit.graphql.data.arch.common.SamplePagedSource +import co.anitrend.arch.paging.legacy.source.SupportPagingDataSource +import co.anitrend.arch.request.callback.RequestCallback +import co.anitrend.arch.request.model.Request +import co.anitrend.retrofit.graphql.data.market.model.query.MarketPlaceListingQuery import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch -internal abstract class MarketPlaceSource( - dispatchers: SupportDispatchers -) : SamplePagedSource(dispatchers) { +internal abstract class MarketPlaceSource : SupportPagingDataSource() { - protected abstract val observable: LiveData> + protected abstract val observable: Flow> - operator fun invoke() = observable + protected abstract suspend fun getMarketPlaceListing( + requestCallback: RequestCallback, + marketPlaceListingQuery: MarketPlaceListingQuery, + ) + + operator fun invoke(): Flow> { + return observable + } + + /** + * Called when the item at the end of the PagedList has been loaded, and access has + * occurred within [Config.prefetchDistance] of it. + * + * + * No more data will be appended to the PagedList after this item. + * + * @param itemAtEnd The first item of PagedList + */ + override fun onItemAtEndLoaded(itemAtEnd: MarketPlaceListing) { + super.onItemAtEndLoaded(itemAtEnd) + scope.launch { + val request = Request.Default(itemAtEnd.cursorId, Request.Type.AFTER) + requestHelper.runIfNotRunning(request) { requestCallback -> + val query = MarketPlaceListingQuery( + after = itemAtEnd.cursorId, + first = supportPagingHelper.pageSize + ) + getMarketPlaceListing(requestCallback, query) + } + } + } + + /** + * Called when the item at the front of the PagedList has been loaded, and access has + * occurred within [Config.prefetchDistance] of it. + * + * + * No more data will be prepended to the PagedList before this item. + * + * @param itemAtFront The first item of PagedList + */ + override fun onItemAtFrontLoaded(itemAtFront: MarketPlaceListing) { + super.onItemAtFrontLoaded(itemAtFront) + scope.launch { + val request = Request.Default(itemAtFront.cursorId, Request.Type.BEFORE) + requestHelper.runIfNotRunning(request) { requestCallback -> + val query = MarketPlaceListingQuery( + before = itemAtFront.cursorId, + first = supportPagingHelper.pageSize + ) + getMarketPlaceListing(requestCallback, query) + } + } + } + + /** + * Called when zero items are returned from an initial load of the PagedList's data source. + */ + override fun onZeroItemsLoaded() { + super.onZeroItemsLoaded() + scope.launch { + val request = Request.Default("initial", Request.Type.INITIAL) + requestHelper.runIfNotRunning(request) { requestCallback -> + val query = MarketPlaceListingQuery( + first = supportPagingHelper.pageSize + ) + getMarketPlaceListing(requestCallback, query) + } + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/usecase/MarketPlaceUseCaseImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/usecase/MarketPlaceUseCaseImpl.kt index 30ca90a6..ab2dc410 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/usecase/MarketPlaceUseCaseImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/market/usecase/MarketPlaceUseCaseImpl.kt @@ -1,8 +1,8 @@ package co.anitrend.retrofit.graphql.data.market.usecase import androidx.paging.PagedList -import co.anitrend.arch.data.model.UserInterfaceState import co.anitrend.arch.data.repository.contract.ISupportRepository +import co.anitrend.arch.data.state.DataState import co.anitrend.retrofit.graphql.data.market.repository.MarketPlaceRepositoryContract import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing import co.anitrend.retrofit.graphql.domain.usecases.MarketPlaceUseCase @@ -20,4 +20,4 @@ internal class MarketPlaceUseCaseImpl( } } -typealias MarketPlaceUseCaseContract = MarketPlaceUseCase>> \ No newline at end of file +typealias MarketPlaceUseCaseContract = MarketPlaceUseCase>> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/converters/UserConverters.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/converters/UserConverters.kt index 69950163..cc2fdd08 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/converters/UserConverters.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/converters/UserConverters.kt @@ -1,64 +1,36 @@ package co.anitrend.retrofit.graphql.data.user.converters import co.anitrend.arch.data.converter.SupportConverter -import co.anitrend.arch.data.mapper.contract.ISupportMapperHelper -import co.anitrend.retrofit.graphql.data.arch.common.SampleMapper import co.anitrend.retrofit.graphql.data.user.entity.UserEntity import co.anitrend.retrofit.graphql.data.user.model.node.UserNode import co.anitrend.retrofit.graphql.domain.entities.user.User internal class UserEntityConverter( - override val fromType: (UserEntity) -> User = { from().transform(it) }, - override val toType: (User) -> UserEntity = { to().transform(it) } -) : SupportConverter() { - companion object : SampleMapper() { - override fun from() = - object : ISupportMapperHelper { - /** - * Transforms the the [source] to the target type - */ - override fun transform(source: UserEntity) = - User( - id = source.id, - avatar = source.avatarUrl, - bio = source.bio.orEmpty(), - status = User.Status( - emoji = source.statusEmoji.orEmpty(), - message = source.statusMessage.orEmpty() - ), - username = source.username - ) - } - - override fun to(): ISupportMapperHelper { - throw Throwable("Not yet implemented") - } - } -} + override val fromType: (UserEntity) -> User = { + User( + id = it.id, + avatar = it.avatarUrl, + bio = it.bio.orEmpty(), + status = User.Status( + emoji = it.statusEmoji.orEmpty(), + message = it.statusMessage.orEmpty() + ), + username = it.username + ) + }, + override val toType: (User) -> UserEntity = { throw NotImplementedError() } +) : SupportConverter() internal class UserModelConverter( - override val fromType: (UserEntity) -> UserNode = { from().transform(it) }, - override val toType: (UserNode) -> UserEntity = { to().transform(it) } -): SupportConverter() { - companion object : SampleMapper() { - override fun from(): ISupportMapperHelper { - throw Throwable("Not yet implemented") - } - - override fun to() = - object : ISupportMapperHelper { - /** - * Transforms the the [source] to the target type - */ - override fun transform(source: UserNode) = - UserEntity( - id = source.id, - username = source.login, - bio = source.bio, - avatarUrl = source.avatarUrl, - statusEmoji = source.status?.emoji, - statusMessage = source.status?.message - ) - } + override val fromType: (UserEntity) -> UserNode = { throw NotImplementedError() }, + override val toType: (UserNode) -> UserEntity = { + UserEntity( + id = it.id, + username = it.login, + bio = it.bio, + avatarUrl = it.avatarUrl, + statusEmoji = it.status?.emoji, + statusMessage = it.status?.message + ) } -} \ No newline at end of file +): SupportConverter() \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/koin/Modules.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/koin/Modules.kt index 85b90ece..7901f56c 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/koin/Modules.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/koin/Modules.kt @@ -24,7 +24,7 @@ private val sourceModule = module { strategy = onlineController(), mapper = get(), converter = get(), - dispatchers = get() + dispatcher = get() ) } } diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/repository/UserRepositoryImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/repository/UserRepositoryImpl.kt index f9861487..4c51c782 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/repository/UserRepositoryImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/repository/UserRepositoryImpl.kt @@ -1,8 +1,8 @@ package co.anitrend.retrofit.graphql.data.user.repository -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.arch.data.model.UserInterfaceState.Companion.create import co.anitrend.arch.data.repository.SupportRepository +import co.anitrend.arch.data.state.DataState +import co.anitrend.arch.data.state.DataState.Companion.create import co.anitrend.retrofit.graphql.data.user.source.contract.UserSource import co.anitrend.retrofit.graphql.domain.entities.user.User import co.anitrend.retrofit.graphql.domain.repositories.UserRepository @@ -16,4 +16,4 @@ internal class UserRepositoryImpl( ) } -typealias UserRepositoryContract = UserRepository> \ No newline at end of file +typealias UserRepositoryContract = UserRepository> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/UserSourceImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/UserSourceImpl.kt index 1776d4b1..69cc72a9 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/UserSourceImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/UserSourceImpl.kt @@ -1,6 +1,7 @@ package co.anitrend.retrofit.graphql.data.user.source -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher +import co.anitrend.arch.request.callback.RequestCallback import co.anitrend.retrofit.graphql.data.arch.controller.strategy.ControllerStrategy import co.anitrend.retrofit.graphql.data.arch.extensions.controller import co.anitrend.retrofit.graphql.data.authentication.settings.IAuthenticationSettings @@ -11,10 +12,12 @@ import co.anitrend.retrofit.graphql.data.user.entity.UserEntity import co.anitrend.retrofit.graphql.data.user.mapper.UserResponseMapper import co.anitrend.retrofit.graphql.data.user.source.contract.UserSource import io.github.wax911.library.model.request.QueryContainerBuilder +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext internal class UserSourceImpl( private val settings: IAuthenticationSettings, @@ -23,13 +26,13 @@ internal class UserSourceImpl( private val remoteSource: UserRemoteSource, private val localSource: UserLocalSource, private val strategy: ControllerStrategy, - dispatchers: SupportDispatchers -) : UserSource(dispatchers) { + override val dispatcher: ISupportDispatcher, +) : UserSource() { override val observable = flow { - val userFlowEntity = if ( - settings.authenticatedUserId.isNotEmpty() - ) localSource.getUserById(settings.authenticatedUserId) + val authId = settings.authenticatedUserId.value + val userFlowEntity = if (authId.isNotEmpty()) + localSource.getUserById(authId) else localSource.getDefaultUser() val userFlow = userFlowEntity.map { entity -> @@ -38,28 +41,32 @@ internal class UserSourceImpl( emitAll(userFlow) } - override suspend fun getCurrentUser() { - super.getCurrentUser() + override suspend fun getCurrentUser(requestCallback: RequestCallback) { + val authId = settings.authenticatedUserId.value // simulating some sort of cache refresh policy - if (settings.authenticatedUserId.isNotEmpty()) return + if (authId.isNotEmpty()) return val deferred = async { val queryBuilder = QueryContainerBuilder() remoteSource.getCurrentUser(queryBuilder) } val controller = - mapper.controller(strategy, dispatchers) + mapper.controller(strategy, dispatcher) - val result = controller(deferred, networkState) + val result = controller(deferred, requestCallback) if (result != null) - settings.authenticatedUserId = result.id + settings.authenticatedUserId.value = result.id } /** * Clears data sources (databases, preferences, e.t.c) + * + * @param context Dispatcher context to run in */ - override suspend fun clearDataSource() { - settings.authenticatedUserId = IAuthenticationSettings.INVALID_USER_ID - localSource.clear() + override suspend fun clearDataSource(context: CoroutineDispatcher) { + withContext(context) { + settings.authenticatedUserId.value = IAuthenticationSettings.INVALID_USER_ID + localSource.clear() + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/contract/UserSource.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/contract/UserSource.kt index 3f61cbed..b1a31cf6 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/contract/UserSource.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/source/contract/UserSource.kt @@ -1,26 +1,28 @@ package co.anitrend.retrofit.graphql.data.user.source.contract -import androidx.lifecycle.asLiveData -import androidx.lifecycle.liveData -import co.anitrend.arch.data.source.coroutine.SupportCoroutineDataSource -import co.anitrend.arch.extension.dispatchers.SupportDispatchers +import co.anitrend.arch.data.source.core.SupportCoreDataSource +import co.anitrend.arch.request.callback.RequestCallback +import co.anitrend.arch.request.model.Request import co.anitrend.retrofit.graphql.domain.entities.user.User import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch -internal abstract class UserSource( - dispatchers: SupportDispatchers -) : SupportCoroutineDataSource(dispatchers) { +internal abstract class UserSource : SupportCoreDataSource() { protected abstract val observable: Flow - protected open suspend fun getCurrentUser() { - retry = { getCurrentUser() } - } + protected abstract suspend fun getCurrentUser( + requestCallback: RequestCallback + ) - operator fun invoke() = liveData { - getCurrentUser() - val userFlow = observable.mapNotNull { it } - emitSource(userFlow.asLiveData()) + operator fun invoke(): Flow { + scope.launch { + requestHelper.runIfNotRunning( + request = Request.Default("getCurrentUser", Request.Type.INITIAL), + handleCallback = ::getCurrentUser + ) + } + return observable.mapNotNull { it } } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/usecase/UserUseCaseImpl.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/usecase/UserUseCaseImpl.kt index 70e2300f..c1c14cf4 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/usecase/UserUseCaseImpl.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/data/user/usecase/UserUseCaseImpl.kt @@ -1,7 +1,7 @@ package co.anitrend.retrofit.graphql.data.user.usecase -import co.anitrend.arch.data.model.UserInterfaceState import co.anitrend.arch.data.repository.contract.ISupportRepository +import co.anitrend.arch.data.state.DataState import co.anitrend.retrofit.graphql.data.user.repository.UserRepositoryContract import co.anitrend.retrofit.graphql.domain.entities.user.User import co.anitrend.retrofit.graphql.domain.usecases.UserUseCase @@ -18,4 +18,4 @@ internal class UserUseCaseImpl( } } -typealias UserUseCaseContract = UserUseCase> \ No newline at end of file +typealias UserUseCaseContract = UserUseCase> \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/BucketRepository.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/BucketRepository.kt index ce64b3ec..00a3d1fd 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/BucketRepository.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/BucketRepository.kt @@ -1,5 +1,7 @@ package co.anitrend.retrofit.graphql.domain.repositories -interface BucketRepository { +import co.anitrend.arch.domain.state.UiState + +interface BucketRepository> { fun getAllFiles() : D } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/MarketPlaceRepository.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/MarketPlaceRepository.kt index 7ac5d675..98c4faa8 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/MarketPlaceRepository.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/MarketPlaceRepository.kt @@ -1,5 +1,7 @@ package co.anitrend.retrofit.graphql.domain.repositories -interface MarketPlaceRepository { +import co.anitrend.arch.domain.state.UiState + +interface MarketPlaceRepository> { fun getMarketPlaceListings(): D } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UploadRepository.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UploadRepository.kt index 3102582b..34c76e86 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UploadRepository.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UploadRepository.kt @@ -1,7 +1,8 @@ package co.anitrend.retrofit.graphql.domain.repositories +import co.anitrend.arch.domain.state.UiState import co.anitrend.retrofit.graphql.domain.models.common.IGraphQuery -interface UploadRepository { +interface UploadRepository> { fun uploadToBucket(mutation: IGraphQuery): D } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UserRepository.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UserRepository.kt index 783dcb06..51349f9a 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UserRepository.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/domain/repositories/UserRepository.kt @@ -1,5 +1,7 @@ package co.anitrend.retrofit.graphql.domain.repositories -interface UserRepository { +import co.anitrend.arch.domain.state.UiState + +interface UserRepository> { fun getCurrentUser(): D } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/App.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/App.kt index e996f466..d97c11e6 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/App.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/App.kt @@ -7,13 +7,6 @@ import io.wax911.emojify.serializer.kotlinx.KotlinxDeserializer class App : SampleApp() { - /** - * Emoji manager instance - */ - override val emojiManager: EmojiManager by lazy { - EmojiManager.create(this, serializer = KotlinxDeserializer()) - } - /** * Called when the application is starting, before any activity, service, * or receiver objects (excluding content providers) have been created. diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/di/AppModules.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/di/AppModules.kt index fae3289a..ac2b89d4 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/di/AppModules.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/di/AppModules.kt @@ -1,6 +1,5 @@ package co.anitrend.retrofit.graphql.sample.di -import android.content.Context import android.net.ConnectivityManager import co.anitrend.arch.core.provider.SupportFileProvider import co.anitrend.arch.extension.ext.systemServiceOf @@ -15,10 +14,13 @@ import co.anitrend.retrofit.graphql.sample.view.MainScreen import co.anitrend.retrofit.graphql.sample.view.content.bucket.BucketContent import co.anitrend.retrofit.graphql.sample.view.content.bucket.ui.adapter.BucketAdapter import co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.BucketViewModel +import co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.UploadViewModel import co.anitrend.retrofit.graphql.sample.view.content.market.MarketPlaceContent import co.anitrend.retrofit.graphql.sample.view.content.market.ui.adapter.MarketPlaceAdapter import co.anitrend.retrofit.graphql.sample.view.content.market.viewmodel.MarketPlaceViewModel import co.anitrend.retrofit.graphql.sample.viewmodel.MainViewModel +import io.wax911.emojify.EmojiManager +import io.wax911.emojify.serializer.kotlinx.KotlinxDeserializer import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.androidx.fragment.dsl.fragment @@ -40,9 +42,13 @@ private val appModule = module { single { SupportConnectivity( androidApplication() - .systemServiceOf( - Context.CONNECTIVITY_SERVICE - ) + .systemServiceOf() + ) + } + single(createdAtStart = true) { + EmojiManager.create( + context = androidApplication(), + serializer = KotlinxDeserializer() ) } } @@ -60,8 +66,12 @@ private val viewModelModule = module { } viewModel { BucketViewModel( - bucketUseCase = get(), - uploadUseCase = get() + useCase = get(), + ) + } + viewModel { + UploadViewModel( + useCase = get(), ) } } @@ -72,7 +82,8 @@ private val presenterModule = module { MainPresenter( context = androidContext(), settings = get(), - stateLayoutConfig = get() + stateLayoutConfig = get(), + emojiManager = get(), ) } } @@ -80,7 +91,8 @@ private val presenterModule = module { scoped { BucketPresenter( context = androidContext(), - settings = get() + settings = get(), + emojiManager = get(), ) } } @@ -95,7 +107,7 @@ private val fragmentModule = module { supportViewAdapter = MarketPlaceAdapter( resources = androidContext().resources, stateConfiguration = stateConfig - ) + ), ) } } diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/extensions/AppExtensions.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/extensions/AppExtensions.kt deleted file mode 100644 index d739c2ef..00000000 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/extensions/AppExtensions.kt +++ /dev/null @@ -1,10 +0,0 @@ -package co.anitrend.retrofit.graphql.sample.extensions - -import android.content.Context -import co.anitrend.retrofit.graphql.sample.App -import io.wax911.emojify.EmojiManager - -fun Context.emojify(): EmojiManager { - val app = applicationContext as App - return app.emojiManager -} \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/BucketPresenter.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/BucketPresenter.kt index ad8d5847..7dbaec5b 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/BucketPresenter.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/BucketPresenter.kt @@ -6,8 +6,11 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import co.anitrend.arch.core.presenter.SupportPresenter +import co.anitrend.arch.domain.entities.LoadState +import co.anitrend.arch.domain.entities.RequestError import co.anitrend.retrofit.graphql.core.settings.Settings import co.anitrend.retrofit.graphql.data.bucket.model.upload.mutation.UploadMutation +import io.wax911.emojify.EmojiManager import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.File @@ -16,7 +19,8 @@ import java.io.InputStream class BucketPresenter( context: Context, - settings: Settings + settings: Settings, + private val emojiManager: EmojiManager, ) : SupportPresenter(context, settings) { /** @@ -33,6 +37,16 @@ class BucketPresenter( return outputFile?.let { UploadMutation(it.absolutePath) } } + fun loadStateFailure(): LoadState { + val emoji = emojiManager.getForAlias("gallery") + return LoadState.Error( + RequestError( + topic = "${emoji?.unicode} No images found", + description = "Try to upload some pictures and they will show up here" + ) + ) + } + companion object { private fun InputStream.optimizeImage(context: Context): File? { val cache = context.externalCacheDir ?: context.cacheDir diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/MainPresenter.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/MainPresenter.kt index 88b71e64..8d85f04e 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/MainPresenter.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/presenter/MainPresenter.kt @@ -7,13 +7,14 @@ import co.anitrend.retrofit.graphql.core.extension.using import co.anitrend.retrofit.graphql.core.settings.Settings import co.anitrend.retrofit.graphql.domain.entities.user.User import co.anitrend.retrofit.graphql.sample.databinding.NavHeaderMainBinding -import co.anitrend.retrofit.graphql.sample.extensions.emojify import coil.transform.CircleCropTransformation +import io.wax911.emojify.EmojiManager import io.wax911.emojify.parser.parseToUnicode class MainPresenter( context: Context, settings: Settings, + private val emojiManager: EmojiManager, private val stateLayoutConfig: StateLayoutConfig ) : SupportPresenter(context, settings) { @@ -22,7 +23,6 @@ class MainPresenter( } fun updateNavigationHeaderView(user: User, binding: NavHeaderMainBinding) { - val emojiManager = context.emojify() binding.navAvatar.using(user.avatar, null, CircleCropTransformation()) binding.navUserName.text = user.username binding.navUserBio.text = user.bio diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/MainScreen.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/MainScreen.kt index 31ff5c7c..cb17ab00 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/MainScreen.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/MainScreen.kt @@ -6,10 +6,10 @@ import android.view.MenuItem import android.widget.Toast import androidx.annotation.IdRes import androidx.annotation.StringRes -import androidx.lifecycle.Observer +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import co.anitrend.arch.domain.entities.NetworkState -import co.anitrend.arch.extension.ext.LAZY_MODE_UNSAFE +import androidx.lifecycle.repeatOnLifecycle +import co.anitrend.arch.extension.ext.UNSAFE import co.anitrend.retrofit.graphql.core.extension.commit import co.anitrend.retrofit.graphql.core.model.FragmentItem import co.anitrend.retrofit.graphql.core.view.SampleActivity @@ -29,11 +29,11 @@ import timber.log.Timber class MainScreen : SampleActivity(), NavigationView.OnNavigationItemSelectedListener { - private val binding by lazy(LAZY_MODE_UNSAFE) { + private val binding by lazy(UNSAFE) { ActivityMainBinding.inflate(layoutInflater) } - private val bottomDrawerBehavior by lazy(LAZY_MODE_UNSAFE) { + private val bottomDrawerBehavior by lazy(UNSAFE) { BottomSheetBehavior.from(binding.bottomNavigationDrawer) } @@ -44,7 +44,7 @@ class MainScreen : SampleActivity(), NavigationView.OnNavigationItemSelectedList private val presenter by inject() - private val headerBinding by lazy(LAZY_MODE_UNSAFE) { + private val headerBinding by lazy(UNSAFE) { val headerView = binding.bottomNavigationView.getHeaderView(0) NavHeaderMainBinding.bind(headerView) } @@ -64,15 +64,13 @@ class MainScreen : SampleActivity(), NavigationView.OnNavigationItemSelectedList binding.bottomNavigationView.apply { setCheckedItem(selectedItem) setNavigationItemSelectedListener(this@MainScreen) - val observer = Observer { - headerBinding.navStateLayout.networkMutableStateFlow.value = it - } presenter.configureNavigationHeader(headerBinding) - viewModel.state.networkState.observe(this@MainScreen, observer) - viewModel.state.refreshState.observe(this@MainScreen, observer) - viewModel.state.model.observe(this@MainScreen, Observer { - presenter.updateNavigationHeaderView(it, headerBinding) - }) + } + viewModelState().combinedLoadState.observe(this@MainScreen) { + headerBinding.navStateLayout.loadStateFlow.value = it + } + viewModelState().model.observe(this@MainScreen) { + presenter.updateNavigationHeaderView(it, headerBinding) } updateUserInterface() } @@ -85,15 +83,20 @@ class MainScreen : SampleActivity(), NavigationView.OnNavigationItemSelectedList } override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(key_navigation_selected, selectedItem) - outState.putInt(key_navigation_title, selectedTitle) + outState.putInt(KEY_NAVIGATION_SELECTED, selectedItem) + outState.putInt(KEY_NAVIGATION_TITLE, selectedTitle) super.onSaveInstanceState(outState) } + /** + * Proxy for a view model state if one exists + */ + override fun viewModelState() = viewModel + override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) - selectedItem = savedInstanceState.getInt(key_navigation_selected) - selectedTitle = savedInstanceState.getInt(key_navigation_title) + selectedItem = savedInstanceState.getInt(KEY_NAVIGATION_SELECTED) + selectedTitle = savedInstanceState.getInt(KEY_NAVIGATION_TITLE) } override fun onBackPressed() { @@ -173,19 +176,22 @@ class MainScreen : SampleActivity(), NavigationView.OnNavigationItemSelectedList } private fun updateUserInterface() { - lifecycleScope.launchWhenResumed { - val stateLayout = headerBinding.navStateLayout - stateLayout.interactionStateFlow.collect { viewModel.state.retry() } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + headerBinding.navStateLayout.interactionFlow.collect { viewModel.retry() } + } } - lifecycleScope.launchWhenResumed { - viewModel.state() + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel() + } } if (selectedItem != 0) onNavigateToTarget(selectedItem) else onNavigateToTarget(R.id.nav_app_store) } companion object { - private const val key_navigation_selected = "key_navigation_selected" - private const val key_navigation_title = "key_navigation_title" + private const val KEY_NAVIGATION_SELECTED = "key_navigation_selected" + private const val KEY_NAVIGATION_TITLE = "key_navigation_title" } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/BucketContent.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/BucketContent.kt index 6523388a..5d757611 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/BucketContent.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/BucketContent.kt @@ -2,44 +2,60 @@ package co.anitrend.retrofit.graphql.sample.view.content.bucket import android.net.Uri import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.lifecycle.Observer +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import co.anitrend.arch.domain.entities.NetworkState -import co.anitrend.arch.domain.extensions.isSuccess -import co.anitrend.arch.extension.dispatchers.SupportDispatchers -import co.anitrend.arch.recycler.adapter.contract.ISupportAdapter +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import co.anitrend.arch.domain.entities.LoadState +import co.anitrend.arch.domain.entities.RequestError +import co.anitrend.arch.extension.dispatchers.contract.ISupportDispatcher +import co.anitrend.arch.recycler.adapter.SupportAdapter +import co.anitrend.arch.ui.fragment.list.contract.ISupportFragmentList +import co.anitrend.arch.ui.fragment.list.presenter.SupportListPresenter +import co.anitrend.arch.ui.view.widget.contract.ISupportStateLayout import co.anitrend.arch.ui.view.widget.model.StateLayoutConfig import co.anitrend.retrofit.graphql.core.view.SampleListFragment import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.sample.R import co.anitrend.retrofit.graphql.sample.databinding.BucketContentBinding -import co.anitrend.retrofit.graphql.sample.extensions.emojify import co.anitrend.retrofit.graphql.sample.presenter.BucketPresenter import co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.BucketViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect +import co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.UploadViewModel +import io.wax911.emojify.EmojiManager import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.androidx.scope.lifecycleScope as koinScope class BucketContent( - override val defaultSpanSize: Int = co.anitrend.arch.ui.R.integer.grid_list_x3, + override val defaultSpanSize: Int = co.anitrend.arch.theme.R.integer.grid_list_x3, override val stateConfig: StateLayoutConfig, - override val supportViewAdapter: ISupportAdapter, - override val inflateLayout: Int = R.layout.bucket_content + override val supportViewAdapter: SupportAdapter, + override val inflateLayout: Int = R.layout.bucket_content, ) : SampleListFragment() { + override val listPresenter = object : SupportListPresenter() { + override val recyclerView: RecyclerView + get() = binding.supportRecyclerView + override val stateLayout: ISupportStateLayout + get() = binding.supportStateLayout + override val swipeRefreshLayout: SwipeRefreshLayout + get() = binding.supportRefreshLayout + } + private lateinit var binding: BucketContentBinding - private val viewModel by viewModel() + private val bucketViewModel by viewModel() + private val uploadViewModel by viewModel() private val presenter by inject() - private val dispatchers by inject() + private val dispatchers by inject() private val activityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> @@ -48,14 +64,11 @@ class BucketContent( val mutation = withContext (dispatchers.io) { presenter.resolve(uri, requireActivity().contentResolver) } - if (mutation != null) - viewModel.uploadState(mutation) - else - Toast.makeText( - context, - "Unable to resolve content", - Toast.LENGTH_SHORT - ).show() + mutation?.also(uploadViewModel::invoke) ?: Toast.makeText( + context, + "Unable to resolve content", + Toast.LENGTH_SHORT + ).show() } } @@ -67,14 +80,45 @@ class BucketContent( */ override fun initializeComponents(savedInstanceState: Bundle?) { super.initializeComponents(savedInstanceState) - lifecycleScope.launchWhenResumed { - binding.uploadStateLayout.interactionStateFlow - .debounce(16) - .filterNotNull() - .collect { viewModel.uploadState.retry() } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + binding.uploadStateLayout.interactionFlow + .debounce(16) + .filterNotNull() + .collect { uploadViewModel.retry() } + } } } + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null (which + * is the default implementation). This will be called between + * [onCreate] and [onActivityCreated]. + * + * If you return a View from here, you will later be called in + * [onDestroyView] when the view is being released. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the [View] for the fragment's UI, or null. + */ + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = BucketContentBinding.inflate(inflater, container, false) + listPresenter.onCreateView(this, binding.root) + return binding.root + } + /** * Called immediately after [onCreateView] has returned, but before any saved state has been * restored in to the view. This gives subclasses a chance to initialize themselves once @@ -87,7 +131,6 @@ class BucketContent( */ override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding = BucketContentBinding.bind(view) binding.uploadStateLayout.stateConfigFlow.value = stateConfig binding.selectUploadFile.setOnClickListener { // filter to only allow images to be selected @@ -104,23 +147,7 @@ class BucketContent( * @see initializeComponents */ override fun onFetchDataInitialize() { - viewModel.bucketState() - } - - /** - * Informs the underlying [SupportStateLayout] of changes to the [NetworkState] - * - * @param networkState New state from the application - */ - @ExperimentalCoroutinesApi - override fun changeLayoutState(networkState: NetworkState?) { - super.changeLayoutState(networkState) - if (networkState?.isSuccess() != true) return - val emoji = context?.emojify()?.getForAlias("gallery") - binding.supportStateLayout.networkMutableStateFlow.value = NetworkState.Error( - heading = "${emoji?.unicode} No images found", - message = "Try to upload some pictures and they will show up here" - ) + bucketViewModel() } /** @@ -128,31 +155,30 @@ class BucketContent( * called in [onViewCreated] */ override fun setUpViewModelObserver() { - val uploadStateObserver = Observer { - binding.uploadStateLayout.networkMutableStateFlow.value = it + uploadViewModel.combinedLoadState.observe( + viewLifecycleOwner + ) { + binding.uploadStateLayout.loadStateFlow.value = it } - viewModel.uploadState.networkState.observe( - viewLifecycleOwner, - uploadStateObserver - ) - viewModel.uploadState.refreshState.observe( - viewLifecycleOwner, - uploadStateObserver - ) - viewModel.uploadState.model.observe( - viewLifecycleOwner, - Observer { viewModelState().refresh() } - ) + uploadViewModel.model.observe( + viewLifecycleOwner + ) { viewModelState().invoke() } + viewModelState().model.observe( - viewLifecycleOwner, - Observer { onPostModelChange(it) } - ) + viewLifecycleOwner + ) { onPostModelChange(it) } + + viewModelState().combinedLoadState.observe(viewLifecycleOwner) { + if (it is LoadState.Error) { + binding.supportStateLayout.loadStateFlow.value = presenter.loadStateFailure() + } + } } /** * Proxy for a view model state if one exists */ - override fun viewModelState() = viewModel.bucketState + override fun viewModelState() = bucketViewModel /** * Called when the view previously created by [onCreateView] has diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/adapter/BucketAdapter.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/adapter/BucketAdapter.kt index 3b94827f..1289d5ef 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/adapter/BucketAdapter.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/adapter/BucketAdapter.kt @@ -8,18 +8,18 @@ import co.anitrend.arch.core.model.IStateLayoutConfig import co.anitrend.arch.recycler.action.contract.ISupportSelectionMode import co.anitrend.arch.recycler.adapter.SupportListAdapter import co.anitrend.arch.recycler.model.contract.IRecyclerItem -import co.anitrend.arch.theme.animator.contract.ISupportAnimator +import co.anitrend.arch.theme.animator.contract.AbstractAnimator import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile import co.anitrend.retrofit.graphql.sample.view.content.bucket.ui.controller.helper.DIFFER import co.anitrend.retrofit.graphql.sample.view.content.bucket.ui.controller.model.BucketFileItem import co.anitrend.retrofit.graphql.sample.view.content.bucket.ui.controller.model.BucketFileItem.Companion.createViewHolder class BucketAdapter( - override val customSupportAnimator: ISupportAnimator? = null, - override val mapper: (BucketFile?) -> IRecyclerItem = { BucketFileItem(it) }, override val resources: Resources, override val stateConfiguration: IStateLayoutConfig, - override val supportAction: ISupportSelectionMode? = null + override val mapper: (BucketFile) -> IRecyclerItem = { BucketFileItem(it) }, + override val supportAction: ISupportSelectionMode? = null, + override val customSupportAnimator: AbstractAnimator? = null, ) : SupportListAdapter(DIFFER) { /** diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/controller/model/BucketFileItem.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/controller/model/BucketFileItem.kt index 701d7c52..fecfccca 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/controller/model/BucketFileItem.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/ui/controller/model/BucketFileItem.kt @@ -18,8 +18,8 @@ import coil.transform.RoundedCornersTransformation import kotlinx.coroutines.flow.MutableStateFlow internal class BucketFileItem( - private val entity: BucketFile? -) : RecyclerItem(entity?.id?.toLong()) { + private val entity: BucketFile +) : RecyclerItem(entity.id.toLong()) { private var disposable: Disposable? = null private var binding: BucketFileItemBinding? = null @@ -38,14 +38,14 @@ internal class BucketFileItem( view: View, position: Int, payloads: List, - stateFlow: MutableStateFlow, + stateFlow: MutableStateFlow, selectionMode: ISupportSelectionMode? ) { binding = BucketFileItemBinding.bind(view) - binding?.bucketImageName?.text = entity?.fileName - val margin = view.resources.getDimension(co.anitrend.arch.ui.R.dimen.lg_margin) + binding?.bucketImageName?.text = entity.fileName + val margin = view.resources.getDimension(co.anitrend.arch.theme.R.dimen.lg_margin) binding?.bucketImage?.using( - entity?.url, + entity.url, ColorDrawable(-0x333334), RoundedCornersTransformation( margin, margin, margin, margin @@ -61,7 +61,7 @@ internal class BucketFileItem( * @param resources optionally useful for dynamic size check with different configurations */ override fun getSpanSize(spanCount: Int, position: Int, resources: Resources): Int { - return resources.getInteger(co.anitrend.arch.ui.R.integer.grid_list_x3) + return resources.getInteger(co.anitrend.arch.theme.R.integer.grid_list_x3) } /** @@ -79,6 +79,6 @@ internal class BucketFileItem( viewGroup: ViewGroup ) = BucketFileItemBinding.inflate( this, viewGroup, false - ).let { SupportViewHolder(it.root) } + ).let { SupportViewHolder(it) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/BucketViewModel.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/BucketViewModel.kt index a7a69185..21d5c4f0 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/BucketViewModel.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/BucketViewModel.kt @@ -1,18 +1,59 @@ package co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import co.anitrend.arch.core.model.ISupportViewModelState +import co.anitrend.arch.data.state.DataState import co.anitrend.retrofit.graphql.data.bucket.usecase.BucketUseCaseContract -import co.anitrend.retrofit.graphql.data.bucket.usecase.upload.UploadUseCaseContract -import co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.state.BucketState -import co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.state.UploadState +import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile +import kotlinx.coroutines.flow.merge class BucketViewModel( - bucketUseCase: BucketUseCaseContract, - uploadUseCase: UploadUseCaseContract -) : ViewModel() { + private val useCase: BucketUseCaseContract +) : ViewModel(), ISupportViewModelState> { - val bucketState = BucketState(bucketUseCase) - val uploadState = UploadState(uploadUseCase) + private val state = MutableLiveData>>() + + override val model = state.switchMap { + it.model.asLiveData(viewModelScope.coroutineContext) + } + + override val loadState = state.switchMap { + it.loadState.asLiveData(viewModelScope.coroutineContext) + } + + override val refreshState = state.switchMap { + it.refreshState.asLiveData(viewModelScope.coroutineContext) + } + + val combinedLoadState = state.switchMap { + val result = merge(it.loadState, it.refreshState) + result.asLiveData(viewModelScope.coroutineContext) + } + + operator fun invoke() { + val result = useCase() + state.postValue(result) + } + + /** + * Triggers use case to perform refresh operation + */ + override suspend fun refresh() { + val uiModel = state.value + uiModel?.refresh?.invoke() + } + + /** + * Triggers use case to perform a retry operation + */ + override suspend fun retry() { + val uiModel = state.value + uiModel?.retry?.invoke() + } /** * This method will be called when this ViewModel is no longer used and will be destroyed. @@ -21,8 +62,7 @@ class BucketViewModel( * prevent a leak of this ViewModel. */ override fun onCleared() { - bucketState.onCleared() - uploadState.onCleared() + useCase.onCleared() super.onCleared() } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/UploadViewModel.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/UploadViewModel.kt new file mode 100644 index 00000000..e53b94b1 --- /dev/null +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/UploadViewModel.kt @@ -0,0 +1,63 @@ +package co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import co.anitrend.arch.core.model.ISupportViewModelState +import co.anitrend.arch.data.state.DataState +import co.anitrend.retrofit.graphql.data.bucket.model.upload.mutation.UploadMutation +import co.anitrend.retrofit.graphql.data.bucket.usecase.upload.UploadUseCaseContract +import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile +import kotlinx.coroutines.flow.merge + +class UploadViewModel( + private val useCase: UploadUseCaseContract +) : ViewModel(), ISupportViewModelState { + + private val state = MutableLiveData>() + + override val model = state.switchMap { + it.model.asLiveData(viewModelScope.coroutineContext) + } + + override val loadState = state.switchMap { + it.loadState.asLiveData(viewModelScope.coroutineContext) + } + + override val refreshState = state.switchMap { + it.refreshState.asLiveData(viewModelScope.coroutineContext) + } + + val combinedLoadState = state.switchMap { + val result = merge(it.loadState, it.refreshState) + result.asLiveData(viewModelScope.coroutineContext) + } + + operator fun invoke(mutation: UploadMutation) { + val result = useCase(mutation) + state.postValue(result) + } + + /** + * Triggers use case to perform refresh operation + */ + override suspend fun refresh() { + val uiModel = state.value + uiModel?.refresh?.invoke() + } + + /** + * Triggers use case to perform a retry operation + */ + override suspend fun retry() { + val uiModel = state.value + uiModel?.retry?.invoke() + } + + override fun onCleared() { + useCase.onCleared() + super.onCleared() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/state/BucketState.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/state/BucketState.kt deleted file mode 100644 index f5a66d90..00000000 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/state/BucketState.kt +++ /dev/null @@ -1,54 +0,0 @@ -package co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.state - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.switchMap -import co.anitrend.arch.core.model.ISupportViewModelState -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.retrofit.graphql.data.bucket.usecase.BucketUseCaseContract -import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile - -class BucketState( - private val useCase: BucketUseCaseContract -) : ISupportViewModelState> { - - private val useCaseResult = MutableLiveData>>() - - override val model = - useCaseResult.switchMap() { it.model } - override val networkState = - useCaseResult.switchMap() { it.networkState } - override val refreshState = - useCaseResult.switchMap() { it.refreshState } - - operator fun invoke() { - val result = useCase() - useCaseResult.postValue(result) - } - - /** - * Called upon [androidx.lifecycle.ViewModel.onCleared] and should optionally - * call cancellation of any ongoing jobs. - * - * If your use case source is of type [co.anitrend.arch.domain.common.IUseCase] - * then you could optionally call [co.anitrend.arch.domain.common.IUseCase.onCleared] here - */ - override fun onCleared() { - useCase.onCleared() - } - - /** - * Triggers use case to perform refresh operation - */ - override fun refresh() { - val uiModel = useCaseResult.value - uiModel?.refresh?.invoke() - } - - /** - * Triggers use case to perform a retry operation - */ - override fun retry() { - val uiModel = useCaseResult.value - uiModel?.retry?.invoke() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/state/UploadState.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/state/UploadState.kt deleted file mode 100644 index 1c85eb14..00000000 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/bucket/viewmodel/state/UploadState.kt +++ /dev/null @@ -1,55 +0,0 @@ -package co.anitrend.retrofit.graphql.sample.view.content.bucket.viewmodel.state - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.switchMap -import co.anitrend.arch.core.model.ISupportViewModelState -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.retrofit.graphql.data.bucket.model.upload.mutation.UploadMutation -import co.anitrend.retrofit.graphql.data.bucket.usecase.upload.UploadUseCaseContract -import co.anitrend.retrofit.graphql.domain.entities.bucket.BucketFile - -class UploadState( - private val useCase: UploadUseCaseContract -) : ISupportViewModelState { - - private val useCaseResult = MutableLiveData>() - - override val model = - useCaseResult.switchMap() { it.model } - override val networkState = - useCaseResult.switchMap() { it.networkState } - override val refreshState = - useCaseResult.switchMap() { it.refreshState } - - operator fun invoke(mutation: UploadMutation) { - val result = useCase(mutation) - useCaseResult.postValue(result) - } - - /** - * Called upon [androidx.lifecycle.ViewModel.onCleared] and should optionally - * call cancellation of any ongoing jobs. - * - * If your use case source is of type [co.anitrend.arch.domain.common.IUseCase] - * then you could optionally call [co.anitrend.arch.domain.common.IUseCase.onCleared] here - */ - override fun onCleared() { - useCase.onCleared() - } - - /** - * Triggers use case to perform refresh operation - */ - override fun refresh() { - val uiModel = useCaseResult.value - uiModel?.refresh?.invoke() - } - - /** - * Triggers use case to perform a retry operation - */ - override fun retry() { - val uiModel = useCaseResult.value - uiModel?.retry?.invoke() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/MarketPlaceContent.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/MarketPlaceContent.kt index 8fc240bb..9b723e99 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/MarketPlaceContent.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/MarketPlaceContent.kt @@ -1,49 +1,68 @@ package co.anitrend.retrofit.graphql.sample.view.content.market import android.os.Bundle -import android.widget.Toast -import androidx.lifecycle.Observer -import androidx.lifecycle.lifecycleScope -import co.anitrend.arch.recycler.adapter.contract.ISupportAdapter -import co.anitrend.arch.recycler.common.DefaultClickableItem +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import co.anitrend.arch.extension.ext.getColorFromAttr +import co.anitrend.arch.recycler.SupportRecyclerView +import co.anitrend.arch.recycler.paging.legacy.adapter.SupportPagedListAdapter +import co.anitrend.arch.recycler.shared.adapter.SupportLoadStateAdapter +import co.anitrend.arch.ui.fragment.list.contract.ISupportFragmentList +import co.anitrend.arch.ui.fragment.list.presenter.SupportListPresenter +import co.anitrend.arch.ui.view.widget.contract.ISupportStateLayout import co.anitrend.arch.ui.view.widget.model.StateLayoutConfig import co.anitrend.retrofit.graphql.core.view.SampleListFragment import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing -import co.anitrend.retrofit.graphql.sample.R +import co.anitrend.retrofit.graphql.sample.databinding.SharedListContentBinding import co.anitrend.retrofit.graphql.sample.view.content.market.viewmodel.MarketPlaceViewModel -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.filterIsInstance import org.koin.androidx.viewmodel.ext.android.viewModel class MarketPlaceContent( - override val defaultSpanSize: Int = co.anitrend.arch.ui.R.integer.single_list_size, + override val inflateLayout: Int = co.anitrend.retrofit.graphql.sample.R.layout.shared_list_content, + override val defaultSpanSize: Int = co.anitrend.arch.theme.R.integer.single_list_size, override val stateConfig: StateLayoutConfig, - override val supportViewAdapter: ISupportAdapter + override val supportViewAdapter: SupportPagedListAdapter ) : SampleListFragment() { + override val listPresenter = object : SupportListPresenter() { + override val recyclerView: RecyclerView + get() = requireNotNull(binding).recyclerView + override val stateLayout: ISupportStateLayout + get() = requireNotNull(binding).stateLayout + override val swipeRefreshLayout: SwipeRefreshLayout + get() = requireNotNull(binding).swipeRefreshLayout + + private var binding: SharedListContentBinding? = null + + override fun onCreateView(fragmentList: ISupportFragmentList, view: View?) { + binding = SharedListContentBinding.bind(requireNotNull(view)) + super.onCreateView(fragmentList, view) + } + } + private val viewModel by viewModel() /** - * Additional initialization to be done in this method, this method will be called in - * [androidx.fragment.app.FragmentActivity.onCreate]. - * - * @param savedInstanceState + * Sets the adapter for the recycler view */ - override fun initializeComponents(savedInstanceState: Bundle?) { - super.initializeComponents(savedInstanceState) - lifecycleScope.launchWhenResumed { - supportViewAdapter.clickableStateFlow.debounce(16) - .filterIsInstance>() - .collect { item -> - item.data?.also { - Toast.makeText( - item.view.context, - "Item clicked: ${it.name}", - Toast.LENGTH_SHORT - ).show() - } - } + override fun setRecyclerAdapter(recyclerView: SupportRecyclerView) { + if (recyclerView.adapter == null) { + val header = SupportLoadStateAdapter(resources, stateConfig).apply { + registerFlowListener() + } + val footer = SupportLoadStateAdapter(resources, stateConfig).apply { + registerFlowListener() + } + + (supportViewAdapter as RecyclerView.Adapter<*>) + .stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY + + recyclerView.adapter = supportViewAdapter.withLoadStateHeaderAndFooter( + header = header, footer = footer + ) } } @@ -59,6 +78,38 @@ class MarketPlaceContent( viewModelState().invoke() } + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null (which + * is the default implementation). This will be called between + * [onCreate] and [onActivityCreated]. + * + * If you return a View from here, you will later be called in + * [onDestroyView] when the view is being released. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the [View] for the fragment's UI, or null. + */ + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = super.onCreateView(inflater, container, savedInstanceState) + listPresenter.swipeRefreshLayout?.setColorSchemeColors( + requireContext().getColorFromAttr(androidx.appcompat.R.attr.colorPrimary), + requireContext().getColorFromAttr(androidx.appcompat.R.attr.colorAccent) + ) + return view + } + /** * Invoke view model observer to watch for changes, this will be called * called in [onViewCreated] @@ -66,14 +117,12 @@ class MarketPlaceContent( override fun setUpViewModelObserver() { viewModelState().model.observe( viewLifecycleOwner, - Observer { - onPostModelChange(it) - } + ::onPostModelChange ) } /** * Proxy for a view model state if one exists */ - override fun viewModelState() = viewModel.state + override fun viewModelState() = viewModel } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceAdapter.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceAdapter.kt index c16e2636..09578814 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceAdapter.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceAdapter.kt @@ -6,9 +6,9 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import co.anitrend.arch.core.model.IStateLayoutConfig import co.anitrend.arch.recycler.action.contract.ISupportSelectionMode -import co.anitrend.arch.recycler.adapter.SupportPagedListAdapter import co.anitrend.arch.recycler.model.contract.IRecyclerItem -import co.anitrend.arch.theme.animator.contract.ISupportAnimator +import co.anitrend.arch.recycler.paging.legacy.adapter.SupportPagedListAdapter +import co.anitrend.arch.theme.animator.contract.AbstractAnimator import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing import co.anitrend.retrofit.graphql.sample.view.content.market.ui.controller.helpers.DIFFER import co.anitrend.retrofit.graphql.sample.view.content.market.ui.controller.model.MarketPlaceListingItem @@ -17,11 +17,9 @@ import co.anitrend.retrofit.graphql.sample.view.content.market.ui.controller.mod class MarketPlaceAdapter( override val resources: Resources, override val stateConfiguration: IStateLayoutConfig, - override val customSupportAnimator: ISupportAnimator? = null, + override val customSupportAnimator: AbstractAnimator? = null, override val supportAction: ISupportSelectionMode? = null, - override val mapper: (MarketPlaceListing?) -> IRecyclerItem = { - MarketPlaceListingItem(it) - } + override val mapper: (MarketPlaceListing) -> IRecyclerItem = ::MarketPlaceListingItem ) : SupportPagedListAdapter(DIFFER) { /** diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceCategoryAdapter.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceCategoryAdapter.kt index 800d0567..8d80dde9 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceCategoryAdapter.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/adapter/MarketPlaceCategoryAdapter.kt @@ -8,18 +8,18 @@ import co.anitrend.arch.core.model.IStateLayoutConfig import co.anitrend.arch.recycler.action.contract.ISupportSelectionMode import co.anitrend.arch.recycler.adapter.SupportListAdapter import co.anitrend.arch.recycler.model.contract.IRecyclerItem -import co.anitrend.arch.theme.animator.contract.ISupportAnimator +import co.anitrend.arch.theme.animator.contract.AbstractAnimator import co.anitrend.arch.ui.view.widget.model.StateLayoutConfig import co.anitrend.retrofit.graphql.sample.view.content.market.ui.controller.helpers.CATEGORY_DIFFER import co.anitrend.retrofit.graphql.sample.view.content.market.ui.controller.model.MarketPlaceCategoryItem import co.anitrend.retrofit.graphql.sample.view.content.market.ui.controller.model.MarketPlaceCategoryItem.Companion.createViewHolder class MarketPlaceCategoryAdapter( - override val customSupportAnimator: ISupportAnimator? = null, - override val mapper: (String?) -> IRecyclerItem = { MarketPlaceCategoryItem(it) }, override val resources: Resources, + override val mapper: (String) -> IRecyclerItem = ::MarketPlaceCategoryItem, override val stateConfiguration: IStateLayoutConfig = StateLayoutConfig(), - override val supportAction: ISupportSelectionMode? = null + override val supportAction: ISupportSelectionMode? = null, + override val customSupportAnimator: AbstractAnimator? = null, ) : SupportListAdapter(CATEGORY_DIFFER) { /** * Should provide the required view holder, this function is a substitute for diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceCategoryItem.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceCategoryItem.kt index 2dc3bc17..5afdd4f5 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceCategoryItem.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceCategoryItem.kt @@ -8,14 +8,13 @@ import co.anitrend.arch.recycler.action.contract.ISupportSelectionMode import co.anitrend.arch.recycler.common.ClickableItem import co.anitrend.arch.recycler.holder.SupportViewHolder import co.anitrend.arch.recycler.model.RecyclerItem -import co.anitrend.retrofit.graphql.sample.R import co.anitrend.retrofit.graphql.sample.databinding.MarketPlaceCategoryItemBinding import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow class MarketPlaceCategoryItem( - private val entity: String? -) : RecyclerItem(entity?.hashCode()?.toLong()) { + private val entity: String +) : RecyclerItem(entity.hashCode().toLong()) { private var binding: MarketPlaceCategoryItemBinding? = null @@ -29,12 +28,11 @@ class MarketPlaceCategoryItem( * @param stateFlow observable to broadcast click events * @param selectionMode action mode helper or null if none was provided */ - @ExperimentalCoroutinesApi override fun bind( view: View, position: Int, payloads: List, - stateFlow: MutableStateFlow, + stateFlow: MutableStateFlow, selectionMode: ISupportSelectionMode? ) { binding = MarketPlaceCategoryItemBinding.bind(view) @@ -49,7 +47,7 @@ class MarketPlaceCategoryItem( * @param resources optionally useful for dynamic size check with different configurations */ override fun getSpanSize(spanCount: Int, position: Int, resources: Resources): Int { - return resources.getInteger(co.anitrend.arch.ui.R.integer.single_list_size) + return resources.getInteger(co.anitrend.arch.theme.R.integer.single_list_size) } /** @@ -65,6 +63,6 @@ class MarketPlaceCategoryItem( viewGroup: ViewGroup ) = MarketPlaceCategoryItemBinding.inflate( this, viewGroup, false - ).let { SupportViewHolder(it.root) } + ).let { SupportViewHolder(it) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceListingItem.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceListingItem.kt index 0b2d0618..aeeab986 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceListingItem.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/ui/controller/model/MarketPlaceListingItem.kt @@ -4,13 +4,13 @@ import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager import co.anitrend.arch.extension.ext.getCompatDrawable import co.anitrend.arch.extension.ext.gone import co.anitrend.arch.extension.ext.visible import co.anitrend.arch.recycler.action.contract.ISupportSelectionMode import co.anitrend.arch.recycler.common.ClickableItem -import co.anitrend.arch.recycler.common.DefaultClickableItem import co.anitrend.arch.recycler.holder.SupportViewHolder import co.anitrend.arch.recycler.model.RecyclerItem import co.anitrend.arch.ui.extension.setUpWith @@ -23,8 +23,8 @@ import coil.request.Disposable import kotlinx.coroutines.flow.MutableStateFlow class MarketPlaceListingItem( - private val entity: MarketPlaceListing? -) : RecyclerItem(entity?.id?.hashCode()?.toLong()) { + private val entity: MarketPlaceListing +) : RecyclerItem(entity.id.hashCode().toLong()) { private var disposable: Disposable? = null private var binding: MarketPlaceItemBinding? = null @@ -39,7 +39,7 @@ class MarketPlaceListingItem( ) ) - adapter.submitList(entity?.categories as List) + adapter.submitList(entity.categories) } } @@ -57,16 +57,12 @@ class MarketPlaceListingItem( view: View, position: Int, payloads: List, - stateFlow: MutableStateFlow, + stateFlow: MutableStateFlow, selectionMode: ISupportSelectionMode? ) { - if (entity == null) return binding = MarketPlaceItemBinding.bind(view) binding?.listingContainer?.setOnClickListener { - stateFlow.value = DefaultClickableItem( - data = entity, - view = view - ) + Toast.makeText(it.context, entity.name, Toast.LENGTH_SHORT).show() } disposable = binding?.listingImage?.using(entity.logoUrl) binding?.listingName?.text = entity.name @@ -76,7 +72,7 @@ class MarketPlaceListingItem( binding?.listingVerification?.setImageDrawable( view.context.getCompatDrawable( R.drawable.ic_whatshot_24dp, - co.anitrend.arch.ui.R.color.colorStateBlue + co.anitrend.arch.theme.R.color.colorStateBlue ) ) } @@ -93,7 +89,7 @@ class MarketPlaceListingItem( * @param resources optionally useful for dynamic size check with different configurations */ override fun getSpanSize(spanCount: Int, position: Int, resources: Resources): Int { - return resources.getInteger(co.anitrend.arch.ui.R.integer.single_list_size) + return resources.getInteger(co.anitrend.arch.theme.R.integer.single_list_size) } /** @@ -112,6 +108,6 @@ class MarketPlaceListingItem( viewGroup: ViewGroup ) = MarketPlaceItemBinding.inflate( this, viewGroup, false - ).let { SupportViewHolder(it.root) } + ).let(::SupportViewHolder) } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/viewmodel/MarketPlaceViewModel.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/viewmodel/MarketPlaceViewModel.kt index c4f81962..0b503185 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/viewmodel/MarketPlaceViewModel.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/viewmodel/MarketPlaceViewModel.kt @@ -1,21 +1,62 @@ package co.anitrend.retrofit.graphql.sample.view.content.market.viewmodel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import androidx.paging.PagedList +import co.anitrend.arch.core.model.ISupportViewModelState +import co.anitrend.arch.data.state.DataState +import co.anitrend.retrofit.graphql.data.bucket.model.upload.mutation.UploadMutation import co.anitrend.retrofit.graphql.data.market.usecase.MarketPlaceUseCaseContract -import co.anitrend.retrofit.graphql.sample.view.content.market.viewmodel.state.MarketPlaceState +import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing +import kotlinx.coroutines.flow.merge class MarketPlaceViewModel( private val useCase: MarketPlaceUseCaseContract -) : ViewModel() { +) : ViewModel(), ISupportViewModelState> { - val state = MarketPlaceState(useCase) + private val state = MutableLiveData>>() + + override val model = state.switchMap { + it.model.asLiveData(viewModelScope.coroutineContext) + } + + override val loadState = state.switchMap { + it.loadState.asLiveData(viewModelScope.coroutineContext) + } + + override val refreshState = state.switchMap { + it.refreshState.asLiveData(viewModelScope.coroutineContext) + } + + val combinedLoadState = state.switchMap { + val result = merge(it.loadState, it.refreshState) + result.asLiveData(viewModelScope.coroutineContext) + } + + operator fun invoke() { + val result = useCase() + state.postValue(result) + } + + /** + * Triggers use case to perform refresh operation + */ + override suspend fun refresh() { + val uiModel = state.value + uiModel?.refresh?.invoke() + } /** - * This method will be called when this ViewModel is no longer used and will be destroyed. - * - * It is useful when ViewModel observes some data and you need to clear this subscription to - * prevent a leak of this ViewModel. + * Triggers use case to perform a retry operation */ + override suspend fun retry() { + val uiModel = state.value + uiModel?.retry?.invoke() + } + override fun onCleared() { useCase.onCleared() super.onCleared() diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/viewmodel/state/MarketPlaceState.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/viewmodel/state/MarketPlaceState.kt deleted file mode 100644 index ced1595b..00000000 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/view/content/market/viewmodel/state/MarketPlaceState.kt +++ /dev/null @@ -1,55 +0,0 @@ -package co.anitrend.retrofit.graphql.sample.view.content.market.viewmodel.state - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.switchMap -import androidx.paging.PagedList -import co.anitrend.arch.core.model.ISupportViewModelState -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.retrofit.graphql.data.market.usecase.MarketPlaceUseCaseContract -import co.anitrend.retrofit.graphql.domain.entities.market.MarketPlaceListing - -data class MarketPlaceState( - private val useCase: MarketPlaceUseCaseContract -) : ISupportViewModelState> { - - private val useCaseResult = MutableLiveData>>() - - override val model = - useCaseResult.switchMap() { it.model } - override val networkState = - useCaseResult.switchMap() { it.networkState } - override val refreshState = - useCaseResult.switchMap() { it.refreshState } - - operator fun invoke() { - val result = useCase() - useCaseResult.postValue(result) - } - - /** - * Called upon [androidx.lifecycle.ViewModel.onCleared] and should optionally - * call cancellation of any ongoing jobs. - * - * If your use case source is of type [co.anitrend.arch.domain.common.IUseCase] - * then you could optionally call [co.anitrend.arch.domain.common.IUseCase.onCleared] here - */ - override fun onCleared() { - useCase.onCleared() - } - - /** - * Triggers use case to perform refresh operation - */ - override fun refresh() { - val uiModel = useCaseResult.value - uiModel?.refresh?.invoke() - } - - /** - * Triggers use case to perform a retry operation - */ - override fun retry() { - val uiModel = useCaseResult.value - uiModel?.retry?.invoke() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/viewmodel/MainViewModel.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/viewmodel/MainViewModel.kt index 4784c4cc..e21cc080 100644 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/viewmodel/MainViewModel.kt +++ b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/viewmodel/MainViewModel.kt @@ -1,13 +1,59 @@ package co.anitrend.retrofit.graphql.sample.viewmodel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope +import co.anitrend.arch.core.model.ISupportViewModelState +import co.anitrend.arch.data.state.DataState import co.anitrend.retrofit.graphql.data.user.usecase.UserUseCaseContract -import co.anitrend.retrofit.graphql.sample.viewmodel.model.MainState +import co.anitrend.retrofit.graphql.domain.entities.user.User +import kotlinx.coroutines.flow.merge class MainViewModel( - useCase: UserUseCaseContract -) : ViewModel() { - val state = MainState(useCase) + private val useCase: UserUseCaseContract +) : ViewModel(), ISupportViewModelState { + + private val state = MutableLiveData>() + + override val model = state.switchMap { + it.model.asLiveData(viewModelScope.coroutineContext) + } + + override val loadState = state.switchMap { + it.loadState.asLiveData(viewModelScope.coroutineContext) + } + + override val refreshState = state.switchMap { + it.refreshState.asLiveData(viewModelScope.coroutineContext) + } + + val combinedLoadState = state.switchMap { + val result = merge(it.loadState, it.refreshState) + result.asLiveData(viewModelScope.coroutineContext) + } + + operator fun invoke() { + val result = useCase() + state.postValue(result) + } + + /** + * Triggers use case to perform refresh operation + */ + override suspend fun refresh() { + val uiModel = state.value + uiModel?.refresh?.invoke() + } + + /** + * Triggers use case to perform a retry operation + */ + override suspend fun retry() { + val uiModel = state.value + uiModel?.retry?.invoke() + } /** * This method will be called when this ViewModel is no longer used and will be destroyed. @@ -16,7 +62,7 @@ class MainViewModel( * prevent a leak of this ViewModel. */ override fun onCleared() { - state.onCleared() + useCase.onCleared() super.onCleared() } } \ No newline at end of file diff --git a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/viewmodel/model/MainState.kt b/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/viewmodel/model/MainState.kt deleted file mode 100644 index 8d25af4b..00000000 --- a/app/src/main/kotlin/co/anitrend/retrofit/graphql/sample/viewmodel/model/MainState.kt +++ /dev/null @@ -1,57 +0,0 @@ -package co.anitrend.retrofit.graphql.sample.viewmodel.model - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.switchMap -import co.anitrend.arch.core.model.ISupportViewModelState -import co.anitrend.arch.data.model.UserInterfaceState -import co.anitrend.arch.domain.entities.LoadState -import co.anitrend.arch.domain.state.UiState -import co.anitrend.retrofit.graphql.data.user.usecase.UserUseCaseContract -import co.anitrend.retrofit.graphql.domain.entities.user.User - -data class MainState( - private val useCase: UserUseCaseContract -) : ISupportViewModelState { - - private val useCaseResult = MutableLiveData>() - - override val model = - useCaseResult.switchMap { it.model } - override val refreshState = - useCaseResult.switchMap { it.refreshState } - override val loadState = - useCaseResult.switchMap { it.loadState } - - operator fun invoke() { - val result = useCase() - useCaseResult.postValue(result) - } - - /** - * Called upon [androidx.lifecycle.ViewModel.onCleared] and should optionally - * call cancellation of any ongoing jobs. - * - * If your use case source is of type [co.anitrend.arch.domain.common.IUseCase] - * then you could optionally call [co.anitrend.arch.domain.common.IUseCase.onCleared] here - */ - override fun onCleared() { - useCase.onCleared() - } - - /** - * Triggers use case to perform refresh operation - */ - override fun refresh() { - val uiModel = useCaseResult.value - uiModel?.refresh?.invoke() - } - - /** - * Triggers use case to perform a retry operation - */ - override fun retry() { - val uiModel = useCaseResult.value - uiModel?.retry?.invoke() - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/market_place_item.xml b/app/src/main/res/layout/market_place_item.xml index 17e01a50..2626b83a 100644 --- a/app/src/main/res/layout/market_place_item.xml +++ b/app/src/main/res/layout/market_place_item.xml @@ -19,7 +19,7 @@ + android:padding="@dimen/xl_margin"> + android:layout_height="@dimen/lg_margin" /> + android:layout_height="@dimen/lg_margin" /> + android:layout_height="@dimen/lg_margin" /> diff --git a/app/src/main/res/layout/shared_list_content.xml b/app/src/main/res/layout/shared_list_content.xml new file mode 100644 index 00000000..c27dc241 --- /dev/null +++ b/app/src/main/res/layout/shared_list_content.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/buildSrc/src/main/java/co/anitrend/retrofit/graphql/buildSrc/plugin/components/AndroidConfiguration.kt b/buildSrc/src/main/java/co/anitrend/retrofit/graphql/buildSrc/plugin/components/AndroidConfiguration.kt index e55685e8..bc3b5f38 100644 --- a/buildSrc/src/main/java/co/anitrend/retrofit/graphql/buildSrc/plugin/components/AndroidConfiguration.kt +++ b/buildSrc/src/main/java/co/anitrend/retrofit/graphql/buildSrc/plugin/components/AndroidConfiguration.kt @@ -58,7 +58,7 @@ private fun DefaultConfig.applyAdditionalConfiguration(project: Project) { internal fun Project.configureAndroid(): Unit = baseExtension().run { compileSdkVersion(34) defaultConfig { - minSdk = if (isSampleModule()) 21 else 17 + minSdk = if (isSampleModule()) 23 else 17 targetSdk = 34 versionCode = props[PropertyTypes.CODE].toInt() versionName = props[PropertyTypes.VERSION] diff --git a/library/proguard-rules.pro b/library/consumer-rules.pro similarity index 100% rename from library/proguard-rules.pro rename to library/consumer-rules.pro