Skip to content

Commit

Permalink
Merge pull request #88 from matheus-corregiari/hotfix/1.3.2
Browse files Browse the repository at this point in the history
  • Loading branch information
Pedro-Bachiega authored Jun 6, 2024
2 parents 1435102 + 3607b75 commit f9c59f4
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,9 @@ class MutableResponseLiveData<T> : ResponseLiveData<T> {
* Like Uncle Ben said, with great powers...
*/
public override fun postValue(value: DataResult<T>?) = super.postValue(value)

/**
* Like Uncle Ben said, with great powers...
*/
public override fun safePostValue(value: DataResult<T>?) = super.safePostValue(value)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package br.com.arch.toolkit.livedata

import android.os.Looper
import androidx.annotation.NonNull
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
Expand Down Expand Up @@ -330,6 +331,17 @@ open class ResponseLiveData<T> : LiveData<DataResult<T>> {
.attachTo(this, owner)
}

/**
* @see br.com.arch.toolkit.util.safePostValue
*/
protected open fun safePostValue(value: DataResult<T>?) {
if (Looper.getMainLooper()?.isCurrentThread == true) {
this.value = value
} else {
postValue(value)
}
}

/**
* @return A new instance of ObserveWrapper<T>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package br.com.arch.toolkit.livedata

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import br.com.arch.toolkit.annotation.Experimental
import br.com.arch.toolkit.result.DataResult
import br.com.arch.toolkit.result.DataResultStatus
import br.com.arch.toolkit.util.mergeAll
Expand Down Expand Up @@ -69,7 +70,7 @@ internal class DefaultResponseLiveDataMergeDelegate : ResponseLiveDataMergeDeleg
val result = SwapResponseLiveData(onMerge.invoke())
.scope(scope)
.transformDispatcher(transformDispatcher)
result.notifyOnlyOnDistinct = true
result.notifyOnlyOnDistinct(true)

newSources.forEach { liveData ->
if (!liveData.hasObservers()) liveData.observeForever(sourceObserver)
Expand All @@ -91,6 +92,7 @@ internal class DefaultResponseLiveDataMergeDelegate : ResponseLiveDataMergeDeleg
return result
}

@OptIn(Experimental::class)
private fun <T, R> chainSources(
scope: CoroutineScope,
transformDispatcher: CoroutineDispatcher,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package br.com.arch.toolkit.livedata

import android.os.Looper
import androidx.lifecycle.MediatorLiveData
import br.com.arch.toolkit.exception.DataResultTransformationException
import br.com.arch.toolkit.result.DataResult
import br.com.arch.toolkit.result.DataResultStatus
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

Expand Down Expand Up @@ -44,7 +44,10 @@ class SwapResponseLiveData<T> : ResponseLiveData<T> {
/**
* Flag to set whether we're notifying on every change or only on distinct values
*/
var notifyOnlyOnDistinct: Boolean = false
private var notifyOnlyOnDistinct: Boolean = false
fun notifyOnlyOnDistinct(notifyOnlyOnDistinct: Boolean) = apply {
this.notifyOnlyOnDistinct = notifyOnlyOnDistinct
}

/**
* Changes the actual DataSource
Expand Down Expand Up @@ -106,7 +109,9 @@ class SwapResponseLiveData<T> : ResponseLiveData<T> {
* Removes source
*/
fun clearSource() {
lastSource?.let { sourceLiveData.removeSource(it) }
lastSource?.let {
scope.launch(Dispatchers.Main) { sourceLiveData.removeSource(it) }
}
lastSource = null
}

Expand All @@ -123,12 +128,14 @@ class SwapResponseLiveData<T> : ResponseLiveData<T> {

override fun onActive() {
super.onActive()
if (!sourceLiveData.hasObservers()) sourceLiveData.observeForever(sourceObserver)
scope.launch(Dispatchers.Main) {
if (!sourceLiveData.hasObservers()) sourceLiveData.observeForever(sourceObserver)
}
}

override fun onInactive() {
super.onInactive()
sourceLiveData.removeObserver(sourceObserver)
scope.launch(Dispatchers.Main) { sourceLiveData.removeObserver(sourceObserver) }
}

private fun <R> executeSwap(
Expand All @@ -137,37 +144,36 @@ class SwapResponseLiveData<T> : ResponseLiveData<T> {
transformation: (DataResult<R>) -> DataResult<T>?
) {
clearSource()
sourceLiveData.addSource(source) { data ->
scope.launch {
withContext(transformDispatcher) {
transformation.runCatching { invoke(data) }
}.onFailure {
val error = DataResultTransformationException(
"Error performing swapSource, please check your transformations",
it
)

val result = DataResult<T>(null, error, DataResultStatus.ERROR)
if (value == result && notifyOnlyOnDistinct) return@onFailure

if (Looper.getMainLooper()?.isCurrentThread == true) {
value = result
} else {
postValue(result)
}
}.getOrNull().let {
if (value == it && notifyOnlyOnDistinct) return@let

if (Looper.getMainLooper()?.isCurrentThread == true) {
value = it
} else {
postValue(it)
}

if (it?.status != DataResultStatus.LOADING && discardAfterLoading) value = null
}
lastSource = source
scope.launch(Dispatchers.Main) {
sourceLiveData.addSource(source) { data ->
onChanged(data, discardAfterLoading, transformation)
}
}
lastSource = source
}

private fun <R> onChanged(
data: DataResult<R>?,
discardAfterLoading: Boolean,
transformation: (DataResult<R>) -> DataResult<T>?
) = scope.launch {
withContext(transformDispatcher) {
transformation.runCatching { data?.let(::invoke) }
}.onFailure {
val error = DataResultTransformationException(
"Error performing swapSource, please check your transformations",
it
)

val result = DataResult<T>(null, error, DataResultStatus.ERROR)
if (value == result && notifyOnlyOnDistinct) return@onFailure

safePostValue(result)
}.getOrNull().let {
if (value == it && notifyOnlyOnDistinct) return@let
safePostValue(it)

if (it?.status != DataResultStatus.LOADING && discardAfterLoading) value = null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

package br.com.arch.toolkit.util

import android.os.Looper
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.map
import br.com.arch.toolkit.livedata.MutableResponseLiveData
Expand Down Expand Up @@ -105,7 +107,7 @@ fun <T> mutableResponseLiveDataOf(value: T, status: DataResultStatus = DataResul
* @return An instance of MutableResponseLiveData<T> with an error set
*/
fun <T> mutableResponseLiveDataOf(error: Throwable) =
MutableResponseLiveData(DataResult(null, error, DataResultStatus.ERROR))
MutableResponseLiveData<T>(DataResult(null, error, DataResultStatus.ERROR))

/**
* Returns an instance of a SwapResponseLiveData<T> with the desired value
Expand All @@ -126,7 +128,7 @@ fun <T> swapResponseLiveDataOf(value: T, status: DataResultStatus = DataResultSt
* @return An instance of SwapResponseLiveData<T> with an error set
*/
fun <T> swapResponseLiveDataOf(error: Throwable) =
SwapResponseLiveData(DataResult(null, error, DataResultStatus.ERROR))
SwapResponseLiveData<T>(DataResult(null, error, DataResultStatus.ERROR))

/**
* Transforms a LiveData<List<T>> into a LiveData<List<R>>
Expand All @@ -140,11 +142,46 @@ fun <T, R> LiveData<List<T>?>.mapList(transformation: (T) -> R): LiveData<List<R
/**
* Transforms a ResponseLiveData<List<T>> into a ResponseLiveData<List<R>>
*
* @param transformAsync Indicate map will execute synchronously or asynchronously
* @param transformation Receive the actual non null T value and return the transformed non null R value
*/
fun <T, R> ResponseLiveData<List<T>>.mapList(
transformation: (T) -> R
): ResponseLiveData<List<R>> {
return map { it.map(transformation) }
}

/**
* This code defines an extension function named safePostValue for the MutableLiveData class in Kotlin.
* Let's break down its purpose and functionality:
*
* - Purpose:
* The primary goal of this function is to provide a safe way to update the value of a MutableLiveData object, regardless of whether the code is being executed on the main thread (UI thread) or a background thread.
*
* - Explanation:
* 1. Extension Function: The safePostValue function is defined as an extension function for MutableLiveData<T>, allowing you to call it directly on any MutableLiveData instance.
* 2. Type Parameter: The <T> indicates that this function is generic and can work with MutableLiveData objects of any type.
* 3. Thread Safety: The core logic of the function ensures thread safety when updating the MutableLiveData. It checks if the current thread is the main thread (UI thread) using Looper.getMainLooper()?.isCurrentThread == true.
*
* - Main Thread: If on the main thread, it directly sets the value of the MutableLiveData using this.value = value. This is safe because modifications to UI elements should always happen on the main thread.
* - Background Thread: If on a background thread, it uses postValue(value) to schedule the update to be executed on the main thread. This prevents potential crashes or unexpected behavior that can occur when modifying UI elements from background threads.
*
* ## Example Usage:
* ```kotlin
* val liveData = MutableLiveData<String>()
*
* // On a background thread
* someBackgroundTask {
* val result = performSomeOperation()
* liveData.safePostValue(result) // Safely updates the LiveData
* }
* ```
*
* > In summary, the safePostValue extension function provides a convenient and thread-safe way to update MutableLiveData objects, ensuring that UI updates are always performed on the main thread, even when called from background threads.
*/
fun <T> MutableLiveData<T>.safePostValue(value: T?) {
if (Looper.getMainLooper()?.isCurrentThread == true) {
this.value = value
} else {
postValue(value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import br.com.arch.toolkit.livedata.MutableResponseLiveData
import br.com.arch.toolkit.livedata.SwapResponseLiveData
import br.com.arch.toolkit.result.DataResult
import br.com.arch.toolkit.result.DataResultStatus
import br.com.arch.toolkit.testSetValue
import br.com.arch.toolkit.util.dataResultLoading
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand Down Expand Up @@ -110,7 +109,7 @@ class SwapResponseLiveDataTest {
Assert.assertTrue(swapLiveData.hasDataSource)
Assert.assertNull(swapLiveData.value)

liveData.testSetValue(null)
liveData.value = null
advanceUntilIdle()
verifyBlocking(mockedObserver) { invoke("data") }
Assert.assertTrue(swapLiveData.hasDataSource)
Expand Down
12 changes: 6 additions & 6 deletions tools/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ jetbrains-coroutines = "1.8.0" # Quebra a parte de tratamento de exception no Ob
jetbrains-kover = "0.7.4"

## AndroidX
androidx-appcompat = "1.6.1"
androidx-appcompat = "1.7.0"
androidx-annotation = "1.8.0"
androidx-constraint = "2.2.0-alpha13"
androidx-recyclerview = "1.3.2"
androidx-lifecycle-livedata = "2.8.0"
androidx-lifecycle-runtime = "2.8.0"
androidx-window = "1.2.0"
androidx-lifecycle-livedata = "2.8.1"
androidx-lifecycle-runtime = "2.8.1"
androidx-window = "1.3.0"
androidx-splash = "1.0.1"
androidx-test-core = "2.2.0"
androidx-test-junit = "1.1.5"
Expand All @@ -41,7 +41,7 @@ androidx-compose-material = "1.6.7"
androidx-compose-material3 = "1.2.1"
androidx-compose-constraint = "1.0.1"
androidx-compose-core = "1.6.7"
androidx-compose-lifecycle = "2.8.0"
androidx-compose-lifecycle = "2.8.1"
androidx-compose-activity = "1.9.0"

## SquareUp
Expand Down Expand Up @@ -127,7 +127,7 @@ x-normalize-006 = { group = "androidx.core", name = "core", version = "1.13.1" }
x-normalize-007 = { group = "androidx.core", name = "core-ktx", version = "1.13.1" }
x-normalize-008 = { group = "androidx.customview", name = "customview", version = "1.1.0" }
x-normalize-009 = { group = "androidx.drawerlayout", name = "drawerlayout", version = "1.2.0" }
x-normalize-010 = { group = "androidx.lifecycle", name = "lifecycle-common", version = "2.8.0" }
x-normalize-010 = { group = "androidx.lifecycle", name = "lifecycle-common", version = "2.8.1" }
x-normalize-011 = { group = "androidx.viewpager2", name = "viewpager2", version = "1.1.0" }
x-normalize-012 = { group = "androidx.test", name = "monitor", version = "1.6.1" }
x-normalize-013 = { group = "androidx.test", name = "runner", version = "1.5.2" }
Expand Down

0 comments on commit f9c59f4

Please sign in to comment.