Jetpack Compose + Navigation: rememberSaveable loses state on rotate - android-jetpack-compose

I've encountered a strange behavior with Jetpack Compose in combination with Navigation: If you use rememberSaveable inside some navigation composable, then the state is not saved as promised (e.g. it is lost after rotation). Here is a simple example:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
MyScreen()
}
}
}
}
#Composable
fun MyScreen() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "xyz") {
composable("xyz") {
var value by rememberSaveable { mutableStateOf("") }
TextField(value = value, onValueChange = { value = it })
}
}
}
The above code produces a single text field in which one can type. Once one rotates the screen, the typed text is lost, even though the value should be saved by rememberSaveable.
Investigating a little bit, I noticed the following:
The problem is really the NavHost. If one moves the line defining the variable "value" to the top of "MyScreen()" (outside of the NavHost) then everything works as intended.
The real issue seems to be that the composable variable "currentCompositeKeyHash" is not retained after configuration changes inside the NavHost. This variable is used as a key for the savedInstanceBundle to retrieve the saved value by rememberSaveable, thus the state gets lost. In particular, if one explicitly specifies a key in rememberSaveable, then everything works as expected.
Is this a bug or am I misunderstanding something?

Update:
Version 2.4.0-alpha07 has been released and fixes the issue:
implementation "androidx.navigation:navigation-compose:2.4.0-alpha07"
Original answer:
This is a known issue with version 2.4.0-alpha05 and 2.4.0-alpha06 of androidx.navigation:navigation-compose. The current solution is to downgrade to 2.4.0-alpha04:
implementation "androidx.navigation:navigation-compose:2.4.0-alpha04"
According to the issue tracker, the issue has been fixed in version 2.4.0-alpha07, which hopefully will be released soon.

Related

Updating a #State var in SwiftUI from an async method doesn't work

I have the following View:
struct ContentView: View {
#State var data = [SomeClass]()
var body: some View {
List(data, id: \.self) { item in
Text(item.someText)
}
}
func fetchDataSync() {
Task.detached {
await fetchData()
}
}
#MainActor
func fetchData() async {
let data = await SomeService.getAll()
self.data = data
print(data.first?.someProperty)
// > Optional(115)
print(self.data.first?.someProperty)
// > Optional(101)
}
}
now the method fetchDataSync is a delegate that gets called in a sync context whenever there is new data. I've noticed that the views don't change so I've added the printouts. You can see the printed values, which differ. How is this possible? I'm in a MainActor, and I even tried detaching the task. Didn't help. Is this a bug?
It should be mentioned that the objects returned by getAll are created inside that method and not given to any other part of the code. Since they are class objects, the value might be changed from elsewhere, but if so both references should still be the same and not produce different output.
My theory is that for some reason the state just stays unchanged. Am I doing something wrong?
Okay, wow, luckily I ran into the Duplicate keys of type SomeClass were found in a Dictionary crash. That lead me to realize that SwiftUI is doing some fancy diffing stuff, and using the == operator of my class.
The operator wasn't used for actual equality in my code, but rather for just comparing a single field that I used in a NavigationStack. Lesson learned. Don't ever implement == if it doesn't signify true equality or you might run into really odd bugs later.

NSKeyValueCoding.setValue(_:forKey:) not working in SwiftUI

