内容简介:Giving a user the ability to download files in your app can be difficult to figure out. In iOS, you can use AlamoFire to download the file locally and then present it with a UIDocumentInteractionController. (The code would look something like this.) It pre
Giving a user the ability to download files in your app can be difficult to figure out. In iOS, you can use AlamoFire to download the file locally and then present it with a UIDocumentInteractionController. (The code would look something like this.) It presents the documents, images, gifs, videos, etc. in the app for you, and you can then download to the device from there.
Unfortunately, this isn’t simple in Android because there are many OEMs for Android devices. (In-app image viewing can be handled using Glide . If you absolutely need to view PDFs in-app, you can probably find some solution, but I would recommend just giving the users download ability and letting their device handle it.) Today, I’ll explain how to download files in Kotlin using Ktor and intents.
Initial Setup
The first thing you will need are some dependencies, Ktor , and coroutines. Add these to your app Gradle. Ktor allows for asynchronous communication, which is very useful for file downloading and reporting file progress.
dependencies { ... implementation "io.ktor:ktor-client-android:1.2.5" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' ... }
Then we need to add this file into res.xml. (You’ll likely need to create the xml resources folder.) It adds an external path.
<paths> <external-path name="external_files" path="."/> </paths>
In the AndroidManifest, make sure to add these permissions:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Add the provider. This uses the external path we defined above for the FileProvider.
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/external_files"/> </provider>
Downloader Coroutine
The coroutine for downloading files will be an extension on Ktor’s HttpClient. First, we need a class to return during the coroutine to report on the status of the download.
sealed class DownloadResult { object Success : DownloadResult() data class Error(val message: String, val cause: Exception? = null) : DownloadResult() data class Progress(val progress: Int): DownloadResult() }
This extension creates a coroutine that takes an output stream and URL. While the file is read in, the current progress is emitted. Once finished, the data is written to the output stream, and success is returned. Otherwise, there was some failure.
suspend fun HttpClient.downloadFile(file: OutputStream, url: String): Flow<DownloadResult> { return flow { try { val response = call { url(url) method = HttpMethod.Get }.response val data = ByteArray(response.contentLength()!!.toInt()) var offset = 0 do { val currentRead = response.content.readAvailable(data, offset, data.size) offset += currentRead val progress = (offset * 100f / data.size).roundToInt() emit(DownloadResult.Progress(progress)) } while (currentRead > 0) response.close() if (response.status.isSuccess()) { withContext(Dispatchers.IO) { file.write(data) } emit(DownloadResult.Success) } else { emit(DownloadResult.Error("File not downloaded")) } } catch (e: TimeoutCancellationException) { emit(DownloadResult.Error("Connection timed out", e)) } catch (t: Throwable) { emit(DownloadResult.Error("Failed to connect")) } } }
This code was originally found on Kotlin Academy .
The ViewModel and Layout
Before making the fragment, we need a view model and a layout. The view model is simple, only containing a Boolean to indicate if the download is occurring.
class MainViewModel : ViewModel() { private val _downloading: MutableLiveData<Boolean> = MutableLiveData() val downloading: LiveData<Boolean> = _downloading fun setDownloading(downloading: Boolean) { _downloading.value = downloading } }
The layout itself is also simple, just a button (which is enabled when the file isn’t downloading) and a horizontal progress bar. Note that material-style circle progress bars can’t have progress set and continually spin; they’re more useful for other types of requests.
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="com.example.kotlin_file_download.MainViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <androidx.appcompat.widget.AppCompatButton android:id="@+id/view_button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="40dp" android:layout_marginEnd="40dp" android:text="Download File" android:enabled="@{!safeUnbox(viewModel.downloading)}" app:layout_constraintVertical_chainStyle="packed" app:layout_constraintBottom_toTopOf="@+id/progress_bar" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ProgressBar android:id="@+id/progress_bar" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:indeterminate="false" android:max="100" android:progress="0" android:progressTint="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/view_button" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
The Fragment
The base of the fragment is straightforward enough. Of note are the permissions and codes: if the app doesn’t have these permissions, the app can’t download files. The codes don’t need to be “1” and “2,” but if there are others in the app, they should all be unique.
When the fragment loads, if we have permissions, we can set the button click listener; otherwise, we have to request permission. Some apps request these permissions on first load if they require them throughout the app. Others only request them where they are actually needed. If your app has a specific place where file downloads occur, you can just request permission there.
class MainFragment : Fragment() { private lateinit var binding: FragmentMainBinding private lateinit var viewModel: MainViewModel private val PERMISSIONS = listOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) private val PERMISSION_REQUEST_CODE = 1 private val DOWNLOAD_FILE_CODE = 2 private val fileUrl = "https://d2v9y0dukr6mq2.cloudfront.net/video/thumbnail/rcxbst_b0itvu9rs2/kitten-in-a-cup-turns-its-head-and-watches_raeb_02je_thumbnail-full01.png" override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = DataBindingUtil.inflate( inflater, R.layout.fragment_main, container, false ) binding.lifecycleOwner = viewLifecycleOwner return binding.root } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProvider(this).get(MainViewModel::class.java) if (hasPermissions(context, PERMISSIONS)) { setDownloadButtonClickListener() } else { requestPermissions(PERMISSIONS.toTypedArray(), PERMISSION_REQUEST_CODE) } } ... }
First, we need functions to check the permissions and the request result. Note that only devices with Marshmallow (API 23) or later need permissions; earlier devices had them by default.
private fun hasPermissions(context: Context?, permissions: List<String>): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { return permissions.all { permission -> ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED } } return true } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == PERMISSION_REQUEST_CODE && hasPermissions(context, PERMISSIONS)) { setDownloadButtonClickListener() } }
If permission needs to be requested, our return should set the click listener when permission is granted. Either you can force the download to occur where you want with the name you provide, or you can let the user decide the name and location. I use the latter case here; the click will start an intent for creating a document with some defaults set.
private fun setDownloadButtonClickListener() { val folder = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) val fileName = "kitten_in_a_cup.png" val file = File(folder, fileName) val uri = context?.let { FileProvider.getUriForFile(it, "${BuildConfig.APPLICATION_ID}.provider", file) } val extension = MimeTypeMap.getFileExtensionFromUrl(uri?.path) val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) binding.viewButton.setOnClickListener { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.setDataAndType(uri, mimeType) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP intent.putExtra(Intent.EXTRA_TITLE, fileName) intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.addCategory(Intent.CATEGORY_OPENABLE) startActivityForResult(intent, DOWNLOAD_FILE_CODE) } }
The result will use the returned URI as the location for downloading the file.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == DOWNLOAD_FILE_CODE && resultCode == Activity.RESULT_OK) { data?.data?.let { uri -> context?.let { context -> downloadFile(context, fileUrl, uri) } } } }
Downloading the file opens the output stream to the URI given and dispatches the download file coroutine. The download itself is handled on the IO thread, but the emitter results are handled on the Main thread. This allows for the correct asynchronous updates (in this case, updating the progress bar).
private fun downloadFile(context: Context, url: String, file: Uri) { val ktor = HttpClient(Android) viewModel.setDownloading(true) context.contentResolver.openOutputStream(file)?.let { outputStream -> CoroutineScope(Dispatchers.IO).launch { ktor.downloadFile(outputStream, url).collect { withContext(Dispatchers.Main) { when (it) { is DownloadResult.Success -> { viewModel.setDownloading(false) binding.progressBar.progress = 0 viewFile(file) } is DownloadResult.Error -> { viewModel.setDownloading(false) Toast.makeText( context, "Error while downloading file", Toast.LENGTH_LONG ).show() } is DownloadResult.Progress -> { binding.progressBar.progress = it.progress } } } } } } }
The viewFile function takes the same URI given and opens it in an intent, if able. If there are multiple applications where the file can be viewed, it presents a chooser.
private fun viewFile(uri: Uri) { context?.let { context -> val intent = Intent(Intent.ACTION_VIEW, uri) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val chooser = Intent.createChooser(intent, "Open with") if (intent.resolveActivity(context.packageManager) != null) { startActivity(chooser) } else { Toast.makeText(context, "No suitable application to open file", Toast.LENGTH_LONG).show() } } }
The code for this can be found on my Github repo: kotlin-file-downloading .
iOS Code Sample
func downloadFile(fileURL: URL, dispatchQueue: DispatchQueue) { viewButton?.isEnabled = false startActivityIndicator() let destination: DownloadRequest.DownloadFileDestination = { _, _ in let documentsURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] if let name = self.document?.name, let type = self.document?.fileType?.lowercased() { let fileURL = documentsURL.appendingPathComponent("\(name).\(type)") return (fileURL, [.removePreviousFile, .createIntermediateDirectories]) } let fileURL = documentsURL.appendingPathComponent(fileURL.lastPathComponent) return (fileURL, [.removePreviousFile, .createIntermediateDirectories]) } print("downloading file \(fileURL.absoluteString)") Alamofire.download(fileURL, to: destination).response(queue: dispatchQueue) { response in DispatchQueue.main.async { self.stopActivityIndicator() self.viewButton?.isEnabled = true if let error = response.error { self.handleCommonAPIErrors(error, alertPresenter: self.alertPresenter) } else if let fileUrl = response.destinationURL { self.docController = UIDocumentInteractionController(url: fileUrl) self.docController!.delegate = self if !self.docController!.presentPreview(animated: true) { self.fileAvailableForViewing(viewable: false) } } else { self.alertPresenter.alertMessage(AlertText.failedDownload, title: AlertText.genericErrorTitle, onTopOf: self, buttonTitle: nil, handler: nil) } } } }
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出Rust
范长春 / 机械工业出版社 / 2018-8-21 / 89.00元
本书详细描述了Rust语言的基本语法,穿插讲解一部分高级使用技巧,并以更容易理解的方式解释其背后的设计思想。全书总共分五个部分。 第一部分介绍Rust基本语法,因为对任何程序设计语言来说,语法都是基础,学习这部分是理解其他部分的前提。 第二部分介绍属于Rust独一无二的内存管理方式。它设计了一组全新的机制,既保证了安全性,又保持了强大的内存布局控制力,而且没有额外性能损失。这部分是本书......一起来看看 《深入浅出Rust》 这本书的介绍吧!