Kotlin coroutines not downloading data - android

I am using Kotlin corountines in my Android Project. I am trying to download some data and display in a textview.
Following is my code
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv.setOnClickListener {
downloadData()
}
}
private fun downloadData() {
runBlocking {
pb_activity_main.visibility = View.VISIBLE
var data = ""
async {
data = downloadDataBlocking()
}.await()
tv.text = data
pb_activity_main.visibility = View.GONE
}
}
private fun downloadDataBlocking(): String {
val client = OkHttpClient()
val request = Request.Builder().url("https://jsonplaceholder.typicode.com/posts").build()
val response = client.newCall(request).execute()
return response.body()?.string() ?: ""
}
}
But the data is not downloaded. I am not able to figure out why.
I have included the internet permission in Manifest and the url is also working.

Try this:
class MainActivity : AppCompatActivity(), CoroutineScope {
private val job = Job()
override val coroutineContext = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv.setOnClickListener {
downloadData()
}
}
private fun downloadData() {
launch {
pb_activity_main.visibility = View.VISIBLE
tv.text = withContext(Dispatchers.IO) { downloadDataBlocking() }
pb_activity_main.visibility = View.GONE
}
}
private fun downloadDataBlocking(): String {
val client = OkHttpClient()
val request = Request.Builder().url("https://jsonplaceholder.typicode.com/posts").build()
val response = client.newCall(request).execute()
return response.body()?.string() ?: ""
}
}
First: you should never use runBLocking out of unit-testing or other special domain.
This function should not be used from coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests.
Second:
Coroutines are always related to some local scope in your application, which is an entity with a limited life-time, like a UI element.
That's why Activity implements CoroutineScope. Honestly, a better place for it is ViewModel or Presenter, but I don't see any in the code...
Third, it is quite pointless to useasync and await right after it's definition. Just use withContext then.

Related

kotlin coroutines trying to build recipes app based on recipessdb with kotlin +coroutines and mvvm but i have an error that i don't know

here is my api sevice
interface GetDataServices {
#GET("categories.php")
suspend fun getCategoryList(): Category
here is my Repositor
class Repository(
var getDataService : GetDataServices ?=null,
var dao: RecipeDao?=null,
var application: Application
){
//-1-get Main category from api
suspend fun getMainCategory(): Category? {
var res_rep= getDataService?.getCategoryList()
return res_rep
Log.v("res_rep_get",res_rep.toString())
}
}
here is my ViewModel
class CategoryViewModel: ViewModel(){
var repository:Repository?=null
var mainCategoryList:MutableLiveData<ArrayList<CategoryItems>?>?=MutableLiveData()
suspend fun getMainCategory(){
viewModelScope.launch(Dispatchers.IO) {
repository= Repository()
val result= withContext(Dispatchers.IO){
repository?.getMainCategory()
}
mainCategoryList!!.value = result!!.categoriesitems as ArrayList<CategoryItems>?
}
}
}
here is my activity
#OptIn(DelicateCoroutinesApi::class)
class SplashActivity : BaseActivity(), EasyPermissions.RationaleCallbacks,
EasyPermissions.PermissionCallbacks {
private var READ_STORAGE_PERM = 123
lateinit var categoryViewModel: CategoryViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
GlobalScope.launch {
// readStorageTask()
withContext(Dispatchers.Main){
getCategories()
}
}
}
suspend fun getCategories() {
categoryViewModel = ViewModelProvider(this).get(CategoryViewModel::class.java)
categoryViewModel!!.getMainCategory()
categoryViewModel.mainCategoryList?.observe(this) { value ->
value?.let {
Log.d("cor_get", value.toString())
}
}
}
here isُError
com.example.foodrecipeapp.viewmodel.CategoryViewModel$getMainCategory$2.invokeSuspend(CategoryViewModel.kt:26)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
here isُ another Error
at com.example.foodrecipeapp.viewmodel.CategoryViewModel$getMainCategory$2.invokeSuspend(CategoryViewModel.kt:26)
here is my dependincies
def lifecycle_version = "2.5.0-alpha01"
def arch_version = "2.1.0"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// ViewModel utilities for Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
kapt("androidx.lifecycle:lifecycle-compiler:2.5.0")
You're doing your api request from the main thread
val result = withContext(Dispatchers.Main) { repository?.getMainCategory() }
Just leave it as
val result = repository?.getMainCategory()
I would guess that you're receiving an error from Retrofit about sending requests from the main thread.
You have too many wrappers withContext(), you don't need them.
You shouldn't use GlobalScope.launch for such cases as in your SplashActivity. Just operate viewModelScope and that would be enough.
Add a brief error handling. result!!.categoriesitems will always fail if your server returns any http error or there is no internet connection
Please also take a look here. It has a great explanation about using scopes and creating coroutines.
var repository is declared but not instantiated.
class CategoryViewModel: ViewModel(){
/*
*Declared but not instantiated
*/
var repository:Repository?=null
var mainCategoryList:MutableLiveData<ArrayList<CategoryItems>?>?=MutableLiveData()
suspend fun getMainCategory(){
viewModelScope.launch(Dispatchers.IO) {
val result= withContext(Dispatchers.Main){
repository?.getMainCategory()
}
mainCategoryList!!.value = result!!.categoriesitems as ArrayList<CategoryItems>?
}
}
}

Return a String inside the companion object function inside the CoroutineScope

In the MainActivity in have a companion object function which consumes the variable outside the function. In the function I would like to return the data as string inside the CoroutineScope. Here is the code:-
class MainActivity : AppCompatActivity() {
private var data = “myName”
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val data = getMyOuterValue()
Toast.makeText(this#MainActivity, data, Toast.LENGTH_SHORT).show()
}
init {
instance = this
}
companion object {
private var instance: MainActivity? = null
fun getMyOuterValue() : String = CoroutineScope(Main).launch {
instance?.data.toString()
}.toString()
}
}
Note inside the function “getMyOuterValue” i would like to return the string but it returns the CoroutineScope object. Kindly assist
fun getMyOuterValue() : String = CoroutineScope(Main).launch
Here you try to force the function to return a String while it's expected to return a Coroutine Job
And you force that with .toString() to avoid type mismatch.
Regardless the purpose of this setup, if you want to return a value from inside a coroutine; you can use async builder instead; and this requires to use a suspend function to utilize the await() method.
Your function should be:
suspend fun getMyOuterValue() = CoroutineScope(Main).async {
return#async instance?.data
}.await()
And as a suspend fun, the call to it must be from a coroutine:
CoroutineScope(Main).launch {
val data = getMyOuterValue()
Toast.makeText(this#MainActivity, data, Toast.LENGTH_SHORT).show()
}
The problem is you are not returning it correctly and not waiting for the Coroutine to complete. try this
fun getMyOuterValue() = CoroutineScope(Dispatchers.IO).async {
return#async instance?.data.toString()
}
in oncreate()
CoroutineScope(Dispatchers.IO).launch {
val data = getMyOuterValue()
data.await()
}

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.

LiveData lazy init with coroutines not working

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
}
}

