Jetpack Compose: Why initially loaded data is reloaded after screen rotation - android

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()
}
}
}

Related

Value is remembered without using remember

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.

How to create list view/grid view/recycler view with retrofit API in jetpack compose

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:

how to save my image while changing the orientation of my phone in androidStudio using Kotlin

how to save the image while changing the orientation of my android app And this is basically a simple roll dice app i.e. base on the random number and on the basis of this we can easily choose image using when() in kotlin
package com.example.rollingdice
import android.location.Address
import android.media.Image
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.PersistableBundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import javax.crypto.KeyGenerator
import kotlin.properties.Delegates
import kotlin.random.Random
abstract class MainActivity : AppCompatActivity() {
lateinit var showingResult:ImageView
lateinit var diceButton:Button
lateinit var result
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
diceButton=findViewById(R.id.rollDice)
showingResult=findViewById(R.id.iv)
diceButton.setOnClickListener {
rollingDice()
}
}
// accordin to me this function is use to save the data or image while changing the
orientation
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
outState.putInt("img",result)
}
//private function named as rollingDice()
private fun rollingDice() {
var number : Int= Random.nextInt(6)+1
// taking random number input in when()
when (number){
// taking number in when() statement to choose the image
1 -> R.drawable.dice_1
2 -> R.drawable.dice_2
3 -> R.drawable.dice_3
4 -> R.drawable.dice_4
5 -> R.drawable.dice_5
else -> R.drawable.dice_6
// else condition give the dice_6 name image as a result
}
showingResult.setImageResource(result)
//i'm taking the result as aimage in the image view
}
}

ViewModel doesn´t retains data when screen rotation

I am putting myself in a RecycleView from Java to Kotlin. I am trying to keep state between screen rotations. But it is not working. Here my code:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
//import androidx.lifecycle.ViewModelProviders
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: ScoreViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider.NewInstanceFactory().create(ScoreViewModel::class.java)
//viewModel = ViewModelProviders.of(this).get(ScoreViewModel::class.java)
println("-> "+viewModel.scoreTeamA);
}
fun addOneForTeamA(view: View) {
viewModel.scoreTeamA++
displayForTeamA()
}
fun addTwoForTeamA(view: View) {
viewModel.scoreTeamA += 2
displayForTeamA()
}
fun addThreeForTeamA(view: View) {
viewModel.scoreTeamA += 3
displayForTeamA()
}
fun addOneForTeamB(view: View) {
++viewModel.scoreTeamB
displayForTeamB()
}
fun addTwoForTeamB(view: View) {
viewModel.scoreTeamB += 2
displayForTeamB()
}
fun addThreeForTeamB(view: View) {
viewModel.scoreTeamB += 3
displayForTeamB()
}
fun displayForTeamA(){
team_a_score.text = viewModel.scoreTeamA.toString()
}
fun displayForTeamB(){
team_b_score.text = viewModel.scoreTeamB.toString()
}
fun resetScore(view: View) {
viewModel.scoreTeamA = 0
viewModel.scoreTeamB = 0
displayForTeamA()
displayForTeamB()
}
}
import androidx.lifecycle.ViewModel
class ScoreViewModel : ViewModel() {
var scoreTeamA: Int = 0
var scoreTeamB: Int = 0
}
Another question is why in one project ViewModelProviders.of appears as deprecated (se commented lines) and in another project with similar gradle dependencies it is OK.
Thanks in advance!
Using ViewModelProvider.NewInstanceFactory().create is going to create a new instance each time - this is never the right thing to use. Instead, follow the ViewModelProviders.of() deprecation message:
Use the by viewModels() Kotlin property delegate or ViewModelProvider(ViewModelStoreOwner), passing in the activity.
viewModel = new ViewModelProvider(this).get(ScoreViewModel::class.java)

viewModel in Kotlin Android

I'm trying to understand view models using kotlin for android, and I'm running into some difficulty. I have a very simple dummy app which allows a user to increment a number and then send that number to a second screen. That second screen will then display a random number between 0 and the sent number.
Here is the problem.
I understand how to send the data from the first page to the second using intents, and I know how to make a viewmodel in the second page. However, if I send the intent and then set the viewmodel equal to the set intent, it doesnt function properly. Rotating the screen will cause the intent to be resent and the viewmodel doesnt maintain the state of the data (the number rerandomizes).
Ideally, I'd like to just be able to just update the viewModel class in place of sending the intent, but the instance of the class is created when the second page is created, so that doesn't work.
Any ideas?
Based on the google codelabs "build my first android app" tutorial.
Here's my code; first page:
package com.example.patientplatypus.babbysfirstandroidapp
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.textView
import org.jetbrains.anko.db.PRIMARY_KEY
import org.jetbrains.anko.db.UNIQUE
import org.jetbrains.anko.db.createTable
import android.database.sqlite.SQLiteDatabase
import android.support.v4.content.ContextCompat.startActivity
import com.example.patientplatypus.babbysfirstandroidapp.R.id.textView
import org.jetbrains.anko.db.*
import org.jetbrains.anko.indeterminateProgressDialog
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main);
}
fun toastMe(view: View) {
val myToast = Toast.makeText(this, "Hello Toast!", Toast.LENGTH_SHORT)
myToast.show()
}
fun countMe (view: View) {
Log.d("insideCountMeCheck", "hey you are inside count me!")
val countString = textView.text.toString()
var count: Int = Integer.parseInt(countString)
count++
textView.text = count.toString()
}
fun randomMe (view: View) {
val randomIntent = Intent(this, SecondActivity::class.java)
val countString = textView.text.toString()
val count = Integer.parseInt(countString)
randomIntent.putExtra(SecondActivity.TOTAL_COUNT, count.toString())
startActivity(randomIntent)
}
}
Here's my code, second page:
package com.example.patientplatypus.babbysfirstandroidapp
import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProviders
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.Toast
import java.util.*
import kotlinx.android.synthetic.main.activity_second.randomText
class CountViewModel : ViewModel() {
var TOTAL_COUNT = "total_count"
}
class SecondActivity : AppCompatActivity() {
lateinit var countModel: CountViewModel
companion object {
const val TOTAL_COUNT = "total_count"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.setContentView(R.layout.activity_second)
countModel = ViewModelProviders.of(this).get(CountViewModel::class.java)
countModel.TOTAL_COUNT = intent.getStringExtra(TOTAL_COUNT)
displayForRandomNum(countModel.TOTAL_COUNT);
showRandomNumber()
}
fun showRandomNumber() {
val count = countModel.TOTAL_COUNT.toInt()
val random = Random()
var randomInt = 0
if (count > 0) {
randomInt = random.nextInt(count + 1)
}
Log.d("randomFinal", Integer.toString(randomInt))
displayForRandomNum(Integer.toString(randomInt))
}
fun displayForRandomNum(totalCount: String){
randomText.text = totalCount
}
}
An orientation change causes the activity be destroyed then recreated. This means onCreate will be called on every rotate. The same intent that started the activity originally would still be available to it. So intent.getStringExtra(TOTAL_COUNT) would return the original value from the intent that start the activity every time the screen is rotated. ViewModel will retain the data through the rotation though.
Your issue is that your overriding you're ViewModel'sTOTAL_COUNT with the original value from the intent every time. What you can do is check that the TOTAL_COUNT value isn't "total_count" (meaning its already been set from the intent) before setting it in onCreate.

Categories

Resources