Jetpack Compose: How to pass values to composables in the tree? - android

I'am using Compose now with passing parameters to functions, but I feel, those parameters are getting longer with each new data to be passed down the tree.
I am wondering if there is a way to pass values to other composables down the tree.

Other than using function arguments for Composables (the recommended way),
you can use CompositionLocal:
Avoid CompositionLocal for concepts that aren't thought as tree-scoped or sub-hierarchy scoped.
First, define variable of compositionLocalOf globally (e.g., in LocalCompositions.kt):
import androidx.compose.runtime.compositionLocalOf
data class Post(val title: String)
val LocalPost = compositionLocalOf { Post(title = "Ahmed") }
and then define it inside #Compose function to be used by other composables down the tree:
#Composable
fun MainView() {
...
val post = Post(title = "Shamil")
CompositionLocalProvider(LocalPost provides post) {
PostContentView()
}
...
}
Now, retrieve the value inside a #Composable:
// composable down the nodes tree
#Composable
fun PostContentView() {
...
val post = LocalPost.current
...
}
Observe variable changes
If you want to achieve the above solution but with watching changes to the variable and update the composable (node) accordingly, you can use MutableState variable.
Redefining the previous example:
LocalCompositions.kt:
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
data class Post(val title: MutableState<String> = mutableStateOf(""))
val LocalPost = compositionLocalOf { Post(title = mutableStateOf("Ahmed")) }
and then define it inside #Compose function to be used by other composables down the tree:
#Composable
fun MainView() {
...
val post = Post(title = remember { mutableStateOf("Shamil") })
CompositionLocalProvider(LocalPost provides post) {
PostContentView()
}
...
post.title.value = "Ali"
...
}
Now, retrieve the value inside a #Composable:
// composable down the nodes tree
#Composable
fun PostContentView() {
...
val post = LocalPost.current
// now this composable will be updated whenever post.title value is updated
if (post.title.value == "Jamal") {
...
}
...
}

Related

Jetpack compose when to create a new lambda instance in Composable?

Please see my code:
#Composable
fun RecomposeLambdaTest() {
var state by remember {
mutableStateOf("1")
}
val stateHolder = remember {
StateHolder()
}
Column {
Button(onClick = {
state += "1"
}) {
Text(text = "change the state")
}
OuterComposable(state = state) {
stateHolder// just a reference to the instance outer the scope
}
}
}
#Composable
fun OuterComposable(state: String, onClick: () -> Unit) {
LogUtil.d("lambda hashcode: ${onClick.hashCode()}")
Column {
Text(text = state)
Button(onClick = onClick) {
Log.d("Jeck", "compose 2")
Text(text = "Text")
}
}
}
//#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always different! It means that a new lambda instance is created when recompose, everytime
and I uncomment the code in StateHolder and make it look like:
#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always the same! It means that when recompose, Composer reuse the lambda
So what' s under the hood?
Edit:
Ok, make it easier, Let's change the code like this:
val stateHolder = remember {
2
}
the result is lambda is reused.
make val to var, the lambda is created when every recompose.
So I think I know that: If the lambda refenence a valuable outer scope and the valuable is not stable, recreate lambda every time.
So the question is:
Why Compose compiler do this?
Why Compiler think the StateHolder before is not stable, it only contains a private var!?
An author met the same question, here is his article——6 Jetpack Compose Guidelines to Optimize Your App Performance
He said, private property still affact stability, it seems it is a Google team's choice.

When I change ViewModel var, Composable Doesn't Update in Kotlin + Compose

