Skip to content

Commit

Permalink
Force refresh Functionality (#28)
Browse files Browse the repository at this point in the history
* feature: Add force Refresh menu option on all the screens to fetch the latest content from the network

* feature: Switch between error Text Message and Toast Message based on the forceRefresh flag

* feature: Add the Refresh button to all the fragments

* feature: Add Swipe Refresh for all the fragment

Swipe Refresh shows the loader to showcase the progress, so it makes ProgressBar redundant

* fix: Fix the app crash when force-refresh is triggered on local unavailable content page

* fix: Cancel the previous toast in the same context before showing a new one
  • Loading branch information
ShivamNagpal authored Jun 23, 2023
1 parent a0959ce commit 15da0b9
Show file tree
Hide file tree
Showing 19 changed files with 269 additions and 153 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ dependencies {
// https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson_module_kotlin"

// https://mvnrepository.com/artifact/androidx.swiperefreshlayout/swiperefreshlayout
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$versions.swipe_refresh_layout"

// Firebase SDK
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nagpal.shivam.vtucslab.core

enum class UIMessageType {
NoActiveInternetConnectionDetailed,
NoActiveInternetConnection,
SomeErrorOccurred,
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ import com.nagpal.shivam.vtucslab.models.LaboratoryResponse
import kotlinx.coroutines.flow.Flow

interface VtuCsLabRepository {
fun fetchLaboratories(url: String): Flow<Resource<LaboratoryResponse, ErrorType>>
fun fetchLaboratories(
url: String,
forceRefresh: Boolean,
): Flow<Resource<LaboratoryResponse, ErrorType>>

fun fetchExperiments(url: String): Flow<Resource<LaboratoryExperimentResponse, ErrorType>>
fun fetchExperiments(
url: String,
forceRefresh: Boolean,
): Flow<Resource<LaboratoryExperimentResponse, ErrorType>>

fun fetchContent(url: String): Flow<Resource<String, ErrorType>>
fun fetchContent(
url: String,
forceRefresh: Boolean,
): Flow<Resource<String, ErrorType>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,46 +28,61 @@ class VtuCsLabRepositoryImpl(
private val labResponseDao: LabResponseDao,
private val jsonMapper: JsonMapper,
) : VtuCsLabRepository {
override fun fetchLaboratories(url: String): Flow<Resource<LaboratoryResponse, ErrorType>> =
override fun fetchLaboratories(
url: String,
forceRefresh: Boolean
): Flow<Resource<LaboratoryResponse, ErrorType>> =
flow {
fetch(
flow = this,
url,
LabResponseType.LABORATORY,
vtuCsLabService::getLaboratoryResponse,
{ data -> jsonMapper.writeValueAsString(data) }
) { stringContent ->
jsonMapper.readValue(
stringContent,
LaboratoryResponse::class.java
)
}
{ data -> jsonMapper.writeValueAsString(data) },
{ stringContent ->
jsonMapper.readValue(
stringContent,
LaboratoryResponse::class.java
)
},
forceRefresh,
)
}

override fun fetchExperiments(url: String): Flow<Resource<LaboratoryExperimentResponse, ErrorType>> =
override fun fetchExperiments(
url: String,
forceRefresh: Boolean
): Flow<Resource<LaboratoryExperimentResponse, ErrorType>> =
flow {
fetch(
flow = this,
url,
LabResponseType.EXPERIMENT,
vtuCsLabService::getLaboratoryExperimentsResponse,
{ data -> jsonMapper.writeValueAsString(data) }
) { stringContent ->
jsonMapper.readValue(
stringContent,
LaboratoryExperimentResponse::class.java
)
}
{ data -> jsonMapper.writeValueAsString(data) },
{ stringContent ->
jsonMapper.readValue(
stringContent,
LaboratoryExperimentResponse::class.java
)
},
forceRefresh,
)
}

override fun fetchContent(url: String): Flow<Resource<String, ErrorType>> = flow {
override fun fetchContent(
url: String,
forceRefresh: Boolean
): Flow<Resource<String, ErrorType>> = flow {
fetch(
flow = this,
url,
LabResponseType.CONTENT,
vtuCsLabService::fetchRawResponse,
{ stringContent -> stringContent }
) { stringContent -> stringContent }
{ stringContent -> stringContent },
{ stringContent -> stringContent },
forceRefresh,
)
}

private suspend fun <D : Any> fetch(
Expand All @@ -77,28 +92,32 @@ class VtuCsLabRepositoryImpl(
fetchFromNetwork: suspend (String) -> ApiResult<D>,
encodeToString: (D) -> String,
decodeFromString: (String) -> D,
forceRefresh: Boolean,
) {
flow.emit(Resource.Loading())

val labResponse = labResponseDao.findByUrl(url)
var foundInDB = false
labResponse?.let {
try {
flow.emit(Resource.Success(decodeFromString.invoke(it.response)))
foundInDB = true
if (it.fetchedAt.after(
StaticMethods.getCurrentDateMinusSeconds(Configurations.RESPONSE_FRESHNESS_TIME)
)
) {
return
if (!forceRefresh) {
val labResponse = labResponseDao.findByUrl(url)
labResponse?.let {
try {
flow.emit(Resource.Success(decodeFromString.invoke(it.response)))
foundInDB = true
if (it.fetchedAt.after(
StaticMethods.getCurrentDateMinusSeconds(Configurations.RESPONSE_FRESHNESS_TIME)
)
) {
return
}
} catch (_: JsonParseException) {
}
} catch (_: JsonParseException) {
}
}

if (!NetworkUtils.isNetworkConnected(application)) {
emitNetworkErrors(
flow,
forceRefresh,
foundInDB,
Resource.Error(ErrorType.NoActiveInternetConnection),
)
Expand All @@ -122,6 +141,7 @@ class VtuCsLabRepositoryImpl(
StaticMethods.logNetworkResultError(LOG_TAG, url, apiResult.code, apiResult.message)
emitNetworkErrors(
flow,
forceRefresh,
foundInDB,
Resource.Error(ErrorType.SomeErrorOccurred),
)
Expand All @@ -131,6 +151,7 @@ class VtuCsLabRepositoryImpl(
StaticMethods.logNetworkResultException(LOG_TAG, url, apiResult.throwable)
emitNetworkErrors(
flow,
forceRefresh,
foundInDB,
Resource.Error(ErrorType.SomeErrorOccurred),
)
Expand All @@ -140,10 +161,11 @@ class VtuCsLabRepositoryImpl(

private suspend fun <D : Any> emitNetworkErrors(
flow: FlowCollector<Resource<D, ErrorType>>,
forceRefresh: Boolean,
foundInDB: Boolean,
errorResource: Resource.Error<D, ErrorType>
) {
if (!foundInDB) {
if (forceRefresh || !foundInDB) {
flow.emit(errorResource)
}
}
Expand Down
51 changes: 29 additions & 22 deletions app/src/main/java/com/nagpal/shivam/vtucslab/screens/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@ object Utils {
uiStateFlow: MutableStateFlow<ContentState<T>>,
fetchJob: Job?,
viewModelScope: CoroutineScope,
fetchExecutable: (String) -> Flow<Resource<T, ErrorType>>,
fetchExecutable: (String, Boolean) -> Flow<Resource<T, ErrorType>>,
getBaseUrl: (T) -> String?,
url: String
url: String,
forceRefresh: Boolean,
): Job? {
if (uiStateFlow.value.stage == Stages.SUCCEEDED) {
if (!forceRefresh && uiStateFlow.value.stage == Stages.SUCCEEDED) {
return fetchJob
}

fetchJob?.cancel()
return viewModelScope.launch(Dispatchers.IO) {
fetchExecutable.invoke(url)
fetchExecutable.invoke(url, forceRefresh)
.onEach { resource ->
when (resource) {
is Resource.Loading -> {
uiStateFlow.update { ContentState(Stages.LOADING) }
uiStateFlow.update {
uiStateFlow.value.copy(stage = Stages.LOADING)
}
}

is Resource.Success -> {
Expand All @@ -53,32 +56,36 @@ object Utils {
is Resource.Error -> {
uiStateFlow.update {
val uiMessage: UIMessage = when (resource.error) {
ErrorType.NoActiveInternetConnection -> UIMessage(UIMessageType.NoActiveInternetConnection)
ErrorType.NoActiveInternetConnection -> if (forceRefresh) UIMessage(
UIMessageType.NoActiveInternetConnection
) else UIMessage(UIMessageType.NoActiveInternetConnectionDetailed)

ErrorType.SomeErrorOccurred -> UIMessage(UIMessageType.SomeErrorOccurred)
}
ContentState(
Stages.FAILED,
errorMessage = uiMessage,
)
if (forceRefresh) {
uiStateFlow.value.copy(
stage = if (uiStateFlow.value.data != null) Stages.SUCCEEDED else Stages.FAILED,
toast = uiMessage,
)
} else {
ContentState(
stage = Stages.FAILED,
errorMessage = uiMessage,
)
}

}
}
}
}.launchIn(this)
}
}

fun <T> resetState(
uiStateFlow: MutableStateFlow<ContentState<T>>,
initialState: ContentState<T>
) {
uiStateFlow.update { initialState }
}

fun UIMessage?.asString(context: Context): String {
return when (this?.messageType) {
UIMessageType.NoActiveInternetConnection -> context.getString(R.string.no_internet_connection)
fun UIMessage.asString(context: Context): String {
return when (this.messageType) {
UIMessageType.NoActiveInternetConnectionDetailed -> context.getString(R.string.no_internet_connection_detailed)
UIMessageType.SomeErrorOccurred -> context.getString(R.string.error_occurred)
null -> context.getString(R.string.error_occurred)
UIMessageType.NoActiveInternetConnection -> context.getString(R.string.no_internet_connection)
}
}

Expand All @@ -99,6 +106,6 @@ object Utils {
newToast.show()
eventEmitter.onEvent(event)
newToast
}
} ?: toast
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ class DisplayFragment : Fragment() {
): View {
_binding = FragmentDisplayBinding.inflate(inflater, container, false)
setupMenuProvider()
setupViews()
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
binding.progressBar.visibility = View.GONE
binding.emptyTextView.visibility = View.GONE
toast = Utils.showToast(
requireContext(),
Expand All @@ -61,9 +61,13 @@ class DisplayFragment : Fragment() {
UiEvent.ResetToast
)

if (it.stage != Stages.LOADING) {
binding.swipeRefresh.isRefreshing = false
}

when (it.stage) {
Stages.LOADING -> {
binding.progressBar.visibility = View.VISIBLE
binding.swipeRefresh.isRefreshing = true
}

Stages.SUCCEEDED -> {
Expand All @@ -76,8 +80,9 @@ class DisplayFragment : Fragment() {
}

Stages.FAILED -> {
val message: String = it.errorMessage.asString(requireContext())
showErrorMessage(message)
it.errorMessage?.let { uiMessage ->
showErrorMessage(uiMessage.asString(requireContext()))
}
}
}
}
Expand All @@ -87,6 +92,12 @@ class DisplayFragment : Fragment() {
return binding.root
}

private fun setupViews() {
binding.swipeRefresh.setOnRefreshListener {
viewModel.onEvent(UiEvent.RefreshContent(url))
}
}

private fun showErrorMessage(message: String) {
binding.emptyTextView.visibility = View.VISIBLE
binding.emptyTextView.text = message
Expand All @@ -103,7 +114,7 @@ class DisplayFragment : Fragment() {

override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.display_menu_item_refresh -> {
R.id.menu_item_refresh -> {
viewModel.onEvent(UiEvent.RefreshContent(url))
return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ class DisplayViewModel(
}

is UiEvent.RefreshContent -> {
resetState()
loadContent(event.url)
loadContent(event.url, true)
}

UiEvent.ResetToast -> {
Expand All @@ -48,21 +47,18 @@ class DisplayViewModel(
}
}

private fun loadContent(url: String) {
private fun loadContent(url: String, forceRefresh: Boolean = false) {
fetchJob = Utils.loadContent(
_uiState,
fetchJob,
viewModelScope,
{ vtuCsLabRepository.fetchContent(it) },
{ urlArg, forceRefreshArg -> vtuCsLabRepository.fetchContent(urlArg, forceRefreshArg) },
{ null },
url
url,
forceRefresh,
)
}

private fun resetState() {
Utils.resetState(_uiState, initialState)
}

companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
Expand Down
Loading

0 comments on commit 15da0b9

Please sign in to comment.