How to manage MediaPlayer Properly in jetpack compose? - kotlin

Whenever i want to navigate to other screen i want my MediaPlayer should Stop where is best place to using the stop() function .
#Composable
fun MenuScreen(navController: NavController) {
val context = LocalContext.current
val menuMusic : MediaPlayer = MediaPlayer.create(context , R.raw.menu_music)
menuMusic.isLooping = true
menuMusic.start()
Box(
modifier = Modifier
.fillMaxSize()
.background(color = darkBackground)
) {...}

If you want your MediaPlayer to start when your composable enters the composition and stop when it leaves the composition, you would do this:
val context = LocalContext.current
val menuMusic: MediaPlayer = remember {
MediaPlayer.create(context, R.raw.menu_music)
}
DisposableEffect(Unit) {
menuMusic.isLooping = true
menuMusic.start()
onDispose {
menuMusic.stop()
}
}

Related

ExoPlayer landscape mode Jetpack Compose

I have a jetpack compose app in which there's a video player. The app is forced to portrait orientation, except for the screen/composable containing the video player. That composable is forced to landscape because I want to display the videos in landscape.
When I navigate to the page containing the video player the app correctly changes the orientation to landscape, but as soon as the video starts playing it rotates back to portrait orientation.
Any help is greatly appreciated...
Here's the relevant code for my problem.
#Composable
fun LockScreenOrientation(orientation: Int) {
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context.findActivity() ?: return#DisposableEffect onDispose {}
val originalOrientation = activity.requestedOrientation
activity.requestedOrientation = orientation
onDispose {
// restore original orientation when view disappears
activity.requestedOrientation = originalOrientation
}
}
}
fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
#Composable
fun VideoView() {
val exoPlayer = remember {
ExoPlayer.Builder(context)
.build()
.apply {
setMediaItem(
MediaItem.Builder()
.apply {
setUri("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
setMediaMetadata(
MediaMetadata.Builder()
.build()
)
}
.build()
)
prepare()
playWhenReady = true
}
}
DisposableEffect(
AndroidView(factory = {
PlayerView(context).apply {
player = exoPlayer
useController = false
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
})
) {
onDispose { exoPlayer.release() }
}
}
#Composable
fun VideoScreen() {
LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
RakeVragenTheme {
Surface(
modifier = Modifier
.fillMaxSize()
color = Color.Black
) {
VideoView()
}
}
}

Navigation with Arguments with Jetpack Compose

I've been troubleshooting this issue for couple of days now about navigating with args in jetpack compose. I feel like I'm missing something simple, really simple that I can't figure out somehow.
I tried making sure I'm coding it correctly but to no avail I still get the error.
So the error:
Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/second_screen/Item Six } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0x442b361f) route=home_screen}
I feel like I'm done everything correct but still cant get through this error. Or there is something simple that I'm missing.
My NavHost:
interface NavigationDestination{
val route: String
}
#Composable
fun MainNavHost(
){
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = HomeScreenNavigation.route,
){
composable(HomeScreenNavigation.route){
HomeScreen(onItemClick = {nameString->
navController.navigate("${SecondNavigationDestination.route}/${nameString}")
{
launchSingleTop = true
}
})
}
composable(
route = SecondNavigationDestination.routeWithArg,
arguments = listOf(navArgument(SecondNavigationDestination.nameArg)
{type = NavType.StringType})
){
SecondScreen()
}
}
}
My Screens
First Screen:
object HomeScreenNavigation: NavigationDestination{
override val route = "home_screen"
}
#Composable
fun HomeScreen(
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
){
ItemList(
itemList = LocalData.itemList,
onItemClick = onItemClick
)
}
#Composable
fun ItemList(
itemList: List<ItemModel>,
onItemClick: (String) -> Unit,
modifier: Modifier = Modifier
){
LazyColumn(
verticalArrangement = Arrangement.spacedBy(9.dp)
){
items(
items = itemList,
key = {it.id}
){
Item(
item = it,
onItemClick = onItemClick
)
}
}
}
#Composable
fun Item(
item: ItemModel,
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
){
Row(
modifier = modifier
.fillMaxWidth()
.padding(9.dp)
.clickable { onItemClick(item.name) },
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
){
Text(
text = item.name
)
}
}
///////////////////////////////////////////////////////
Second Screen:
object SecondNavigationDestination: NavigationDestination{
override val route = "second_screen"
const val nameArg = "nameArg"
val routeWithArg = "${route}/$nameArg"
}
#Composable
fun SecondScreen(
){
val viewModel: MainViewModel = viewModel()
Box(
contentAlignment = Alignment.Center
){
Text(
viewModel.id
)
}
}
/////////////////////////////////////////////////
ViewModel:
class MainViewModel(
savedStateHandle: SavedStateHandle
): ViewModel(){
val id: String = savedStateHandle[SecondNavigationDestination.nameArg] ?: "empty"
}
Change this
val routeWithArg = "${route}/$nameArg"
to this
val routeWithArg = "${route}/{$nameArg}"
For more info: docs