When I change ViewModel variable, Composable Doesn't Update the View and I'm not sure what to do.
This is my MainActivity:
class MainActivity : ComponentActivity() {
companion object {
val TAG: String = MainActivity::class.java.simpleName
}
private val auth by lazy {
Firebase.auth
}
var isAuthorised: MutableState<Boolean> = mutableStateOf(FirebaseAuth.getInstance().currentUser != null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val user = FirebaseAuth.getInstance().currentUser
setContent {
HeroTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
if (user != null) {
Menu(user)
} else {
AuthTools(auth, isAuthorised)
}
}
}
}
}
}
I have a a View Model:
class ProfileViewModel: ViewModel() {
val firestore = FirebaseFirestore.getInstance()
var profile: Profile? = null
val user = Firebase.auth.currentUser
init {
fetchProfile()
}
fun fetchProfile() {
GlobalScope.async {
getProfile()
}
}
suspend fun getProfile() {
user?.let {
val docRef = firestore.collection("Profiles")
.document(user.uid)
return suspendCoroutine { continuation ->
docRef.get()
.addOnSuccessListener { document ->
if (document != null) {
this.profile = getProfileFromDoc(document)
}
}
.addOnFailureListener { exception ->
continuation.resumeWithException(exception)
}
}
}
}
}
And a Composable View upon user autentication:
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel()
Column(
modifier = Modifier
.background(color = Color.White)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("Signed in!");
ProfileVModel.profile?.let {
Text(it.username);
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = {
FirebaseAuth.getInstance().signOut()
context.startActivity(Intent(context, MainActivity::class.java))
}) {
Text(
color = Color.Black,
text = "Sign out?",
modifier = Modifier.padding(all = 8.dp)
)
}
}
}
}
When my Firestore method returns, I update the profile var, and "expect" it to be updated in the composable, here:
ProfileVModel.profile?.let {
Text(it.username);
}
However, nothing is changing?
When I was adding firebase functions from inside composable, I could just do:
context.startActivity(Intent(context, MainActivity::class.java))
And it would update the view. However, I'm not quite sure how to do this from inside a ViewModel, since "context" is a Composable-specific feature?
I've tried to look up Live Data, but every tutorial is either too confusing or differs from my code. I'm coming from SwiftUI MVVM so when I update something in a ViewModel, any view that's using the value updates. It doesn't seem to be the case here, any help is appreciated.
Thank you.
Part 1: Obtaining a ViewModel correctly
On the marked line below you are setting your view model to a new ProfileViewModel instance on every recomposition of your Menu composable, which means your view model (and any state tracked by it) will reset on every recomposition. That prevents your view model to act as a view state holder.
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel = ProfileViewModel() // <-- view model resets on every recomposition
// ...
}
You can fix this by always obtaining your ViewModels from the ViewModelStore. In that way the ViewModel will have the correct owner (correct lifecycle owner) and thus the correct lifecycle.
Compose has a helper for obtaining ViewModels with the viewModel() call.
This is how you would use the call in your code
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
// or this way, if you prefer
// val ProfileVModel = viewModel<ProfileViewModel>()
// ...
}
See also ViewModels in Compose that outlines the fundamentals related to ViewModels in Compose.
Note: if you are using a DI (dependency injection) library (such as Hilt, Koin...) then you would use the helpers provided by the DI library to obtain ViewModels.
Part 2: Avoid GlobalScope (unless you know exactly why you need it) and watch out for exceptions
As described in Avoid Global Scope you should avoid using GlobalScope whenever possible. Android ViewModels come with their own coroutine scope accessible through viewModelScope. You should also watch out for exceptions.
Example for your code
class ProfileViewModel: ViewModel() {
// ...
fun fetchProfile() {
// Use .launch instead of .async if you are not using
// the returned Deferred result anyway
viewModelScope.launch {
// handle exceptions
try {
getProfile()
} catch (error: Throwable) {
// TODO: Log the failed attempt and/or notify the user
}
}
}
// make it private, in most cases you want to expose
// non-suspending functions from VMs that then call other
// suspend factions inside the viewModelScope like fetchProfile does
private suspend fun getProfile() {
// ...
}
// ...
}
More coroutine best practices are covered in Best practices for coroutines in Android.
Part 3: Managing state in Compose
Compose tracks state through State<T>. If you want to manage state you can create MutableState<T> instances with mutableStateOf<T>(value: T), where the value parameter is the value you want to initialize the state with.
You could keep the state in your view model like this
// This VM now depends on androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class ProfileViewModel: ViewModel() {
var profile: Profile? by mutableStateOf(null)
private set
// ...
}
then every time you would change the profile variable, composables that use it in some way (i.e. read it) would recompose.
However, if you don't want your view model ProfileViewModel to depend on the Compose runtime then there are other options to track state changes while not depending on the Compose runtime. From the documentation section Compose and other libraries
Compose comes with extensions for Android's most popular stream-based
solutions. Each of these extensions is provided by a different
artifact:
Flow.collectAsState() doesn't require extra dependencies. (because it is part of kotlinx-coroutines-core)
LiveData.observeAsState() included in the androidx.compose.runtime:runtime-livedata:$composeVersion artifact.
Observable.subscribeAsState() included in the androidx.compose.runtime:runtime-rxjava2:$composeVersion or
> androidx.compose.runtime:runtime-rxjava3:$composeVersion artifact.
These artifacts register as a listener and represent the values as a
State. Whenever a new value is emitted, Compose recomposes those parts
of the UI where that state.value is used.
This means that you could also use a MutableStateFlow<T> to track changes inside the ViewModel and expose it outside your view model as a StateFlow<T>.
// This VM does not depend on androidx.compose.runtime.* anymore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class ProfileViewModel : ViewModel() {
private val _profileFlow = MutableStateFlow<Profile?>(null)
val profileFlow = _profileFlow.asStateFlow()
private suspend fun getProfile() {
_profileFlow.value = getProfileFromDoc(document)
}
}
And then use StateFlow<T>.collectAsState() inside your composable to get the State<T> that is needed by Compose.
A general Flow<T> can also be collected as State<T> with Flow<T : R>.collectAsState(initial: R), where the initial value has to be provided.
#Composable
fun Menu(user: FirebaseUser) {
val context = LocalContext.current
val ProfileVModel: ProfileViewModel = viewModel()
val profile by ProfileVModel.profileFlow.collectAsState()
Column(
// ...
) {
// ...
profile?.let {
Text(it.username);
}
// ...
}
}
To learn more about working with state in Compose see the documentation section on Managing State. This is fundamental information to be able to work with state in Compose and trigger recompositions efficiently. It also covers the fundamentals of state hoisting. If you prefer a coding tutorial here is the code lab for State in Jetpack Compose.
An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.
Profile in view model should be State<*>
private val _viewState: MutableState<Profile?> = mutableStateOf(null)
val viewState: State<Profile?> = _viewState
In composable
ProfileVModel.profile.value?.let {
Text(it.username);
}
I recommend using MutableStateFlow.
a simple sample is described in this Medium article :
https://farhan-tanvir.medium.com/stateflow-with-jetpack-compose-7d9c9711c286

