LiveData lazy init with coroutines not working - android

I want to load data from an API when activity is started. Currently, I call a view model's method from the activity to load data and it's working fine, but I don't know if it's the best way to do it:
Activity
override fun onCreate(savedInstanceState: Bundle?) {
//initialize stuff...
viewModel.myData.observe(this) {
//do things with the data
}
lifeCycleScope.launch { viewModel.loadData() }
}
ViewModel
class MyViewModel : ViewModel() {
val myData = MutableLiveData<MyData>()
suspend fun loadData() = withContext(Dispatchers.IO) {
val data = api.getData()
withContext(Dispatchers.Main) {
myData.value = data
}
}
}
I have seen some examples using lazy initialization, but I don't know how to implement it with coroutines. I have tried this:
Activity
override fun onCreate(savedInstanceState: Bundle?) {
//initialize stuff...
viewModel.myData().observe(this) {
//do things with the data
}
}
ViewModel
private val myData : MutableLiveData<MyData> by lazy {
MutableLiveData<MyData>().also {
viewModelScope.launch {
loadData()
}
}
}
fun myData() = myData
suspend fun loadData() = // same as above
But data is not fetched and nothing is displayed.

If you've added dependency livedata-ktx then you can use livedata builder to also have API call in same block and emit. Checkout how you can do it:
class MyViewModel : ViewModel() {
val myData: LiveData<MyData> = liveData {
val data = api.getData() // suspended call
emit(data) // emit data once available
}
}

Related

How to properly set Observable in the Activity to Pass data from API call in view model into Activity + Data Class for the list. Android Compose

I think my observable is set incorrectly here. I am using Retrofit2 + Moshi as the deserializer, and the API call from Retrofit is working.
But once I make the API call, I am trying to set up the Observable in my Activity and then use the API call data from the data class.
Here is my view model code:
class DealsViewModel(val repository: MainRepository) : ViewModel() {
val movieList = MutableLiveData<List<DealItems>>()
var job: Job? = null
val loading = MutableLiveData<Boolean>()
val errorMessage = MutableLiveData<String>()
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
fun getMovies() {
viewModelScope.launch{
// View Model Scope gives the Coroutine that will be canceled when the ViewModel is cleared.
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val items = repository.getProduct()
withContext(Dispatchers.Main) {
if (items.isNullOrEmpty()) {
loading.value = false
// put error message in here later
} else {
dealList.postValue(items)
return#withContext
}
}
}
}
}
private fun onError(message: String) {
errorMessage.value = message
loading.value = false
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
And here is my MainActivity code.
I am using JetpackCompose in my activity, LiveData for the API response container. In my main repository is where I am validating a successful API response and then the coroutines for the call are inside of the view model.
My API call is successful, but I am not sure where to call the ViewModel.GetMovies() inside of the activity and I am not sure if the observables are set properly and/or where to pass the API's livedata into my composable function.
Thanks for any help you can provide. I am new to android and trying to use Coroutines for the first time.
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val retrofitService = RetrofitService.getInstance()
val viewModel = ViewModelProvider(this,
MyViewModelFactory(MainRepository(retrofitService = retrofitService))).get(DealsViewModel::class.java)
// viewModel.getProducts()
setContent {
myApp {
MyScreenContent()
}
viewModel.movieList.observe(
this, { it ->
if( it != null) {
it.forEach {
var movieLocation = it.movieLocation
val description = it.description
val id = it.id
val title = it.title
val regularPrice = it.regularPrice
}
}
})
return#setContent
}
viewModel.errorMessage.observe(this, {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
})
viewModel.loading.observe(
this,
Observer {
if (it) {
}
})
}
}
I assume that it always depends when should you call especially in the activity we have many lifecycles; however, the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the getMovies is called only one time, and also guarantee the service itself is not called unless someone is listening to it.
You may check the full documentation in this link
Here is a code example
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
When using this code, you do not have to call getMovies at all in the activity, you just listen to the observer.

How to return value from async coroutine scope such as ViewModelScope to your UI?

