-
-
Notifications
You must be signed in to change notification settings - Fork 228
Refactor PdfDownloader to use coroutines and OkHttp's async API #233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This change refactors the `PdfDownloader` to use Kotlin coroutines and OkHttp's asynchronous `enqueue` method instead of the synchronous `execute` method. This makes the download process non-blocking and allows for better cancellation handling. The `makeNetworkRequest` function in `PdfDownloader` is now a suspend function and uses a new `awaitCall` extension function for `OkHttpClient` to bridge the callback-based API with coroutines. The `FileUtils.writeFile` function is also updated to be a suspend function and uses `coroutineScope` and `ensureActive` for better coroutine integration and cancellation. A new `initWithUrl` overload is added to `PdfRendererView` that accepts a `LifecycleOwner`. This allows the PDF download lifecycle to be tied to the provided `LifecycleOwner`, ensuring that downloads are properly cancelled when the lifecycle is destroyed.
This commit updates the following: - Gradle wrapper from 8.11.1 to 8.14.3 - Android Gradle Plugin from 8.10.0 to 8.12.1 - Kotlin version from 2.1.20 to 2.2.10 - Dokka plugin version from 1.9.20 to 2.0.0 - Compile SDK from 35 to 36 - JVM toolchain from 17 to 21 - Compose BOM from 2025.04.01 to 2025.08.00 (and 2025.03.00 to 2025.08.00 in app module) - Various library dependencies to their latest versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the PdfDownloader to use Kotlin coroutines and OkHttp's asynchronous API instead of synchronous operations, improving non-blocking behavior and cancellation handling.
- Converts
makeNetworkRequestandFileUtils.writeFileto suspend functions with proper coroutine integration - Adds a new
initWithUrloverload that accepts aLifecycleOwnerfor automatic lifecycle-based cancellation - Updates dependencies to newer versions including OkHttp 5.1.0, Gradle, and various Android libraries
Reviewed Changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| PdfDownloader.kt | Refactors network requests to use coroutines with async OkHttp API and proper cancellation handling |
| FileUtils.kt | Converts file writing to suspend function with coroutine scope and cancellation checks |
| PdfRendererView.kt | Adds lifecycle-aware PDF initialization method that binds download operations to LifecycleOwner |
| pdfViewer/build.gradle.kts | Updates dependencies including OkHttp, Kotlin, Android libraries and build tools |
| app/build.gradle.kts | Updates app-level dependencies to match library changes |
| gradle-wrapper.properties | Updates Gradle wrapper to version 8.14.3 |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { | ||
| override fun onCreate(owner: LifecycleOwner) { | ||
| super.onCreate(owner) | ||
| val lifecycleScope = lifecycleOwner.lifecycleScope | ||
| this@PdfRendererView.cacheStrategy = cacheStrategy | ||
| PdfDownloader( | ||
| lifecycleScope, | ||
| headers, | ||
| url, | ||
| cacheStrategy, | ||
| PdfDownloadCallback( | ||
| context, | ||
| onStart = { | ||
| statusListener?.onPdfLoadStart() | ||
| }, | ||
| onProgress = { progress, current, total -> | ||
| statusListener?.onPdfLoadProgress(progress, current, total) | ||
| }, | ||
| onSuccess = { | ||
| try { | ||
| initWithFile(it, cacheStrategy) | ||
| statusListener?.onPdfLoadSuccess(it.absolutePath) | ||
| } catch (e: Exception) { | ||
| statusListener?.onError(e) | ||
| } | ||
| }, | ||
| onError = { | ||
| statusListener?.onError(it) | ||
| } | ||
| )).start() | ||
| } | ||
|
|
||
| override fun onDestroy(owner: LifecycleOwner) { | ||
| super.onDestroy(owner) | ||
| lifecycleOwner.lifecycle.removeObserver(this) | ||
| } | ||
| }) | ||
| } | ||
|
|
Copilot
AI
Sep 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lifecycle observer is added but the PDF download is started in onCreate. If the lifecycle is already past the CREATED state when this method is called, the onCreate callback will never be triggered and the PDF will never download. Consider checking the current lifecycle state and starting the download immediately if already created, or use a different lifecycle event like onStart.
| lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { | |
| override fun onCreate(owner: LifecycleOwner) { | |
| super.onCreate(owner) | |
| val lifecycleScope = lifecycleOwner.lifecycleScope | |
| this@PdfRendererView.cacheStrategy = cacheStrategy | |
| PdfDownloader( | |
| lifecycleScope, | |
| headers, | |
| url, | |
| cacheStrategy, | |
| PdfDownloadCallback( | |
| context, | |
| onStart = { | |
| statusListener?.onPdfLoadStart() | |
| }, | |
| onProgress = { progress, current, total -> | |
| statusListener?.onPdfLoadProgress(progress, current, total) | |
| }, | |
| onSuccess = { | |
| try { | |
| initWithFile(it, cacheStrategy) | |
| statusListener?.onPdfLoadSuccess(it.absolutePath) | |
| } catch (e: Exception) { | |
| statusListener?.onError(e) | |
| } | |
| }, | |
| onError = { | |
| statusListener?.onError(it) | |
| } | |
| )).start() | |
| } | |
| override fun onDestroy(owner: LifecycleOwner) { | |
| super.onDestroy(owner) | |
| lifecycleOwner.lifecycle.removeObserver(this) | |
| } | |
| }) | |
| } | |
| val startDownload = { | |
| val lifecycleScope = lifecycleOwner.lifecycleScope | |
| this@PdfRendererView.cacheStrategy = cacheStrategy | |
| PdfDownloader( | |
| lifecycleScope, | |
| headers, | |
| url, | |
| cacheStrategy, | |
| PdfDownloadCallback( | |
| context, | |
| onStart = { | |
| statusListener?.onPdfLoadStart() | |
| }, | |
| onProgress = { progress, current, total -> | |
| statusListener?.onPdfLoadProgress(progress, current, total) | |
| }, | |
| onSuccess = { | |
| try { | |
| initWithFile(it, cacheStrategy) | |
| statusListener?.onPdfLoadSuccess(it.absolutePath) | |
| } catch (e: Exception) { | |
| statusListener?.onError(e) | |
| } | |
| }, | |
| onError = { | |
| statusListener?.onError(it) | |
| } | |
| )).start() | |
| } | |
| if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) { | |
| startDownload() | |
| } else { | |
| lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { | |
| override fun onCreate(owner: LifecycleOwner) { | |
| super.onCreate(owner) | |
| startDownload() | |
| } | |
| override fun onDestroy(owner: LifecycleOwner) { | |
| super.onDestroy(owner) | |
| lifecycleOwner.lifecycle.removeObserver(this) | |
| } | |
| }) | |
| } |
|
Thanks for the solid refactor! 🙌 The coroutine bridge and lifecycle-aware API are definitely the right direction, and this will make downloads more robust and cancellable. A couple of things I’d like to see before merging:
Other than that, the direction looks great. Once we fix the coroutine wiring and add the cleanup guards, I think this will be ready to go 🚀 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.
|
|
||
| lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { | ||
| override fun onCreate(owner: LifecycleOwner) { | ||
| super.onCreate(owner) | ||
| val lifecycleScope = lifecycleOwner.lifecycleScope | ||
| this@PdfRendererView.cacheStrategy = cacheStrategy | ||
| PdfDownloader( | ||
| lifecycleScope, | ||
| headers, | ||
| url, | ||
| cacheStrategy, | ||
| PdfDownloadCallback( | ||
| context, | ||
| onStart = { | ||
| statusListener?.onPdfLoadStart() | ||
| }, | ||
| onProgress = { progress, current, total -> | ||
| statusListener?.onPdfLoadProgress(progress, current, total) | ||
| }, | ||
| onSuccess = { | ||
| try { | ||
| initWithFile(it, cacheStrategy) | ||
| statusListener?.onPdfLoadSuccess(it.absolutePath) | ||
| } catch (e: Exception) { | ||
| statusListener?.onError(e) | ||
| } | ||
| }, | ||
| onError = { | ||
| statusListener?.onError(it) | ||
| } | ||
| )).start() | ||
| } | ||
|
|
Copilot
AI
Sep 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The download is started in onCreate, but this may execute multiple times if the lifecycle owner goes through multiple create/destroy cycles. The download should be started immediately when initWithUrl is called, not deferred to onCreate. Consider starting the download directly and using lifecycle observer only for cleanup.
| lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { | |
| override fun onCreate(owner: LifecycleOwner) { | |
| super.onCreate(owner) | |
| val lifecycleScope = lifecycleOwner.lifecycleScope | |
| this@PdfRendererView.cacheStrategy = cacheStrategy | |
| PdfDownloader( | |
| lifecycleScope, | |
| headers, | |
| url, | |
| cacheStrategy, | |
| PdfDownloadCallback( | |
| context, | |
| onStart = { | |
| statusListener?.onPdfLoadStart() | |
| }, | |
| onProgress = { progress, current, total -> | |
| statusListener?.onPdfLoadProgress(progress, current, total) | |
| }, | |
| onSuccess = { | |
| try { | |
| initWithFile(it, cacheStrategy) | |
| statusListener?.onPdfLoadSuccess(it.absolutePath) | |
| } catch (e: Exception) { | |
| statusListener?.onError(e) | |
| } | |
| }, | |
| onError = { | |
| statusListener?.onError(it) | |
| } | |
| )).start() | |
| } | |
| // Start the download immediately | |
| val lifecycleScope = lifecycleOwner.lifecycleScope | |
| this.cacheStrategy = cacheStrategy | |
| PdfDownloader( | |
| lifecycleScope, | |
| headers, | |
| url, | |
| cacheStrategy, | |
| PdfDownloadCallback( | |
| context, | |
| onStart = { | |
| statusListener?.onPdfLoadStart() | |
| }, | |
| onProgress = { progress, current, total -> | |
| statusListener?.onPdfLoadProgress(progress, current, total) | |
| }, | |
| onSuccess = { | |
| try { | |
| initWithFile(it, cacheStrategy) | |
| statusListener?.onPdfLoadSuccess(it.absolutePath) | |
| } catch (e: Exception) { | |
| statusListener?.onError(e) | |
| } | |
| }, | |
| onError = { | |
| statusListener?.onError(it) | |
| } | |
| ) | |
| ).start() | |
| // Add observer only for cleanup | |
| lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { |
| suspend fun writeFile(inputStream: InputStream, file: File, totalLength: Long, onProgress: (Long) -> Unit) = | ||
| coroutineScope { |
Copilot
AI
Sep 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The function signature uses an expression body but the implementation spans multiple lines. Consider using a block body for better readability and consistency with the multi-line implementation.
This change refactors the
PdfDownloaderto use Kotlin coroutines and OkHttp's asynchronousenqueuemethod instead of the synchronousexecutemethod. This makes the download process non-blocking and allows for better cancellation handling.The
makeNetworkRequestfunction inPdfDownloaderis now a suspend function and uses a newawaitCallextension function forOkHttpClientto bridge the callback-based API with coroutines.The
FileUtils.writeFilefunction is also updated to be a suspend function and usescoroutineScopeandensureActivefor better coroutine integration and cancellation.A new
initWithUrloverload is added toPdfRendererViewthat accepts aLifecycleOwner. This allows the PDF download lifecycle to be tied to the providedLifecycleOwner, ensuring that downloads are properly cancelled when the lifecycle is destroyed.