Fix detekt issue:

Lambda parameters in a @Composable that are referenced directly inside of restarting effects can cause issues or unpredictable behavior.

If restarting the effect is ok, you can add the reference to this parameter as a key in that effect, so when the parameter changes, a new effect is created.
However, if the effect is not to be restarted, you will need to use `rememberUpdatedState` on the parameter and use its result in the effect.

See https://mrmans0n.github.io/compose-rules/rules/#be-mindful-of-the-arguments-you-use-inside-of-a-restarting-effect for more information. [LambdaParameterInRestartableEffect]
This commit is contained in:
Benoit Marty 2024-01-19 20:44:43 +01:00 committed by Benoit Marty
parent fa09b70411
commit cff076b508
12 changed files with 25 additions and 15 deletions

View file

@ -32,7 +32,7 @@ fun MigrationScreenView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (migrationState.isMigrating.not()) { if (migrationState.isMigrating.not()) {
LaunchedEffect(Unit) { LaunchedEffect(onMigrationFinished) {
onMigrationFinished() onMigrationFinished()
} }
} }

View file

@ -57,7 +57,7 @@ fun InviteListView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.acceptedAction is AsyncData.Success) { if (state.acceptedAction is AsyncData.Success) {
LaunchedEffect(state.acceptedAction) { LaunchedEffect(state.acceptedAction, onInviteAccepted) {
onInviteAccepted(state.acceptedAction.data) onInviteAccepted(state.acceptedAction.data)
} }
} }

View file

@ -18,6 +18,8 @@ package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager import io.element.android.features.lockscreen.impl.biometric.BiometricUnlockManager
import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback import io.element.android.features.lockscreen.impl.biometric.DefaultBiometricUnlockCallback
import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback
@ -30,15 +32,16 @@ class PinUnlockHelper @Inject constructor(
) { ) {
@Composable @Composable
fun OnUnlockEffect(onUnlock: () -> Unit) { fun OnUnlockEffect(onUnlock: () -> Unit) {
val latestOnUnlock by rememberUpdatedState(onUnlock)
DisposableEffect(Unit) { DisposableEffect(Unit) {
val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() { val biometricUnlockCallback = object : DefaultBiometricUnlockCallback() {
override fun onBiometricUnlockSuccess() { override fun onBiometricUnlockSuccess() {
onUnlock() latestOnUnlock()
} }
} }
val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() { val pinCodeVerifiedCallback = object : DefaultPinCodeManagerCallback() {
override fun onPinCodeVerified() { override fun onPinCodeVerified() {
onUnlock() latestOnUnlock()
} }
} }
biometricUnlockManager.addCallback(biometricUnlockCallback) biometricUnlockManager.addCallback(biometricUnlockCallback)

View file

@ -63,7 +63,7 @@ fun ChangeServerView(
} }
} }
is AsyncData.Loading -> ProgressDialog() is AsyncData.Loading -> ProgressDialog()
is AsyncData.Success -> LaunchedEffect(state.changeServerAction) { is AsyncData.Success -> LaunchedEffect(state.changeServerAction, onDone) {
onDone() onDone()
} }
AsyncData.Uninitialized -> Unit AsyncData.Uninitialized -> Unit

View file

@ -53,7 +53,7 @@ fun LogoutActionDialog(
onDismiss = onDismissError, onDismiss = onDismissError,
) )
is AsyncAction.Success -> is AsyncAction.Success ->
LaunchedEffect(state) { LaunchedEffect(state, onSuccessLogout) {
onSuccessLogout(state.data) onSuccessLogout(state.data)
} }
} }

View file

@ -294,7 +294,7 @@ private fun AttachmentStateView(
) { ) {
when (state) { when (state) {
AttachmentsState.None -> Unit AttachmentsState.None -> Unit
is AttachmentsState.Previewing -> LaunchedEffect(state) { is AttachmentsState.Previewing -> LaunchedEffect(state, onPreviewAttachments) {
onPreviewAttachments(state.attachments) onPreviewAttachments(state.attachments)
} }
is AttachmentsState.Sending -> { is AttachmentsState.Sending -> {

View file

@ -57,7 +57,7 @@ fun AttachmentsPreviewView(
} }
if (state.sendActionState is SendActionState.Done) { if (state.sendActionState is SendActionState.Done) {
LaunchedEffect(state.sendActionState) { LaunchedEffect(state.sendActionState, onDismiss) {
onDismiss() onDismiss()
} }
} }

View file

@ -42,6 +42,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -193,10 +194,11 @@ private fun BoxScope.TimelineScrollHelper(
} }
} }
val latestOnScrollFinishedAt by rememberUpdatedState(onScrollFinishedAt)
LaunchedEffect(isScrollFinished, isTimelineEmpty) { LaunchedEffect(isScrollFinished, isTimelineEmpty) {
if (isScrollFinished && !isTimelineEmpty) { if (isScrollFinished && !isTimelineEmpty) {
// Notify the parent composable about the first visible item index when scrolling finishes // Notify the parent composable about the first visible item index when scrolling finishes
onScrollFinishedAt(lazyListState.firstVisibleItemIndex) latestOnScrollFinishedAt(lazyListState.firstVisibleItemIndex)
} }
} }

View file

@ -73,7 +73,7 @@ private fun TakeScreenshot(
onScreenshotTaken: (ImageResult) -> Unit onScreenshotTaken: (ImageResult) -> Unit
) { ) {
val view = LocalView.current val view = LocalView.current
LaunchedEffect(Unit) { LaunchedEffect(onScreenshotTaken) {
view.screenshot { view.screenshot {
onScreenshotTaken(it) onScreenshotTaken(it)
} }

View file

@ -67,7 +67,7 @@ fun <T> AsyncActionView(
} }
} }
is AsyncAction.Success -> { is AsyncAction.Success -> {
LaunchedEffect(async) { LaunchedEffect(async, onSuccess) {
onSuccess(async.data) onSuccess(async.data)
} }
} }

View file

@ -18,6 +18,8 @@ package io.element.android.libraries.textcomposer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.libraries.androidutils.ui.awaitWindowFocus import io.element.android.libraries.androidutils.ui.awaitWindowFocus
@ -40,7 +42,8 @@ internal fun <T> SoftKeyboardEffect(
predicate: (T) -> Boolean, predicate: (T) -> Boolean,
) { ) {
val view = LocalView.current val view = LocalView.current
LaunchedEffect(key) { val latestOnRequestFocus by rememberUpdatedState(onRequestFocus)
LaunchedEffect(key, predicate) {
if (predicate(key)) { if (predicate(key)) {
// Await window focus in case returning from a dialog // Await window focus in case returning from a dialog
view.awaitWindowFocus() view.awaitWindowFocus()
@ -49,7 +52,7 @@ internal fun <T> SoftKeyboardEffect(
view.showKeyboard(andRequestFocus = true) view.showKeyboard(andRequestFocus = true)
// Refocus to the correct view // Refocus to the correct view
onRequestFocus() latestOnRequestFocus()
} }
} }
} }

View file

@ -42,6 +42,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -274,12 +275,13 @@ fun TextComposer(
} }
val menuAction = state.menuAction val menuAction = state.menuAction
val latestOnSuggestionReceived by rememberUpdatedState(onSuggestionReceived)
LaunchedEffect(menuAction) { LaunchedEffect(menuAction) {
if (menuAction is MenuAction.Suggestion) { if (menuAction is MenuAction.Suggestion) {
val suggestion = Suggestion(menuAction.suggestionPattern) val suggestion = Suggestion(menuAction.suggestionPattern)
onSuggestionReceived(suggestion) latestOnSuggestionReceived(suggestion)
} else { } else {
onSuggestionReceived(null) latestOnSuggestionReceived(null)
} }
} }
} }