observeAsState() not surviving navigating between apps - with "Don't keep activities" on - android-jetpack-compose

I am experimenting with Jetpack Compose. I'm used to set the Developer Option Don't keep Activities to test whether my app still shows data after leaving and entering the app. Currently i have composable function which shows an image retrieved from a piece of LiveData in the view model. Via the following code in the Composable i retrieve this image
#Composable
fun MovieDetailScreen(viewModel: MoviesViewModel = viewModel()
) {
val selectedMovie by viewModel.selectedMovie.observeAsState()
Row(
modifier = Modifier
.fillMaxWidth()
.height(260.dp)
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(selectedMovie?.getBackdropUrl())
.placeholder(R.drawable.ic_baseline_movie_24)
.crossfade(true)
.memoryCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = stringResource(R.string.app_name),
contentScale = ContentScale.Crop,
)
}
}
Basically when I navigate to another app and then navigate back the image is gone. observeAsState() values don't survive Don't keep activities.
So my question is how can I let this survive configuration changes?

Related

Access Context & SurfaceView from ViewModel

I'm having trouble complying with the MVVM with Jetpack Compose, because my ViewModel needs to access a view.
I need to stream a camera source onto a SurfaceView. However, this source has an independant lifecycle : it can be plugged in or removed anytime, while the view is visible or not.
To start streaming, I need to provide the SurfaceView to an external SDK. How can I do that without breaking the MVVVM?
I want to start streaming :
whenever the view appears (first composition), if the source is available
whenever the source becomes available, if the view is visible
What I've done so far is to inject the SurfaceView into the ViewModel
class CameraVM: ViewModel() {
lateinit var surfaceView: SurfaceView
init {
listenForStreamSourceAvailability()
}
// Called when source becomes available
fun startStreaming() {
camera.setupStreaming(surfaceView)
camera.startStreaming()
}
And in CameraView
#Composable
fun CameraView(viewModel: CameraVM = viewModel()) {
val context = LocalContext.current
val localSurfaceView: SurfaceView = remember {
val surfaceView = SurfaceView(context)
// Inject surface view only at first composition
viewModel.surfaceView = surfaceView
surfaceView
}
AndroidView(
factory = {
localSurfaceView
}
)
}
Maybe DisposableEffect could be useful ?

How does this function keep track of clicks?

On the "Thinking in Compose" page I don't get this code, how does $clicks keep track of number of clicks?
#Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
I'm learning Kotlin at the same time as Compose, so I get puzzled all the time.
It's omitted in that example but it should store the click-count in a MutableState<T> wrapped with remember
var clickCount by remember { mutableStateOf(0)}
ClickCounter(clicks = clickCount, onClick = {clickCount += it})
For real real real beginner like me, let me add my comment and code.
When the ClickCounter composable is called we need to pass two parameters.
To 'clicks', we pass initial value 0 which will be changed everytime we click the button. Also the initial value need to be 'remembered' and tracked for further changes. So we create new variable using 'mutableStateOf' function inside 'remember' function.
To 'onClick', we need to pass function which adds 1 everytime the button is clicked.
As a caller of the ClickCounter composable, I made a simple preview composable as below.
#Preview(showBackground = true)
#Composable
fun ClickCounterPreview(){
var clicks by remember { mutableStateOf(0) }
// We call ClickCounter composable passing two parameters.
ClickCounter(clicks = clicks, onClick = { clicks += 1 })
}

How to get full list of available events for the Android Jetpack Compose Material TextField (seeking event for Enter of virtual numeric keyboard)?

TextField is Android Jetpack Compose Material component which (as part of Material suite) is described in https://material.io/components/text-fields/android and whose reference is on page https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary (on should use right-side menu of components to position this fairly large page at the right place). While this documentation is good and it explaids modifier and keyboardOptions (e.g.), it does not provide list of events, e.g., there is no documentation about onValueChange, see the following code which uses very varied events from different structures:
TextField(
value = uiState.scannedAmount.toString(),
onValueChange = { orderViewModel.handleScannedAmountString(it) },
//keyboardActions = KeyboardActions(onDone = { orderViewModel.handleScannedAmountDone() }),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
modifier = Modifier.focusRequester(focusRequester)
.onKeyEvent { if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER){
orderItemViewModel.handleScannedAmountDone()
true
} else {
orderItemViewModel.handleKeyEvent(it.nativeKeyEvent.keyCode.toString())
}
false }
)
So, how can I know that TextField has onValueChange event and what else events it can have?
Specifically - my TextField is trying to listen to and to catch Enter events from the virtual keybord with keyboardType = KeyboardType.Number. The device has Android 8. I can not catch this event using the above code. However this code catched Enter from virtual keyboard that is displayed on emulator. So, I hope that there may be some on... that hears Enter from virtual numeric keyboard on real (Android 8) device. This is motivation, but the original question is just about method to see all the available events.

How do I make rememberSaveable work inside movableContentOf?

I made two similar navigation #Composables with a slot for content, one of them is used depending on the screen size class. I also use rememberSaveable inside the content to handle screen rotation. The problem is, without movableContentOf, their state is handled as separate, but with it, the saved state is gone after every screen rotation. A simplified example:
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val content = remember {
movableContentOf {
var a by remember { mutableStateOf(false) }
Checkbox(a, { a = it })
var b by rememberSaveable { mutableStateOf(false) }
Checkbox(b, { b = it })
}
}
var switch by rememberSaveable { mutableStateOf(false) }
Checkbox(switch, { switch = it })
if (switch) Row { content() } else Column { content() }
}
Mode switching works just fine, but only the upper checkbox keeps its state after screen rotation.
This is intended behaviour: you create movableContentOf inside remember, which means it's gonna be recreated on rotation.
You can store it inside a view model, but if you need to do so, you can store your data there as well, which makes storing movableContentOf redundant: in this case, re-creating it in remember is perfectly fine.

SwiftUI DocumentGroup and switching to background

I have made an app based on the new SwiftUI multi platform target for a "Document based app".
However, I face weird issues. As long as an app is in the foreground, it works just fine. If it is moved to the background by task switching, and then again to the foreground, mutations are being saved to the document, but the SwiftUI Views don't receive mutations. So whenever you press a button in the UI that mutates the document you see nothing happening while the mutation is there once you reload the document from disk.
So i am thinking, I use ObservedObjects, they probably get kicked out of memory once I move to the background. could this be the cause of my bug?
But then I added a print line to the App struct.
import SwiftUI
#main
struct MyApp: App {
fileprivate func myLogging(_ file: FileDocumentConfiguration<MyDocument>) -> some View {
print("""
IT IS CALLED
""")
return MainView().environmentObject(BindingWrapper(file.$document))
}
var body: some Scene {
DocumentGroup(newDocument: MyDocument()) { (file) in
return myLogging(file)
}.commands { AppCommands() }
}
}
and guess what... this print always executes just before a mutation is being rendered. Which makes sense. because file.$document is a binding, and if you do a mutating action, the binding will warn Apple that the file is dirty, but it will also invalidate the entire hierarchy. This logging will still print once the bug has occurred!
So on the line MainView().environmentObject(BindingWrapper(file.$document)) I assume everything is created from scratch. BindingWrapper is a custom class I made to convert a binding in an observable object. And this is one of the objects I worried about, that they might be freed. but if they are created newly.... they should be always there, right?
And by the way, this object is owned by the environment. So it should not be freed.
So, now I am stuck. is Apple doing some clever caching on bindings / ObservedObjects which will inject old objects into my view hierarchy even though I think everything is created newly?
Try moving any wiring/instantiation to the first view of the document group. If that view houses StateObjects you expect to share the lifetime of the document window, they will not be rebuilt.
In the example below, a WindowStore is housed as an #StateObject as described. A RootStore housed in App creates the WindowStore, which includes vending services and registering it in a managed array of windows. Either could enable your logging service. (For me, that array helps WindowGroups operate on a specific document when #FocusedValue would fail (i.e., the top-most document is no longer the key window).)
#main
struct ReferenceFileDoc: App {
#StateObject var root: RootStore
var body: some Scene {
DocumentGroup { ProjectDocument() } editor: { doc in
DocumentGroupRoot(
window: root.makeWindowStore(doc.document),
factory: SwiftUIFactory(root, doc.document)
)
.environmentObject(doc.document)
.environment(\.documentURL, doc.fileURL)
.injectStores(from: root)
}.commands { Menus(root: root) }
.... other scenes ...
struct DocumentGroupRoot: View {
#EnvironmentObject var doc: ProjectDocument
#Environment(\.undoManager) var undoManager
#Environment(\.documentURL) var url
#StateObject var window: WindowStore
#StateObject var factory: UIFactory
var body: some View {
passUndoManagerToDocument()
factory.reference(window)
return DocumentWindow(vm: factory.makeThisVM()) // Actual visible window
.focusedValue(\.keyWindow, window)
.focusedValue(\.keyDocument, doc)
.onAppear { /// Tasks }
.reportHostingNSWindow { [weak window] in
window?.setWindow($0)
}
.onChange(of: url) { [weak window] in window?.setFileURL($0) }
.environmentObject(/// sub-state stores from WindowStore)
.environmentObject(window)
.environmentObject(factory)
}
}

Resources