Use okhttp to download file and show progress bar
Okhttp also can be used to download files from the internet to the app-specific storage . In this post, download progress is shown by the progress bar. The outcome can be viewed in this link.
First, Internet permission is required in the manifest file
Then we add the dependency of Okhttp in the module’s build.gradle file. The download process is done in coroutines so kotlinx-coroutines dependency also is required
The developer of okhttp library has provided the example for download and get the progress in https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
After studying the code, I implemented it in Kotlin and modified it a bit. A class is created to handle the download. The constructor has CoroutineScope and ProgressListener as parameter. ProgressListener is declared as an inner class of this OkhttpDownload class, which responsible to update the number of bytes has been download to UI. The network connection, download and saving file is doine in this CoroutineScope.
Then we have the download function with download link and the File object of the saving destination. If the connection is successful (reaching the server), we transfer the data from the website to the device inside the function writeResponseBodyToStorage. The number of bytes has written to the device is counted and pass to the ProgressListener.
class OkhttpDownload(val scope: CoroutineScope, val progressListener: ProgressListener?) { val TAG = "DownloadWithProgress" @Throws(Exception::class) fun download(downloadLink: String, savePath: File) { val request = Request.Builder() .url(downloadLink) .build() val client = OkHttpClient.Builder() .build() scope.launch { client.newCall(request).execute().use { if (it.isSuccessful) { writeResponseBodyToStorage(it.body!!, savePath) } else throw IOException("unexpected code " + it) } } } suspend fun writeResponseBodyToStorage(body: ResponseBody, path: File){ var inputStream: InputStream? = null var outputStream: OutputStream? = null val fileSize = body.contentLength() var byteDownloaded: Long = 0 // Log.d(TAG, "file will save to ${path.absoluteFile} with size $fileSize") try { inputStream = body.byteStream() outputStream = FileOutputStream(path) val buffer = ByteArray(4 * 1024) while(true){ val byteRead = inputStream.read(buffer) if(byteRead < 0) { progressListener?.update(byteDownloaded, fileSize, true) break }else { outputStream.write(buffer, 0, byteRead) byteDownloaded += byteRead progressListener?.update(byteDownloaded, fileSize, byteRead.equals(-1)) } } outputStream.flush() Log.d(TAG, "download completed. file is saved to ${path.absoluteFile}") } catch (ex: IOException) { Log.e(TAG, "fail to save file", ex) } finally { inputStream?.close() outputStream?.close() } } interface ProgressListener{ fun update(bytesRead: Long, totalSize: Long, done: Boolean) } }
A button, a horizontal progress bar and a textview are added to the layout. The download is started by clicking the button. The progress bar and textview show the percentage of downloaded bytes.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/downloadButton" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:text="download" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/downloadButton"> <ProgressBar android:id="@+id/downloadProgressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"/> <TextView android:id="@+id/downloadProgressText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0 %" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
In the onCreate of the MainActivity. The object of ProgressListener is instantiated. Since ProgressListener.update() is invoked in a coroutinesScope in the class OkhttpDownload. The code inside update() also be executed in the same coroutineScope. The update of progress bar and text must be executed in the runOnUiThread. Otherwise, an exception is thrown because Android does not allow to modify UI components outside the UI thread.
class MainActivity : AppCompatActivity() { val TAG = "MainActivity" lateinit var job: Job lateinit var networkRequestScope: CoroutineScope lateinit var downloadProgressBar: ProgressBar lateinit var downloadProgressText: TextView val downloadLink = "https://drive.google.com/uc?export=download&id=1XSTImggtvJ8oZYIB8sEByMuZipiabCaS" val savedFileName = "test_picture1.zip" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) job = Job() networkRequestScope = CoroutineScope(job) downloadProgressBar = findViewById(R.id.downloadProgressBar) downloadProgressText = findViewById(R.id.downloadProgressText) val progressListener = object: OkhttpDownload.ProgressListener{ override fun update(bytesRead: Long, totalSize: Long, done: Boolean){ runOnUiThread { if(done){ downloadProgressBar.progress = 100 downloadProgressText.text = "100%" }else { val percentage = (((bytesRead * 100) / totalSize)) downloadProgressBar.progress = percentage.toInt() downloadProgressText.text = "${percentage.toInt()}%" } } } } val mapDirectory = File(applicationContext.cacheDir, "picture") if( !mapDirectory.exists()) mapDirectory.mkdir() val savePath = File(mapDirectory, savedFileName) val okHttpDownloadButton = findViewById(R.id.downloadButton) as Button okHttpDownloadButton.setOnClickListener{ Log.d(TAG, "click okHttpDownload") val downloadwithProgress = OkhttpDownload(networkRequestScope, progressListener) try{ downloadwithProgress.download(downloadLink, savePath) }catch (ex: Exception){ Log.d(TAG, "fail to download", ex) } } } override fun onDestroy() { super.onDestroy() job.cancel() } }
After download is completed, the file is saved at the destination.
留言
發佈留言