I've an app that is have 3 GlobalScopes:
First reading a stream from url, and return InputStream
Second, start after the completion of the first one, save the InputStream in the device, and return the saved file uri
Third, start after the completion of the second one, and do some processing with the file
I've something wrong in the second scope, as the file is not saved, my full code is below:
MainActivity
package com.example.dl
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.example.dl.databinding.ActivityMainBinding
import kotlinx.coroutines.*
import java.io.File
import java.io.InputStream
import java.net.URL
import java.util.*
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
// setContentView(R.layout.activity_main)
val context = this
val urlFile:URL = URL( "https://drive.google.com/uc?export=download&id="+
"1kRtYw3_Yd7he0HjbgNlAAl9we9tQEGvm")
// show image url in text view
binding.tvDownload.text = urlFile.toString()
val tag = "Main Activity"
Log.i(tag, "Trying t get stream")
binding.button.setOnClickListener {
it.isEnabled = false // disable button
binding.progressBar.visibility = View.VISIBLE
// GlobalScope 1
// async task to get / download bitmap from url
val result: Deferred<InputStream?> = GlobalScope.async {
urlFile.toStream(context)
}
// GlobalScope 2
val saved: Deferred<Uri?> = GlobalScope.async {
// get the downloaded bitmap
val fileStream : InputStream? = result.await()
// if downloaded then saved it to internal storage
Log.i(tag, "Stream collected, trying to save it") // <--- This is printing on the LogCat
fileStream?.saveToInternalStorage(context) // <-- This looks not to be executed!
}
// GlobalScope 3
GlobalScope.launch(Dispatchers.Main) {
val savedUri : Uri? = saved.await() // <-- This looks not to be executed!
Log.i(tag, "Stream saved")
val execFile = File(savedUri.toString())
Log.i(tag, "Setting file executable")
// execFile?.setExecutable(true)
Log.i(tag, "Running executable file")
// Runtime.getRuntime().exec(savedUri.toString())
// display saved bitmap to image view from internal storage
binding.imageView.setImageURI(savedUri)
// show bitmap saved uri in text view
binding.tvSaved.text = savedUri.toString()
it.isEnabled = true // enable button
binding.progressBar.visibility = View.INVISIBLE
}
}
}
}
The function that is running in the first scope is:
package com.example.dl
import android.content.Context
import android.util.Log
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
// extension function to get / download bitmap from url
fun URL.toStream(context : Context): InputStream? {
return try {
val tag = "Getting stream"
Log.i(tag, "Reading the stream from the web")
//this is the name of the local file you will create
val u = URL(this.toString())
val c = u.openConnection() as HttpURLConnection
c.requestMethod = "GET"
c.doOutput = true
c.connect()
c.inputStream
} catch (e: IOException){
null
}
}
The function that is running in the second scope, whihc looks to be no reached or not working properly, is:
package com.example.dl
import android.content.Context
import android.content.ContextWrapper
import android.net.Uri
import android.util.Log
import android.widget.Toast
import java.io.*
// extension function to save an image to internal storage
fun InputStream.saveToInternalStorage(context: Context): Uri?{
val tag = "Saving stream"
Log.i(tag, "Saving the stream from the web")
val targetFileName: String? = "server"
val wrapper = ContextWrapper(context)
var file = wrapper.getDir("images", Context.MODE_PRIVATE)
// create a file to save the downloaded one
file = File(file, targetFileName)
// get the file output stream
val stream: OutputStream = FileOutputStream(file)
Toast.makeText(context, "downloading", Toast.LENGTH_LONG).show()
var len1 = 0
return try {
// this.copyTo(stream)
var size: Long = 0
val buffer = ByteArray(1024)
Log.i(tag, "stream size ${this.readBytes().size}")
while (this.read(buffer).also { len1 = it } > 0) {
stream.write(buffer, 0, len1)
size += len1;
Log.i(tag, "file saved $size")
}
// flush the stream
stream.flush()
// close stream
stream.close()
this.close()
Log.i(tag, "file saved")
// compress bitmap
// compress(Bitmap.CompressFormat.JPEG, 100, stream)
// return the saved image uri
Uri.parse(file.absolutePath)
} catch (e: IOException){ // catch the exception
e.printStackTrace()
null
}
}
UPDATE
Updated my code based on the commint given about GlobalScope, still getting same result
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
// setContentView(R.layout.activity_main)
val context = this
val urlFile:URL = URL( "https://drive.google.com/uc?export=download&id="+
"1kRtYw3_Yd7he0HjbgNlAAl9we9tQEGvm")
// show image url in text view
binding.tvDownload.text = urlFile.toString()
binding.button.setOnClickListener {
it.isEnabled = false // disable button
binding.progressBar.visibility = View.VISIBLE
// DownloadExe(context)
val tag = "Main Activity"
Log.i(tag, "Trying t get stream")
runBlocking {
Log.i(tag, "Collect stream")
val download = async(context = Dispatchers.IO) { urlFile.toStream(context) }
val fileStream : InputStream? = download.await()
Log.i(tag, "Stream collected, trying to save it")
val save = async(context = Dispatchers.IO) {
fileStream?.saveToInternalStorage(context) // <-- Not working
}
val savedUri : Uri? = save.await()
Log.i(tag, "Stream saved, trying to get path")
val execFile = File(savedUri.toString())
Log.i(tag, "Setting file executable")
// execFile?.setExecutable(true)
Log.i(tag, "Running executable file")
// Runtime.getRuntime().exec(savedUri.toString())
// display saved bitmap to image view from internal storage
// binding.imageView.setImageURI(savedUri)
// show bitmap saved uri in text view
binding.tvSaved.text = savedUri.toString()
it.isEnabled = true // enable button
binding.progressBar.visibility = View.INVISIBLE
}
}
}
}
I got in the catlog:
I/Choreographer: Skipped 632 frames! The application may be doing too much work on its main thread.
So, I changed my code to:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
// setContentView(R.layout.activity_main)
val context = this
val urlFile:URL = URL( "https://drive.google.com/uc?export=download&id="+
"1kRtYw3_Yd7he0HjbgNlAAl9we9tQEGvm")
// show image url in text view
binding.tvDownload.text = urlFile.toString()
binding.button.setOnClickListener {
it.isEnabled = false // disable button
binding.progressBar.visibility = View.VISIBLE
// DownloadExe(context)
val tag = "Main Activity"
Log.i(tag, "Trying t get stream")
var savedUri : Uri?
object : Thread() {
override fun run() {
runBlocking {
Log.i(tag, "Collect stream")
val download = async(context = Dispatchers.IO) { urlFile.toStream(context) }
val fileStream : InputStream? = download.await()
Log.i(tag, "Stream collected, trying to save it")
val save = async(context = Dispatchers.IO) {
fileStream?.saveToInternalStorage(context) // <-- Not working
}
savedUri = save.await()
Log.i(tag, "Stream saved, trying to get path")
val execFile = File(savedUri.toString())
Log.i(tag, "Setting file executable")
// execFile?.setExecutable(true)
Log.i(tag, "Running executable file")
// Runtime.getRuntime().exec(savedUri.toString())
// display saved bitmap to image view from internal storage
// binding.imageView.setImageURI(savedUri)
}
try {
// code runs in a thread
runOnUiThread {
// show bitmap saved uri in text view
binding.tvSaved.text = savedUri.toString()
it.isEnabled = true // enable button
binding.progressBar.visibility = View.INVISIBLE
}
} catch (ex: Exception) {
Log.i("---", "Exception in thread")
}
}
}.start()
}
}
}
But still getting the same, any my cat og is:
I/Main Activity: Trying t get stream
I/Main Activity: Collect stream
I/Getting stream: Reading the stream from the web
I/Main Activity: Stream collected, trying to save it
I/Main Activity: Stream saved, trying to get path
Setting file executable
Running executable file
W/BpBinder: Slow Binder: BpBinder transact took 227ms, interface=android.view.IWindowSession, code=5 oneway=false
W/System: A resource failed to call end.
This doesn't really solve your specific issue concretely, but I wanted to share how your use of coroutines could be cleaner, which might help track down the problem. As I mentioned in the comment, I think your URL.toStream extension function is likely returning null, so that would be the place to start with your debugging.
You don't need three separate coroutines because you have a sequence of tasks that you want to run in order. So they can be in a single coroutine. One of the main benefits of coroutines is that you can run sequential asynchronous code using a syntax that looks synchronous.
You should use lifecycleScope to run your coroutine, add it as.
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
}
One of the main points of a CoroutineScope is to provide a way to easily cancel all coroutines associated with some lifecycle of your application flow. In this case, you would want to cancel the coroutine when the Activity or Fragment goes away, so the pre-existing lifecycleScope is the natural choice. It is set up by the Android framework to automatically cancel all the coroutines it has launched if the Activity is destroyed while they are still running.
It should be extremely rare to ever use runBlocking, since it blocks the current thread while the coroutine is running, defeating the purpose of using coroutines. It's main purpose is in a JVM app's main function to make the whole app wait for all the coroutines to finish before shutting down. Or it can be used for unit tests. The only reason I can think of for it to ever appear in an Android application is if you have a module that is using Java 8 concurrency that you don't want to migrate to coroutines, but it's calling suspend functions from a library from a background thread.
lifecycleScope runs on the Main dispatcher. You only have to wrap blocking calls in withContext.
binding.button.setOnClickListener { button ->
lifecycleScope.launch {
button.isEnabled = false // disable button
binding.progressBar.visibility = View.VISIBLE
// perform these tasks off the main thread
val savedUri : Uri? = withContext(Dispatchers.IO) {
// get / download bitmap from url
val fileStream : InputStream? = urlFile.toStream(context)
Log.i(tag, "Stream $fileStream collected, trying to save it") // I put the reference in your log so if it is null you'll see.
fileStream?.saveToInternalStorage(context)
}
// You need to check for null. Otherwise savedUri.toString() gives you the String "null"
if (savedUri == null) {
Log.e(tag, "URI was null. Nothing to save")
return#launch
}
val execFile = File(savedUri.toString())
Log.i(tag, "Stream saved")
// Log.i(tag, "Setting file executable")
// execFile?.setExecutable(true)
// Log.i(tag, "Running executable file")
// Runtime.getRuntime().exec(savedUri.toString())
// display saved bitmap to image view from internal storage
binding.imageView.setImageURI(savedUri)
// show bitmap saved uri in text view
binding.tvSaved.text = savedUri.toString()
button.isEnabled = true // enable button
binding.progressBar.visibility = View.INVISIBLE
}
}
Related
I just started learning Kotlin. I am making an application that saves bank (IBAN) information. I save the information locally using sharedpreferences.
I save the information to the list and save it as sharedpreferences.
Information is recorded on page 2 and I want to list this recorded information on page 1.
I was doing this simply in Flutter, but I couldn't understand how to do it in Kotlin.
package com.example.ibansfer
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.ibansfer.databinding.ActivityAddIbanBinding
import com.example.ibansfer.models.IbanModel
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class AddIbanActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddIbanBinding
private var ibanList: ArrayList<IbanModel> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddIbanBinding.inflate(layoutInflater)
setContentView(binding.root)
setTitle("IBAN bilgisi ekle")
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true);
val sharedPreferences = this.getSharedPreferences("ibans", MODE_PRIVATE)
val editor = sharedPreferences.edit()
fun saveData() {
val gson = Gson()
val json = gson.toJson(ibanList)
editor.putString("ibans", json)
editor.apply()
}
binding.ibanSaveBttn.setOnClickListener {
val ibanOwner = binding.ibanOwner.text.toString()
val bankName = binding.bankName.text.toString()
val ibanAdress = binding.ibanAdress.text.toString()
if (ibanOwner.isEmpty() || bankName.isEmpty() || ibanAdress.isEmpty()) {
Toast.makeText(this, "Lütfen tüm alanları doldurunuz.", Toast.LENGTH_LONG).show()
} else {
ibanList.add(IbanModel(ibanOwner, bankName, ibanAdress))
saveData()
Toast.makeText(this, "IBAN bilgileri başarıyla eklendi.", Toast.LENGTH_LONG).show()
finish()
}
}
}
}
If you save the data in SharedPrefferences, you can always retrieve it back in the other screen.
val sharedPref = activity?.getPreferences("ibans", Context.MODE_PRIVATE) ?: return
val myString = sharedPref.getString("ibans", defaultValue)
You can even set up a SharedPrefferences listener that will get notified whenever new data is added to it and act accordingly.
val sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
// code to execute on changes here
}
activity?.getPreferences("ibans", Context.MODE_PRIVATE)
?.registerOnSharedPreferenceChangeListener(sharedPreferencesListener )
Also don't forget to unregister using unregisterOnSharedPreferenceChangeListener
Or alternatively, use Intents as the comment suggests to pass data between Activities/Fragments.
Error: Document references must have an even number of segments, but Users has 1
I have been looking through different posts on here and on different forums but all have the problem when first loading but my problem is after I logout or reset the password. When I load the contents from firebase I get the information but when I click on the sign out then go to login again it crash's and I get this error. I have logged the users.uid and Document references and does not change after logging out.
My collection path is done with Constants so I don't have a mis type.
I have found that the error is in the Fragment side of my app in the FirestoreClass().loadUserData_fragment(this)
As commenting this line out after the log out will allow the app to run but in the activity the data can still be loaded as the activity load data and the fragment is the same so I don't get why it wouldn't load into the fragment after the sign out but will load first time.
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
FirestoreClass().loadUserData_fragment(this)
}
Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUpdateProfileBinding.inflate(layoutInflater)
val view : LinearLayout = binding.root
setContentView(view)
setupActionBar()
FirestoreClass().loadUserData(this)
}
GetCurrentUserID
fun getCurrentUserID():String{
// auto login
var currentUser = FirebaseAuth.getInstance().currentUser
var currentUserId = ""
if (currentUser != null){
currentUserId = currentUser.uid
Log.i("uis",currentUser.uid)
}
return currentUserId
}
Activity version
fun loadUserData(activity:Activity){
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
val loggedInUser = document.toObject(User::class.java)!!
Log.i("uis",getCurrentUserID() + Constants.USERS)
when(activity){
is UpdateProfileActivity ->{
activity.setUserDataInUI(loggedInUser)
}
is LoginActivity -> {
// Call a function of base activity for transferring the result to it.
activity.userLoggedInSuccess(loggedInUser)
}
}
}
}
Fragment version
fun loadUserData_fragment(fragment: Fragment){
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
val loggedInUser = document.toObject(User::class.java)!!
Log.i("uis",getCurrentUserID() + Constants.USERS)
when(fragment){
is HomeFragment ->{
fragment.setUserDataInUIFragment(loggedInUser)
}
}
}
}
It seems that your getCurrentUserID() returns no value, which you're not handling in your code. The best option is to only call loadUserData when there is an active user, but alternatively you can also check whether getCurrentUserID() returns a value:
fun loadUserData(activity:Activity){
if (getCurrentUserID() != "") { // 👈
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
...
}
}
}
I have a Fragment and a View Model.
The layout of the Fragment contains a button.
When the button is clicked, we try to get an API response, which contains a url.
That url is used to start an intent to open a web page.
I am currently accomplishing this with event driven programming.
The button in the Fragment is clicked.
The function in the view model is called to get the API response, which contains the url.
The url in the view model is assigned as live data, which is observed in the fragment.
The fragment observes the url live data has changed. It attempts to launch the WebView with the new url.
Can the Fragment skip Observing for the url and directly get the ViewModel function to return a string?
Here is the code for the Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Set the OnClickListener
myButton.setOnClickListener {
myViewModel.getUrlQueryResults()
}
// Observables to open WebView from Url
myViewModel.myUrl.observe(viewLifecycleOwner, Observer {
it?.let{
if (it.isEmpty()) {
// No Url found in this API response
}
else {
// Open the WebView
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(it))
startActivity(intent)
}
catch (e: Exception) {
// Log the catch statement
}
}
}
})
}
Here is the code for the ViewModel:
// Live data observed in fragment. When this changes, fragment will attempt to launch Website with the url
private val _myUrl = MutableLiveData<String>()
val myUrl: LiveData<String>
get() = _myUrl
// Possible to make this return a string?
fun getUrlQueryResults() {
InfoQueryApi.retrofitService.getInfo(apiKey).enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
try {
// Store the response here
apiResponse = parseInfoJsonResult(JSONObject(response.body()!!))
// Grab the url from the response
var urlFromResponse = apiResponse?.url
if (urlFromResponse.isNullOrEmpty()) {
urlFromResponse = ""
}
// Store the urlFromResponse in the live data so Fragment can Observe and act when the value changes
_myUrl.value = urlFromResponse
} catch (e: Exception) {
// Log catch statement
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
// Log error
}
})
}
In the tutorial, they teach how to support real-time p2p command-line messaging using websockets by implementing both client and server. I'm trying to finish an exercise where I have the client input messages via a javafx gui and receive messages inside the gui in the form of a chat log (basically a chat room)
I'm having trouble simply starting up the gui and the websocket together. I tried GlobalScope.launch in hopes that both would get run, but only the GUI gets launched. If I use runBlocking instead, only the websocket is active.
Here's what I have so far.
Other issues:
Don't know how to reference the javafx label variable inside the outputMessages function, so that we can update the chatlog. I try placing the label variable in the global scope, but it only results in a compile error, so I put it back inside SAKApplication.
How to update the label field to move to the next line (tried adding "/n" but it literally added "\n")
import java.util.Queue
import java.util.LinkedList
//var a = Label("s")
val messagesToSend: Queue<String> = LinkedList<String>()
class SAKApplication : Application() {
val l = Label("no text")
override fun start(primaryStage: Stage) {
val btn = Button()
btn.text = "Say 'Hello World'"
btn.onAction = EventHandler<ActionEvent> { println("Hello World!") }
val root = StackPane()
root.children.add(btn)
val textField = TextField()
// a = l
// action event
val event: EventHandler<ActionEvent> =
EventHandler {
l.text += "/n" + textField.getText()
messagesToSend.add(textField.getText())
}
// when enter is pressed
textField.setOnAction(event)
// add textfield
root.children.add(textField)
root.children.add(l)
val scene = Scene(root, 300.0, 250.0)
if (primaryStage != null) {
primaryStage.title = "Hello World!"
primaryStage.scene = scene
primaryStage.show()
}
val client = HttpClient {
install(WebSockets)
}
GlobalScope.launch {
client.webSocket(method = HttpMethod.Get, host = "127.0.0.1", port = 8080, path = "/chat") {
while(true) {
val messageOutputRoutine = launch { outputMessages() }
val userInputRoutine = launch { inputMessages() }
userInputRoutine.join() // Wait for completion; either "exit" or error
messageOutputRoutine.cancelAndJoin()
}
}
}
client.close()
}
}
fun main(args: Array<String>) {
Application.launch(SAKApplication::class.java, *args)
}
suspend fun DefaultClientWebSocketSession.outputMessages() {
try {
for (message in incoming) {
message as? Frame.Text ?: continue
// a.text += "/n" + message.readText()
println(message.readText())
}
} catch (e: Exception) {
println("Error while receiving: " + e.localizedMessage)
}
}
suspend fun DefaultClientWebSocketSession.inputMessages() {
val name = readLine() ?: ""
send(name)
while (true) {
sleep(1)
if (messagesToSend.isEmpty()) { continue }
val message = messagesToSend.remove()
if (message.equals("exit", true)) return
try {
send(message)
} catch (e: Exception) {
println("Error while sending: " + e.localizedMessage)
return
}
}
}
I am attempting to build a library that allows an app to download a json file I provide, and then based on its contents, download images from the web. I have implemented it thus far with Kotlin Coroutines along with Ktor, but I have an issue which is evading my grasp of what to do.
This is the data class I am using to define each image:
data class ListImage(val name: String, val url: String)
The user calls an init function which downloads the new json file. Once that file is downloaded, the app needs to download a number of images as defined by the file using getImages. Then a list is populated using the data class and an adapter.
Here is the code I am using to fetch the file:
fun init(context: Context, url: String): Boolean {
return runBlocking {
return#runBlocking fetchJsonData(context, url)
}
}
private suspend fun fetchJsonData(context: Context, url: String): Boolean {
return runBlocking {
val client: HttpClient(OkHttp) {
install(JsonFeature) {}
}
val data = async {
client.get<String>(url)
}
try {
val json = data.await()
withContext(Dispatchers.IO) {
context
.openFileOutput("imageFile.json", Context.MODE_PRIVATE)
.use { it.write(json.toByteArray()) }
}
} catch (e: Exception) {
return#runBlocking false
}
}
}
This works and gets the file written locally. Then I have to get the images based on the contents of the file.
suspend fun getImages(context: Context) {
val client = HttpClient(OkHttp)
// Gets the image list from the json file
val imageList = getImageList(context)
for (image in imageList) {
val imageName = image.name
val imageUrl = image.url
runBlocking {
client.downloadFile(context, imageName, imageUrl)
.collect { download ->
if (download == Downloader.Success) {
Log.e("SDK Image Downloader", "Successfully downloaded $imageName.")
} else {
Log.i("SDK Image Downloader", "Failed to download $imageName.")
}
}
}
}
}
private suspend fun HttpClient
.downloadFile(context: Context, fileName: String, url: String): Flow<Downloader> {
return flow {
val response = this#downloadFile.request<HttpResponse> {
url(url)
method = HttpMethod.Get
}
val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0
do {
val currentRead = response.content.readAvailable(data, offset, data.size)
offset += currentRead
} while (currentRead > 0)
if (response.status.isSuccess()) {
withContext(Dispatchers.IO) {
val dataPath =
"${context.filesDir.absolutePath}${File.separator}${fileName}"
File(dataPath).writeBytes(data)
}
emit(Downloader.Success)
} else {
emit(Downloader.Error("Error downloading image $fileName"))
}
}
}
If the file is already on the device and I am not attempting to redownload it, this also works. The issue is when I try to get the file and then the images in order when the app is first run. Here is an example of how I am trying to call it:
lateinit var loaded: Deferred<Boolean>
lateinit var imagesLoaded: Deferred<Unit>
#InternalCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val context: Context = this
loaded = GlobalScope.async(Dispatchers.Default) {
init(context)
}
GlobalScope.launch { loaded.await() }
imagesLoaded = GlobalScope.async(Dispatchers.Default) {
getDeviceImages(context)
}
GlobalScope.launch { imagesLoaded.await() }
configureImageList(getImageList(context))
}
fun configureImageList(imageList: MutableList<Image>) {
val imageListAdapter = ImageListAdapter(this, imageList)
with(image_list) {
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
itemAnimator = null
adapter = imageListAdapter
}
}
This falls apart. So the two scenarios that play out are:
I run this code as-is: The file is downloaded, and ~75% of the images are downloaded before the app crashes with a java.io.IOException: unexpected end of stream on the url. So it seems that the images are starting to download before the file is fully written.
I run the app once without the image code. The file is downloaded. I comment out the file downloading code, and uncomment out the image downloading code. The images are downloaded, the app works as I want. This suggests to me that it would work if the first coroutine was actually finished before the second one started.
I have written and rewritten this code as many ways as I could think of, but I cannot get it to run without incident with both the file writing and image downloading completing successfully.
What am I doing wrong in trying to get these coroutines to complete consecutively?
I just figured it out after stumbling upon this question. It appears as though my HttpClient objects were sharing a connection instead of creating new ones, and when the server closed the connection it caused in-flight operations to unexpectedly end. So the solution is:
val client = HttpClient(OkHttp) {
defaultRequest {
header("Connection", "close")
}
}
I added this to each of the Ktor client calls and it now works as intended.