I am unable to produce a proper minimal working example, mainly due to my novice level understanding of iOS development, but I do have a simple SwiftUI project that may help.
In my ContentView.swift:
import SwiftUI
struct ContentView: View {
#State var viewText :String
var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject {
var txt :String = ""
var useSetVal :Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
and in my PracticeApp.swift
import SwiftUI
#main
struct PracticeApp: App {
var body: some Scene {
WindowGroup {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
}
In this app, I expect to see the text toggle between "used =" and "used setVal" as I push the button. Instead, I get an exception when I call setValue:
Thread 1: "[<Practice.MyClass 0x60000259dc20> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key txt."
I've been reviewing the answers here but since most answers refer to xib and storyboard files (which I either don't have, or don't know how to find), I don't see how they relate.
By the way, even though the app I'm actually trying to fix doesn't use SwiftUI and the issue with setValue is different, it's still true that I either don't have .xib or .storyboard files or I just don't know where to find them.
I'd appreciate help from any one who could either help me figure out the issue with my example, or who can get me closer to solving the issue with my actual app (including how to produce a proper MWE).
I believe what I've already written is sufficient for the issue (at least for a start), but for those interested, I thought I'd add the full story.
The Full Story
I'm new to iOS development, and I've just taken ownership of an old iOS app. It hasn't really been touched since 2017. I noticed an animation that is not working. Though I cannot verify that it ever did work, I have good reason to assume that it once did, but I can't say when it stopped working.
One issue I noticed is that animated properties are supposed to be updated with the NSKeyValueCoding.setValue(_:forKey:) function, but nothing seems to happen when the function is called.
I was able to work around the issue by overriding the setValue function with my own which basically uses a switch statement to map each key to its corresponding value. However, this did not fix the animation or explain why the setValue function isn't working.
Because both the setValue function and the CABasicAnimation.add(_:forKey:) rely on the same keyPath, I wonder if solving one issue might help me solve the other. I've decided to focus on the setValue issue (at least for now).
When I went to work starting a new project to use as an MWE, I noticed that neither the Storyboard nor the SwiftUI interface options provided by Xcode 13.0 (13A233) started me out with a project structure that matched my existing project. It was clear to me that SwiftUI was new and very different from my existing project, but the Storyboard interface wasn't familiar either and after several minutes a reading tutorials, I failed to build a storyboard app that would respond to button presses at all (all the storyboard app tutorials I found seemed to be set up for older versions of Xcode).
SwiftUI will require that you use #ObservedObject to react to changes in an object. You can make this compliant with both observedobject and key-value manipulation as follows:
struct ContentView: View {
#State var viewText :String
#ObservedObject var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject, ObservableObject {
#objc dynamic var txt: String = ""
#Published var useSetVal: Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
You need to make the txt property available to Objective-C, in order to make it work with KVO/KVC. This is required as the Key-Value Observing/Coding mechanism is an Objective-C feature.
So, either
class MyClass: NSObject {
#objc var txt: String = ""
, or
#objcMembers class MyClass: NSObject {
var txt: String = ""
This will fix the error, and should make your app behave as expected. However, as others have said, you need to make more changes to the code in order to adhere to the SwiftUI paradigms.

Compose java.lang.IllegalStateException: pending composition has not been applied

I just want to run the simple test
class Exa {
#get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
// createComposeRule() if you don't need access to the activityTestRule
#Test
fun MyTest() {
// Start the app
composeTestRule.setContent {
Greeting2("Nurs")
}
composeTestRule.onNodeWithText("Hello Nurs!").assertIsDisplayed()
}
}
#Composable
fun Greeting2(name: String) {
Text(text = "Hello $name!")
}
but it gives me the following error:
java.lang.IllegalStateException: pending composition has not been applied
the strange thing that if I run it in another project it works
Wanted to add some detail for anyone who comes here from Google. The exception listed in the title:
java.lang.IllegalStateException: pending composition has not been applied
is thrown if a Composable throws an exception while composing or recomposing the view. In the case that the original poster was noticing, the exception was likely thrown because the activity itself was not able to host the composition, but other code may cause this if an exception is thrown within a composable method.
This happend to me when I put images to my drawable folder from the outside. In a release variant of app I had the same exception, but not in debug one. I just cleaned project(Build -> Clean Project) and everything worked fine.
If you are using Canvas Composable then give it some size. I was facing the same issue which was resolved after setting a fixed size.
Canvas(modifier = Modifier.size(400.dp)) {
...
}
You may use fillMaxWidth() or something similar.
From Documentation :
Canvas Composable is a component that allow you to specify an area on the screen and perform canvas drawing on this area. You MUST specify size with modifier, whether with exact sizes via Modifier.size modifier, or relative to parent, via Modifier.fillMaxSize, ColumnScope.weight, etc. If parent wraps this child, only exact sizes must be specified.
the problem was in MainActivity:
class MainActivity3 :
ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}
#Composable
fun Greeting(name: String)
you have to use clear activity without any dependency

How can I draw a image loaded from an asset on a Canvas object

I'm toying with Flutter and I have implemented a custom-drawn widget using CustomPainter class.
Primitives are quite well documented. However, it seems that there's Image (the widget) and Image (the data structure), because if I istantiate an Image (the widget) I cannot pass it to the Canvas method drawImage because it needs another type of Image.
Problem is, I cannot wrap my head on how to load a resource into an Image data structure.
Has anyone tackled this problem?
[edit] Thanks to rmtmckenzie I solved it this way:
rootBundle.load("assets/galaxy.jpg").then( (bd) {
Uint8List lst = new Uint8List.view(bd.buffer);
UI.instantiateImageCodec(lst).then( (codec) {
codec.getNextFrame().then(
(frameInfo) {
bkImage = frameInfo.image;
print ("bkImage instantiated: $bkImage");
}
}
);
});
});
A working solution:
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/services.dart' show rootBundle;
/// Load [Image] from asset path.
/// https://stackoverflow.com/a/61338308/1321917
Future<ui.Image> loadUiImage(String assetPath) async {
final data = await rootBundle.load(assetPath);
final list = Uint8List.view(data.buffer);
final completer = Completer<ui.Image>();
ui.decodeImageFromList(list, completer.complete);
return completer.future;
}
To do this, you need to load the image directly, and then instantiate it.
I'll leave the loading part up to you - you're either going to have to do an Http request directly or use AssetBundle.load; or you could possibly use a NetworkImage/AssetImage (which both inherit ImageProvider - see the docs which contain an example).
If you take the loading directly route, you can then instantiate an image (in the form of a codec -> frame -> image) using instantiateImageCodec.
If you take the other route, you have to listen to streams etc as it is done in the docs, but you should get an Image directly.
Edit:
Thanks to the OP who has included the working code in his question. Here is what worked for him:
rootBundle.load("assets/galaxy.jpg").then( (bd) {
Uint8List lst = new Uint8List.view(bd.buffer);
UI.instantiateImageCodec(lst).then( (codec) {
codec.getNextFrame().then(
(frameInfo) {
bkImage = frameInfo.image;
print ("bkImage instantiated: $bkImage");
}
}
);
});
});

toObservable doesn't seem to be working

I'm using Web UI to do observable data binding. Here is the brief snippet of code I'm working with:
import 'dart:html';
import 'dart:json';
import 'package:web_ui/web_ui.dart';
import 'package:admin_front_end/admin_front_end.dart';
//var properties = toObservable(new List<Property>()..add(new Property(1, new Address('','','','','',''))));
var properties = toObservable(new List<Property>());
void main() {
HttpRequest.request('http://localhost:26780/api/properties', requestHeaders: {'Accept' : 'application/json'})
.then((HttpRequest req){
final jsonObjects = parse(req.responseText);
for(final obj in jsonObjects){
properties.add(new Property.fromJsonObject(obj));
}
});
}
In index.html, I bind properties to it's respective property in the template:
<div is="x-property-table" id="property_table" properties="{{properties}}"></div>
In the first snippet of code, I'm populating the observable properties list, but it never reflects in the UI (I've stepped through the code and made sure elements were in-fact being added). If I pre-populate the list (see the commented out line), it does display, so the binding is at least working properly. Am I doing something wrong here?
The problem is most likely that you don't have any variables or types marked as #observable. In lack of observables, Web UI relies on call to watchers.dispatch() in order to update GUI.
You have following options:
1) import watchers library and call dispatch() explicitly:
import 'package:web_ui/watcher.dart' as watchers;
...
void main() {
HttpRequest.request(...)
.then((HttpRequest req){
for(...) { properties.add(new Property.fromJsonObject(obj)); }
watchers.dispatch(); // <-- update observers
});
}
2) mark any field of your x-property-table component as observable, or just the component type, e.g.:
#observable // <-- this alone should be enough
class PropertyTable extends WebComponent {
// as an alternative, mark property list (or any other field) as observable.
#observable
var properties = ...;
NOTE:
when a collection is marked #observable, UI elements bound to the collection are updated only when the collection object itself is changed (item added, removed, reordered), not when its contents are changed (e.g. an object in the list has some property modified). However, as your original properties list is an ObservableList, #observable annotation only serves here as a way to turn on the observable mechanism. Changes to the list are queued as a part of ObservableList implementation.
I think solution 2 (#observable) is better. As far as I know, watchers is the old way to track changes and will probably be removed.

Resources