suspendCancellableCoroutine returns CompletedWithCancellation instead of the actual type

I ran into a weird issue that manifested itself when I updated the kotlinx-coroutines-core dependency from 1.3.2 to 1.3.3. However, the self-contained example below reproduces the issue with 1.3.2 as well.
I have an extension method for a callback-based operation queue. This extension method uses suspendCancellableCoroutine to wrap the callback usage and to convert it to a suspend function. Now, it all works otherwise, but the resulting object that is returned from the suspending function is not of type T, but CompletedWithCancellation<T>, which is a private class of the coroutine library.
The weird thing is, if I call c.resume("Foobar" as T, {}) inside the suspendCancellableCoroutine, it works just fine. When using the callback routine, the value is a String before passing to to c.resume(), but it gets wrapped in a CompletedWithCancellation object.
Here's the code that reproduces the issue:
#ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
#SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.plant(Timber.DebugTree())
setContentView(R.layout.activity_main)
val vm = ViewModelProviders.of(this)
.get(MainViewModel::class.java)
vm.liveData.observe(this, Observer {
findViewById<TextView>(R.id.mainText).text = "Got result: $it"
})
vm.getFoo()
}
}
#ExperimentalCoroutinesApi
class MainViewModel : ViewModel() {
private val manager = OperationManager()
val liveData = MutableLiveData<String>()
fun getFoo() {
viewModelScope.launch {
val op = Operation(manager, "Foobar")
val rawResult = op.get<Any>()
Timber.d("Raw result: $rawResult")
val op2 = Operation(manager, "Foobar")
val result = op2.get<String>()
Timber.d("Casted result: $result")
liveData.postValue(result)
}
}
}
class OperationManager {
private val operationQueue = ConcurrentLinkedQueue<Operation>()
private val handler = Handler(Looper.getMainLooper())
private val operationRunnable = Runnable { startOperations() }
private fun startOperations() {
val iter = operationQueue.iterator()
while (iter.hasNext()) {
val operation = iter.next()
operationQueue.remove(operation)
Timber.d("Executing operation $operation")
operation.onSuccess(operation.response)
}
}
fun run(operation: Operation) {
addToQueue(operation)
startDelayed()
}
private fun addToQueue(operation: Operation) {
operationQueue.add(operation)
}
private fun startDelayed() {
handler.removeCallbacks(operationRunnable)
handler.post(operationRunnable)
}
}
open class Operation(private val manager: OperationManager, val response: Any) {
private val listeners = mutableListOf<OperationListener>()
fun addListener(listener: OperationListener) {
listeners.add(listener)
}
fun execute() = manager.run(this)
fun onSuccess(data: Any) = listeners.forEach { it.onResult(data) }
}
#ExperimentalCoroutinesApi
suspend fun <T> Operation.get(): T = suspendCancellableCoroutine { c ->
#Suppress("UNCHECKED_CAST")
val callback = object : OperationListener {
override fun onResult(result: Any) {
Timber.d("get().onResult() -> $result")
c.resume(result as T, {})
}
}
addListener(callback)
execute()
}
interface OperationListener {
fun onResult(result: Any)
}
Do note that just before calling c.resume(), the type of result is String, as it should be. However, it's not String in getFoo() once the suspend function completes. What causes this?
The solution was in this:
c.resume(result as T)
Instead of:
c.resume(result as T, {})
It seems that the former handles the execution of resume() correctly after getResult() is called, whereas the latter only works if resume() is called before getResult().

Categories

Resources