Related
(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>
(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>
I want to be able to connect different LiveData<X> to the same Observer.
Up to now, my little module has been working extremely fine as hell, but to avoid linking the ViewModel to a LifecycleOwner, I included a way for the module to use the .observeForever() function if owner is null,
The observers are wrapped inside a bigger one that stores an int value that declares if the onChange() on each of the LiveData<>'s is the first one or not... this was done because in some cases I needed to ignore the initial onChange() callback.
Because there may be many observers (Keeping track of this int value)... depending on the amount of LiveData<X> ("sources" as the docs call them), it was easy for me to clear all the observers with the liveData.removeObservers(owner); function, automatically clearing all the observers at that specific owner.
But because the owner is null, now I need to keep a reference to all the observers in a single one, and remove THEM by name liveData.removeObserver(observer);
My first concern is that by declaring new on each iteration, I'm losing the reference to that observer forever.
If that's the case I can remove the Observer inside the wrapper, which is, as intended, the same among all observers, BUT the first thing that comes intuitively to mind, is that the obvious thing to do is not to destroy the most inner observer, but the outer one, because that destroys the inner one as well.
The problem is that the outer ones are different, while the inner one is the one that is common, so:
Which one should I remove, and How should they be declared?
private RunTimeObserverWrapper<? super T> runTimeObserverWrapper;
/*This is the recent change I made in the hopes of being able to remove it/them */
private void connectObservers(
List<LiveData<T>> liveDatas,
boolean ignoreInitialization
) {
this.listLiveData = liveDatas;
Observer<? super T> itemObserver = itemObserverInitializer();
/*This is the common observer*/
for (LiveData<T> liveData: listLiveData
) {
if (!liveData.hasObservers()) {
runTimeObserverWrapper = new RunTimeObserverWrapper<>(ignoreInitialization, itemObserver);
/*This is the recent change I made placing it as a field variable*/
// RunTimeObserverWrapper<? super T> runTimeObserverWrapper = new RunTimeObserverWrapper<>(ignoreInitialization, itemObserver);
/*This was working as intended, but I want to disassociate the module from the LifeCycleOwner*/
if (owner != null) {
liveData.observe(
owner,
runTimeObserverWrapper
);
} else {
liveData.observeForever(
runTimeObserverWrapper
);
}
/*New Snippet*/
// liveData.observe(
// owner,
// runTimeObserverWrapper
// );
/*Old snippet*/
}
}
}
public void destroyObserversAndList() {
for (LiveData<T> liveData: listLiveData
) {
// liveData.removeObservers(owner);
/*This was correctly removing ALL Observers*/
if (owner != null) {
liveData.removeObservers(owner);
} else {
liveData.removeObserver(runTimeObserverWrapper);
}
/*This is the new snippet to account for a lack of LifeCycleOwner*/
}
listLiveData.clear();
}
So, as you can see, my concern is that by calling liveData.removeObserver(runTimeObserverWrapper);, I''ll only be removing the last observer defined by new inside the iteration.
What should I do?
The solution was a lot harder than I first realized.
Primarily, because the answer was found by solving a secondary unrelated problem: the Database would at times return an empty value for the table in which all the rows in it were erased, and my list differ module, didnt knew what type of list it should erase.
The solution to this problem helped solve 2 problems at once, maybe a 3rd one, and one of them was the problem mentioned above.
The solution was to wrap both: the LiveData with its own Observer, now both should be given an Id, so that its answer could be stored with its Id in a LinkedHashMap.
If the answer was empty, it would just store the empty list with its corresponding Id, erasing the previous value.
Now that the LiveData was bundled with its own Observer, I could make a method that could release it from its own Observer, and, by iterating throught each LiveDataWrapper, I could call this method and remove them.
public void destroyObserversAndList() {
if (listLiveData != null) {
for (LiveDataSourceWrapper<T> liveData: listLiveData
) {
liveData.removeObserver();
}
listLiveData.clear();
}
}
And the LiveDataSourceWrapper class:
public class LiveDataSourceWrapper<X> {
private int liveDataId;
private SourceObserverWrapper<X> identityLiveDataObserver;
private LiveData<X> liveData;
public LiveDataSourceWrapper(
LiveData<X> liveData,
int liveDataId
) {
this.liveData = liveData;
this.liveDataId = liveDataId;
}
public void observeForever(#NonNull SourceObserver<X> observer) {
identityLiveDataObserver = new SourceObserverWrapper<X>(liveDataId, observer);
liveData.observeForever(identityLiveDataObserver);
}
public void removeObserver() {
liveData.removeObserver(identityLiveDataObserver);
}
public boolean hasObservers() {
return liveData.hasObservers();
}
}
My usage currently is to store 5 specific button's info into a database (room) to persist it across reboots. My current usage doesn't rely on changes of the data because the only one changing the data is the user upon long press of the button (then i update the database). Hence I do not need a LiveData variable, and this is making it difficult for me to initialize my ViewModel.
Essentially, since the LiveData objects only update on change, my data never gets initialized.
Thus the app always will cause a null-pointer on startup.
I'll share a gist of my setup so far bellow. I'm hoping there is some way to make this work where I don't have to observe any LiveData object, and I can just grab data when I instantiate the Model.
Entity:
#Entity(tableName = "myEntity")
public class MyEntity {
#PrimaryKey
public int buttonID;
// other fields...
}
DAO:
#Dao
interface MyDAO {
#Query("Select * from myDB")
LiveData<List<MyEntity>> getEntityList();
// I think this needs to change to just List<MyEntity>?
// also insert and update here...
}
Repository:
class MyRepository {
private MyDAO myDAO;
private LiveData<List<MyEntity>> allEntities;
MyRepository(Application application) {
MyDatabase db = MyDatabase.getInstance(application);
myDAO = db.myDAO();
allEntities = myDAO.getAllEntities();
}
LiveData<List<MyEntity>> getAllEntities() { return allEntities; }
// Update entity...
}
ViewModel:
public class ViewModel extends AndroidViewModel {
private MyRepository repository;
private List<MyEntity> tempList;
private HashMap<MyEntity> allEntities;
public ViewModel (Application application) {
super(application);
repository = new MyRepository(application);
Observer<List<MyEntity>> observer = data -> tempList = data;
ObserveOnce(repository.getAllEntities(), observer); // ObserveOnce implementation found in this answer: https://stackoverflow.com/a/59845763/10013384
allEntities = new HashMap<>();
for (int i = 0; i < tempList.size(); i++) { // Nullpointer here, as tempList doesn't have any items yet.
allEntities.put(tempList.get(i).buttonID, templist.get(i));
}
}
// getter and update methods...
}
Activity:
// ...
protected void onCreate(Bundle savedInstanceState) {
// ...
viewModel = new ViewModelProvider(this).get(ViewModel.class);
// Initialize UI views with data from ViewModel
}
Then in the respective listeners:
#Override
public boolean onLongClick(View v) {
int index = (Integer) v.getTag();
data.get(index).foo = fooNewUIData;
ButtonArray[index].setText(fooNewUIData);
ViewModel.update(data.get(index)); // if updated, update the ViewModel and the database
}
since the LiveData objects only update on change, my data never gets initialized
That is not your problem. Your problem is that you think that ObserveOnce() is a blocking call, and that the results will be ready immediately when it returns. In reality, LiveData from Room does work on a background thread. You need to react to when the data is available in your Observer, not assume that it will be available in the next statement.
OfCourse you can, you can simply return the normal object class. LiveData needs to be used only if you want observe the changes to those rows.
You can also use to kotlin flows to still listen to the changes and not use LiveData
Without LiveData:
List<MyEntity> getAllEntities();
With Kotlin Flows:
fun getAllEntities(): Flow<List<MyEntity>>
Hope this helps !!
Yes, of course you can. Just change the return type of the specific function in your DAO from LiveData<MyDataClass> to MyDataClass.
See this Codelab for further tutorial about Room.
Okay, thanks all to provided answers and feedback, I appreciate it!
Ultimately I agree with #CommonsWare that the easiest way to handle this is probably not to have the ViewModel consume the LiveData. I'm opting to setup an Observer as normal in my Activity, and convert it into the format that I want (ArrayList) when I save that data to my Activity class.
The issue I was originally concerned about with data not being sorted to my liking could otherwise be solved with a simple sort:
Collections.sort(this.data, (o1, o2) -> Integer.compare(o1.buttonID, o2.buttonID));
As for the async issue, I'm just falling back to allowing the observer's callback update the data as it gets it (and update the UI accordingly). If this situation involves a lot more data, then perhaps I'll need some sort of a splash screen while the app loads data. But luckily I don't have to do any of that quite yet.
Activity:
// onCreate() {
ViewModel = new ViewModelProvider(this).get(ViewModel.class);
Observer<List<MyEntity>> observer = this::setData;
observeOnce(ViewModel.getAllEntities(), observer);
if (data.size() > 0) {
// Initialize UI views
}
// }
//...
public void setData(List<MyEntity> entities) {
this.data = entities
Collections.sort(this.data, (o1, o2) -> Integer.compare(o1.buttonID, o2.buttonID));
// also update UI if need-be
}
The only thing left is to solve the issue of my Database not persisting across reboots, but that is out of the scope of this question.
I have an single activity application with jetpack navigation, I need an object variable for all my application in many fragments. So I use a ViewModel, and I've created a Parent Fragment class which provide the ViewModel :
class MyViewModel : ViewModel() {
var myData : CustomClass? = null
...
}
open class ParentFragment : Fragment {
val model : MyViewModel by activityViewModels()
lateinit var myData : CustomClass
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.myData?.let {
myData = it
}
}
}
myDatashould not be null where I use ParentFragment, but sometimes, randomly I get kotlin.UninitializedPropertyAccessException: lateinit property myData has not been initialized when I use myData
Is it possible that my ViewModel doesn't keep myData? How can I be sure that my property has been initialized ?
UPDATE : Try 1
I've tried this code in my ParentFragment:
open class ParentFragment : Fragment {
val model : MyViewModel by activityViewModels()
lateinit var backingData : CustomClass
val myData : CustomClass
get() {
if (!::backingData.isInitialized)
model.getData()?.let {
backingData = it
}
return backingData
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.getData?.let {
backingData = it
}
}
}
But the problem doesn't disappear when I call myData, it seem's the ViewModelloses my data
UPDATE 2 : More code details
Before to go inside a fragment which extends ParentFragment, I set my data in ViewModel and then I navigate to the next fragment as below :
// Inside FirstFragment
if (myData != null) {
model.setData(myData)
findNavController().navigate(FirstFragmentDirections.actionFirstToNextFragment())
}
Is it possible that my NavController does navigation before the data was setted ?
EDIT 3 : Try to use custom Application class
According to an answer below, I've implemented a custom Application class, and I've tried to pass my object through this class :
class MyApplication: Application() {
companion object {
var myObject: CustomClass? = null
}
}
But unfortunately, there is no change for me. Maybe my object is too big to allocate correctly ?
Try this:
class MyViewModel : ViewModel() {
var myObject : CustomClass? = null
...
}
open class ParentFragment : Fragment {
lateinit var model : MyViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model = ViewModelProvider(this).get(MyViewModel::class.java)
if(model.myObject == null) {
// initialize myObject, will be persisted by ViewModel
}
}
}
Note that MyViewModel and its member objects should not hold any references to Activity, Fragment, or Context, that include any indirect references to Context such as UI Views.
I will not recommend LiveData (or MutableLiveData) in your case, because a "feature" of LiveData is that their values are posted and updated asynchronously, hence call to observe may be too late.
Your wording hints some design flaws, namely:
You are refering to your data as object variable and to make it accessible at all times you chose to use a ViewModel. To me it sounds that you overthought your options.
Suggestion
Your object lifecycle appears to be managed manually by yourself. Therefore you should just use a static variable. This translates to Kotlin as a property within an (companion) object. I suggest you declare a custom Application class within your manifest and in its onCreate-method, you allocate your object and put it into the companion object of this class. Of course you can allocate it at any given time later on as well.
This will result in the following:
Access is always be possible via YourApplication.mData within your code.
Objects which relying on implementations outside the JVM can be managed properly.
For example: If you already bound to a port you won't be able to do this on a successive call - When the viewModel restores its state, for example. Maybe the underlying implementation did not report an error back to Java but allocating did not succeed. To manifest this assumption you would need to provide an description of your object variable. But As an famous example in the world of Android for this behaviour, try creating a soundPool via the SystemServices. You will experience lints about the correct usage of this object.
Deallocating can be done in the onTerminate() method of your
Application.class // edit_4: Doc of super.onTerminate() says the system will just kill your app. Therefore one needs to deallocate within an your activity. See code snippets below.
Clarification
The ViewModel of the JetPack Components is mainly responsible for saving and restoring the state of the view and binding to its model.
Meaning it handles the lifecycle across activities, fragments and possibly views. This is why you have to use an activity as the lifecycle owner in case you want to share an viewModel across multiple fragments. But I still suppose your object is more complex than just a POJO and my above suggestion results in your expected behaviour.
Also note that when multithreading, you shall not rely on the correct order of the lifecycle methods. There are only limited lifecycle-callbacks which are guaranteed to be called in a specific order by the android system, but the frequently used ones are unfortunately not included here. In this case, you should start processing at a more approrpiate time.
Even though the data should be similiar to the previous state, the exact reference depends on the hashCode implementation, but this is an JVM specific.
// edit:
ParentFragment is also bad naming, since you created a class which others shall inherit instead of refer to. If you want to access a specific variable within all your fragments, this needs to be implemented as an object (Singleton), since the Navigation component will prevent you from accessing the fragmentManager directly.
In plain android, one fragment can always refer to its parentFragment, iff this parentFragment has used its own childFragmentManager to commit the fragmentTransaction. Meaning also that fragments added by your Activity-fragmentManager have never an parentFragment.
// edit_2+3:
ViewModelProvider(activity!!, ViewModelFactory())[clazz]
is the correct call for creating and accessing a sharedViewModel:
The lifecycle owner needs to be the activity, otherwise after each fragmentTransaction done there will be a callback to the onCleared() method and the viewModel will release all references to avoid memory leaks.
// edit_4:
That your object was not correctly initialized was just an assumption which only would oocure if you tried to initialize it again. For example if you use an get()-method on an val where not appropriate.
Nonetheless, handling your object this way ensures that its lifecycle is outside your fragments. Here is an code example to clarify my wording:
// edit_5: To assert that the object reference is not damaged, include null checking (only if construction of CustomClass is non trivial)
Declare your CustomApplication
class CustomApplication : Application() {
companion object SharedInstances {
/**
* Reference to an object accessed in various places in your application.
*
* This property is initialized at a later point in time. In your case, once
* the user completed a required workflow in some fragment.
*
* #Transient shall indicate that the state could also be not Serializable/Parcelable
* This _could_ require manually releasing the object.
* Also prohibits passing via safeArgs
*/
#Transient var complex: CustomClass? = null
}
}
Intialization and Usage within your classes:
class InitializeComplexStateFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (complex != null) return#onViewCreated // prohibit successive initialization.
if (savedInstanceState != null) { /* The fragment was recreated but the object appears to be lost. */ }
// do your heavy lifting and initialize your data at any point.
CustomApplication.SharedInstances.complex = object : CustomClass() {
val data = "forExampleAnSessionToken"
/* other objects could need manual release / deallocation, like closing a fileDescriptor */
val cObject = File("someFileDescriptorToBindTo")
}
}
}
class SomeOtherFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
CustomApplication.SharedInstances.complex?.let {
// do processing
}
?: propagateErrorStateInFragment()
}
private fun propagateErrorStateInFragment() { throw NotImplementedError("stub") }
}
Deallocation if required
class SomeActivity: Activity() {
override fun onStop() {
super.onStop()
/* with multiple activities the effort increases */
CustomApplication.complex?.close()
}
}
You can check by using isInitialized on your property.
As the documentation says:
Returns true if this lateinit property has been assigned a value, and false otherwise.
You could initialize your property as null and do a null-check with the let as you already do though, no need to use lateinit and be careful with it, it is not a substitute for using a nullable var
You can use like this:
class MyViewModel : ViewModel() {
var mData: MutableLiveData<CustomClass>? = null
init {
mData = MutableLiveData<CustomClass>()
mData!!.value = CustomClass()
}
fun getData(): LiveData<CustomClass>? {
return mData
}
}
And your fragment :
open class ParentFragment : Fragment {
lateinit var model : MyViewModel
lateinit var myObject : CustomClass
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model = ViewModelProvider(this).get(MyViewModel::class.java)
model.getData()?.observe(viewLifecycleOwner, Observer {
myObject = it
})
}
}
Ideally you should tie the sharedVM lifecycle to activity and then use the same sharedVM instance in all fragments. Also initialise the myObject in parentFragment/ activity class using setter(). Then get the object using getter().
sample code:
// SharedViewModel
var myObject : CustomClass? = null
fun setMyObject(obj : CustomClass?){
myObject = obj
}
fun getMyObject():CustomClass?{
return myObject
}
// Activity
val model: SharedViewModel by viewModels()
model.setMyObject(objectValue)
// ParentFragment
private val model: SharedViewModel by activityViewModels()
val obj = model.getMyObject()
Hope this helps you.Happy Coding :)