I'm trying to retrieve a single entry from the Database and successfully getting the value back in my View Model with the help of viewModelScope, but I want this value to be returned back to the calling function which resides in the fragment so it can be displayed on a TextView. I tried to return the value the conventional way but it didn't work. So, How Can I return this value from viewModelScope.launch to the calling function?
View Model
fun findbyID(id: Int) {
viewModelScope.launch {
val returnedrepo = repo.delete(id)
Log.e(TAG,returnedrepo.toString())
// how to return value from here to Fragment
}
}
Repository
suspend fun findbyID(id : Int):userentity{
val returneddao = Dao.findbyID(id)
Log.e(TAG,returneddao.toString())
return returneddao
}
LiveData can be used to get value from ViewModel to Fragment.
Make the function findbyID return LiveData and observe it in the fragment.
Function in ViewModel
fun findbyID(id: Int): LiveData</*your data type*/> {
val result = MutableLiveData</*your data type*/>()
viewModelScope.launch {
val returnedrepo = repo.delete(id)
result.postValue(returnedrepo)
}
return result.
}
Observer in Fragment
findbyId.observer(viewLifeCycleOwner, Observer { returnedrepo ->
/* logic to set the textview */
})
Thank you Nataraj KR for your Help!
Following is the code that worked for me.
View Model
class ViewModel(application: Application):AndroidViewModel(application) {
val TAG = "ViewModel"
val repo: theRepository
val alldata:LiveData<List<userentity>>
val returnedVal = MutableLiveData<userentity>()
init {
val getDao = UserRoomDatabase.getDatabase(application).userDao()
repo = theRepository(getDao)
alldata = repo.allUsers
}
fun findbyID(id: Int){
viewModelScope.launch {
returnedVal.value = repo.findbyID(id)
}
}
}
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val usermodel = ViewModelProvider(this).get(ViewModel::class.java)
usermodel.alldata.observe(this, Observer {
Log.e(TAG,usermodel.alldata.value.toString())
})
usermodel.returnedVal.observe(this, Observer {
tv1.text = usermodel.returnedVal.value.toString()
})
allData.setOnClickListener {
tv1.text = usermodel.alldata.value.toString()
}
findByID.setOnClickListener {
usermodel.findbyID(et2.text.toString().toInt())
}
}
Another way without using LiveData would be like this,
Similar to viewModelScope there is also a lifecycleScope available with lifecycle-aware components, which can be used from the UI layer. Following is the example,
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
findByID.setOnClickListener {
lifecycleScope.launch{
val res = usermodel.findbyID(et2.text.toString().toInt())
// use returned value to do anything.
}
}
}
ViewModel
//1st option
// make the function suspendable itself.use aync instead of launch and then
// use await to collect the returned value.
suspend fun findbyID(id: Int): userEntity {
val job = viewModelScope.async {
val returnedrepo = repo.delete(id)
Log.e(TAG,returnedrepo.toString())
return#async returnedrepo
}
return job.await()
}
//2nd option
// make the function suspendable itself. but switch the execution on IO
// thread.(since you are making a DB call)
suspend fun findbyID(id: Int): userEntity {
return withContext(Dispatchers.IO){
val returnedrepo = repo.delete(id)
Log.e(TAG,returnedrepo.toString())
return#withContext returnedrepo
}
}
Since LiveData is specific to Android Environment, Using Kotlin Flow becomes a better option in some places, which offers similar functionality.

Retroift coroutines livedata called only first time

I'm just new with kotlin coroutines. I just created new project for testing livedata but i cant observe data changes. I didn't understand the concept of livedata. When it'll be trigger? Because when i observe ROOM database(Not the coroutines way. i used the MutableLiveData) it was working very well. Observer was always triggered whenever data changed.
I just wanted to clean and modern code. My expectations: when I click btnLogin button (when user login with another account or you can say when data changes) livedata must trigger.
Here is my example:
Retrofit interface:
interface RetroMainClient {
#POST("login.php")
suspend fun login(#Body model: UserLoginModel): Response<UserLoginModel>
companion object {
val getApi: RetroMainClient by lazy {
Retrofit.Builder().baseUrl("https://example.com/")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())).build()
.create(RetroMainClient::class.java)
}
}
}
My repository:
class Repository {
suspend fun getLoginApi(model: UserLoginModel) = RetroMainClient.getApi.login(model)
}
My viewModel:
class MainViewModel : ViewModel() {
fun login(model: UserLoginModel) = liveData(IO) {
try {
emit(Repository().getLoginApi(model))
} catch (e: Exception) {
Log.e("exception", "${e.message}")
}
}
}
and my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
var model = UserLoginModel("user1", "123456")
viewModel.login(model).observe(this, Observer {
if (it.isSuccessful) {
btnLogin.text = it.body()?.username
}
})
btnLogin.setOnClickListener {
model = UserLoginModel("user2", "123456")
CoroutineScope(IO).launch {
try {
Repository().getLoginApi(model)
} catch (e: Exception) {
Log.e("exception:", "${e.message}")
}
}
}
}
}
When you call viewModel.login() method you create a new instance of LiveData class. In order to execute corresponding block in viewModel.login() after every click on btnLogin button you need call LiveData.observe() method for every viewModel.login() call.
In MainActivity's onCreate method:
btnLogin.setOnClickListener {
model = UserLoginModel("user2", "123456")
viewModel.login(model).observe(this, Observer { data ->
if (it.isSuccessful) {
btnLogin.text = data.body()?.username
}
})
}
ANOTHER APPROACH:
is to launch a coroutine in MainViewModel class and update LiveData field manually:
class MainViewModel : ViewModel() {
val loginResponse: LiveData<Response<UserLoginModel>> = MutableLiveData<Response<UserLoginModel>>()
fun login(model: UserLoginModel) = viewModelScope.launch(IO) {
try {
(loginResponse as MutableLiveData).postValue(Repository().getLoginApi(model))
} catch (e: Exception) {
Log.e("exception", "${e.message}")
}
}
}
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
var model = UserLoginModel("user1", "123456")
viewModel.loginResponse.observe(this, Observer {
if (it.isSuccessful) {
btnLogin.text = it.body()?.username
}
})
btnLogin.setOnClickListener {
model = UserLoginModel("user2", "123456")
viewModel.login(model)
}
}
}
To use viewModelScope in MainViewModel class add dependency to build.gradle file:
final LIFECYCLE_VERSION = "2.2.0-rc03" // add most recent version
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION"

