Development record of developer who study hard everyday.

레이블이 Compose인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Compose인 게시물을 표시합니다. 모든 게시물 표시
, , ,

SideEffect in Android Composable(collectAsStateWithLifecycle, LaunchedEffect, rememberCoroutineScope)

 SideEffect in Android Composable


Android developer blog

✋✋✋ SideEffect

A side-effect in Compose is a change to the state of the app that happens outside the scope of a composable function.

For example, opening a new screen when the user taps on a button, or showing a message when the app doesn't have Internet connection.


1. collectAsStateWithLifecycle

dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0"
}

First, add dependency of "androidx.lifecycle:lifecycle-runtime-compose"


@OptIn(ExperimentalMaterialApi::class)
@Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
// TODO Codelab: collectAsStateWithLifecycle step - consume stream of data from the ViewModel
// val suggestedDestinations: List<ExploreModel> = remember { emptyList() }
val suggestedDestinations by viewModel.suggestedDestinations.collectAsStateWithLifecycle()

val onPeopleChanged: (Int) -> Unit = { viewModel.updatePeople(it) }
var tabSelected by remember { mutableStateOf(CraneScreen.Fly) }

BackdropScaffold(
modifier = modifier,
scaffoldState = rememberBackdropScaffoldState(BackdropValue.Revealed),
frontLayerScrimColor = Color.Unspecified,
appBar = {
HomeTabBar(openDrawer, tabSelected, onTabSelected = { tabSelected = it })
},
backLayerContent = {
SearchContent(
tabSelected,
viewModel,
onPeopleChanged
)
},
frontLayerContent = {
when (tabSelected) {
CraneScreen.Fly -> {
ExploreSection(
title = "Explore Flights by Destination",
exploreList = suggestedDestinations,
onItemClicked = onExploreItemClicked
)
}
CraneScreen.Sleep -> {
ExploreSection(
title = "Explore Properties by Destination",
exploreList = viewModel.hotels,
onItemClicked = onExploreItemClicked
)
}
CraneScreen.Eat -> {
ExploreSection(
title = "Explore Restaurants by Destination",
exploreList = viewModel.restaurants,
onItemClicked = onExploreItemClicked
)
}
}
}
)
}

Second, in CrameHomeContent composable, you can consume the list of destinations as form of state using collectAsstateWithLifecycle() method.


💪💪💪 Compose also offers APIs for Android's most popular stream-based solutions:

- LiveData.observeAsState() in "androidx.compose.runtime:runtime-livedata"

- Observable.subscribeAsState() in "androidx.compose.runtime:runtime-rxjava2"


2. LaunchedEffect and rememberUpdatedState

To call suspend functions safely from inside a composable, use the LaunchedEffect API, which triggers a coroutine scoped side-effect in Compose.

When LaunchedEffect enters the Composition, it launches a coroutine with the block of code passed as a key parameter.

The coroutine will be canceled if LaunchedEffect leaves the composition.


LaunchedEffect take a variable number of keys as a parameter that are used to restart the effect whenever one of those keys changes.

@Composable
fun LandingScreen(onTimeout: () -> Unit, modifier: Modifier = Modifier) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
val currentOnTimeOut by rememberUpdatedState(onTimeout)    //1

LaunchedEffect(key1 = Unit) {    //2
delay(SplashWaitTime)
onTimeout.invoke()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}

1 => The rememberUpdatedState() make us refer to the latest onTimeout function that LandingScreen was recomposed with

2 => Create an coroutine scope that matches the lifecycle of LandingScreen.

Because the key parameter is Unit that is not changed, the delay shouldn't start again.


3. rememberCoroutineScope

API to open the navigation drawer is suspend function.

How do we call suspend function in callback function?

