Make Coroutine wait for Jsoup response in Kotlin - android

I'm kind of new developing with Android, and have problems understanding how things works. I made an app to scrape content from a page, and download elements, first with AsyncTask and worked, but since AsyncTask doesn't let me communicate with the UI Activity on progress, i decided to change to coroutines, checked an example, and the same code i used doesn't seems to work.
I used a few logs to try to determine the problem, and seems like it doesn't wait for the Jsoup request. The coroutine first calls a method scrapePage() to download the HTML and scrape the links, and then calls downloadImages() to add the links to Android's DownloadManager. In the logs Log.d("action", "Start Scraping") is printed, but Log.d("action", "Page downloaded") doesn't, still we get Log.d("action", "End") from the coroutine, which makes me think that instead of waiting for the Jsoup request to answer, it goes with an empty response, causing the rest of the code to not work correctly.
DownloadService.kt
object DownloadService {
private val parentJob = Job()
...
private val coroutineScope = CoroutineScope(Dispatchers.Main + parentJob +
coroutineExceptionHandler)
fun StartService(URL: String, location:String, contx:Context) {
coroutineScope.launch(Dispatchers.Main) {
Log.d("action", "Start")
val links = scrapePage(URL)
val download = downloadImages(links, location, contx)
Log.d("action", "End")
}
}
private suspend fun scrapePage(url: String): MainActivity.Scraped =
withContext(Dispatchers.IO) {
var URL = url
var scrape = MainActivity.Scraped()
try {
Log.d("action", "Start Scraping")
var response = Jsoup.connect(URL).get()
Log.d("action", "Page downloaded")
response.getElementsByClass("link").forEach {
/*Scrape URLs*/
Log.d("action", "Add "+link)
}
} catch (e: Exception) {
when(e) {
is HttpStatusException -> {
System.out.println(e.getStatusCode())
scrape.error = true
error = true
}
}
}
return#withContext scrape
}
...
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
...
fun makeRequest(URL : String) {
WorkingURL = URL
var uri = ""
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
uri = MediaStore.Downloads.EXTERNAL_CONTENT_URI.toString()
log.text = uri
} else {
uri = getStoragePath()
log.text = uri
}
DownloadService.StartService(URL, uri, this)
Log.d("links", DownloadService.getError().toString())
}
}
I am not sure where the problem is, nor where to start searching. I know the code for the Scraping works, because i used it before with AsyncTask, so the problem seems to be passing it to coroutines.

Here Jsoup.connect(URL).get() is throwing an error. so, Log.d("action", "Page downloaded") is not called.
But since you are handling the exception, the code runs the catch part and completes the suspend function and moves on to downloadImages().
Solution
First, add a log in the catch part of scrapePage() function and find out what is causing the exception. Everything else in your code is good.

Related

Android Health Connect freezing when making a request after 15 seconds

