Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Add entries under **Unreleased** in each pull request. The *Bump version* workfl
- README states that DevDeck stays out of release builds only when added with `debugImplementation`.

### Fixed
- Apps no longer crash when `DevDeck.install` adds the drawer to an edge-to-edge Compose activity on recent Android ("Cannot locate windowRecomposer"); the overlay is now added once the window is attached.
- Search placeholders and the network count row stay on one line on small phones.
- The permissions list refreshes after a permission dialog or settings screen closes, and shows when a revoke only applies after the app restarts.
- Tap breadcrumbs no longer record what was typed into text fields, so passwords can't end up in crash files or bug reports.
Expand Down
10 changes: 9 additions & 1 deletion devdeck/src/main/kotlin/io/devdeck/DevDeck.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.doOnAttach
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
Expand Down Expand Up @@ -114,10 +115,17 @@ public object DevDeck {

private fun attach(activity: Activity) {
val current = config ?: return
if (activity !is ComponentActivity) return
if (activity !is ComponentActivity || activity.isFinishing || activity.isDestroyed) return
if (DeckExclusions.isExcluded(activity, current.excludedActivityPrefixes) || !current.includeActivity(activity)) return
val decorView = activity.window.decorView as? ViewGroup ?: return
if (decorView.findViewWithTag<View>(DECK_OVERLAY_TAG) != null || HostedActivities.contains(activity)) return
// A ComposeView can't be measured before it's attached, and some windows (edge-to-edge activities on recent
// Android) measure views added in onStart without attaching them first. So the overlay is added after the
// window is attached, posted so it isn't added in the middle of the attach pass (which would skip it).
if (!decorView.isAttachedToWindow) {
decorView.doOnAttach { decorView.post { attach(activity) } }
return
}

val overlay = ComposeView(activity).apply {
tag = DECK_OVERLAY_TAG
Expand Down
4 changes: 3 additions & 1 deletion sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ The sample shows the two ways to attach the drawer:
|---|---|---|
| **Main screen** (Compose) | `DevDeckHost(plugins = …)` wraps the screen's content | `ui/SampleScreen.kt` |
| **View-based screen** (plain Views, no Compose) | `DevDeck.install(...)` in `Application.onCreate` adds it to the activity automatically | `SampleApp.kt`, `LegacyViewActivity.kt` |
| **Endless feed** (Compose, edge-to-edge) | Same as the View-based screen, through `includeActivity` | `EndlessFeedActivity.kt` |

Both screens share one plugin list, so their drawers show the same menu.
All screens share one plugin list, so their drawers show the same menu. The endless feed loads 20 more items whenever you reach the bottom; use it to try *Take a long screenshot* on content that never ends.

## Project layout

Expand All @@ -18,6 +19,7 @@ sample/src/main/kotlin/io/devdeck/sample/
├── SampleApp.kt Creates the flag store and plugin list, and installs DevDeck app-wide
├── MainActivity.kt Hosts the Compose screen
├── LegacyViewActivity.kt View-only screen; also the target of the devdeck://sample/legacy deep link
├── EndlessFeedActivity.kt Compose list that keeps loading pages, for trying long screenshots
├── debugmenu/ Everything that defines what's inside the drawer
│ ├── SampleDeckStyle.kt The drawer's title and subtitle, shared by both screens
│ ├── SamplePlugins.kt The plugin list and network highlight rules: the whole debug menu in one place
Expand Down
3 changes: 3 additions & 0 deletions sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".EndlessFeedActivity"
android:exported="false" />
<activity
android:name=".LegacyViewActivity"
android:exported="true">
Expand Down
128 changes: 128 additions & 0 deletions sample/src/main/kotlin/io/devdeck/sample/EndlessFeedActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.devdeck.sample

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import io.devdeck.logs.DeckLogger
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter

/**
* A paged feed that never ends: reaching the bottom loads 20 more items after a short delay, like a real
* "load more" list. Use it to try *Take a long screenshot* on endless content.
*/
class EndlessFeedActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
Surface(Modifier.fillMaxSize()) { EndlessFeed() }
}
}
}
}

@Composable
private fun EndlessFeed() {
var count by remember { mutableIntStateOf(PAGE_SIZE) }
var pages by remember { mutableIntStateOf(1) }
var loading by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
val nearEnd by remember {
derivedStateOf {
val info = listState.layoutInfo
val last = info.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf false
last >= info.totalItemsCount - LOAD_AHEAD
}
}
LaunchedEffect(listState) {
snapshotFlow { nearEnd }.filter { it }.collect {
if (loading) return@collect
loading = true
delay(LOAD_DELAY_MILLIS)
count += PAGE_SIZE
pages++
loading = false
DeckLogger.log("Feed page $pages loaded", tag = "Feed", payload = "$count items")
}
}
Column(Modifier.fillMaxSize().safeDrawingPadding()) {
Text(
"Endless feed",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
)
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.weight(1f),
) {
items(count, key = { it }) { index -> FeedItem(index + 1) }
item(key = "loading") {
Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
CircularProgressIndicator(Modifier.size(28.dp))
}
}
}
}
}

@Composable
private fun FeedItem(number: Int) {
// Each item gets its own colour and number, so seams or repeated pieces in a long screenshot are easy to spot.
val hue = (number * 37 % 360).toFloat()
Row(
Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(12.dp))
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(Modifier.size(48.dp).background(Color.hsv(hue, 0.55f, 0.85f), RoundedCornerShape(8.dp)))
Column {
Text("Item $number", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
Text("Page ${(number - 1) / PAGE_SIZE + 1} · loaded as you scroll", style = MaterialTheme.typography.bodyMedium)
}
}
}

private const val PAGE_SIZE = 20
private const val LOAD_AHEAD = 3
private const val LOAD_DELAY_MILLIS = 800L
1 change: 1 addition & 0 deletions sample/src/main/kotlin/io/devdeck/sample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class MainActivity : ComponentActivity() {
plugins = app.plugins,
flagStore = app.flagStore,
onOpenLegacy = { startActivity(Intent(this, LegacyViewActivity::class.java)) },
onOpenFeed = { startActivity(Intent(this, EndlessFeedActivity::class.java)) },
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion sample/src/main/kotlin/io/devdeck/sample/SampleApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SampleApp : Application() {
application = this,
plugins = plugins,
style = sampleDeckStyle,
includeActivity = { it is LegacyViewActivity },
includeActivity = { it is LegacyViewActivity || it is EndlessFeedActivity },
)
DeckLogger.log("App started")
Timber.i("Sample app started")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import io.devdeck.rememberDeckState
import io.devdeck.sample.debugmenu.sampleDeckStyle

@Composable
fun SampleScreen(plugins: List<DeckPlugin>, flagStore: FeatureFlagStore, onOpenLegacy: () -> Unit) {
fun SampleScreen(plugins: List<DeckPlugin>, flagStore: FeatureFlagStore, onOpenLegacy: () -> Unit, onOpenFeed: () -> Unit) {
val drawerState = rememberDeckState()
val options = rememberDrawerOptions()

Expand Down Expand Up @@ -71,6 +71,9 @@ fun SampleScreen(plugins: List<DeckPlugin>, flagStore: FeatureFlagStore, onOpenL
OutlinedButton(onClick = onOpenLegacy, modifier = Modifier.fillMaxWidth()) {
Text("Open View-based screen (global install)")
}
OutlinedButton(onClick = onOpenFeed, modifier = Modifier.fillMaxWidth()) {
Text("Open endless feed (try a long screenshot)")
}
}
}
}
Expand Down
Loading