Search code examples
androidfilesaveretrofit2kotlin-coroutines

Download file from url using Retrofit and Kotlin coroutines


I have below approach to download the pdf file from the url.

My ApiService.kt

interface ApiService {

    
    @GET("8d370189-4ec8-11ec-8469-005056ae4067/Aktionsprospekt-06-12-2021-11-12-2021-06.pdf?_ga=2.119177993.1645728632.1638630355-145806302.1638630355")
    suspend fun getData(): Response<ResponseBody>
}

My ApiClient.kt

object ApiClient {
    fun getClient(): ApiService {
        return Retrofit.Builder().baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create()).build()
            .create(ApiService::class.java)
    }
}

In ViewModel I have called coroutine scope as below

class MainActivityViewModel(private val apiService: ApiService, private val context: Context) : ViewModel() {

    fun downloadFile() = viewModelScope.launch {
        val responseBody = apiService.getData().body()
        saveFile(responseBody)
    }

    private fun saveFile(body: ResponseBody?) : String {
        if (body == null) {
            return ""
        }

        var input: InputStream? = null
        try{
            input = body.byteStream()
            val path = context.getExternalFilesDir(null)!!.path
            val fos = FileOutputStream(path)
            fos.use { output ->
                val buffer = ByteArray(4 * 1024) // or other buffer size
                var read: Int
                while (input.read(buffer).also { read = it } != -1) {
                    output.write(buffer, 0, read)
                }
                output.flush()
            }
            return path
        } catch(e: Exception) {
            Log.e("saveFile",e.toString())
        }
        finally {
            input?.close()
        }
        return ""
    }

}

In Main Activity I have called above function of downloadFile() as below:

val apiService: ApiService = ApiClient.getClient()
        viewModel = getViewModel(apiService)
        downloadBtn.setOnClickListener {
            viewModel.downloadFile()
        }

I am getting below error in saveFile() function

2021-12-05 02:50:04.023 7027-7027/com.mms.compareandchoose E/saveFile: java.io.FileNotFoundException: /storage/emulated/0/Android/data/com.mms.compareandchoose/files (Is a directory)
2021-12-05 02:50:04.055 7027-7027/com.mms.compareandchoose E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.mms.compareandchoose, PID: 7027
    android.os.NetworkOnMainThreadException
        at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1303)
        at com.android.org.conscrypt.Platform.blockGuardOnNetwork(Platform.java:300)
        at com.android.org.conscrypt.OpenSSLSocketImpl$SSLOutputStream.write(OpenSSLSocketImpl.java:839)
        at okio.OutputStreamSink.write(JvmOkio.kt:53)
        at okio.AsyncTimeout$sink$1.write(AsyncTimeout.kt:103)
        at okio.RealBufferedSink.flush(RealBufferedSink.kt:247)
        at okhttp3.internal.http2.Http2Writer.rstStream(Http2Writer.kt:135)
        at okhttp3.internal.http2.Http2Connection.writeSynReset$okhttp(Http2Connection.kt:354)
        at okhttp3.internal.http2.Http2Stream.close(Http2Stream.kt:240)
        at okhttp3.internal.http2.Http2Stream.cancelStreamIfNecessary$okhttp(Http2Stream.kt:506)
        at okhttp3.internal.http2.Http2Stream$FramingSource.close(Http2Stream.kt:488)
        at okio.ForwardingSource.close(ForwardingSource.kt:34)
        at okhttp3.internal.connection.Exchange$ResponseBodySource.close(Exchange.kt:309)
        at okio.RealBufferedSource.close(RealBufferedSource.kt:498)
        at okio.ForwardingSource.close(ForwardingSource.kt:34)
        at okio.RealBufferedSource.close(RealBufferedSource.kt:498)
        at okio.RealBufferedSource$inputStream$1.close(RealBufferedSource.kt:170)
        at com.mms.compareandchoose.models.MainActivityViewModel.saveFile(MainActivityViewModel.kt:46)
        at com.mms.compareandchoose.models.MainActivityViewModel$downloadFile$1.invokeSuspend(MainActivityViewModel.kt:20)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6776)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1518)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1408)

Can Anybody say why this error is there?? Also advise if this approach is good or not??


Solution

  • You read data from remote stream when you call input.read(buffer). But viewModelScope launches code on the Main thread by default. To fix the exception you should explicitly specify that you perform the download on another (IO) thread.

    fun downloadFile() = viewModelScope.launch(Dispatchers.IO) {
            val responseBody = apiService.getData().body()
            saveFile(responseBody)
    }