https://kotlinlang.org logo
#compose
Title
# compose
o

Omkar Amberkar

10/29/2023, 3:51 AM
I have a question regarding jetpack compose navigation and PIP. Considering I have 3 screens, one Mainactivity hosting two composable screens home and details. and another activity Player hosting player screen. If player screen has a button to navigate back to details screen of main activity while itself entering into PIP first, how should i trigger that call to navigate back to the main activity's details? posting a sample POC in 🧵. both main and player activity have launch mode singleTask.
Copy code
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            PictureInPicturePOCTheme {
                val navController = rememberNavController()
                val navigationActions = remember(navController) {
                    PictureInPicturePOCNavigationActions(navController)
                }
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    PictureInPicturePOCNavGraph(
                        modifier = Modifier.fillMaxSize(),
                        navController = navController,
                        navigateToDetails = navigationActions.navigateToDetails,
                        navigateToPlayer = navigationActions.navigateToPlayer
                    )
                }
            }
        }
    }
}

object PictureInPicturePOCDestinations {
    const val HOME_ROUTE = "home"
    const val DETAILS_ROUTE = "details"
    const val PLAYER_ROUTE = "player"
}

class PictureInPicturePOCNavigationActions(navController: NavHostController) {
    val navigateToHome: () -> Unit = {
        navController.navigate(PictureInPicturePOCDestinations.HOME_ROUTE) {
            // Pop up to the start destination of the graph to
            // avoid building up a large stack of destinations
            // on the back stack as users select items
            popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
            }
            // Avoid multiple copies of the same destination when
            // reselecting the same item
            launchSingleTop = true
            // Restore state when reselecting a previously selected item
            restoreState = true
        }
    }

    val navigateToDetails: () -> Unit = {
        navController.navigate(PictureInPicturePOCDestinations.DETAILS_ROUTE) {
            launchSingleTop = true
            restoreState = true
        }
    }

    val navigateToPlayer: () -> Unit = {
        navController.navigate(PictureInPicturePOCDestinations.PLAYER_ROUTE) {
            launchSingleTop = true
            restoreState = true
        }
    }
}

@Composable
fun PictureInPicturePOCNavGraph(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: String = PictureInPicturePOCDestinations.HOME_ROUTE,
    navigateToDetails: () -> Unit,
    navigateToPlayer: () -> Unit
) {
    NavHost(
        navController = navController,
        startDestination = startDestination,
        modifier = modifier
    ) {
        composable(PictureInPicturePOCDestinations.HOME_ROUTE) {
            HomeRoute(
                navigateToDetails = navigateToDetails,
                navigateToPlayer = navigateToPlayer
            )
        }
        composable(PictureInPicturePOCDestinations.DETAILS_ROUTE) {
            DetailsRoute(
                navigateToDetails = navigateToDetails,
                navigateToPlayer = navigateToPlayer
            )
        }
        activity(PictureInPicturePOCDestinations.PLAYER_ROUTE) {
            activityClass = PlayerActivity::class
        }
    }
}

@Composable
fun HomeRoute(
    navigateToDetails: () -> Unit,
    navigateToPlayer: () -> Unit
) {
    Column(
        Modifier
            .fillMaxSize()
            .background(Color.White)
            .padding(100.dp)
    ) {
        Button(modifier = Modifier.wrapContentSize(), onClick = { navigateToDetails.invoke() }) {
            Text(text = "navigate to details")
        }
        Button(modifier = Modifier.wrapContentSize(), onClick = { navigateToPlayer.invoke() }) {
            Text(text = "navigate to player")
        }
    }
}

@Composable
fun DetailsRoute(
    navigateToDetails: () -> Unit,
    navigateToPlayer: () -> Unit
) {
    Column(
        Modifier
            .fillMaxSize()
            .background(Color.Yellow)
            .padding(100.dp)
    ) {
        Button(modifier = Modifier.wrapContentSize(), onClick = { navigateToDetails.invoke() }) {
            Text(text = "navigate to details")
        }
        Button(modifier = Modifier.wrapContentSize(), onClick = { navigateToPlayer.invoke() }) {
            Text(text = "navigate to player")
        }
    }
}

class PlayerActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            PictureInPicturePOCTheme {
                val navController = rememberNavController()
                val navigationActions = remember(navController) {
                    PictureInPicturePOCNavigationActions(navController)
                }
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    PlayerRoute(
                        navigateToDetails = navigationActions.navigateToDetails,
                        enterPip = { enterPictureInPictureMode() }
                    )
                }
            }
        }
    }
}

@Composable
fun PlayerRoute(
    navigateToDetails: () -> Unit,
    enterPip: () -> Unit
) {
    Column {
        Box(
            Modifier
                .aspectRatio(1.7777778F)
                .background(Color.Black)
        ) {
            Button(modifier = Modifier
                .wrapContentSize()
                .align(Alignment.Center),
                onClick = { enterPip.invoke() }
            ) {
                Text(text = "enter pip")
            }
        }
        Box(
            Modifier
                .fillMaxSize()
                .background(Color.White)
        ) {
            Button(
                modifier = Modifier
                    .wrapContentSize()
                    .align(Alignment.Center),
                onClick = {
                    enterPip.invoke()
                    navigateToDetails.invoke()
                }
            ) {
                Text(text = "navigate to details")
            }
        }
    }
}
when user clicks on the Details on the Player Screen the app crashes since there is no route for details inside player activity. How would i be able to map that to the previous Mainactivity's graph?
i

Ian Lake

10/29/2023, 5:59 AM
If they're separate activities, there's no call to
navigate
that is going to go back to the previous activity - consider every activity as completely separate from one another. Your other activity should already be below your PIP activity anyways, so entering PIP should automatically make your other activity visible right?
o

Omkar Amberkar

10/29/2023, 1:56 PM
Hmm, yeah but we were wanting to have a single activity for the app and since player needs to enter PIP, wanted to do a POC to explore the possibilities since we have a similar requirement to enter PIP if media player is playing and user clicks a content below the player to navigate to different screen and to keep using the media playing we enter PIP. How we have it setup right now is that we have box which host the application and the player sits on top of it so its easy to achieve this scenario using motionlayout but that is getting quite out of hand with different motion scene since we have different UI for Expanded vs Compat screen sizes hence was exploring the possibilities. What would you ideally recommend for a media player application which needs to enter PIP as such?
i

Ian Lake

10/29/2023, 3:08 PM
As explained in the

Single Activity talk

, picture in picture is one of the few areas where do need separate activities (on separate tasks) if you want to browse and watch a system controlled picture in picture at the same time. Faking picture-in-picture in app (e.g., your MotionLayout like approach) and using a single task and single activity for a system controlled picture-in-picture when users leave your app entirely and go back to launcher might be a good middle ground if you want something more akin to YouTube's model on phones. That talk goes through that option also and explains when you'd want one over the other (it is based on what you want clicking on the app icon from the launcher while you're in PiP to do)
o

Omkar Amberkar

10/29/2023, 5:02 PM
Yeah thats the reason why we went with the above approach our ours. I guess we would just need to live-with our implementation. Could there be a special case of composable that can enter PIP/multiwindowmode in future? or are we limited in this case to use an activity?
i

Ian Lake

10/29/2023, 5:06 PM
If you rewind that talk a bit, you'll find that anytime you're tying yourself to the window management of the Android framework, activities are what you need to use.
🥲 1
o

Omkar Amberkar

10/29/2023, 5:08 PM
thanks a lot Ian 🙂 we will probably stick to single activity then
i

Ian Lake

10/29/2023, 5:12 PM
Like that talk says, it really depends on what user experience you want. If you want your video player / PIP to be a separate task (for example, on Chromebooks, it would be a separate window) where you can interact with both the browse and interact with the player simultaneously, then that's absolutely a supported scenario. Whether you want that kind of ability is more of a product decision than anything else
o

Omkar Amberkar

10/29/2023, 5:29 PM
Gotcha, I think we would want to stick with single activity for now. Thanks again Ian.
2 Views