Eventough I haven't used remember keyword values are still remembered between recomposition.
While tutorials say that it shouldn't.
I am trying to understand all this mess around Compose State Variables but things work differently then explained.
package com.example.testcompose
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var state by mutableStateOf(1)
Button({state += 1 }) { Text("$state") }
}
}
}
Here is extended code which demonstrated that Button and Text behave differently.
In both cases I am NOT using remember.
But Button is behaving as if I am using rmemeber.
//==================================================================
// MAIN ACTIVITY
//==================================================================
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
MyButton() //Behaves incorrectly like there is remember
MyText() //Behaves correctly not remembering the value
}
}
}
}
#Composable
fun MyButton(){
Column {
var state by mutableStateOf("Hello ")
Button({ state += "you " }) { Text(state) }
}
}
#Composable
fun MyText(){
Column {
var state by mutableStateOf("Hello ")
Text(state, modifier = Modifier.clickable(onClick = { state += "you " } ) )
}
}
As mentioned by Google:
Recomposition is the process of calling your composable functions
again when inputs change. This happens when the function's inputs
change. When Compose recomposes based on new inputs, it only calls the
functions or lambdas that might have changed, and skips the rest. By
skipping all functions or lambdas that don't have changed parameters,
Compose can recompose efficiently.
Let's change your code to this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
MyComposable()
}
}
}
}
#Composable
fun MyComposable() {
Column {
var state1 by mutableStateOf("Hello ")
Button({ state1 += "you " }) { Text(state1) }
var state2 by mutableStateOf("Hello ")
Text(state2, modifier = Modifier.clickable(onClick = { state2 += "you " }))
}
}
You will see that even though button behaves correctly, text changes both values due to recomposition. So as mentioned by Google because of the underline mechanism of Compose it is unpredictable how the code runs or might skip part of the execution of the code. So you should always use remember on this situations in order to have the expected outcome.
Please read carefully Thinking in Compose.
Related
I was trying to achieve the below layout
I tried using Row(Modifier.weight(50f)) that's when the compiler start throwing
If imported from ColumnInstance - import androidx.compose.foundation.layout.ColumnScopeInstance.weight
Cannot access 'ColumnScopeInstance': it is internal in 'androidx.compose.foundation.layout'
If imported from RowInstance - androidx.compose.foundation.layout.RowScopeInstance.weight
Cannot access 'RowScopeInstance': it is internal in 'androidx.compose.foundation.layout'
Attaching my Composable code below
#Composable
fun BoxLayout(){
Row(Modifier.weight(50f)) {
BoxWithText()
BoxWithText()
}
}
Attaching entire file for reference
package me.sanjaykapilesh.layoutmastery
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScopeInstance.weight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import me.sanjaykapilesh.layoutmastery.ui.theme.LayoutMasteryTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LayoutMasteryTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
BoxWithText()
}
}
}
}
}
#Composable
fun BoxLayout(){
Row(Modifier.weight(50f)) {
BoxWithText()
BoxWithText()
}
}
#Composable
fun BoxWithText() {
Column() {
Text(text = "Hello Box!")
Text(text = "Displays text and follows Material Design guidelines")
}
}
#Preview(showBackground = true)
#Composable
fun BoxLayoutPreview() {
LayoutMasteryTheme {
BoxLayout()
}
}
I am not sure why I am getting an error. I am also unable to achieve Modifier.weight
Question - https://developer.android.com/codelabs/basic-android-kotlin-compose-composables-practice-problems?authuser=2&continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-compose-unit-1-pathway-3%3Fauthuser%3D2%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-compose-composables-practice-problems#3
You can use an extension function to get the context. For example:
#Composable
fun ColumnScope.BoxLayout(){
Row(Modifier.weight(50f)) {
BoxWithText()
BoxWithText()
}
}
Some modifiers are unique to scopes that they are defined in like Modifier.weight is only available in RowScope or ColumnScope by default. Or Modifier.align is only available inside BoxScope.
When you wish to access these Modifiers you either need to have your Composables functions in these scopes or create a function that takes #Composable argument with Receiver of these scopes
#Composable
fun BoxLayout(){
Row(Modifier.weight(50f)) {
BoxWithText()
BoxWithText()
}
}
BoxLayout should return RowScope/ColumnScope as this to be able to use Modifier.weight and this can be done as
#Composable
fun BoxWithLayout(content: #Composable RowScope.()->Unit){
Row {
content()
}
}
#Composable
private fun Sample() {
BoxWithLayout {
Row(Modifier.weight(50f)) {
BoxWithText()
BoxWithText()
}
}
}
I've had problems like that too.
trouble
You can try to block "RowScope" and then press Alt + Enter select "Surround with widget" ended select "Surround with column"
solution
I want to change the text that's appearing on the button each time I click it, so I have written the following code, but it's not working. Where am I going wrong?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var i=0;
Button(onClick = {i++ }) {
Text("Clicked $i times") //!!not updating here
}
}
}
}
Check how compose works with states and recomposition.
Use something like:
var i by remember { mutableStateOf(0) }
Button(onClick = {i++ }) {
Text("Clicked $i times")
}
How can the properties of a top app bar be accessed and reused several times? Are there any limitations towards this approach? I want to be able to change the text in a top app bar when necessary.
class MainActivity : ComponentActivity() {
private val topBarTitle: String = "?"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AirlinesTheme {
setContent {
Scaffold(
topBar = { TopBar() },
content = {}
)
}
}
}
}
#Composable
fun TopBar() {
SmallTopAppBar(title = { Text(text = topBarTitle) })
}
}
Usually the important data is stored inside a ViewModel
class MyVM : ViewModel(){
var topBarProperty by mutableStateOf("Initial")
}
Then initialize the VM like this
val vm by viewModels<MyVM>()
Now, use the property in your top-bar
Scaffold(
topBar = { Text(text = vm.topBarProperty) },
content = {}
)
Now, you could do something like vm.topBarProperty = "Updated", from anywhere in your activity, and it will update the value on the topBar. This is because we are using a MutableState<T> type variable which will trigger recompositions on the Composables reading it, when modified.
Minimal reproducable example, so that after screen rotation the shown random value changed :
package composesandbox
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
val rnd = Random()
suspend fun loadRandomNumber(): Int {
delay(1000L) //emulating network delay
return rnd.nextInt(1000)
}
class RandomNumberViewModel : ViewModel() {
val num = mutableStateOf(0)
fun reload() {
viewModelScope.launch {
num.value = loadRandomNumber()
}
}
}
#Composable
fun RandomNumberScreen() {
val vm = viewModel<RandomNumberViewModel>()
Column {
LaunchedEffect(Unit) {
vm.reload()
}
Text(text = "Random Number:\n ${vm.num.value}")
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RandomNumberScreen()
}
}
}
I want the data (viewmodel.num) to be only loaded ONCE despite the screen rotations. Please explain, why data is reloaded on rotations.
On a screen rotation onCreate gets called again.
This means everything inside the Activity is recomposed.
On Recompose your Composed function RandomNumberScreen() is called again and this calls your reload() function again, which changes the value of num.
Keep in mind, that on a recompose the whole Composable gets executed again to keep the State up to date.
Your intention might be, that everythime num changes, the Composable should recompose. You should not trigger a function on recompose which changes num again.
You could use something like a variable in the viewmodel to make sure num is initialized and doesn't change when initialized. check if the variable is true or false before executing reload.
if (!isInitialized) reload() something like this.
It is because you are calling the reload method inside a LaunchedEffect, which is executed upon the first composition. When you rotate the screen, the entire screen is destroyed. All the Composables re-render themselves and hence, the inconsistent value.
Ok so you could do something like this
class RandomNumberViewModel : ViewModel() {
val num = mutableStateOf(0)
fun reload(): Int { //return something so that it can be saved
viewModelScope.launch {
num.value = loadRandomNumber()
}
return 0
}
}
#Composable
fun RandomNumberScreen() {
val vm = viewModel<RandomNumberViewModel>()
Column {
rememberSaveable {
vm.reload()
}
Text(text = "Random Number:\n ${vm.num.value}")
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RandomNumberScreen()
}
}
}
I have created Kotlin Code for parsing APIs with retrofit in list view/grid view/recycler view, I wanted to know how can I do the same using jetpack compose? I have used retrofit to parse GET API responses using different ViewGroups. View Binding is used to interact with the views on this screen.
Code
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import okhttp3.ResponseBody
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import retrofit.Retrofit2
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import supports.*
import viewmodel.SIViewModel
import java.io.IOException
import java.net.SocketTimeoutException
import java.util.*
class TestIndex : AppCompatActivity() {
var adapter: Adapter1? = null
var dialog: AlertDialog? = null
var builder: AlertDialog.Builder? = null
private val viewModel: SIViewModel? by viewModels()
var test_arr = ArrayList<TestModel>()
var binding: TestGridBinding? = null
#SuppressLint("CommitPrefEdits", "ClickableViewAccessibility", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.test_grid)
setSupportActionBar(binding?.view?.toolbarr)
supportActionBar!!.elevation = 0f
viewModel
adapter = Adapter1(this#TestIndex, R.layout.row, test_arr)
//binding ViewModel retrofit API with activity, here ID1 and ID2 coming from the previous screen.
viewModel!!.getList(this#TestIndex , ID1!!, ID2!!)
binding?.gvTest?.adapter = adapter
binding?.swipeRefreshLayout?.setOnRefreshListener {
binding?.swipeRefreshLayout?.isRefreshing = true
if (ID1 != null && ID2 != null) {
// getting same server response on swipe refresh widget
getdata(ID1!!, ID2!!)
} else {
builder = AlertDialog.Builder(MyApplication.instance)
builder!!.setCancelable(false)
builder!!.setTitle("Alert")
builder!!.setNegativeButton("Cancel") { dialog: DialogInterface, which: Int ->
dialog.dismiss()
finish()
}
builder!!.setPositiveButton("OK") { dialog: DialogInterface, which: Int -> dialog.dismiss() }
dialog = builder!!.create()
dialog?.show()
}
}
subscribeObservers()
}
//this is checked on the dev portal but I don't know I could I use it //dynamically with adapters and ArrayList.
#Composable
fun LazyRowItemsDemo() {
LazyRow {
items((1..title_arr.size).toList()) {
Text(text = "Item $it")
}
}
}
private fun getdata(id1: String, id2: String) {
val mProgressDialog = ProgressDialog(this#TestIndex)
mProgressDialog.isIndeterminate = true
mProgressDialog.setMessage(Keys.KEY_pre_msg)
if (!this.isFinishing) {
mProgressDialog.show()
}
val retrofit = Retrofit.Builder()
.baseUrl(Keys.testURL)
.client(OkHttpClient().build())
.addConverterFactory(GsonConverterFactory.create())
.build()
val retrofitInterface = retrofit.create(
RetrofitInterface::class.java
)
val call = retrofitInterface.getTestdata(id1, id2)
call!!.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(call: Call<ResponseBody?>, response: Response<ResponseBody?>) {
var remoteResponse: String? = null
if (response.code() == 200) {
try {
assert(response.body() != null)
remoteResponse = response.body()!!.string()
} catch (e: Exception) {
e.printStackTrace()
}
} else {
try {
if (response.errorBody() != null) {
remoteResponse = response.errorBody()!!.string()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
if (remoteResponse != null) {
//getting response fields and parsing list view or grid view/recycler view in different screens
adapter =
Adapter1(this#TestIndex, R.layout.row, test_arr)
binding!!.gvTest.adapter = adapter
adapter!!.notifyDataSetChanged()
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Log.d(Keys.KEY_TAG, "onFailure: " + t.localizedMessage)
}
})
if (mProgressDialog.isShowing) mProgressDialog.dismiss()
}
//subscribed the Observers here from view model
private fun subscribeObservers() {
viewModel!!.lifting.observe(this, { TestModel: List<TestModel>? ->
adapter!!.updateTests(TestModel)
binding!!.swipeRefreshLayout.isRefreshing = false
}
}
Kindly let me know how can I do the same using jetpack compose for listview, grid view, recycler view. Thanks.
It's more a general example, without retrofit. You can implement your data fetch inside my getTestData method.
To begin with, in order to understand the basic principles of working with Compose, I suggest you study compose tutorials.
Compose uses view models to perform complex data manipulations. I will use the basic version, but you can also check out Hilt for more complex architecture.
In order for changing the state of an object to lead to a recomposition, you can use:
The mutableStateObject - this is a specially created container for compose that will update the view if the value has changed
you can also use LiveData and Flow, they can both be cast to mutableStateObject.
Note that mutableStateObject will not alert you to changes in the container object fields if you pass a complex class there. It will only notify you when the value itself changes, so it is recommended to use it only for simple types.
You can also use mutableStateListOf to store collections. In my example you will see both: with mutableStateListOf it is convenient to add/delete objects to the collection, while mutableStateObject with List lying inside is easier to completely replace with new objects.
Inside Composable functions you need to wrap your mutable state objects with remember to prevent reinitializing them on each composition, and inside your view model you don't need to do that, because it's not gonna be reinitialized in any case.
SwipeRefresh is not part of compose, it's a library made by compose maintainers too. To install it follow this instructions.
I'm using two columns here just to show difference between mutableStateOf and mutableStateListOf, you can remove Row and one of LazyColumn
class ScreenViewModel : ViewModel() {
var list by mutableStateOf(emptyList<String>())
var mutableList = mutableStateListOf<String>()
var isRefreshing by mutableStateOf(false)
init {
refresh()
}
fun refresh() {
isRefreshing = true
viewModelScope.launch {
list = getTestData()
mutableList.addAll(0, list)
isRefreshing = false
}
}
suspend fun getTestData(): List<String> {
// emulate network call
delay(1000)
return List(100) {
Random.nextInt(100).toString()
}
}
}
#Composable
fun TestView() {
val viewModel: ScreenViewModel = viewModel()
SwipeRefresh(
state = rememberSwipeRefreshState(viewModel.isRefreshing),
onRefresh = {
viewModel.refresh()
},
) {
Row {
LazyColumn(
modifier = Modifier.weight(1f) // this needed inly for two columns case
) {
itemsIndexed(viewModel.list) { i, item ->
Text("$i $item")
}
}
LazyColumn(
modifier = Modifier.weight(1f) // this needed inly for two columns case
) {
itemsIndexed(viewModel.mutableList) { i, item ->
Text("$i $item")
}
}
}
}
}
Result: