Unable to trigger re-composition after re-assigning mutableStateList inside a coroutine scope - kotlin

I'm still a bit of a beginner with Jetpack compose and understanding how re-composition works.
So I have a piece of code calls below inside a ViewModel.
SnapshotStateList
var mutableStateTodoList = mutableStateListOf<TodoModel>()
private set
during construction of the view model, I execute a room database call
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
mutableStateTodoList = listTypeTodo.toMutableStateList()
}
}
}
then I have an action from a the ui that triggers adding of a new Todo to the list and expecting a re-composition from the ui that shows a card composable
fun onFabClick() {
todoList.add(TodoModel())
}
I can't figure out why it doesn't trigger re-composition.
However if I modify the init code block below, and invoke the onFabClick() action, it triggers re-composition
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
mutableStateTodoList.addAll(listTypeTodo)
}
}
}
or this, taking out the re-assigning of the mutableStateList outside of the coroutine scope also works (triggers re-composition).
init {
// just trying to test a re-assigning of the mutableStateList property
mutableStateTodoList = emptyList<TodoModel>().toMutableStateList()
}
Not quite sure where the problem if it is within the context of coroutine or SnapshotStateList itself.
Everything is also working as expected when the code was implemented this way below, using standard list inside a wrapper and performing a copy (creating new reference) and re-assigning the list inside the wrapper.
var todoStateWrapper by mutableStateOf<TodoStateWrapper>(TodoStateWrapper)
private set
Same init{...} call
init {
viewModelScope.launch {
fetchTodoUseCase.execute()
.collect { listTypeTodo ->
todoStateWrapper = todoStateWrapper.copy (
todoList = listTypeTodo
)
}
}
}
To summarize, inside a coroutine scope, why this works
// mutableStateList
todoList.addAll(it)
while this one does not?
// mutableStateList
todoList = it.toMutableStateList()
also why does ordinary list inside a wrapper and doing copy() works?

The mutable state in Compose can only keep track of updates to the containing value. Here is simplified code on how MutableState could be implemented:
class MutableState<T>(initial: T) {
private var _value: T = initial
private var listeners = MutableList<Listener>
var value: T
get() = _value
set(value) {
if (value != _value) {
_value = value
listeners.forEach {
it.triggerRecomposition()
}
}
}
fun addListener(listener: Listener) {
listeners.add(listener)
}
}
When the state is used by some view, this view subscribes to updates of this particular state.
So, if you declare the property as follows:
var state = MutableState(1)
and try to update it with state = 2.toMutableState() (this is analogous to your mutableStateTodoList = listTypeTodo.toMutableStateList()), triggerRecomposition cannot be called because you create a new object which resets all the listeners. Instead, to trigger recomposition you should update it with state.value = 2.
With mutableStateList, the analog of updating value is any method of MutableList interface that updates containing list, including addAll.
Inside init it works because no view is subscribed to this state so far, and that's the only place where methods such as toMutableStateList should be used.
It is important to always define mutable states as immutable property with val in order to prevent such mistakes. To make it mutable only from view model, you can define it like this, and make updates on _mutableStateTodoList:
private val _mutableStateTodoList = mutableStateListOf<TodoModel>()
val mutableStateTodoList: List<TodoModel> = _mutableStateTodoList
The only exception when you can use var is using mutableStateOf with delegation - this is where you can use it with private set because in that case the delegation does the work for you by not modifying the container, but only it's value property. Such method cannot be applied to mutableStateListOf, because there's no single value field that's responsible for the data in case of list.
var someValue by mutableStateOf(1)
private set

Related

How to Observe LiveData with custom pair class in Kotlin

I am trying to observe the LiveData for the method which returns custom pair having 2 values. I want the observable to be triggered when I change either one of the values. But it is not getting triggered. Following is the code:
CustomPair.kt
data class CustomPair<T, V>(
var first : T,
var second : V
)
Observable:
falconViewModel.getPlanet1Name().observe(this) {
planet1.text = it.first
planet1.isEnabled = it.second
}
Getter and setter methods in ViewModel falconViewModel
private val planet1EnabledAndText = MutableLiveData<CustomPair<String, Boolean>>()
fun getPlanet1Name() : LiveData<CustomPair<String, Boolean>> {
return planet1EnabledAndText
}
fun setPlanet1Name(planetName : String, visibility : Boolean) {
planet1EnabledAndText.value?.run {
first = planetName
second = visibility
}
}
Can't we observe the value in such case? Please help what is wrong here.
It started working when I tried to set a new value of CustomPair instead of modifying the existing values in the object.
Replaced
planet1EnabledAndText.value = CustomPair(planetName, visibility)
with
planet1EnabledAndText.value?.run {
first = planetName
second = visibility
}
and it worked.

