Last active
December 5, 2024 12:52
-
-
Save inidamleader/b6a76b3c503e1fff4400603e9a175d33 to your computer and use it in GitHub Desktop.
In-app update composable function implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.update | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.runtime.snapshotFlow | |
import androidx.compose.ui.platform.LocalContext | |
import com.google.android.play.core.appupdate.AppUpdateInfo | |
import com.google.android.play.core.appupdate.AppUpdateManagerFactory | |
import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager | |
import com.google.android.play.core.install.InstallState | |
import com.google.android.play.core.install.model.AppUpdateType | |
import com.google.android.play.core.install.model.InstallStatus | |
import com.google.android.play.core.install.model.UpdateAvailability | |
import com.inidamleader.ovtracker.global.AppRemoteConfig | |
import com.inidamleader.ovtracker.util.Holder | |
import com.inidamleader.ovtracker.util.getValue | |
import com.inidamleader.ovtracker.util.isGooglePlayServicesAvailable | |
import com.inidamleader.ovtracker.util.setValue | |
import kotlinx.coroutines.flow.collectLatest | |
import kotlinx.coroutines.tasks.await | |
@Composable | |
fun InAppUpdater( | |
daysForUpdate: () -> AppRemoteConfig.DaysForUpdate, | |
completeUpdate: () -> Boolean, | |
completeUpdateOnStop: () -> Boolean, | |
onInstallStateChange: (InstallState) -> Unit, | |
onIsDownloadedChange: (Boolean) -> Unit, | |
testMode: Boolean = false, | |
testUpdateType: UpdateType = UpdateType.FLEXIBLE, | |
) { | |
val context = LocalContext.current | |
if (!context.isGooglePlayServicesAvailable && !testMode) return | |
val appUpdateManager = remember(key1 = testMode) { | |
if (testMode) FakeAppUpdateManager(context).apply { | |
setUpdateAvailable(Int.MAX_VALUE) | |
setClientVersionStalenessDays(Int.MAX_VALUE) | |
} | |
else AppUpdateManagerFactory.create(context) | |
} | |
var updateType by remember { mutableStateOf<UpdateType?>(null) } | |
var appUpdateInfo by remember { Holder<AppUpdateInfo?>(null) } | |
LaunchedEffect(key1 = Unit) { | |
snapshotFlow(daysForUpdate).collectLatest { daysForUpdate -> | |
if (daysForUpdate != AppRemoteConfig.DaysForUpdate(-1, -1)) | |
try { | |
updateType = updateType( | |
daysForUpdate = daysForUpdate, | |
appUpdateInfo = appUpdateManager.appUpdateInfo.await() | |
.also { | |
appUpdateInfo = it | |
}, | |
testMode = testMode, | |
testUpdateType = testUpdateType | |
) | |
} catch (_: Exception) { | |
// Getting appUpdateInfo failed | |
} | |
} | |
} | |
when (updateType) { | |
UpdateType.IMMEDIATE -> appUpdateInfo?.let { | |
HandleImmediateUpdate( | |
appUpdateManager = appUpdateManager, | |
appUpdateInfo = it, | |
testMode = testMode, | |
) | |
} | |
UpdateType.FLEXIBLE -> appUpdateInfo?.let { | |
HandleFlexibleUpdate( | |
appUpdateManager = appUpdateManager, | |
appUpdateInfo = it, | |
completeUpdate = completeUpdate, | |
completeUpdateOnStop = completeUpdateOnStop, | |
onInstallStateChange = onInstallStateChange, | |
onIsDownloadedChange = onIsDownloadedChange, | |
testMode = testMode, | |
) | |
} | |
null -> {} | |
} | |
} | |
private fun updateType( | |
daysForUpdate: AppRemoteConfig.DaysForUpdate, | |
appUpdateInfo: AppUpdateInfo, | |
testMode: Boolean, | |
testUpdateType: UpdateType, | |
) = | |
if (testMode) testUpdateType | |
else { | |
val clientVersionStalenessDays = appUpdateInfo.clientVersionStalenessDays() | |
val isUpdateAvailable = | |
appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE | |
val flexible = { | |
appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED || | |
(isUpdateAvailable && | |
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) && | |
clientVersionStalenessDays != null && | |
clientVersionStalenessDays >= daysForUpdate.flexible) | |
} | |
val immediate = { | |
appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS || | |
(isUpdateAvailable && | |
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) && | |
clientVersionStalenessDays != null && | |
clientVersionStalenessDays >= daysForUpdate.immediate) | |
} | |
when { | |
daysForUpdate.immediate >= 0 && daysForUpdate.flexible < 0 && immediate() -> | |
UpdateType.IMMEDIATE | |
daysForUpdate.immediate < 0 && daysForUpdate.flexible >= 0 && flexible() -> | |
UpdateType.FLEXIBLE | |
daysForUpdate.immediate >= 0 && daysForUpdate.flexible >= 0 -> | |
when { | |
immediate() -> UpdateType.IMMEDIATE | |
flexible() -> UpdateType.FLEXIBLE | |
else -> null | |
} | |
else -> null | |
} | |
} | |
const val TAG = "InAppUpdater" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.update | |
sealed interface ProgressState { | |
data object Inactive : ProgressState | |
data object PendingOrInstalling : ProgressState | |
data class Downloading(val progress: Float) : ProgressState | |
} | |
data class DaysForUpdate(val immediate: Long, val flexible: Long) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.update | |
import com.google.android.play.core.install.InstallState | |
import com.google.android.play.core.install.model.InstallErrorCode | |
import com.google.android.play.core.install.model.InstallStatus | |
import kotlinx.coroutines.delay | |
import kotlin.math.pow | |
suspend fun progressUiTest( | |
interval: Long = 1000, | |
delay: Long = 2000, | |
downloadedTime: Long = 5000, | |
onInstallStateChange: (InstallState) -> Unit, | |
) { | |
delay(delay) | |
// test 0 100 value | |
onInstallStateChange( | |
InstallState.zza( | |
InstallStatus.DOWNLOADING, | |
0, | |
interval, | |
InstallErrorCode.NO_ERROR, | |
"", | |
) | |
) | |
delay(interval) | |
// test 100 0 value | |
onInstallStateChange( | |
InstallState.zza( | |
InstallStatus.DOWNLOADING, | |
interval, | |
0, | |
InstallErrorCode.NO_ERROR, | |
"", | |
) | |
) | |
delay(interval) | |
// test 0 0 value | |
onInstallStateChange( | |
InstallState.zza( | |
InstallStatus.DOWNLOADING, | |
0, | |
0, | |
InstallErrorCode.NO_ERROR, | |
"", | |
) | |
) | |
delay(interval) | |
val times = 10 | |
repeat(times) { | |
onInstallStateChange( | |
InstallState.zza( | |
InstallStatus.DOWNLOADING, | |
times * (it + 1).toLong(), | |
times.toFloat().pow(2).toLong(), | |
InstallErrorCode.NO_ERROR, | |
"", | |
) | |
) | |
delay(interval / times) | |
} | |
onInstallStateChange( | |
InstallState.zza( | |
InstallStatus.DOWNLOADED, | |
0, | |
0, | |
InstallErrorCode.NO_ERROR, | |
"", | |
) | |
) | |
delay(downloadedTime) | |
onInstallStateChange( | |
InstallState.zza( | |
InstallStatus.INSTALLING, | |
0, | |
0, | |
InstallErrorCode.NO_ERROR, | |
"", | |
) | |
) | |
delay(interval) | |
onInstallStateChange( | |
InstallState.zza( | |
InstallStatus.INSTALLED, | |
0, | |
0, | |
InstallErrorCode.NO_ERROR, | |
"", | |
) | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.update | |
import androidx.compose.animation.core.animateFloatAsState | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
@Composable | |
fun UpdaterProgressIndicator( | |
progressState: () -> ProgressState, | |
onPendingOrInstallingContent: @Composable () -> Unit, | |
onDownloadingContent: @Composable (() -> Float) -> Unit, | |
) { | |
when (val currentProgressState = progressState()) { | |
is ProgressState.PendingOrInstalling -> onPendingOrInstallingContent() | |
is ProgressState.Downloading -> { | |
val animatedProgress by animateFloatAsState( | |
targetValue = currentProgressState.progress, | |
label = "animatedProgress" | |
) | |
onDownloadingContent { animatedProgress } | |
} | |
ProgressState.Inactive -> {} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.update | |
enum class UpdateType { | |
FLEXIBLE, | |
IMMEDIATE, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val Context.isGooglePlayServicesAvailable | |
get() = GoogleApiAvailability.getInstance() | |
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.update | |
import android.util.Log | |
import androidx.activity.compose.rememberLauncherForActivityResult | |
import androidx.activity.result.contract.ActivityResultContracts | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.setValue | |
import androidx.compose.runtime.snapshotFlow | |
import androidx.lifecycle.compose.LifecycleResumeEffect | |
import androidx.lifecycle.compose.LifecycleStartEffect | |
import com.google.android.play.core.appupdate.AppUpdateInfo | |
import com.google.android.play.core.appupdate.AppUpdateManager | |
import com.google.android.play.core.appupdate.AppUpdateOptions | |
import com.google.android.play.core.install.InstallState | |
import com.google.android.play.core.install.model.AppUpdateType | |
import com.google.android.play.core.install.model.InstallStatus | |
import com.google.android.play.core.ktx.installStatus | |
import com.inidamleader.ovtracker.util.Holder | |
import com.inidamleader.ovtracker.util.compose.ImmutableWrapper | |
import com.inidamleader.ovtracker.util.compose.getValue | |
import com.inidamleader.ovtracker.util.getValue | |
import com.inidamleader.ovtracker.util.setValue | |
import kotlinx.coroutines.Job | |
import kotlinx.coroutines.delay | |
import kotlinx.coroutines.flow.collectLatest | |
import kotlinx.coroutines.launch | |
import kotlinx.coroutines.tasks.await | |
@Composable | |
fun HandleFlexibleUpdate( | |
appUpdateManager: ImmutableWrapper<AppUpdateManager>, | |
appUpdateInfo: ImmutableWrapper<AppUpdateInfo>, | |
completeUpdate: () -> Boolean, | |
completeUpdateOnStop: () -> Boolean, | |
onInstallStateChange: (InstallState) -> Unit, | |
onIsDownloadedChange: (isDownloaded: Boolean) -> Unit, | |
testMode: Boolean, | |
) { | |
val activityResultLauncher = rememberLauncherForActivityResult( | |
contract = ActivityResultContracts.StartIntentSenderForResult(), | |
onResult = {} | |
) | |
val currentAppUpdateManager by appUpdateManager | |
val currentAppUpdateInfo by appUpdateInfo | |
var isDownloaded by remember { mutableStateOf(false) } | |
LaunchedEffect(key1 = Unit) { | |
snapshotFlow { isDownloaded }.collectLatest { | |
onIsDownloadedChange(it) | |
} | |
} | |
LaunchedEffect(key1 = Unit) { | |
snapshotFlow(completeUpdate).collectLatest { | |
if (it) currentAppUpdateManager.completeUpdate() | |
} | |
} | |
// OnCreate | |
DisposableEffect(key1 = Unit) { | |
val installStateUpdatedListener: (InstallState) -> Unit = { installState -> | |
if (testMode) Log.d(TAG, "InAppUpdater: installStatus = ${installState.installStatus}") | |
onInstallStateChange(installState) | |
isDownloaded = installState.installStatus == InstallStatus.DOWNLOADED | |
} | |
currentAppUpdateManager.registerListener(installStateUpdatedListener) | |
if (currentAppUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) | |
isDownloaded = true | |
else | |
currentAppUpdateManager.startUpdateFlowForResult( | |
currentAppUpdateInfo, | |
activityResultLauncher, | |
AppUpdateOptions.defaultOptions(AppUpdateType.FLEXIBLE), | |
) | |
if (testMode) Log.d(TAG, "HandleFlexibleUpdate: Start FLEXIBLE Update Flow") | |
// OnDestroy | |
onDispose { | |
currentAppUpdateManager.unregisterListener(installStateUpdatedListener) | |
} | |
} | |
val coroutineScope = rememberCoroutineScope() | |
// OnStop: install update silently after 5s to be sure that is not a configuration change | |
var onStopJob by remember { Holder<Job?>(null) } | |
LifecycleStartEffect { | |
onStopJob?.cancel() | |
onStopOrDispose { | |
onStopJob = coroutineScope.launch { | |
delay(5000) // to be sure that is not a configuration change | |
if (completeUpdateOnStop()) { | |
try { | |
@Suppress("NAME_SHADOWING") | |
val appUpdateInfo = currentAppUpdateManager.appUpdateInfo.await() | |
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) | |
currentAppUpdateManager.completeUpdate() | |
} catch (_: Exception) { | |
} | |
} | |
} | |
} | |
} | |
// Checks that the update is not stalled during 'onResume()'. | |
// However, you should execute this check at all app entry points. | |
LifecycleResumeEffect { | |
val onResumeJob = coroutineScope.launch { | |
try { | |
@Suppress("NAME_SHADOWING") | |
val appUpdateInfo = currentAppUpdateManager.appUpdateInfo.await() | |
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) | |
// If the update is downloaded but not installed, | |
// notify the user to complete the update. | |
isDownloaded = true | |
} catch (_: Exception) { | |
} | |
} | |
onPauseOrDispose { | |
onResumeJob.cancel() | |
} | |
} | |
if (testMode) | |
LaunchedEffect(key1 = Unit) { | |
repeat(100) { | |
progressUiTest { | |
onInstallStateChange(it) | |
isDownloaded = it.installStatus() == InstallStatus.DOWNLOADED | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose.update | |
import android.util.Log | |
import androidx.activity.compose.rememberLauncherForActivityResult | |
import androidx.activity.result.contract.ActivityResultContracts | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.setValue | |
import androidx.compose.runtime.snapshotFlow | |
import androidx.lifecycle.compose.LifecycleResumeEffect | |
import com.google.android.play.core.appupdate.AppUpdateInfo | |
import com.google.android.play.core.appupdate.AppUpdateManager | |
import com.google.android.play.core.appupdate.AppUpdateOptions | |
import com.google.android.play.core.install.model.AppUpdateType | |
import com.google.android.play.core.install.model.UpdateAvailability | |
import com.inidamleader.ovtracker.util.Holder | |
import com.inidamleader.ovtracker.util.compose.ImmutableWrapper | |
import com.inidamleader.ovtracker.util.compose.getValue | |
import com.inidamleader.ovtracker.util.getValue | |
import com.inidamleader.ovtracker.util.setValue | |
import kotlinx.coroutines.flow.collectLatest | |
import kotlinx.coroutines.launch | |
import kotlinx.coroutines.tasks.await | |
@Composable | |
fun HandleImmediateUpdate( | |
appUpdateManager: ImmutableWrapper<AppUpdateManager>, | |
appUpdateInfo: ImmutableWrapper<AppUpdateInfo>, | |
testMode: Boolean, | |
) { | |
val activityResultLauncher = rememberLauncherForActivityResult( | |
contract = ActivityResultContracts.StartIntentSenderForResult(), | |
onResult = {} | |
) | |
val currentAppUpdateManager by appUpdateManager | |
var currentAppUpdateInfo by remember { Holder(appUpdateInfo.value) } | |
var restart by remember { mutableStateOf(Any()) } | |
LaunchedEffect(key1 = Unit) { | |
snapshotFlow { restart }.collectLatest { | |
currentAppUpdateManager.startUpdateFlowForResult( | |
currentAppUpdateInfo, | |
activityResultLauncher, | |
AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE), | |
) | |
if (testMode) Log.d(TAG, "HandleImmediateUpdate: Start IMMEDIATE Update Flow") | |
} | |
} | |
// Checks that the update is not stalled during 'onResume()'. | |
// However, you should execute this check at all entry points into the app. | |
val coroutineScope = rememberCoroutineScope() | |
LifecycleResumeEffect { | |
val job = coroutineScope.launch { | |
try { | |
currentAppUpdateInfo = currentAppUpdateManager.appUpdateInfo.await() | |
if (currentAppUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) | |
// If an in-app update is already running, resume the update. | |
restart = Any() | |
} catch (_: Exception) { | |
} | |
} | |
onPauseOrDispose { | |
job.cancel() | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.Stable | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.saveable.listSaver | |
import kotlin.reflect.KProperty | |
@Stable | |
data class Holder<T>(var value: T) { | |
companion object { | |
fun <T> getDefaultSaver() = listSaver( | |
save = { listOf(it.value) }, | |
restore = { Holder<T>(it[0]) } | |
) | |
} | |
} | |
operator fun <T> Holder<T>.getValue(thisRef: Any?, property: KProperty<*>) = value | |
operator fun <T> Holder<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) { | |
this.value = value | |
} | |
@Composable | |
fun <T> rememberUpdatedHolder(newValue: T): Holder<T> = remember { | |
Holder(newValue) | |
}.apply { value = newValue } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Box { | |
val daysForUpdate by produceState(initialValue = AppRemoteConfig.DaysForUpdate(-1L, -1L)) { | |
delay(xxxxxx) | |
// value = AppRemoteConfig.DaysForUpdate(90L, 7L) | |
value = AppRemoteConfig.daysForUpdate | |
} | |
var showRestartToCompleteUpdateConfirmationDialog by remember { mutableStateOf(false) } | |
var completeUpdate by remember { mutableStateOf(false) } | |
var isDialogClosed by rememberSaveable(saver = Holder.getDefaultSaver()) { | |
Holder(false) | |
} | |
var progressState by remember { mutableStateOf<ProgressState>(ProgressState.Inactive) } | |
ComposeScope(show = { showRestartToCompleteUpdateConfirmationDialog && !isDialogClosed }) { | |
ConfirmationDialog( | |
title = R.string.relaunch_app_to_complete_update, | |
onConfirm = { | |
completeUpdate = true | |
showRestartToCompleteUpdateConfirmationDialog = false | |
isDialogClosed = true | |
}, | |
onDismiss = { | |
completeUpdate = false | |
showRestartToCompleteUpdateConfirmationDialog = false | |
isDialogClosed = true | |
}, | |
imageVector = Icons.Default.RestartAlt, | |
) | |
} | |
InAppUpdater( | |
daysForUpdate = { daysForUpdate }, | |
completeUpdate = { completeUpdate }, | |
completeUpdateOnStop = { true }, | |
onInstallStateChange = { installState -> | |
progressState = when (installState.installStatus) { | |
InstallStatus.DOWNLOADING -> { | |
val totalBytes = installState.totalBytesToDownload().toFloat() | |
val bytesDownloaded = installState.bytesDownloaded().toFloat() | |
val progress = (bytesDownloaded / totalBytes).takeIf { | |
it.isFinite() && it in 0f..1f | |
} | |
progress?.let { ProgressState.Downloading(it) } ?: ProgressState.Inactive | |
} | |
InstallStatus.INSTALLING -> ProgressState.PendingOrInstalling | |
InstallStatus.PENDING -> ProgressState.PendingOrInstalling | |
InstallStatus.CANCELED -> ProgressState.Inactive | |
InstallStatus.DOWNLOADED -> ProgressState.Inactive | |
InstallStatus.FAILED -> ProgressState.Inactive | |
InstallStatus.INSTALLED -> ProgressState.Inactive | |
InstallStatus.REQUIRES_UI_INTENT -> ProgressState.Inactive | |
InstallStatus.UNKNOWN -> ProgressState.Inactive | |
else -> ProgressState.Inactive | |
} | |
}, | |
onIsDownloadedChange = { isDownloaded -> | |
showRestartToCompleteUpdateConfirmationDialog = isDownloaded | |
}, | |
) | |
UpdaterProgressIndicator( | |
progressState = { progressState }, | |
onPendingOrInstallingContent = { | |
LinearProgressIndicator( | |
modifier = Modifier | |
.align(Alignment.TopCenter) | |
.fillMaxWidth() | |
.zIndex(2F), | |
) | |
}, | |
onDownloadingContent = { | |
LinearProgressIndicator( | |
modifier = Modifier | |
.align(Alignment.TopCenter) | |
.fillMaxWidth() | |
.zIndex(2F), | |
progress = it, | |
) | |
}, | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.inidamleader.ovtracker.util.compose | |
import androidx.compose.runtime.Immutable | |
import kotlin.reflect.KProperty | |
@Immutable | |
data class ImmutableWrapper<T>(val value: T) | |
fun <T> T.toImmutableWrapper() = ImmutableWrapper(this) | |
operator fun <T> ImmutableWrapper<T>.getValue(thisRef: Any?, property: KProperty<*>) = value |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Could someone kindly review this code, please?