LiveData is observed multiple times inside onClickListener in Android

I have a repository setup like this
class ServerTimeRepo #Inject constructor(private val retrofit: Retrofit){
var liveDataTime = MutableLiveData<TimeResponse>()
fun getServerTime(): LiveData<TimeResponse> {
val serverTimeService:ServerTimeService = retrofit.create(ServerTimeService::class.java)
val obs = serverTimeService.getServerTime()
obs.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).unsubscribeOn(Schedulers.io())
.subscribe(object : Observer<Response<TimeResponse>> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: Response<TimeResponse>) {
val gson = Gson()
val json: String?
val code = t.code()
val cs = code.toString()
if (!cs.equals("200")) {
json = t.errorBody()!!.string()
val userError = gson.fromJson(json, Error::class.java)
} else {
liveDataTime.value = t.body()
}
}
override fun onError(e: Throwable) {
}
})
return liveDataTime
}
}
Then I have a viewmodel calling this repo like this
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
fun getServerTime(): LiveData<TimeResponse> {
return serverTimeRepo.getServerTime()
}
}
Then I have an activity where I have an onClickListener where I am observing the livedata, like this
tvPWStart.setOnClickListener {
val stlv= serverTimeViewModel.getServerTime()
stlv.observe(this#HomeScreenActivity, Observer {
//this is getting called multiple times??
})
}
I don't know what's wrong in this. Can anyone point me in the right direction? Thanks.
Issue is that every time your ClickListener gets fired, you observe LiveData again and again. So, you can solve that problem by following solution :
Take a MutableLiveData object inside your ViewModel privately & Observe it as LiveData.
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
private val serverTimeData = MutableLiveData<TimeResponse>() // We make private variable so that UI/View can't modify directly
fun getServerTime() {
serverTimeData.value = serverTimeRepo.getServerTime().value // Rather than returning LiveData, we set value to our local MutableLiveData
}
fun observeServerTime(): LiveData<TimeResponse> {
return serverTimeData //Here we expose our MutableLiveData as LiveData to avoid modification from UI/View
}
}
Now, we observe this LiveData directly outside of ClickListener and we just call API method from button click like below :
//Assuming that this code is inside onCreate() of your Activity/Fragment
//first we observe our LiveData
serverTimeViewModel.observeServerTime().observe(this#HomeScreenActivity, Observer {
//In such case, we won't observe multiple LiveData but one
})
//Then during our ClickListener, we just do API method call without any callback.
tvPWStart.setOnClickListener {
serverTimeViewModel.getServerTime()
}

How livedata send the data to activity if any changes

Hi I am reading this example of LiveData and Observer https://code.tutsplus.com/tutorials/introduction-to-android-architecture--cms-28749
MainActivityViewModel.kt
class MainActivityViewModel : ViewModel() {
private var notes: MutableLiveData<List<String>>? = null
fun getNotes(): LiveData<List<String>> {
if (notes == null) {
notes = MutableLiveData<List<String>>()
loadNotes()
}
return notes!!
}
private fun loadNotes() {
// do async operation to fetch notes
}
}
MainActivity.kt
class MainActivity : LifecycleActivity(), AnkoLogger {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProviders.of(this)
.get(MainActivityViewModel::class.java)
viewModel.getNotes().observe(
this, Observer {
notes -> info("notes: $notes")
}
)
}
}
How LiveData is sending data to MainActivity if there is any changes in notes (new or delete). I see activity is calling viewModel.getNotes() which may not get called once onCreate method finish.
LiveData isn't sending anything to MainActivity, it's "sending" to the Observer passed to the observe method. This Observer has a reference to MainActivity where it was created and can call its methods.

Categories

Resources