problem with first state of application state in redux kotlin

I'm using Redux and somewhere in application I need to check if returning data is not null, then run a function.
as you know in redux we should initialize the states like this:
data class ApplicationState(
val movieByGenre : List<DomainMovieModel> = emptyList()
)
the problem is when I check the data ,at the very beginning level, data has been set to empty list, and it satisfies the IF statement
by the way here is the implementation inside view model
fun getMovieByGenre(genreId: Int){
viewModelScope.launch {
val response=repository.getMovieByGenre(genreId)
store.update { currentState->
return#update currentState.copy(movieByGenre = response)
}
}
}
and here is the implementation in fragment:
viewModel.store.stateFlow.map { it.movieByGenre }.distinctUntilChanged().asLiveData().observe(viewLifecycleOwner){
if (it.isEmpty() ){
....
}else{
.....
}
}

Can I use State<ArrayList<T>> or State<mutableListOf()> for observed by Compose to trigger recomposition when they change?

The following content is from the article.
1: I don't understand fully if I can use State<ArrayList<T>> or State<mutableListOf()> for observed by Compose to trigger recomposition when they change?
2: I'm very strange why State<List<T>> and the immutable listOf() can be observed by Compose to trigger recomposition when they change but in fact List<T> and immutable listOf() are immutable, could you give me some sample codes?
Caution: Using mutable objects such as ArrayList or mutableListOf() as state in Compose will cause your users to see incorrect or stale data in your app.
Mutable objects that are not observable, such as ArrayList or a mutable data class, cannot be observed by Compose to trigger recomposition when they change.
Instead of using
non-observable mutable objects, we recommend you use an observable
data holder such as State<List> and the immutable listOf().
Image
The core concept is
Recomposition happens only when an observable state change happens.
For mutable objects, we have options to use add(), remove() and other methods and modify the object directly.
But the change would not trigger a recomposition as the change is not observable. (The object instance is NOT changed)
Even for mutable objects, we can trigger proper recomposition by assigning them to a new object instance. (The object instance is changed)
Hence using mutable objects is error-prone.
We can also, see a lint error due to this problem.
On the other hand, an immutable object like list can not be modified. They are replaced with a new object instance.
Hence they are observable and proper recomposition happens. (The object instance is changed)
Use this as an example to understand the concept.
#Composable
fun ComposeListExample() {
var mutableList: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var mutableList1: MutableState<MutableList<String>> = remember {
mutableStateOf(mutableListOf())
}
var arrayList: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var arrayList1: MutableState<ArrayList<String>> = remember {
mutableStateOf(ArrayList())
}
var list: MutableState<List<String>> = remember {
mutableStateOf(listOf())
}
Column(
Modifier.verticalScroll(state = rememberScrollState())
) {
// Uncomment the below 5 methods one by one to understand how they work.
// Don't uncomment multiple methods and check.
// ShowListItems("MutableList", mutableList.value)
// ShowListItems("Working MutableList", mutableList1.value)
// ShowListItems("ArrayList", arrayList.value)
// ShowListItems("Working ArrayList", arrayList1.value)
// ShowListItems("List", list.value)
Button(
onClick = {
mutableList.value.add("")
arrayList.value.add("")
val newMutableList1 = mutableListOf<String>()
mutableList1.value.forEach {
newMutableList1.add(it)
}
newMutableList1.add("")
mutableList1.value = newMutableList1
val newArrayList1 = arrayListOf<String>()
arrayList1.value.forEach {
newArrayList1.add(it)
}
newArrayList1.add("")
arrayList1.value = newArrayList1
val newList = mutableListOf<String>()
list.value.forEach {
newList.add(it)
}
newList.add("")
list.value = newList
},
) {
Text(text = "Add")
}
}
}
#Composable
private fun ShowListItems(title: String, list: List<String>) {
Text(title)
Column {
repeat(list.size) {
Text("$title Item Added")
}
}
}
P.S: Use mutableStateListOf if you have a list of items that needs to be modified as well as trigger recomposition properly.
I managed to do like this:
#Composable
fun ComposeListExample(
allObjects: List<Object>,
selectedObjects: List<Object>
) {
val selectedItems = remember {
mutableStateListOf<Object>().apply { addAll(selectedObjects) }
}
Column {
allObjects.forEach { item ->
SomeView(
title = item.title,
onSelect = {
if (selectedItems.contains(item)) {
selectedItems.remove(item)
} else {
selectedItems.add(item)
}
})
}
}
}

Variable value is still null even after assigning a value inside the listener block [duplicate]

(Disclaimer: There are a ton of questions which arise from people asking about data being null/incorrect when using asynchronous operations through requests such as facebook,firebase, etc. My intention for this question was to provide a simple answer for that problem to everyone starting out with asynchronous operations in android)
I'm trying to get data from one of my operations, when I debug it using breakpoints or logs, the values are there, but when I run it they are always null, how can I solve this ?
Firebase
firebaseFirestore.collection("some collection").get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
//I want to return these values I receive here...
});
//...and use the returned value here.
Facebook
GraphRequest request = GraphRequest.newGraphPathRequest(
accessToken,
"some path",
new GraphRequest.Callback() {
#Override
public void onCompleted(GraphResponse response) {
//I want to return these values I receive here...
}
});
request.executeAsync();
//...and use the returned value here.
Kotlin coroutine
var result: SomeResultType? = null
someScope.launch {
result = someSuspendFunctionToRetrieveSomething()
//I want to return the value I received here...
}
Log.d("result", result.toString()) //...but it is still null here.
Etc.
What is a Synchronous/Asynchronous operation ?
Well, Synchronous waits until the task has completed. Your code executes "top-down" in this situation.
Asynchronous completes a task in the background and can notify you when it is complete.
If you want to return the values from an async operation through a method/function, you can define your own callbacks in your method/function to use these values as they are returned from these operations.
Here's how for Java
Start off by defining an interface :
interface Callback {
void myResponseCallback(YourReturnType result);//whatever your return type is: string, integer, etc.
}
next, change your method signature to be like this :
public void foo(final Callback callback) { // make your method, which was previously returning something, return void, and add in the new callback interface.
next up, wherever you previously wanted to use those values, add this line :
callback.myResponseCallback(yourResponseObject);
as an example :
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
// create your object you want to return here
String bar = document.get("something").toString();
callback.myResponseCallback(bar);
})
now, where you were previously calling your method called foo:
foo(new Callback() {
#Override
public void myResponseCallback(YourReturnType result) {
//here, this result parameter that comes through is your api call result to use, so use this result right here to do any operation you previously wanted to do.
}
});
}
How do you do this for Kotlin ?
(as a basic example where you only care for a single result)
start off by changing your method signature to something like this:
fun foo(callback:(YourReturnType) -> Unit) {
.....
then, inside your asynchronous operation's result :
firestore.collection("something")
.document("document").get()
.addOnSuccessListener {
val bar = it.get("something").toString()
callback(bar)
}
then, where you would have previously called your method called foo, you now do this :
foo() { result->
// here, this result parameter that comes through is
// whatever you passed to the callback in the code aboce,
// so use this result right here to do any operation
// you previously wanted to do.
}
// Be aware that code outside the callback here will run
// BEFORE the code above, and cannot rely on any data that may
// be set inside the callback.
if your foo method previously took in parameters :
fun foo(value:SomeType, callback:(YourType) -> Unit)
you simply change it to :
foo(yourValueHere) { result ->
// here, this result parameter that comes through is
// whatever you passed to the callback in the code aboce,
// so use this result right here to do any operation
// you previously wanted to do.
}
these solutions show how you can create a method/function to return values from async operations you've performed through the use of callbacks.
However, it is important to understand that, should you not be interested in creating a method/function for these:
#Override
public void onSuccess(SomeApiObjectType someApiResult) {
// here, this `onSuccess` callback provided by the api
// already has the data you're looking for (in this example,
// that data would be `someApiResult`).
// you can simply add all your relevant code which would
// be using this result inside this block here, this will
// include any manipulation of data, populating adapters, etc.
// this is the only place where you will have access to the
// data returned by the api call, assuming your api follows
// this pattern
})
There's a particular pattern of this nature I've seen repeatedly, and I think an explanation of what's happening would help. The pattern is a function/method that calls an API, assigning the result to a variable in the callback, and returns that variable.
The following function/method always returns null, even if the result from the API is not null.
Kotlin
fun foo(): String? {
var myReturnValue: String? = null
someApi.addOnSuccessListener { result ->
myReturnValue = result.value
}.execute()
return myReturnValue
}
Kotlin coroutine
fun foo(): String? {
var myReturnValue: String? = null
lifecycleScope.launch {
myReturnValue = someApiSuspendFunction()
}
return myReturnValue
}
Java 8
private String fooValue = null;
private String foo() {
someApi.addOnSuccessListener(result -> fooValue = result.getValue())
.execute();
return fooValue;
}
Java 7
private String fooValue = null;
private String foo() {
someApi.addOnSuccessListener(new OnSuccessListener<String>() {
public void onSuccess(Result<String> result) {
fooValue = result.getValue();
}
}).execute();
return fooValue;
}
The reason is that when you pass a callback or listener to an API function, that callback code will only be run some time in the future, when the API is done with its work. By passing the callback to the API function, you are queuing up work, but the current function (foo() in this case) returns immediately before that work begins and before that callback code is run.
Or in the case of the coroutine example above, the launched coroutine is very unlikely to complete before the function that started it.
Your function that calls the API cannot return the result that is returned in the callback (unless it's a Kotlin coroutine suspend function). The solution, explained in the other answer, is to make your own function take a callback parameter and not return anything.
Alternatively, if you're working with coroutines, you can make your function suspend instead of launching a separate coroutine. When you have suspend functions, somewhere in your code you must launch a coroutine and handle the results within the coroutine. Typically, you would launch a coroutine in a lifecycle function like onCreate(), or in a UI callback like in an OnClickListener.
Other answer explains how to consume APIs based on callbacks by exposing a similar callbacks-based API in the outer function. However, recently Kotlin coroutines become more and more popular, especially on Android and while using them, callbacks are generally discouraged for such purposes. Kotlin approach is to use suspend functions instead. Therefore, if our application uses coroutines already, I suggest not propagating callbacks APIs from 3rd party libraries to the rest of our code, but converting them to suspend functions.
Converting callbacks to suspend
Let's assume we have this callback API:
interface Service {
fun getData(callback: Callback<String>)
}
interface Callback<in T> {
fun onSuccess(value: T)
fun onFailure(throwable: Throwable)
}
We can convert it to suspend function using suspendCoroutine():
private val service: Service
suspend fun getData(): String {
return suspendCoroutine { cont ->
service.getData(object : Callback<String> {
override fun onSuccess(value: String) {
cont.resume(value)
}
override fun onFailure(throwable: Throwable) {
cont.resumeWithException(throwable)
}
})
}
}
This way getData() can return the data directly and synchronously, so other suspend functions can use it very easily:
suspend fun otherFunction() {
val data = getData()
println(data)
}
Note that we don't have to use withContext(Dispatchers.IO) { ... } here. We can even invoke getData() from the main thread as long as we are inside the coroutine context (e.g. inside Dispatchers.Main) - main thread won't be blocked.
Cancellations
If the callback service supports cancelling of background tasks then it is best to cancel when the calling coroutine is itself cancelled. Let's add a cancelling feature to our callback API:
interface Service {
fun getData(callback: Callback<String>): Task
}
interface Task {
fun cancel();
}
Now, Service.getData() returns Task that we can use to cancel the operation. We can consume it almost the same as previously, but with small changes:
suspend fun getData(): String {
return suspendCancellableCoroutine { cont ->
val task = service.getData(object : Callback<String> {
...
})
cont.invokeOnCancellation {
task.cancel()
}
}
}
We only need to switch from suspendCoroutine() to suspendCancellableCoroutine() and add invokeOnCancellation() block.
Example using Retrofit
interface GitHubService {
#GET("users/{user}/repos")
fun listRepos(#Path("user") user: String): Call<List<Repo>>
}
suspend fun listRepos(user: String): List<Repo> {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
val service = retrofit.create<GitHubService>()
return suspendCancellableCoroutine { cont ->
val call = service.listRepos(user)
call.enqueue(object : Callback<List<Repo>> {
override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
if (response.isSuccessful) {
cont.resume(response.body()!!)
} else {
// just an example
cont.resumeWithException(Exception("Received error response: ${response.message()}"))
}
}
override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
cont.resumeWithException(t)
}
})
cont.invokeOnCancellation {
call.cancel()
}
}
}
Native support
Before we start converting callbacks to suspend functions, it is worth checking whether the library that we use does support suspend functions already: natively or with some extension. Many popular libraries like Retrofit or Firebase support coroutines and suspend functions. Usually, they either provide/handle suspend functions directly or they provide suspendable waiting on top of their asynchronous task/call/etc. object. Such waiting is very often named await().
For example, Retrofit supports suspend functions directly since 2.6.0:
interface GitHubService {
#GET("users/{user}/repos")
suspend fun listRepos(#Path("user") user: String): List<Repo>
}
Note that we not only added suspend, but also we no longer return Call, but the result directly. Now, we can use it without all this enqueue() boilerplate:
val repos = service.listRepos(user)
TL;DR The code you pass to these APIs (e.g. in the onSuccessListener) is a callback, and it runs asynchronously (not in the order it is written in your file). It runs at some point later in the future to "call back" into your code. Without using a coroutine to suspend the program, you cannot "return" data retrieved in a callback from a function.
What is a callback?
A callback is a piece of code you pass to some third party library that it will run later when some event happens (e.g. when it gets data from a server). It is important to remember that the callback is not run in the order you wrote it - it may be run much later in the future, could run multiple times, or may never run at all. The example callback below will run Point A, start the server fetching process, run Point C, exit the function, then some time in the distant future may run Point B when the data is retrieved. The printout at Point C will always be empty.
fun getResult() {
// Point A
var r = ""
doc.get().addOnSuccessListener { result ->
// The code inside the {} here is the "callback"
// Point B - handle result
r = result // don't do this!
}
// Point C - r="" still here, point B hasn't run yet
println(r)
}
How do I get the data from the callback then?
Make your own interface/callback
Making your own custom interface/callback can sometimes make things cleaner looking but it doesn't really help with the core question of how to use the data outside the callback - it just moves the aysnc call to another location. It can help if the primary API call is somewhere else (e.g. in another class).
// you made your own callback to use in the
// async API
fun getResultImpl(callback: (String)->Unit) {
doc.get().addOnSuccessListener { result ->
callback(result)
}
}
// but if you use it like this, you still have
// the EXACT same problem as before - the printout
// will always be empty
fun getResult() {
var r = ""
getResultImpl { result ->
// this part is STILL an async callback,
// and runs later in the future
r = result
}
println(r) // always empty here
}
// you still have to do things INSIDE the callback,
// you could move getResultImpl to another class now,
// but still have the same potential pitfalls as before
fun getResult() {
getResultImpl { result ->
println(result)
}
}
Some examples of how to properly use a custom callback: example 1, example 2, example 3
Make the callback a suspend function
Another option is to turn the async method into a suspend function using coroutines so it can wait for the callback to complete. This lets you write linear-looking functions again.
suspend fun getResult() {
val result = suspendCoroutine { cont ->
doc.get().addOnSuccessListener { result ->
cont.resume(result)
}
}
// the first line will suspend the coroutine and wait
// until the async method returns a result. If the
// callback could be called multiple times this may not
// be the best pattern to use
println(result)
}
Re-arrange your program into smaller functions
Instead of writing monolithic linear functions, break the work up into several functions and call them from within the callbacks. You should not try to modify local variables within the callback and return or use them after the callback (e.g. Point C). You have to move away from the idea of returning data from a function when it comes from an async API - without a coroutine this generally isn't possible.
For example, you could handle the async data in a separate method (a "processing method") and do as little as possible in the callback itself other than call the processing method with the received result. This helps avoid a lot of the common errors with async APIs where you attempt to modify local variables declared outside the callback scope or try to return things modified from within the callback. When you call getResult it starts the process of getting the data. When that process is complete (some time in the future) the callback calls showResult to show it.
fun getResult() {
doc.get().addOnSuccessListener { result ->
showResult(result)
}
// don't try to show or return the result here!
}
fun showResult(result: String) {
println(result)
}
Example
As a concrete example here is a minimal ViewModel showing how one could include an async API into a program flow to fetch data, process it, and display it in an Activity or Fragment. This is written in Kotlin but is equally applicable to Java.
class MainViewModel : ViewModel() {
private val textLiveData = MutableLiveData<String>()
val text: LiveData<String>
get() = textLiveData
fun fetchData() {
// Use a coroutine here to make a dummy async call,
// this is where you could call Firestore or other API
// Note that this method does not _return_ the requested data!
viewModelScope.launch {
delay(3000)
// pretend this is a slow network call, this part
// won't run until 3000 ms later
val t = Calendar.getInstance().time
processData(t.toString())
}
// anything out here will run immediately, it will not
// wait for the "slow" code above to run first
}
private fun processData(d: String) {
// Once you get the data you may want to modify it before displaying it.
val p = "The time is $d"
textLiveData.postValue(p)
}
}
A real API call in fetchData() might look something more like this
fun fetchData() {
firestoreDB.collection("data")
.document("mydoc")
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val data = task.result.data
processData(data["time"])
}
else {
textLiveData.postValue("ERROR")
}
}
}
The Activity or Fragment that goes along with this doesn't need to know anything about these calls, it just passes actions in by calling methods on the ViewModel and observes the LiveData to update its views when new data is available. It cannot assume that the data is available immediately after a call to fetchData(), but with this pattern it doesn't need to.
The view layer can also do things like show and hide a progress bar while the data is being loaded so the user knows it's working in the background.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val model: MainViewModel by viewModels()
// Observe the LiveData and when it changes, update the
// state of the Views
model.text.observe(this) { processedData ->
binding.text.text = processedData
binding.progress.visibility = View.GONE
}
// When the user clicks the button, pass that action to the
// ViewModel by calling "fetchData()"
binding.getText.setOnClickListener {
binding.progress.visibility = View.VISIBLE
model.fetchData()
}
binding.progress.visibility = View.GONE
}
}
The ViewModel is not strictly necessary for this type of async workflow - here is an example of how to do the same thing in the activity
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// When the user clicks the button, trigger the async
// data call
binding.getText.setOnClickListener {
binding.progress.visibility = View.VISIBLE
fetchData()
}
binding.progress.visibility = View.GONE
}
private fun fetchData() {
lifecycleScope.launch {
delay(3000)
val t = Calendar.getInstance().time
processData(t.toString())
}
}
private fun processData(d: String) {
binding.progress.visibility = View.GONE
val p = "The time is $d"
binding.text.text = p
}
}
(and, for completeness, the activity XML)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/text"
android:layout_margin="16dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:id="#+id/get_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Get Text"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/text"
/>
<ProgressBar
android:id="#+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="48dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/get_text"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

questions about DI, ViewModel etc

I have the following code:
class ExampleView :View("My Example view") {
val model:ExampleModel by inject()
override val root= vbox {
textfield(model.data)
button("Commit") {
setOnAction {
model.commit()
closeModal()
}
}
button("Rollback") {
setOnAction {
model.rollback()
closeModal()
}
}
button("Just quit") {
setOnAction {
closeModal()
}
}
}
}
class Example() {
var data by property<String>()
fun dataProperty() = getProperty(Example::data)
}
class ExampleModel(example: Example) : ItemViewModel<Example>() {
init {
item = example
}
val data = bind { item?.dataProperty() }
}
class MainView : View() {
val example:Example
override val root = BorderPane()
init {
example = Example()
example.data = "Data for example"
val exampleModel = ExampleModel(example)
with(root){
top {
menubar {
menu("Test") {
menuitem("Example - 1") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
menuitem("Example - 2") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
}
}
}
}
}
}
I have two questions for this example:
1) If i change the value and close the window without a commit (User can do this with help [X] button) then only ViewModel will store changes (and it will be displayed in GUI even after the re-opening ), but model POJO object will keep the old data.
if I used instance of Example class (without DI) then this instance received all the changes at once.
For example i don't want commit/rollback functionality but i want DI and immediate updating. What i should do? (ofcource i can call "commit" for "textfield change value event")
2) ViewModel has constructor with parameter and if i try open ExampleView like this
find<ExampleView>(Scope()).openWindow()
then I got an an obvious RuntimeException. Can I avoid this for example, by a compiler warnings (or by something else)?
1) This is the correct default behavior of the ViewModel. If you bind a property of the view model to an input, the changes are immediately reflected in that bound property, but will only be flushed into the underlying model object once you commit it.
If you want to autocommit changes in the view model properties back into the underlying model object, you can create the binding with the autocommit property set to true:
val data = bind(true) { item?.dataProperty() }
You can also write bind(autocommit = true) if that looks clearer to you. This will cause any changes to be automatically flushed back into the underlying object.
I also want to make you aware that by requiring an item in the constructor of your view model, you're effectively preventing it from being used with injection unless you prime it like you do using setInScope. This might be fine for your use case, but worth noting.
2) The upcoming TornadoFX 1.5.10 will give you a better runtime error message if you forget to pass a parameter. It also introduces default values for parameters. See https://github.com/edvin/tornadofx/pull/227 for more info.