How to make kotlin operator a Composable with parameters having default value

I'm trying to make Kotlin's invoke operator a #Composable, everything works fine, until I add a parameter to it, which should have a default value. See the code below:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent{
Button()
}
}
}
object Button{
#Composable
operator fun invoke(text: String = "SomeText"){
println(text) // prints: null
}
}
When the operator is not annotated as #Composable the output is SomeText, as it should be.
Is this some error in Jetpack Compose, or am I missing something?
The behavior is the same on the latest stable Compose v 1.1.1 and on 1.2.0-beta01. Kotlin 1.6.21
Based on the info provided in the comments, I decided to answer:
I'll maybe think of something better, but off the top of my head, this is what you can do for now
enum class ButtonType {
Primary,
Secondary,
Tertiary
}
Return the correct type of Button
#Composable
fun MasterButton(type: ButtonType) {
when(type) {
primary -> PrimaryButton()
secondary -> SecondaryButton()
else -> TertiaryButton() // Must provide an 'else' branch
}
}
This will do the job for you.
CORRECT APPROACH I:
I just got the correct one the moment I started typing the first approach.
#Composable
fun ( #Composable () -> Unit ).Primary(...) {
PrimaryButton()
}
Make copies for every other button.
STRONG NOTICE: This is a RIDICULOUS way of "cleaning" up the code. Nobody should ever use anything remotely resembling this ever, but since that is just what the question is about, this is how you go about doing it. Know that this will attach an extension function called Primary(...) to every single #Composable function, and that cannot change. You can't apply it to select Composable(s) only, since this is basically just an extension function that I have applied on a general labmda, since 'extension functions for extension functions' are not something that exist as of now.
I am going to take this as your question (even though it is in the comments) and try to answer the way I achieve this.
What I'm trying to achieve is a way to clean up the namespace, so that
not all Composables are available as a top-level function. The general
idea is to group all flavors of let's say Buttons (Primary, Secondary,
Tertiary) to be Composables declared as a function of object Button.
But I would like to be able to use also this Button object as a
default Button (let it be Primary) in a Compose way, so just by using
it as it would be a function, thus invoke() operator. I would have
Button.Primary(), Button.Secondary() and Button() which would be an
"alias" for Button.Primary().
My implementation is quite simple,
Expose only one top-level Composable function to have a cleaner namespace.
Pass an argument that denotes the type of the required Composable, using a sealed class.
Button Type
sealed class MyIconButtonType(
open val typeName: String,
) {
data class Default(
override val typeName: String = "default",
) : MyIconButtonType(
typeName = typeName,
)
data class BorderIconButton(
override val typeName: String = "border",
// The variant specific attributes can be added here
val borderWidth: Int,
) : MyIconButtonType(
typeName = typeName,
)
}
Button (The only composable exposed to other files)
#Composable
fun MyTestIconButton(
onClickLabel: String,
modifier: Modifier = Modifier,
data: MyIconButtonType = MyIconButtonType.Default(),
onClick: () -> Unit,
content: #Composable () -> Unit,
) {
when (data) {
is MyIconButtonType.Default -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required
)
}
is MyIconButtonType.BorderIconButton -> {
// This composable should be private
MyTestIconDefaultButton(
// parameter as required, also make sure to pass variant specific attributes here
)
}
}
}
Usage
// For default impl
MyTestIconButton(
// default parameters
) {
}
// For specific variants
MyTestIconButton(
// default parameters
data = MyIconButtonType.BorderIconButton(
borderWidth = 10,
),
) {
}
Note:
Data class requires at least one attribute. Use object if no attributes like the typeName are required.
Like this,
sealed class MyIconButtonType {
object Default : MyIconButtonType()
data class BorderIconButton(
val borderWidth: Int,
) : MyIconButtonType()
}
Kotlin concepts that are used for reference,
Sealed classes, data classes and objects
when statement
Visibility modifiers

