I have read the official document about Composable functions
But I can't understand Composable functions.
For example, rememberWatchState() is a Composable function in Code A, but it doesn't define my app's UI just like a normal function.
What is the difference between the Composable function and the normal function in Android?
Code A
#Composable
fun ScreenHome_Watch(
watchState:WatchState = rememberWatchState()
){
val density= watchState.density
}
#Composable
fun rememberWatchState(): WatchState {
val context: Context = LocalContext.current
val temp = loadDBWarningValueWithPreference(context)
val watchState = WatchState(temp.toFloat(),LocalDensity.current)
return remember {
watchState
}
}
class WatchState(
private val alarmValue:Float,
val density: Density
){
...
}
fun drawDial(
drawScope: DrawScope,
watchState:WatchState
) {
...
}
#Composable annotation is like a scope that gives access to Compose functions such as LaunchedEffect, SideEffect, remember or objects such as currentComposer and resembles suspend functions.
Section below is quoted from Under the hood of Jetpack Compose — part 2 of 2 article by Leland Richardson.
// function declaration
suspend fun MyFun() { … }
// lambda declaration
val myLambda = suspend { … }
// function type
fun MyFun(myParam: suspend () -> Unit) { … }
Kotlin’s suspend keyword operates on function types: you can have a function declaration that’s suspend, a lambda, or a type. Compose works in the same way: it can alter function types.
// function declaration
#Composable fun MyFun() { … }
// lambda declaration
val myLambda = #Composable { … }
// function type
fun MyFun(myParam: #Composable () -> Unit) { … }
The important point here is that when you annotate a function type with #Composable you’re changing its type: the same function type without the annotation is not compatible with the annotated type. Also, suspend functions require a calling context, meaning that you can only call suspend functions inside of another suspend function.
fun Example(a: () -> Unit, b: suspend () -> Unit) {
a() // allowed
b() // NOT allowed
}
suspend
fun Example(a: () -> Unit, b: suspend () -> Unit) {
a() // allowed
b() // allowed
}
Composable works the same way. This is because there’s a calling context object that we need to thread through all of the invocations.
fun Example(a: () -> Unit, b: #Composable () -> Unit) {
a() // allowed
b() // NOT allowed
}
#Composable
fun Example(a: () -> Unit, b: #Composable () -> Unit) {
a() // allowed
b() // allowed
}
what is this calling context thing that we’re passing around and why do we need to do it?
We call this object the Composer. The implementation of the Composer contains a data structure that is closely related to a Gap Buffer. This data structure is commonly used in text editors.
Let’s look at an example of a counter.
#Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
}
This is the code that we would write, but let’s look at what the compiler does.
When the compiler sees the Composable annotation, it inserts additional parameters and calls into the body of the function.
First, the compiler adds a call to the composer.start method and passes it a compile time generated key integer.
fun Counter($composer: Composer) {
$composer.start(123)
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
$composer.end()
}
LazyRow or LazyColumns for instance have content with LazyListScope receiver that is not annotated with #Composable which doesn't let you to call other Composables
#Composable
fun LazyColumn(
// Rest of params
// Non Composable scope
content: LazyListScope.() -> Unit
) {
// Implementation details
}
LazyColumn(){ // LazyListScope
// You can't add Composable here because this scope is LazyListScope
// which is not annotated with #Composable, and returns compiler error
// Row(){}
items(100){
Row() {
}
}
}
You can't call Composable functions from LazyListScope but you can call inside items because its content is #Composable function.
fun item(
key: Any? = null,
contentType: Any? = null,
content: #Composable LazyItemScope.() -> Unit
) {
error("The method is not implemented")
}
You need a scope with #Composable annotation to call other #Composable functions.
It's not mandatory for Composable functions to be UI functions. Some functions such as SideEffect or LaunchedEffect are used to run when your Composable function enters composition successfully.
#Composable
#NonRestartableComposable
#OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
Or not only to check when it enters composition also to check when it exits with DisposableEffect
#Composable
private fun NonUIComposableSample() {
val context = LocalContext.current
var counter by remember { mutableStateOf(0) }
var color by remember { mutableStateOf(Color.Red) }
if (counter in 3..5) {
DisposableEffect(Unit) {
Toast.makeText(context, "Entering Composition counter: $counter", Toast.LENGTH_SHORT).show()
color = Color.Yellow
onDispose {
color = Color.Green
Toast.makeText(context, "Exiting Composition counter: $counter", Toast.LENGTH_SHORT).show()
}
}
}
Button(onClick = { counter++ }) {
Text("Counter: $counter", color = color)
}
}
In this example Block with DisposableEffect enters composition when counter is 3 stay in composition while and exits when condition is not met anymore.
Also produceState is another #Composable non ui function launches a coroutine scoped to the Composition that can push values into a returned State. Use it to convert non-Compose state into Compose state, for example bringing external subscription-driven state such as Flow, LiveData, or RxJava into the Composition.
Compose is a data structure holds all of the objects from the composition, the entire tree in execution order, effectively a depth first traversal of the tree.
Related
I have this code that increments a value in the database:
override fun incrementQuantity() = flow {
try {
emit(Result.Loading)
heroIdRef.update("quantity", FieldValue.increment(1)).await()
emit(Result.Success(true))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
This function is called from within a ViewModel class, and then from a composable function I read the response like this:
when(val response = viewModel.incrementResponse) {
is Result.Loading -> showProgressBar()
is Result.Success -> Log.d("TAG", "Quantity incremented.")
is Result.Failure -> Log.d("TAG", response.e.message)
}
This means that in the flow I receive two events, the first one is Loading, and the second one is the data or a failure. My question is:
Is it recommended to do it that way? Or is it better to not using a flow and have something like this:
override fun incrementQuantity(): Result<Boolean> {
try {
heroIdRef.update("quantity", FieldValue.increment(1)).await()
Result.Success(true)
} catch (e: Exception) {
Result.Failure(e)
}
}
And inside the composable:
showProgressBar()
when(val response = viewModel.incrementResponse) {
is Result.Success -> {
hideProgressBar()
Log.d("TAG", "Quantity incremented.")
}
is Result.Failure -> {
hideProgressBar()
Log.d("TAG", response.e.message)
}
}
Are there any downsides when using Kotlin flow?
Here is my incrementResponse object:
var incrementResponse by mutableStateOf<Response<Boolean>>(Response.Success(false))
private set
It is ok to return flow from the repository. You can collect it in the ViewModel and update the state accordingly.
Also note that Compose follows a declarative UI model, so you shouldn't be calling showProgressBar() when state is Result.Loading. Instead you should just call the progress bar composable. Similarly for the other two cases of Success and Failure you should place the necessary UI elements there. It will be something like:
// In your composable function
val result = yourFlow.collectAsState()
when(result) {
Result.Loading -> ProgressBarComposable()
Result.Success -> SuccessComposable()
Result.Failure -> FailureComposable()
}
Using StateFlow, you can do it like this:
// In ViewModel:
private val _incrementResultFlow = MutableStateFlow<Result<Boolean>?>(null) // Set the initial value to be null to avoid a Progress Bar in the beginning
val incrementResultFlow = _incrementResultFlow.asStateFlow()
fun onIncrementButtonClick() {
repository.incrementQuantity().collect { result->
_incrementResultFlow.value = result
}
}
// In your composable
val result by viewModel.incrementResultFlow.collectAsState()
if(result != null) {
when(result) {
...
}
}
An alternative to StateFlow is directly using the compose State inside your ViewModel. If, in your project, you don't mind putting compose dependencies in your ViewModel, I will suggest using this approach.
// In ViewModel:
var incrementResponse by mutableStateOf<Result<Boolean>?>(null)
private set // So that it can only be modified from the ViewModel
fun onIncrementButtonClick() {
repository.incrementQuantity().collect { result->
incrementResponse = result
}
}
// In your composable
viewModel.incrementResponse?.let {
when(it) {
Result.Loading -> ProgressBarComposable()
Result.Success -> SuccessComposable()
Result.Failure -> FailureComposable()
}
}
Another alternative is to modify your repository function to return a Result<Boolean> instead of the flow and handle the Loading logic in the ViewModel.
// Repository
suspend fun incrementQuantity(): Result<Boolean> {
return try {
heroIdRef.update("quantity", FieldValue.increment(1)).await()
Result.Success(true)
} catch (e: Exception) {
Result.Failure(e)
}
}
// ViewModel
var incrementResponse by mutableStateOf<Result<Boolean>?>(null)
private set
fun onIncrementButtonClick() {
incrementResponse = Result.Loading
incrementResponse = repository.incrementQuantity()
}
In jetpack compose you don't need to toggle visibility manually. You can just play with the states. You can create a separate progress bar component that can be used for other Screens as well. Below code will only get triggered if the state is loading if the state changes it will get automatically hidden
#Composable
fun CircularIndeterminateProgressBar(
uiState: UiState
) {
if (uiState is UiState.Loading) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()// provided by compose library
}
}
}
and then call this component from your main screen composable function and pass state inside it
CircularIndeterminateProgressBar(uiState = uiState)
uiState you will get from viewmodel
In viewmodel you will use
private val _uiState = mutableStateOf<ModelStateName>()
the above is mutable state which is lifecycle aware and will be listed in composable function directly
val uiState by remember {
viewmodel.uiState
}
Read more about mutable states here
mutable states in jetpack compose
Jetpack compose workshop with increment example
jetpack compose workshop
A have a screen where I display 10 users. Each user is represented by a document in Firestore. On user click, I need to get its details. This is what I have tried:
fun getUserDetails(uid: String) {
LaunchedEffect(uid) {
viewModel.getUser(uid)
}
when(val userResult = viewModel.userResult) {
is Result.Loading -> CircularProgressIndicator()
is Result.Success -> Log.d("TAG", "You requested ${userResult.data.name}")
is Result.Failure -> Log.d("TAG", userResult.e.message)
}
}
Inside the ViewModel class, I have this code:
var userResult by mutableStateOf<Result<User>>(Result.Loading)
private set
fun getUser(uid: String) = viewModelScope.launch {
repo.getUser(uid).collect { result ->
userResult = result
}
}
As you see, I use Result.Loading as a default value, because the document is heavy, and it takes time to download it. So I decided to display a progress bar. Inside the repo class I do:
override fun getUser(uid: String) = flow {
try {
emit(Result.Loading)
val user = usersRef.document(uid).get().await().toObject(User::class.java)
emit(Result.Success(user))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
I have two questions, if I may.
Is there something wrong with this code? As it works fine when I compile.
I saw some questions here, that recommend using collectAsState() or .collectAsStateWithLifecycle(). I tried changing userResult.collectAsState() but I cannot find that function. Is there any benefit in using collectAsState() or .collectAsStateWithLifecycle() than in my actual code? I'm really confused.
If you wish to follow Uncle Bob's clean architecture you can split your architecture into Data, Domain and Presentation layers.
For android image below shows how that onion shape can be simplified to
You emit your result from Repository and handle states or change data, if you Domain Driven Model, you store DTOs for data from REST api, if you have db you keep database classes instead of passing classes annotated with REST api annotation or db annotation to UI you pass a UI.
In repository you can pass data as
override fun getUser(uid: String) = flow {
val user usersRef.document(uid).get().await().toObject(User::class.java)
emit(user)
}
In UseCase you check if this returns error, or your User and then convert this to a Result or a class that returns error or success here. You can also change User data do Address for instance if your business logic requires you to return an address.
If you apply business logic inside UseCase you can unit test what you should return if you retrieve data successfully or in case error or any data manipulation happens without error without using anything related to Android. You can just take this java/kotlin class and unit test anywhere not only in Android studio.
In ViewModel after getting a Flow< Result<User>> you can pass this to Composable UI.
Since Compose requires a State to trigger recomposition you can convert your Flow with collectAsState to State and trigger recomposition with required data.
CollectAsState is nothing other than Composable function produceState
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
And produceState
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
As per discussion in comments, you can try this approach:
// Repository
suspend fun getUser(uid: String): Result<User> {
return try {
val user = usersRef.document(uid).get().await().toObject(User::class.java)
Result.Success(user)
} catch (e: Exception) {
Result.Failure(e)
}
}
// ViewModel
var userResult by mutableStateOf<Result<User>?>(null)
private set
fun getUser(uid: String) {
viewModelScope.launch {
userResult = Result.Loading // set initial Loading state
userResult = repository.getUser(uid) // update the state again on receiving the response
}
}
#Composable
fun MyComposable(submit: (String) -> Unit) {
MyButton(title = "Submit") {
submit("Hello World!")
}
}
#Composable
fun MyButton(title: String, submit: (String) -> Unit) {
Button(onClick = submit) {
Text(text = title)
}
}
How can I unit test that submit were to be called?
While in the test I can set the submit result inside MyComposable, it never actually gets called when the test is being ran.
The test would look like this:
#Test
fun trySubmit_expectPrint() {
composeRule.setContent { GetDefaultScreen() }
//Too lazy to write the hasTestTag, but assume it gets the correct node.
composeRule.onNodeWithTag("MyComposable").performClick()
//Assertions here...
}
#Composable
private fun GetDefaultScreen() =
MyComposable() {
Log.d("TEST", "Submitting")
}
My end goal here is to have the scope of that composable in the test change a variable, in order to allow me to verify that that block was ran.
This is all under the assumption that we are still unable to Mock or Spy on final classes with an androidTest (UI test).
If that assumption is wrong, please correct me if there's a method in which I can do that.
I've since come to found something that works for me.
In the test function, just adding a value that should get changed by the callback, and then making an assertion on that value:
#Test
fun myTest() {
var testValue = ""
composeTestRule.setContent {
Button() { testValue = "Hello, World!" }
}
assertThat(testValue).equals("Hello, World!")
}
Hi I try to call this a compose function from updatedata(it)
viewModel.ResponseStatus.observe(viewLifecycleOwner) { status ->
when (status) {
FragmentViewModel.PROCESSING,
FragmentViewModel.NOT_PROCESSING -> {
viewModel.object.let {
updatedata(it). <--- error?
} ?: faildialog()
}
This is how my updatadata() function
#ExperimentalFoundationApi
#OptIn(ExperimentalAnimationApi::class)
#ExperimentalUnitApi
#Composable
private fun updatedata(authdata: Payload) {
composefunction(authdata.client_name)
}
The error I get is #Composable invocations can only happen from the context of a #Composable function
That is the compose function I am calling
#ExperimentalUnitApi
#ExperimentalAnimationApi
#ExperimentalAnimationGraphicsApi
#ExperimentalFoundationApi
#Composable
fun composefunction(name: String) {
Box(){}
}
I am not familiar with flow or live data, much less on composable, which part I must change to get the live data to pass into the compose function?
In a Composable world, you don't tell the view what to do after a state changes.
#ExperimentalFoundationApi
#OptIn(ExperimentalAnimationApi::class)
#ExperimentalUnitApi
#Composable
private fun updatedata(viewModel: YourViewModel, authdata: Payload) {
val responseState by viewModel.ResponseStatus.observeAsState()
when (status) {
FragmentViewModel.PROCESSING,
FragmentViewModel.NOT_PROCESSING -> {
viewModel.object.let {
composefunction(authdata.client_name)
} ?: faildialog()
}
}
}
I highly encourage that you learn all the stuff you just mentioned: LiveData, MutableState and how to think in Composable as these things work together.
What I've tried so far
fun getCPByID(ids: List<Int>): List<CheckingPointVo> {
var list : List<CheckingPointVo> = emptyList()
coroutineScope.launch {
list = someMethod()
}
return list
}
here I tried to use async and await but that cannot be run from a non suspend function. Is there a way to do this ?
Not really with the current structure, you're basically trying to combine synchronous code with async.
You have 3 possible options though to make it async:
Use a callback:
fun getCPByID(ids: List<Int>, listCallback: (List<CheckingPointVo>) -> Unit) {
coroutineScope.launch {
listCallback(someMethod())
}
}
Note: If you're using it from Java, this should work with either Java lambdas or Function. But you may create an interface for this, like :
Interface ListCallback {
fun onListReceived(list: List<CheckingPointVo>)
}
fun getCPByID(ids: List<Int>, listCallback: ListCallback) {
.... // Same implementation
}
// Call it from Java
getCPByID(ids, new ListCallback() {
void onListReceived(List<CheckingPointVo> list) {
...
}
});
Use either an observable pattern, use a Flow or LiveData. A possible example:
fun getCPByID(ids: List<Int>) = coroutineScope.launch {
flow {
emit(someMethod())
}
}
}
Make your function a suspend function and use coroutineScope.launch from the caller