@Composable
fun CraneHome(
onExploreItemClicked: OnExploreItemClicked,
modifier: Modifier = Modifier,
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
modifier = Modifier.statusBarsPadding(),
drawerContent = {
CraneDrawer()
}
) { padding ->
val scope = rememberCoroutineScope()
CraneHomeContent(
modifier = modifier.padding(padding),
onExploreItemClicked = onExploreItemClicked,
openDrawer = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
}

We want a CoroutineScope that follows the lifecycle of its call-site.

The rememberCoroutineScope API returns a CoroutineScope bound to the point in the Composition where you call it.

The scope will be automatically canceled once it leaves the Composition.


✊✊✊ LaunchedEffect vs rememberCoroutineScope

Using LauncedEffect is not possible to use in regular callback which is outside of the Composition.

Both can produce CoroutineScope but, LaunchedEffect can control when the side-effect would be called.


Share:
Read More
, , , , ,

How to use UnityPlayer in Android Compose

How to use UnityPlayer in Android Compose

Android Development blog

In my company, I have to use 3D avatar animation using UnityPlayer derived from Unity module.

The base code I will use is consist of Android Compose.

So, I want to use UnityPlayer with Android Compose.

However, There were no information about using UnityPlayer with Compose.

This is why I write this post.


☝I introduce this post on the condition that you add UnityModule.

If you not, read this post first!!

https://antwhale94.blogspot.com/2024/03/AddingUnityModuleToAndroid.html


1. Add dependency

android {
...
buildFeatures {
viewBinding true
}     ...
}

dependencies {

implementation 'androidx.compose.ui:ui-viewbinding:1.6.0'

}

You should add viewBinding dependency.

And you should add "androidx.compose.ui:ui-viewbinding" to take xml into Compose.


2.  MainActivity.kt

class MainActivity : ComponentActivity() {
val TAG = MainActivity::class.java.simpleName
lateinit var mUnityPlayer: UnityPlayer

@ExperimentalMaterial3Api
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

mUnityPlayer = UnityPlayer(this)
val glesMode = mUnityPlayer.getSettings().getInt("gles_mode", 1)
val trueColor8888 = false
mUnityPlayer.init(glesMode, trueColor8888)

setContent {
Surface(
modifier = Modifier
.fillMaxSize()
) {
MLCChatTheme {
NavView()
}
}
}


}

override fun onDestroy() {
Log.d(TAG, "onDestroy: ")
super.onDestroy()

mUnityPlayer.quit()
}

override fun onPause() {
Log.d(TAG, "onPause: ")
super.onPause()
mUnityPlayer.pause()
}

override fun onResume() {
Log.d(TAG, "onResume: ")
super.onResume()
mUnityPlayer.resume()
}

override fun onLowMemory() {
Log.d(TAG, "onLowMemory: ")
super.onLowMemory()
mUnityPlayer.lowMemory()
}

override fun onTrimMemory(level: Int) {
Log.d(TAG, "onTrimMemory: ")
super.onTrimMemory(level)
if(level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL)
mUnityPlayer.lowMemory()
}

override fun onConfigurationChanged(newConfig: Configuration) {
Log.d(TAG, "onConfigurationChanged: ")
super.onConfigurationChanged(newConfig)
mUnityPlayer.configurationChanged(newConfig)
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
Log.d(TAG, "onWindowFocusChanged: $hasFocus")
super.onWindowFocusChanged(hasFocus)
mUnityPlayer.windowFocusChanged(hasFocus)
}

}

fun Context.getActivity(): ComponentActivity? = when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}

In my experience, The UnityPlayer derived from activity only can play animation.

I don't know why, so I make UnityPlayer object in MainActivity using MainActivity context.

The lowest function getActivity is used to get MainActivity from Composable function.


3. xml layout to put UnityPlayer

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frameLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

</FrameLayout>

This layout file's name is "avatar_view_layout.xml"

Obviously, you can change this file name if you want.


4. Composable function to show UnityPlayer

@Composable
fun AvatarView() {
val TAG = "AvatarView"
val localContext = LocalContext.current

Log.d(TAG, "AvatarView composition")

val unityPlayer = (localContext.getActivity() as MainActivity).mUnityPlayer    //1
AndroidViewBinding(AvatarViewLayoutBinding::inflate) {     //2
if(unityPlayer.parent == null) {
Log.d(TAG,
"AvatarView added")
frameLayout.addView(unityPlayer.view)        //3
unityPlayer.resume()
}
}

DisposableEffect(key1 = localContext) {
onDispose {        //4
Log.d(TAG, "remove AvatarView")
(unityPlayer.
parent as FrameLayout).removeView(unityPlayer.view)

unityPlayer.pause()

}
}
}

1 => Get UnityPlayer object using local context

2 => AndroidViewBinding to use xml in Compose

3 => Add UnityPlayer to FrameLayout and resume UnityPlayer

4 => DisposableEffect to notice when AvatarView composable leave.

You should remove UnityPlayer from framelayout and pause it to restart when recomposing.


I don't know this is good way to use UnityPlayer with Compose, but This is only way I got this.




Share:
Read More