Invocations can only happen from the context of an #composable function using Compose Navigation

Hi Im currently struggling with navigation in Jetpack Compose due to #composable invocations can only happen from the context of an #composable function. I have a function:
private fun signInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
user = FirebaseAuth.getInstance().currentUser
Log.e("MainActivity.kt", "Innlogging vellykket")
ScreenMain()
} else {
Log.e("MainActivity.kt", "Feil med innlogging" + response?.error?.errorCode)
}
}
and used with my navigation class shown under I only get the error message shown above, how do I fix it?
#Composable
fun ScreenMain(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Routes.Vareliste.route) {
composable(Routes.SignUp.route) {
SignUp(navController = navController)
}
composable(Routes.ForgotPassword.route) { navBackStack ->
ForgotPassword(navController = navController)
}
composable(Routes.Vareliste.route) { navBackStack ->
Vareliste(navController = navController)
}
composable(Routes.Handlekurv.route) { navBackStack ->
Handlekurv(navController = navController)
}
composable(Routes.Profileromoss.route) { navBackStack ->
Profileromoss(navController = navController)
}
}
}
EDIT WITH COMPLETE CODE
Here is the whole code for the class if you guys wanted to see it!
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
LoginPage()
}
}
}
}
private var user: FirebaseUser? = FirebaseAuth.getInstance().currentUser
private lateinit var auth: FirebaseAuth
#Composable
fun LoginPage() {
Box(modifier = Modifier.fillMaxSize()) {
}
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Velkommen til ITGuys", style = TextStyle(fontSize = 36.sp))
Spacer(modifier = Modifier.height(20.dp))
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
Button(
onClick = { signIn() },
shape = RoundedCornerShape(50.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(text = "Logg inn")
}
}
}
}
private fun signIn() {
val providers = arrayListOf(
AuthUI.IdpConfig.EmailBuilder().build(),
AuthUI.IdpConfig.GoogleBuilder().build()
)
val signinIntent = AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(providers)
.build()
signInLauncher.launch(signinIntent)
}
private val signInLauncher = registerForActivityResult(
FirebaseAuthUIActivityResultContract()
) {
res -> this.signInResult(res)
}
private fun signInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
user = FirebaseAuth.getInstance().currentUser
Log.e("MainActivity.kt", "Innlogging vellykket")
ScreenMain()
} else {
Log.e("MainActivity.kt", "Feil med innlogging" + response?.error?.errorCode)
}
}
}
I need to add more text to be allowed to post this much code you can ignore this text cause it is just for being able to post.
As #z.y mentioned, you can pass a lambda with a onFirebaseAuthSuccess. I would also add that as you are passing the navController to the signup screen, the lambda callback you need to pass should look something like
onFirebaseAuthSuccess = { navController.navigate(Routes.Profileromoss.route) } - or whatever route you need
Based on
composable(Routes.SignUp.route) {
SignUp(navController = navController)
}
I would assume your signIn screen is called from inside the scope of a composable. If you can add the extract of code containing how you are calling the signInResult function we can be sure about it.
I'm not familiar with Firebase Authentication so I'm not sure where do you call or how you use your signInResult function but you cannot invoke a function that is annotated with #Composable (ScreenMain) from a scope that is not annotated by it such as ordinary function (signInResult).
You can consider adding a lambda callback for signInResult which will be called in the RESULT_OK condition block.
private fun signInResult(result: FirebaseAuthUIAuthenticationResult, onFirebaseAuthSuccess: () -> Unit) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
...
...
onFirebaseAuthSuccess() // this callback
} else {
...
}
}
Edit: #sgtpotatoe has better answer, you can invoke a navigation in your root composable from the lambda callback that will navigate to your target screen.
Ok so, in your MainActivity, you want your navigational component to be at the top:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ScreenMain()
}
}
}
}
Then add a route to your navhost for the login page:
composable(Routes.LoginPage.route) {
LoginPage(navController = navController)
}
I think its a bit of a major change, but you would have to rely on a view model to make the authentication, so it can handle the calls, not blocking the ui or showing a loading screen, and communicate with the view
It would look something like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = MyViewModel()
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ScreenMain(viewModel)
}
}
}
}
In the LoginPage you want to access the viewmodel to start the auth service calls
In the LoginPage you want to access the viewmodel to observe if the call is succesfull, and in that case do the navigation
In the MyViewModel you want to have the authentication calls, and to update the variable that triggers the navigation if auth is succesfull
Here is an example of a sample firebase authentication app in compose, I would use it as a guide https://firebase.blog/posts/2022/05/adding-firebase-auth-to-jetpack-compose-app