I am using Health Connect to read records, like steps and exercises. I use Health Connect in a few different places in Kotlin, and the code generally looks something like:
suspend fun fetchStepData(
healthConnectClient: HealthConnectClient,
viewModel: StepViewViewModel,
): StepViewViewModel {
kotlin.runCatching {
val todayStart = Instant.now().atZone(ZoneId.systemDefault()).toLocalDate().atStartOfDay();
val response: ReadRecordsResponse<StepsRecord>
try {
response = healthConnectClient.readRecords(
ReadRecordsRequest(
StepsRecord::class,
timeRangeFilter = TimeRangeFilter.after(todayStart)
)
)
var steps: Long = 0;
if (response.records.isNotEmpty()) {
for (stepRecord in response.records) {
steps += stepRecord.count
}
}
return viewModel
} catch (e: Exception) {
Log.e("StepUtil", "Unhandled exception, ", e)
}
}
return viewModel
}
I have an update function that is run when focus changes to ensure that the app is in the foreground.
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
binding.root.invalidate()
val fragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main)?.childFragmentManager?.primaryNavigationFragment
if (fragment is MyFragment) {
displayLoadingIndicator(true)
runBlocking {
if (fragment.fetchStepData(this#MainActivity.healthConnectClient, fragment.getViewModel()) != null) {
displayLoadingIndicator(false)
}
}
}
I have a loading indicator present when I am fetching the data.
I use a drawer, and if I wait about 15 seconds and press the drawer button corresponding with MyFragment, the application hangs on the loading indicator, never successfully dismissing it.
I've tried stepping through the application in debug mode, and as I do, I always hang on
response = healthConnectClient.readRecords(
ReadRecordsRequest(
StepsRecord::class,
timeRangeFilter = TimeRangeFilter.after(todayStart)
)
)
in fetchStepData. I did at one point have my application making multiple requests for HealthConnectClient.getOrCreate(context), but I have since consolidated them to one instantiation call. I'm thinking I may be reading the data wrong and maybe I need to use getChanges, or maybe I'm being rate limited. Does anyone have any insight? Thanks in advance!

Kotlin coroutine does not run synchronously

In all cases that I have been using corrutines, so far, it has been executing its "lines" synchronously, so that I have been able to use the result of a variable in the next line of code.
I have the ImageRepository class that calls the server, gets a list of images, and once obtained, creates a json with the images and related information.
class ImageRepository {
val API_IMAGES = "https://api.MY_API_IMAGES"
suspend fun fetch (activity: AppCompatActivity) {
activity.lifecycleScope.launch() {
val imagesResponse = withContext(Dispatchers.IO) {
getRequest(API_IMAGES)
}
if (imagesResponse != null) {
val jsonWithImagesAndInfo = composeJsonWithImagesAndInfo(imagesResponse)
} else {
// TODO Warning to user
Log.e(TAG, "Error: Get request returned no response")
}
...// All the rest of code
}
}
}
Well, the suspend function executes correctly synchronously, it first makes the call to the server in the getRequest and, when there is response, then composes the JSON. So far, so good.
And this is the call to the "ImageRepository" suspension function from my main activity:
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { neoRepository.fetch(this#MainActivity) }
Log.i(TAG, "After suspend fun")
}
The problem is that, as soon as it is executed, it calls the suspension function and then displays the log, obviously empty. It doesn't wait for the suspension function to finish and then display the log.
Why? What am I doing wrong?
I have tried the different Dispatchers, etc, but without success.
I appreciate any help.
Thanks and best regards.
It’s because you are launching another coroutine in parallel from inside your suspend function. Instead of launching another coroutine there, call the contents of that launch directly in your suspend function.
A suspend function is just like a regular function, it executes one instruction after another. The only difference is that it can be suspended, meaning the runtime environment can decide to halt / suspend execution to do other work and then resume execution later.
This is true unless you start an asynchronous operation which you should not be doing. Your fetch operation should look like:
class ImageRepository {
suspend fun fetch () {
val imagesResponse = getRequest(API_IMAGES)
if (imagesResponse != null) {
val jsonWithImagesAndInfo = composeJsonWithImagesAndInfo(imagesResponse)
} else {
// TODO Warning to user
Log.e(TAG, "Error: Get request returned no response")
}
... // All the rest of code
}
}
-> just like a regular function. Of course you need to all it from a coroutine:
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { neoRepository.fetch() }
Log.i(TAG, "After suspend fun")
}
Google recommends to inject the dispatcher into the lower level classes (https://developer.android.com/kotlin/coroutines/coroutines-best-practices) so ideally you'd do:
val neoRepository = ImageRepository(Dispatchers.IO)
lifecycleScope.launch {
val result = neoRepository.fetch()
Log.i(TAG, "After suspend fun")
}
class ImageRepository(private val dispatcher: Dispatcher) {
suspend fun fetch () = withContext(dispatcher) {
val imagesResponse = getRequest(API_IMAGES)
if (imagesResponse != null) {
val jsonWithImagesAndInfo = composeJsonWithImagesAndInfo(imagesResponse)
} else {
// TODO Warning to user
Log.e(TAG, "Error: Get request returned no response")
}
... // All the rest of code
}
}

How to run function before running other?

Hey there i m trying to post a data to my rest api so the idea is to upload the picture to the firebase storage then get the link of the picture uploaded then excute the function that post to my RESTAPI
The POST method
fun addItem(imageUri: Uri, title: String, description: String) {
isItemAdditionFinished.value = false
runBlocking {
Log.d(PACKAGE_NAME_LOG,"Blocking cuntion")
uploadPicture(imageUri)
}
viewModelScope.launch {
if (repository.addItem(title, description, imageUrl.value).code() == 201) {
Log.d(PACKAGE_NAME_LOG, isItemAdditionFinished.toString())
isItemAdditionFinished.value = true
}
}
}
the upload picture function to firebase
fun uploadPicture(imageUri: Uri) {
val imageName = storageReference.child("image" + imageUri.pathSegments)
imageName.putFile(imageUri).addOnSuccessListener {
imageName.downloadUrl.addOnSuccessListener {
Log.d(PACKAGE_NAME_LOG, it.toString())
imageUrl.value = it.toString()
}
}
}
the runblocking didn't work and the rest api save the title and description value only
i want it to wait until the pictue uploaded then post it
For runBlocking to work(e.g block the execution). You need to have a suspending function inside it.
At the moment you're just calling a normal function which does not suspend the coroutine.
So, Instead of fun uploadPicture you need to have suspend fun uploadPicture.
This should work.
Apart from that if you next function depends on the result of the first function. I'll suggest you use async/await for more cleaner code.
https://levelup.gitconnected.com/kotlin-coroutines-async-await-part-3-6108bf6be5c4
https://joebirch.co/android/using-firebase-on-android-with-kotlin-coroutines/

android - kotlin - mvvm - posting data to webservice

I want to post some data to webservice and get the result . this is my code :
fab.setOnClickListener {
viewModel.newBimeGozar(name)
.observe(this#BimeGozarAct, Observer {
dialogbimegozarNew?.hidePg()
})
}
this is my viewmodel :
class BimeNewViewModel:ViewModel() {
private val repository=BimeNewRepository()
fun newBimeGozar(name: String): MutableLiveData<StatModel> {
return repository.newBimeGozar(name)
}
this is my repository :
fun newBimeShode(
name: String
): MutableLiveData<StatModel> {
scope.launch {
val request = api.newBimeShode(name)
withContext(Dispatchers.Main) {
try {
val response = request.await()
regBimeshodeLiveData.value = response
} catch (e: HttpException) {
Log.v("this", e.message);
} catch (e: Throwable) {
Log.v("this", e.message);
}
}
}
return regBimeshodeLiveData;
}
it works fine but there is a problem . I think the observer keeps running and if the result's answer is an error and user press fab button again , it creates a new observer and after this , it returns two value , the first value is the first run and the second value is the second run
how can I fix this ? what is the correct way for submitting forms ?
If your problem is because of LiveData, you should use SingleLiveEvent like as follow
// For first article
val _liveData = MutableLiveData<Event<StatModel>>()
// For second article
val _liveData = SingleLiveEvent<StatModel>()
If you do not know SingleLiveEvent, you can find it here and here.
If your problem is because of your ui element, I think the best solution is to disable the submit button after submitting for the first time.

NetworkOnMainThreadException with URL().readText() in kotlin coroutines

I'm working in android studio, and am using kotlin coroutines to retrieve results from an API.
I need to wait til the coroutine is finished, so I can assign a global variable from it.
I've tested the URL and it's OK.
I've tried regular threads, which works, but am not able to make the main thread wait for it to finish.
I tried with Fuel.get() and it worked fine, but would like to use URL().
var response = "";
val req = "url.com"
runBlocking { launch {
response = URL(req).readText()
} }
Can anyone tell me why this code doesn't work? It throws an NetworkOnMainThreadException, but it's wrapped in a coroutine.
I fixed it. Ended up using a AsyncTask to read from URL, and a Handler to schedule handling the results.
var response = ""
#SuppressLint("StaticFieldLeak")
inner class Retriever : AsyncTask<String, String, String>() {
override fun doInBackground(vararg args : String?): String {
val urlRequest = args[0].toString()
var urlResponse = "";
//Try to extract url
try {
urlResponse = URL(urlRequest).readText()
println("SUCCESS in Retrieve.")
} catch (e : Exception) {
println("EXCEPTION in Retrieve.")
e.printStackTrace()
}
return urlResponse;
}
//Assigns value to response
override fun onPostExecute(result: String?) {
response = result.toString() //Result possibly void type
}
}
override fun onCreate() {
Retriever().execute("url.com")
Handler().{/*Handle response here*/, 10000)
}
Try this way
var response = "";
val req = "url.com"
runBlocking<Unit> {
GlobalScope.launch {
response = URL(req).readText()
}
//Work with the response here
}
You can preview all coroutine documentation here
If you just want handle the result of URL(req).readText() in another thread,do as following codes.
var response = "";
val req = "url.com"
runBlocking<Unit> {
GlobalScope.launch {
response = URL(req).readText()
//here is another thread,handle response here
}
//here is main thread, you can't get the result of URL(req).readText() because io operation need a long time .
}
If you handle the result in the main thread , please use Hanlder class
For me the below code works smoothly.
var response = "";
val req = "yoururl.com"
runBlocking {
try {
withTimeout(5000) { // 5 seconds for timeout
launch(Dispatchers.IO) { // using IO Dispatcher and not the default
response = URL(req).readText()
} // launch
} // timeout
} catch (e:Exception) { // Timeout, permission, URL or network error
response="Error" // Here one uses a not valid message
}
// Here one manages 'response' variable for error handling or valid output.
It's also important add the permission in AndroidManifest.xml, inside main manifest tag:
<uses-permission android:name="android.permission.INTERNET"/>
An advantage of this type of approach (for tasks that are not too long ) is simplicity because it does not require the use of a callback routine, since it is a sequential code.

Categories

Resources