Can I store a composable function in a variable?

In Kotlin, function is a first-class citizen. We can store a function in a variable as below
val functionVariable: () -> Unit = ::myFunction
fun myFunction() { }
However, for #Composable function, how can I do so?
If I did the below, it will cry foul i.e. org.jetbrains.kotlin.diagnostics.SimpleDiagnostic#e93b05f8 (error: could not render message)
val functionVariable: () -> Unit = ::myFunction
#Composable
fun myFunction() { }
Is there a way to store composable function as a variable?
Composable function reference is not yet supported (and that's what the error message actually is). Besides, #Composable annotation is part of the function signature because it adds some parameters to the function. So you need to use val functionVariable: #Composable () -> Unit = { myFunction() }.
You can declare a composable function variable, like:
var functionVariable: #Composable () -> Unit = {}
Later you can reassign it (essentially redefining it) like:
functionVariable = { myFunction1() }
or
functionVariable = { myFunction2() }
But if you have functionVariable already planted in your larger Composable, the reassignment likely won't trigger recomposation. What I do is declare a state variable like:
var functionAssigned by mutableStateOf(0)
And in you larger Composable where you plant functionVariable, do:
if (functionAssigned >= 0) functionVariable()
And whenever you want to reassign the variable, do:
functionVariable = { myFunction2() }
functionAssigned++
That will trigger needed recomposation.

Android Compose - Store #Composables into Array

I would like to implement simple navigation by storing Composables into Array/Stack so that I could get them back with Back button. But I don't know how to add Composable into Array. Tried declaring anonymous Composable so that I could put its variable into stack but it doesn't compile? Can I somwhow put function name into Array?
var Details1 = #Composable
fun() {
Column(Modifier.fillMaxSize()) {
Text("Details 1")
}
}
var views = arrayOf(Details1)
This seems to work
// make an alias
typealias ComposableFun = #Composable () -> Unit
// composable function as lambda
val Test : ComposableFun = { Text("Test") }
// list of composable functions
val composableFuns = listOf(Test, Test, Test)
// elsewhere
composableFuns[0]()

Categories

Resources