Kotlin Jetpack Compose, Card, change color on click

I cant get the Card in the LazyColumn to check if the number is in val guestNumbers and then change the color on the Card.
But the numbers in val guestNumbers changes on startup.
Is the Card the right stuff to use or should i use buttons?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val guestNumbers = rememberSaveable {
mutableStateOf(mutableSetOf<Int>(10,11,2,22))
}
NumberGuessingGameTheme {
Scaffold(
topBar = {
TopAppBar {
}
}
) {
Row(modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(4f)
.background(color = Color.LightGray)
) {
Text(
text = "1F",
style = MaterialTheme.typography.caption
)
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(color = Color.LightGray)
) {
LazyColumn {
items(1000 + 1) {
Card(modifier = Modifier
.fillMaxSize()
.padding(5.dp)
//.background(if ("$it".toInt() !in guestNumbers.value) Color.Green else Color.Red)
.clickable {
guestNumbers.value.add("$it".toInt())
Log.d("Tag", "${guestNumbers.value}")
},
elevation = 10.dp,
backgroundColor = if ("$it".toInt() in guestNumbers.value) Color.Red else Color.LightGray
) {
Text(text = "$it", fontSize = 28.sp, textAlign = TextAlign.Center)
}
}
}
}
}
}
}
}
}
}
mutableStateOf cannot track changes of the underlaying object. It can only trigger recomposition when you replace its value with an other object.
You can store an immutable set, create a modifiable copy to change it, and set the new value to your mutable state. This will create a new object.
var guestNumbers by rememberSaveable {
mutableStateOf(setOf(10,11,2,22))
}
Button({
val mutableSet = guestNumbers.toMutableSet()
mutableSet.add(if (Random.nextBoolean()) 2 else Random.nextInt())
guestNumbers = mutableSet.toSet()
}) {
Text(guestNumbers.toString())
}
Note, that if you'll face same problem with a list, you gonna need to call .toImmutableList(), because .toList() is only erasing the type and not actually creating a new object if called on a mutable list.

