Last active
May 28, 2024 10:50
-
-
Save PrashamTrivedi/c7398c8f6bbf3d7cedb7919cd563302f to your computer and use it in GitHub Desktop.
Download File with progress indicator, written in Kotlin with Co-routines
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
suspend fun downloadFile(url: String, | |
downloadFile: File, | |
downloadProgressFun: (bytesRead: Long, contentLength: Long, isDone: Boolean) -> Unit) { | |
async(CommonPool) { | |
val request = with(Request.Builder()) { | |
url(url) | |
}.build() | |
val client = with(OkHttpClient.Builder()) { | |
addNetworkInterceptor { chain -> | |
val originalResponse = chain.proceed(chain.request()) | |
val responseBody = originalResponse.body() | |
responseBody?.let { | |
originalResponse.newBuilder().body(ProgressResponseBody(it, | |
downloadProgressFun)).build() | |
} | |
} | |
}.build() | |
try { | |
val execute = client.newCall(request).execute() | |
val outputStream = FileOutputStream(downloadFile) | |
val body = execute.body() | |
body?.let { | |
with(outputStream) { | |
write(body.bytes()) | |
close() | |
} | |
} | |
} catch (e: Exception) { | |
e.printStackTrace() | |
} | |
} | |
} | |
//Coroutines and callbacks do not fit nice... So this function. | |
suspend fun downloadFile(url: String, downloadFile: File): Channel<ApiResponse<FileProgress>> { | |
val channel = Channel<ApiResponse<FileProgress>>() | |
async(CommonPool) { | |
val request = with(Request.Builder()) { | |
url(url) | |
}.build() | |
val client = with(OkHttpClient.Builder()) { | |
addNetworkInterceptor { chain -> | |
val originalResponse = chain.proceed(chain.request()) | |
if (originalResponse.isSuccessful) { | |
val responseBody = originalResponse.body() | |
responseBody?.let { | |
val progressResponseBody = ProgressResponseBody(it) { bytesRead, contentLength, isDone -> | |
launch(UI) { | |
channel.send(SuccessData(FileProgress(bytesRead = bytesRead, | |
contentLength = contentLength, | |
isDone = isDone))) | |
if (isDone) { | |
channel.close() | |
} | |
} | |
} | |
originalResponse.newBuilder().body(progressResponseBody).build() | |
} | |
} else { | |
launch(UI) { | |
channel.send(ApiError(errorCode = originalResponse.code(), | |
errorMessage = getCommonErrorMessage(originalResponse.code()))) | |
channel.close() | |
downloadFile.deleteRecursively() | |
} | |
originalResponse | |
} | |
} | |
}.build() | |
try { | |
val execute = client.newCall(request).execute() | |
val outputStream = FileOutputStream(downloadFile) | |
val body = execute.body() | |
body?.let { | |
with(outputStream) { | |
write(body.bytes()) | |
close() | |
} | |
} | |
} catch (e: Exception) { | |
launch(UI) { | |
channel.send(ApiError<FileProgress>(errorMessage = e.message ?: "Something gone wrong during download")) | |
channel.close() | |
downloadFile.deleteRecursively() | |
} | |
e.printStackTrace() | |
} | |
} | |
return channel | |
} | |
fun getCommonErrorMessage(code: Int): String = when (code) { | |
400 -> "This request is wrong" | |
401 -> "You are not authorised to access this file" | |
404 -> "The file/page is not found" | |
else -> "Something gone wrong" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private fun downloadZip(zipUrl: String) { | |
launch(UI) { | |
try { | |
initProductDir() | |
val url = URL(zipUrl) | |
val fileName = url.path.substringAfterLast("/") | |
val kotlinExternalFileWriter = KotlinExternalFileWriter() | |
val createFile = if (!kotlinExternalFileWriter.isFileExists(fileName, productDir)) { | |
kotlinExternalFileWriter.createFile(fileName, parent = productDir) | |
} else { | |
File(productDir, fileName) | |
} | |
downloadFile(url = zipUrl, | |
downloadFile = createFile) { bytesRead, contentLength, isDone -> | |
launch(UI) { | |
try { | |
val percentage = (bytesRead * 100) / contentLength | |
txtProgress.text = "$percentage%" | |
progress.progress = percentage.toInt() | |
if (isDone) { | |
extractZipHere(createFile) | |
afterDataComes() | |
} | |
} catch (e: Exception) { | |
e.printStackTrace() | |
} | |
} | |
} | |
} catch (e: Exception) { | |
e.printStackTrace() | |
} | |
} | |
} | |
private fun downloadZipWithoutUrl(zipUrl: String) { | |
launch(UI) { | |
try { | |
initProductDir() | |
val fileName = "$productId.zip" | |
// val fileName = url.path.substringAfterLast("/") | |
val kotlinExternalFileWriter = KotlinExternalFileWriter() | |
val parentDirectory = productDir.parentFile | |
if (!kotlinExternalFileWriter.isFileExists(fileName, parentDirectory)) { | |
val createFile: File = kotlinExternalFileWriter.createFile(fileName, | |
parent = parentDirectory) | |
launch(UI) { | |
for (data in downloadFile(url = zipUrl, downloadFile = createFile)) { | |
when (data) { | |
is SuccessData<FileProgress> -> { | |
data.data?.let { | |
val (bytesRead, contentLength, isDone) = it | |
val percentage = (bytesRead * 100) / contentLength | |
if (isDone || percentage == 100L) { | |
progress.isIndeterminate = true | |
delay(2, SECONDS) | |
extractZipHere(createFile) { | |
afterExtract() | |
} | |
} | |
txtProgress.text = "$percentage%" | |
progress.progress = percentage.toInt() | |
} | |
} | |
is ApiError<FileProgress> -> { | |
progress.showSnackBar(data.errorMessage) | |
} | |
} | |
} | |
} | |
} else { | |
val createFile = File(parentDirectory, fileName) | |
progress.isIndeterminate = true | |
if (productDir.listFiles().isNotEmpty()) { | |
afterExtract() | |
} else { | |
extractZipHere(createFile) { | |
afterExtract() | |
} | |
} | |
} | |
} catch (e: Exception) { | |
e.printStackTrace() | |
progress.showSnackBar(e.message.toString()) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data class FileProgress(val bytesRead: Long, val contentLength: Long, val isDone: Boolean) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import okhttp3.MediaType | |
import okhttp3.ResponseBody | |
import okio.* | |
class ProgressResponseBody(val responseBody: ResponseBody, | |
val downloadProgressFun: (bytesRead: Long, contentLength: Long, isDone: Boolean) -> Unit) : ResponseBody() { | |
lateinit var bufferedSource: BufferedSource | |
override fun contentLength(): Long = responseBody.contentLength() | |
override fun contentType(): MediaType? = responseBody.contentType() | |
override fun source(): BufferedSource { | |
if (!::bufferedSource.isInitialized) { | |
bufferedSource = Okio.buffer(source(responseBody.source())) | |
} | |
return bufferedSource | |
} | |
private fun source(source: Source): Source { | |
return object : ForwardingSource(source) { | |
var totalBytesRead: Long = 0 | |
override fun read(sink: Buffer, byteCount: Long): Long { | |
val read: Long = super.read(sink, byteCount) | |
totalBytesRead += if (read != -1L) read else 0 | |
downloadProgressFun(totalBytesRead, responseBody.contentLength(), read == -1L) | |
return read | |
} | |
} | |
} | |
} |
good example
I often got file is empty problem. I found the channel should be closed after finished writing bytes to file. Moved channel.close()
down after close outputSteram fixes the problem for me.
body?.let {
with(outputStream) {
write(body.bytes())
close()
}
}
channel.close() // move to here
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice and awesome @PrashamTrivedi