Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh live compose scan by observing RecomposerInfo instead of only the top-most recompose scope. #135

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.util.Log
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog.Builder
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand All @@ -25,7 +24,9 @@ import androidx.compose.material.TextButton
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.Recomposer.State.Idle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -40,6 +41,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
import radiography.ExperimentalRadiographyComposeApi
import radiography.Radiography
import radiography.ScanScopes.FocusedWindowScope
Expand All @@ -59,7 +74,6 @@ internal const val LIVE_HIERARCHY_TEST_TAG = "live-hierarchy"
ComposeSampleApp()
}

@OptIn(ExperimentalRadiographyComposeApi::class, ExperimentalAnimationApi::class)
@Composable fun ComposeSampleApp() {
val context = LocalContext.current
val liveHierarchy = remember { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -131,12 +145,8 @@ internal const val LIVE_HIERARCHY_TEST_TAG = "live-hierarchy"
Text("SHOW STRING RENDERING DIALOG")
}

SideEffect {
liveHierarchy.value = Radiography.scan(
viewStateRenderers = DefaultsIncludingPii,
// Don't trigger infinite recursion.
viewFilter = skipComposeTestTagsFilter(LIVE_HIERARCHY_TEST_TAG)
)
LaunchedEffect(Unit) {
runLiveScanning { liveHierarchy.value = it }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool so this effectively says "rescan every time we've recomposed". btw does that not retrigger a composition / any risk of infinite loop?

Copy link
Collaborator Author

@zach-klippenstein zach-klippenstein Mar 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. It does not trigger infinite composition loop, although I'm not super clear on why not. I can look into it and document, because it does seem like that should happen.

  • Figure out why no infinite loop

Copy link
Collaborator Author

@zach-klippenstein zach-klippenstein Mar 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I get why there's no infinite loop now. Because we exclude the scan result composable from the scan, a scan in which only the scan output changes will result in the scan string being set to the same value, which won't invalidate any recompose scopes.

I'll add a comment about this and punch up the docs on the Recomposers stuff.

}
}
}
Expand Down Expand Up @@ -210,3 +220,51 @@ private fun showResult(
messageView.textSize = 9f
messageView.typeface = Typeface.MONOSPACE
}

@OptIn(
ExperimentalCoroutinesApi::class,
FlowPreview::class,
ExperimentalRadiographyComposeApi::class
)
private suspend fun runLiveScanning(onScanResult: (String) -> Unit) {
// A Flow that emits false when all Recomposers are idle, and true when at least one Recomposer
// is non-idle.
val isComposing: Flow<Boolean> = Recomposer.runningRecomposers
.flatMapLatest { recomposers ->
combine(recomposers.map { it.state }) { states ->
states.any { it != Idle }
}
}
.distinctUntilChanged()

fun emitEvery(periodMs: Long): Flow<Unit> = flow {
while (true) {
emit(Unit)
delay(periodMs)
}
}

@Suppress("NAME_SHADOWING")
val scanTrigger = isComposing.flatMapLatest { isComposing ->
if (isComposing) {
// While any Recomposer's state is not idle, do a scan every 500 millis. This effectively
// throttles scans during continuously-recomposing periods like animations.
emitEvery(500)
} else {
// Emit once to scan the now-settled composition, then stop.
flowOf(Unit)
}
}

// If a scan takes too long and other scan requests were fired in the meantime, coalesce them.
scanTrigger.buffer(1, DROP_OLDEST)
.collect {
withContext(Dispatchers.Default) {
Radiography.scan(
viewStateRenderers = DefaultsIncludingPii,
// Don't trigger infinite recursion.
viewFilter = skipComposeTestTagsFilter(LIVE_HIERARCHY_TEST_TAG)
).let(onScanResult)
}
}
}