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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import androidx.compose.ui.unit.dp
import me.nya_n.notificationnotifier.model.InstalledApp
import me.nya_n.notificationnotifier.ui.R
import me.nya_n.notificationnotifier.ui.theme.AppTheme
import me.nya_n.notificationnotifier.ui.util.LocalAnimatedVisibilityScope
import me.nya_n.notificationnotifier.ui.util.LocalSharedTransitionScope
import me.nya_n.notificationnotifier.ui.util.iconSharedTransitionKey

@Composable
fun AppList(
Expand All @@ -36,7 +39,12 @@ fun AppList(
items(
count = items.size,
key = { "($it)${items[it]}" },
itemContent = { AppListItem(app = items[it], onAppSelected = onAppSelected) }
itemContent = {
AppListItem(
app = items[it],
onAppSelected = onAppSelected
)
}
)
}
}
Expand All @@ -47,7 +55,10 @@ fun AppListItem(
app: InstalledApp,
onAppSelected: (InstalledApp) -> Unit
) {
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current
val interactionSource = remember { MutableInteractionSource() }

Box(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -62,7 +73,20 @@ fun AppListItem(
) {
GrayScaleAppIcon(
app = app,
modifier = Modifier.size(56.dp),
modifier = Modifier
.size(56.dp)
.then(
if (sharedTransitionScope != null && animatedVisibilityScope != null) {
with(sharedTransitionScope) {
Modifier.sharedElement(
rememberSharedContentState(key = app.iconSharedTransitionKey),
animatedVisibilityScope = animatedVisibilityScope
)
}
} else {
Modifier
}
),
isInListView = true
)
Box(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,13 @@ import me.nya_n.notificationnotifier.ui.theme.AppTheme
@Composable
fun AppScaffold(
snackbarHostState: SnackbarHostState,
hasBackContent: Boolean = false,
onBack: () -> Unit = { },
onBack: (() -> Unit)? = null,
bottomBar: @Composable () -> Unit = { },
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
topBar = {
TopBar(
hasBackContent = hasBackContent,
onBack = onBack
)
TopBar(onBack = onBack)
},
bottomBar = bottomBar,
containerColor = MaterialTheme.colorScheme.secondary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import me.nya_n.notificationnotifier.ui.theme.AppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopBar(
hasBackContent: Boolean = false,
onBack: () -> Unit = { }
onBack: (() -> Unit)? = null
) {
TopAppBar(
title = {
Expand All @@ -29,10 +28,11 @@ fun TopBar(
)
},
navigationIcon = {
if (hasBackContent) {
IconButton(
onClick = onBack
) {
IconButton(
onClick = onBack ?: { },
enabled = onBack != null
) {
if (onBack != null) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.back)
Expand All @@ -59,7 +59,6 @@ private fun TopBarPreview() {
private fun SubContentTopBarPreview() {
AppTheme {
TopBar(
hasBackContent = true,
onBack = { }
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package me.nya_n.notificationnotifier.ui.screen.app
import android.content.Intent
import android.provider.Settings
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
Comment on lines +6 to +10
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n AndroidApp/ui/src/main/kotlin/me/nya_n/notificationnotifier/ui/screen/app/AppScreen.kt | head -130

Repository: ptkNktq/AndroidNotificationNotifier

Length of output: 6222


🏁 Script executed:

# Find build.gradle files to check dependencies
find . -name "build.gradle*" -o -name "*.toml" | grep -E "(gradle|toml)" | head -20

Repository: ptkNktq/AndroidNotificationNotifier

Length of output: 375


🏁 Script executed:

# Search for composition/dependency information
find . -type f \( -name "build.gradle" -o -name "build.gradle.kts" -o -name "*.toml" \) | xargs grep -l "androidx.compose\|androidx.navigation" 2>/dev/null | head -5

Repository: ptkNktq/AndroidNotificationNotifier

Length of output: 223


🏁 Script executed:

cat AndroidApp/gradle/libs.versions.toml

Repository: ptkNktq/AndroidNotificationNotifier

Length of output: 4381


🏁 Script executed:

cat AndroidApp/ui/build.gradle.kts

Repository: ptkNktq/AndroidNotificationNotifier

Length of output: 1065


🏁 Script executed:

# Find where LocalAnimatedVisibilityScope is defined
find . -type f -name "*.kt" | xargs grep -l "LocalAnimatedVisibilityScope" | head -5

Repository: ptkNktq/AndroidNotificationNotifier

Length of output: 436


🏁 Script executed:

cat -n ./AndroidApp/ui/src/main/kotlin/me/nya_n/notificationnotifier/ui/util/CompositionLocals.kt

Repository: ptkNktq/AndroidNotificationNotifier

Length of output: 521


🌐 Web query:

androidx.navigation:navigation-compose 2.9.6 composable lambda scope AnimatedVisibilityScope AnimatedContentScope

💡 Result:

Short answer: In navigation-compose 2.9.6 the destination content lambda is invoked with an AnimatedContentScope receiver (which is a sealed interface that extends AnimatedVisibilityScope). The NavHost/composable transition lambdas use AnimatedContentTransitionScope. See the API docs:

  • NavHost / NavGraphBuilder.composable signatures showing content: @composable AnimatedContentScope.(NavBackStackEntry) -> Unit and transition parameters: AnimatedContentTransitionScope.() -> Enter/ExitTransition. [1]
  • AnimatedVisibilityScope (and that AnimatedContentScope extends it) — explains available transition/animateEnterExit helpers. [2]
  • Compose source showing AnimatedContentScope : AnimatedVisibilityScope. [3]

Sources:
[1] Android NavHost / composable API reference.
[2] AnimatedVisibilityScope API reference.
[3] Compose AnimatedContent source (AnimatedContentScope implements AnimatedVisibilityScope).


🌐 Web query:

SharedTransitionLayout androidx.compose.animation 2025 SharedElement sharedElement scope requirements

💡 Result:

Short answer — requirements and rules for SharedTransitionLayout / SharedTransitionScope and Modifier.sharedElement:

  • Any composable that will participate in a shared-element/shared-bounds transition must be a descendant (direct or indirect) of a SharedTransitionLayout or of a SharedTransitionScope. [1][2]
  • Keys: shared elements are matched by the same key (Any) — use the same key on source and target. [1]
  • If you use SharedTransitionScope (the variant that does NOT create a new Layout) you MUST apply the Modifier provided by the scope to the first/top-most child in your scope content — that Modifier obtains root coordinates and creates the overlay used to render transitioning elements. If the first child isn’t the top-most (highest zIndex), prefer SharedTransitionLayout instead. [2]
  • SharedElement behavior: when a matched sharedElement starts a transition it is rendered into the scope’s overlay (to avoid parent clipping/fade). Any clipping/fade that should affect the element during transition must be applied after the sharedElement modifier (i.e., to the child inside the sharedElement). Parent clipping is applied only if there is a parent sharedBounds with clipping defined. [2][3]

References: Android Compose Animation docs (SharedTransitionLayout / SharedTransitionScope / shared elements). [1][2][3]

Sources
[1] API docs — SharedTransitionLayout / overview.
[2] API docs — SharedTransitionScope / sharedElement details.
[3] Guide — Shared elements with Navigation / AnimatedContent examples.


Gson からの null 返却を処理してください

行 109-112 の Gson().fromJson() は JSON 解析に失敗したり、引数が null/空の場合に null を返しますが、null チェックがないため DetailScreen に null を渡す可能性があります。以下のように修正してください:

composable(Screen.Detail.route) {
    val appJson = it.arguments?.getString("app")
    val app = appJson?.let { json -> Gson().fromJson(json, InstalledApp::class.java) }
    if (app != null) {
        CompositionLocalProvider(
            LocalSharedTransitionScope provides this@SharedTransitionLayout,
            LocalAnimatedVisibilityScope provides this@composable
        ) {
            DetailScreen(navController = navController, app = app)
        }
    }
}

SharedTransitionLayout と AnimatedVisibilityScope の型は互換性があります(AnimatedContentScope は AnimatedVisibilityScope を継承)。

import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -16,14 +18,13 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.gson.Gson
import me.nya_n.notificationnotifier.model.InstalledApp
import me.nya_n.notificationnotifier.ui.common.RequireNotificationPermissionDialog
import me.nya_n.notificationnotifier.ui.common.RequirePackageVisibilityDialog
import me.nya_n.notificationnotifier.ui.screen.about.AboutScreen
import me.nya_n.notificationnotifier.ui.screen.detail.DetailScreen
import me.nya_n.notificationnotifier.ui.screen.license.LicenseScreen
import me.nya_n.notificationnotifier.ui.screen.main.MainScreen
import me.nya_n.notificationnotifier.ui.theme.AppTheme
import me.nya_n.notificationnotifier.ui.util.LocalSharedTransitionScope
import org.koin.androidx.compose.koinViewModel
import java.net.URLEncoder

Expand Down Expand Up @@ -75,35 +76,33 @@ fun AppScreen(
}
}

NavHost(
navController = navController,
startDestination = Screen.Main.name,
enterTransition = {
slideInHorizontally(initialOffsetX = { it })
},
exitTransition = {
slideOutHorizontally(targetOffsetX = { -it })
},
popEnterTransition = {
slideInHorizontally(initialOffsetX = { -it })
},
popExitTransition = {
slideOutHorizontally(targetOffsetX = { it })
},
) {
composable(Screen.Main.route) { MainScreen(navController) }
composable(Screen.License.route) { LicenseScreen(navController) }
composable(Screen.Detail.route) {
val app = Gson().fromJson(
it.arguments?.getString("app"),
InstalledApp::class.java
)
DetailScreen(
navController = navController,
app = app
)
SharedTransitionLayout {
NavHost(
navController = navController,
startDestination = Screen.Main.name,
enterTransition = {
slideInHorizontally(initialOffsetX = { it })
},
exitTransition = {
slideOutHorizontally(targetOffsetX = { -it })
},
popEnterTransition = {
slideInHorizontally(initialOffsetX = { -it })
},
popExitTransition = {
slideOutHorizontally(targetOffsetX = { it })
},
) {
composable(Screen.Main.route) {
CompositionLocalProvider(
LocalSharedTransitionScope provides this@SharedTransitionLayout
) {
MainScreen(navController = navController)
}
}
composable(Screen.License.route) { LicenseScreen(navController) }
composable(Screen.About.route) { AboutScreen() }
}
composable(Screen.About.route) { AboutScreen() }
}
}
}
Expand All @@ -113,7 +112,10 @@ sealed class Screen(
val name: String,
private val args: List<String> = emptyList()
) {
data object Main : Screen("main")
data object Main : Screen("main") {
data object Targets : Screen("targets")
data object Detail : Screen("detail", listOf("app"))
}
data object License : Screen("license")
data object Detail : Screen("detail", listOf("app"))
data object About : Screen("about")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ import androidx.navigation.NavController
import me.nya_n.notificationnotifier.model.InstalledApp
import me.nya_n.notificationnotifier.ui.R
import me.nya_n.notificationnotifier.ui.common.AppOutlinedButton
import me.nya_n.notificationnotifier.ui.common.AppScaffold
import me.nya_n.notificationnotifier.ui.common.Category
import me.nya_n.notificationnotifier.ui.common.GrayScaleAppIcon
import me.nya_n.notificationnotifier.ui.common.SnackbarMessage
import me.nya_n.notificationnotifier.ui.theme.AppColors
import me.nya_n.notificationnotifier.ui.theme.AppTheme
import me.nya_n.notificationnotifier.ui.util.LocalAnimatedVisibilityScope
import me.nya_n.notificationnotifier.ui.util.LocalSharedTransitionScope
import me.nya_n.notificationnotifier.ui.util.iconSharedTransitionKey
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf

Expand All @@ -61,12 +63,8 @@ fun DetailScreen(
viewModel.messageShown()
}
DetailContent(
snackbarHostState = snackbarHostState,
app = app,
condition = uiState.condition,
onBack = {
navController.popBackStack()
},
onDeleteApp = {
viewModel.deleteTarget()
navController.previousBackStackEntry?.apply {
Expand All @@ -81,27 +79,21 @@ fun DetailScreen(
/** 詳細画面のコンテンツ本体 */
@Composable
fun DetailContent(
snackbarHostState: SnackbarHostState,
app: InstalledApp,
condition: String,
onBack: () -> Unit,
onDeleteApp: () -> Unit,
onConditionChanged: (String) -> Unit
) {
AppScaffold(
snackbarHostState = snackbarHostState,
hasBackContent = true,
onBack = onBack
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp),
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
.padding(horizontal = 20.dp),
) {
AppInfo(app, onDeleteApp)
NotificationSetting(condition, onConditionChanged)
}
AppInfo(
app = app,
onDeleteApp = onDeleteApp
)
NotificationSetting(condition, onConditionChanged)
}
}

Expand All @@ -113,14 +105,30 @@ private fun AppInfo(
app: InstalledApp,
onDeleteApp: () -> Unit
) {
val sharedTransitionScope = LocalSharedTransitionScope.current
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current

Category(name = stringResource(id = R.string.app_info))
Box(
modifier = Modifier.padding(vertical = 20.dp)
) {
Row {
GrayScaleAppIcon(
app = app,
modifier = Modifier.size(80.dp),
modifier = Modifier
.size(80.dp)
.then(
if (sharedTransitionScope != null && animatedVisibilityScope != null) {
with(sharedTransitionScope) {
Modifier.sharedElement(
rememberSharedContentState(key = app.iconSharedTransitionKey),
animatedVisibilityScope = animatedVisibilityScope
)
}
} else {
Modifier
}
),
isInListView = false
)
Box(
Expand Down Expand Up @@ -188,13 +196,10 @@ private fun NotificationSetting(
@Preview(backgroundColor = 0xFFC7B5A8, showBackground = true)
@Composable
private fun DetailPreview() {
val snackbarHostState = remember { SnackbarHostState() }
AppTheme {
DetailContent(
snackbarHostState = snackbarHostState,
app = InstalledApp("Sample App Name", "example.sample.test"),
condition = "^.*$",
onBack = { },
onDeleteApp = { },
onConditionChanged = { }
)
Expand All @@ -204,16 +209,13 @@ private fun DetailPreview() {
@Preview(backgroundColor = 0xFFC7B5A8, showBackground = true)
@Composable
private fun LongAppNameDetailPreview() {
val snackbarHostState = remember { SnackbarHostState() }
AppTheme {
DetailContent(
snackbarHostState = snackbarHostState,
app = InstalledApp(
"Sample App Name So Loooooooooooooooooooong",
"example.sample.test"
),
condition = "",
onBack = { },
onDeleteApp = { },
onConditionChanged = { }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ fun LicenseScreen(navController: NavController) {
val snackbarHostState = remember { SnackbarHostState() }
AppScaffold(
snackbarHostState = snackbarHostState,
hasBackContent = true,
onBack = {
navController.popBackStack()
}
Expand Down
Loading