Is a good way to use State<Boolean> in view model with Android Compose?

I have read some sample codes for learning Compose.
I find many sample projects use Code A to create a StateFlow in view model, then convert it to State in #Composable function, the UI will be updated automatically when drawerOpen is changed.
1: I think both Code B and Code C can do the same thing, right? Why does many projects seldom to use them?
2: Is Code A a good way ?
3: I needn't to add rememberSaveable for variable drawerOpen in #Composable fun myRoute(...) because view model will store data, right?
Code A
class MainViewModel : ViewModel() {
private val _drawerShouldBeOpened = MutableStateFlow(false)
val drawerShouldBeOpened: StateFlow<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen by MainViewModel.drawerShouldBeOpened.collectAsState() //Do I need add rememberSaveable ?
...
}
Code B
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = mutableStateOf(false)
val drawerShouldBeOpened: State<Boolean> = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = MainViewModel.drawerShouldBeOpened //Do I need add rememberSaveable ?
...
}
Code C
class MainViewModel : ViewModel() {
private var _drawerShouldBeOpened = false
val drawerShouldBeOpened: Boolean = _drawerShouldBeOpened
...
}
#Composable
fun myRoute(
val drawerOpen = rememberSaveable { mutableStateOf(MainViewModel.drawerShouldBeOpened)) //Can I remove rememberSaveable ?
}
There are multiple questions here.
Let me answer whatever is possible.
1. Where should you use remember / rememberSaveable? (Code A, B, or C)
Only in code C it is required.
(No issues in using in code A and B as well, but no advantages there)
Reason,
In code A and B - the state is maintained in the view model. Hence the value survives recomposition.
But in code C, the state is created and maintained inside the composable. Hence remember is required for the value to survive recomposition.
More details in Docs
2. Why Code C is not used much?
Composable recomposition happens whenever there is a change in state, not the value.
Given below is a simple example to demonstrate the same.
class ToggleViewModel : ViewModel() {
private val _enabledStateFlow = MutableStateFlow(false)
val enabledStateFlow: StateFlow<Boolean> = _enabledStateFlow
private val _enabledState = mutableStateOf(false)
val enabledState: State<Boolean> = _enabledState
private var _enabled = false
val enabled: Boolean = _enabled
fun setEnabledStateFlow(isEnabled: Boolean) {
_enabledStateFlow.value = isEnabled
}
fun setEnabledState(isEnabled: Boolean) {
_enabledState.value = isEnabled
}
fun setEnabled(isEnabled: Boolean) {
_enabled = isEnabled
}
}
#Composable
fun BooleanToggle(
viewmodel: ToggleViewModel = ToggleViewModel(),
) {
val enabledStateFlow by viewmodel.enabledStateFlow.collectAsState()
val enabledState by viewmodel.enabledState
val enabled by rememberSaveable {
mutableStateOf(viewmodel.enabled)
}
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledStateFlow) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledStateFlow(!enabledStateFlow) }) {
Text("Toggle State Flow")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabledState) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabledState(!enabledState) }) {
Text("Toggle State")
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Text(text = if (enabled) {
"Enabled"
} else {
"Disabled"
})
Button(onClick = { viewmodel.setEnabled(!enabled) }) {
Text("Toggle Value")
}
}
}
}
You can see that the third text will NOT update on clicking the button.
The reason is that the mutable state inside the composable was created using an initial value from the view model data. But further updates to that data will not be reflected in the composable.
To get updates, we have to use reactive data like Flow, LiveData, State, and their variants.
3. Using StateFlow vs State.
From the docs, you can see that compose supports Flow, LiveData and RxJava.
You can see in the usage that we are using collectAsState() for StateFlow.
The method converts StateFlow to State. So both can be used.
Use Flow if the layers beyond ViewModel (like repo) are the data sources and they use Flow data type.
Else MutableState should be fine.