آپلود فایل و نمایش پیشروی آن در کاتلین — از صفر تا صد

۱۰۹ بازدید
آخرین به‌روزرسانی: ۰۱ مهر ۱۴۰۲
زمان مطالعه: ۴ دقیقه
آپلود فایل و نمایش پیشروی آن در کاتلین — از صفر تا صد

اگر می‌خواهید یک فایل را با استفاده از ساختار سرراست Retrofit آپلود کنید، شاید متوجه شوید که امکان دریافت نتیجه و همچنین نمایش نوار پیشروی در کاتلین را ندارید. ما در این مقاله از Retrofit برای آپلود فایل استفاده می‌کنیم و ساختاری پیاده‌سازی می‌کنیم که امکان دریافت میزان پیشرفت کار را در بازه‌های زمانی مختلف دارد و نهایتاً با تکمیل پاسخ API ریموت، ‌پایان می‌یابد.

هر زمان که عملیاتی بلندمدت در حال اجرا است، خوب است که روند پیشروی کار را به کاربر نشان دهیم. در مورد آپلود فایل می‌توانیم پیشرفت واقعی کار را که بر حسب بایت‌های انتقال‌یافته از کل اندازه فایل محاسبه می‌شود به کاربر نشان دهیم.

در ادامه از API-های موجود در کتابخانه‌های Retrofit ،OkHttp و Okio برای ساخت یک کلاس استفاده می‌کنیم که برای نمایش روند پیشروی آپلود فایل مورد استفاده قرار می‌گیرند.

نقطه انتهایی

فرض کنید در حال توسعه یک اپلیکیشن پیام‌رسانی هستیم که امکان الصاق فایل به رشته پیام را دارد. لازم به ذکر است که این کامپوننت واکنشی از RxJava استفاده می‌کند، اما می‌توان از Callback-های معمولی یا کوروتین‌ها و تابع‌های تعلیقی کاتلین نیز استفاده کرد.

نقطه انتهایی (endpoint) ما یک درخواست POST است که شامل بدنه چندبخشی است که از اجزای نام فایل، نوع MIME فایل، ‌اندازه فایل و خود فایل تشکیل یافته است. آن را می‌توانیم با استفاده از Retrofit و با تعیین بخش‌های لازم به شیوه‌ای همچون کد زیر تعریف کنیم:

1@Multipart
2@POST("file")
3fun attachFile(
4    @Part("name") filename: RequestBody,
5    @Part("type") mimeType: RequestBody,
6    @Part("size") fileSize: RequestBody,
7    @Part filePart: MultipartBody.Part
8): Single<AttachmentUploadedRemoteDto>

شمارش پیشروی

اگر صرفاً می‌خواستیم فایل را بدون نمایش روند پیشرفت، آپلود کنیم، کافی بود فایل را به بدنه درخواست تبدیل کنیم و آن را در درخواست خود بفرستیم.

1fun createUploadRequestBody(file: File, mimeType: String) = 
2    file.asRequestBody(mimeType.toMediaType())

نظارت بر پیشرفت آپلود نیز با استفاده از CountingRequestBody حاصل می‌شود که در فایل RequestBody قرار دارد و قبلاً استفاده کردیم. داده‌هایی که ارسال می‌شوند همانند قبل هستند و به فایل خام RequestBody امکان می‌دهیم که نماینده نوع محتوا و طول محتوا باشد.

1class CountingRequestBody(
2    private val requestBody: RequestBody,
3    private val onProgressUpdate: CountingRequestListener
4) : RequestBody() {
5    override fun contentType() = requestBody.contentType()
6
7    @Throws(IOException::class)
8    override fun contentLength() = requestBody.contentLength()
9
10    ...
11}

انتقال بدنه درخواست با نوشتن یک Sink انجام می‌یابد و Sink پیش‌فرض را درون آن Sink قرار می‌دهیم که بایت‌های انتقالی را می‌شمارد و آن‌ها را از طریق یک Callback پیشروی بازگشت می‌دهد.

1typealias CountingRequestListener = (bytesWritten: Long, contentLength: Long) -> Unit
2
3class CountingSink(
4    sink: Sink,
5    private val requestBody: RequestBody,
6    private val onProgressUpdate: CountingRequestListener
7) : ForwardingSink(sink) {
8    private var bytesWritten = 0L
9
10    override fun write(source: Buffer, byteCount: Long) {
11        super.write(source, byteCount)
12
13        bytesWritten += byteCount
14        onProgressUpdate(bytesWritten, requestBody.contentLength())
15    }
16}

درون CountingRequestBody می‌توانیم Sink پیش‌فرض را در CountingSink جدید خود قرار دهیم و نسخه بافر شده‌ی آن را بنویسیم تا هم فایل انتقال یابد و هم فرایند پیشروی را ببینیم.

1class CountingRequestBody(...) : RequestBody() {
2    ...
3
4    @Throws(IOException::class)
5    override fun writeTo(sink: BufferedSink) {
6        val countingSink = CountingSink(sink, this, onProgressUpdate)
7        val bufferedSink = countingSink.buffer()
8
9        requestBody.writeTo(bufferedSink)
10
11        bufferedSink.flush()
12    }
13}

نتیجه

در زمان مشاهده پیشروی فرایند آپلود، ‌دو حالت وجود دارد یا فایل در حال آپلود شدن است و یا کار به اتمام رسیده و از این رو بهتر است از یک کلاس مهروموم‌شده (sealed) استفاده کنیم. این کلاس امکان می‌دهد که یک نوع بازگشتی CountingRequestResult داشته باشیم و فراخوانی‌کننده‌ها می‌توانند هم به‌روزرسانی‌های پیشروی به و هم نتیجه تکمیل‌شده را مدیریت کنند.

1sealed class CountingRequestResult<ResultT> {
2    data class Progress<ResultT>(
3        val progressFraction: Double
4    ) : CountingRequestResult<ResultT>()
5
6    data class Completed<ResultT>(
7        val result: ResultT
8    ) : CountingRequestResult<ResultT>()
9}

اجرای آپلود

اکنون که روشی برای آپلود کردن فایل و دریافت روند پیشروی آپلود یافتیم، می‌توانیم FileUploader خود را بنویسیم. ایجاد بدنه درخواست برای درخواست آپلود شامل استفاده از CountingRequestBody است که پیشروی و تکمیل را به یک PublishSubject گزارش می‌دهد.

1private fun createUploadRequestBody(
2    file: File,
3    mimeType: String,
4    progressEmitter: PublishSubject<Double>
5): RequestBody {
6    val fileRequestBody = file.asRequestBody(mimeType.toMediaType())
7    return CountingRequestBody(fileRequestBody) { bytesWritten, contentLength ->
8        val progress = 1.0 * bytesWritten / contentLength
9        progressEmitter.onNext(progress)
10
11        if (progress >= 1.0) {
12            progressEmitter.onComplete()
13        }
14    }
15}

درخواست آپلود شامل استفاده از تابع Retrofit است که در ابتدای مقاله پیاده‌سازی کردیم و جزییات فایل و بدنه ایجاد شده درخواست که فرایند پیشروی را گزارش می‌دهند. تعریف Retrofit و قالب بخش‌های درخواست به شیوه خاص هر API بستگی دارند. در این مقاله ما از یک درخواست استفاده می‌کنیم که شامل بخش‌های متنی ساده مختلفی برای جزییات فایل است و سپس یک بخش برای خود فایلی که قرار است آپلود شود، ارائه می‌کند.

1private fun createUploadRequest(
2    filename: String,
3    file: File,
4    mimeType: String,
5    progressEmitter: PublishSubject<Double>
6): Single<AttachmentUploadedRemoteDto> {
7    val requestBody = createUploadRequestBody(file, mimeType, progressEmitter)
8    return remoteApi.attachFile(
9        filename = filename.toPlainTextBody(),
10        mimeType = mimeType.toPlainTextBody(),
11        fileSize = file.length().toString().toPlainTextBody(),
12        filePart = MultipartBody.Part.createFormData(
13            name = "files[]",
14            filename = filename,
15            body = requestBody
16        )
17    )
18}
19
20private fun String.toPlainTextBody() = toRequestBody("text/plain".toMediaType())

تابع اصلی آپلود ما می‌تواند همه این بخش‌ها را کنار هم قرار داده و یک استریم منفرد ایجاد کند. بدین ترتیب می‌توانیم این فرایند را مشاهده کرده و روند پیشروی آپلود را نیز در نتیجه نهایی ببینیم.

1fun uploadAttachment(
2    filename: String, file: File, mimeType: String
3): Observable<AttachmentUploadRemoteResult> {
4    val progressEmitter = PublishSubject.create<Double>()
5    val uploadRequest = createUploadRequest(
6        filename, file, mimeType, progressEmitter
7    )
8
9    val uploadResult = uploadRequest
10        .map<AttachmentUploadRemoteResult> { 
11            CountingRequestResult.Completed(it.result) 
12        }
13        .toObservable()
14
15    val progressResult = progressEmitter
16        .map<AttachmentUploadRemoteResult> { 
17            CountingRequestResult.Progress(it) 
18        }
19
20    return progressResult.mergeWith(uploadResult)
21}
22
23typealias AttachmentUploadRemoteResult = 
24    CountingRequestResult<AttachmentUploadedRemoteDto>

اکنون می‌توانیم فایل را به API خود آپلود کنیم و نما را به صورت پیشروی‌های درخواست به‌روزرسانی کنیم که برای عملیات طولانی مدت مانند آپلود فایل‌های حجیم مفید است.

1uploader.uploadAttachment(request.filename, request.file, request.mimeType)
2    .subscribeOn(appRxSchedulers.io)
3    .observeOn(appRxSchedulers.main)
4    .subscribeBy(
5        onError = { error ->
6            // Display error alert
7        },
8        onComplete = {
9            // Display completed Snackbar
10        },
11        onNext = { progress ->
12            // Update progress bar
13        }
14    )
15    .addTo(disposeBag)

سخن پایانی

نظارت بر پیشرفت یک درخواست وب ممکن است در زمان خواندن یک Retrofit API چندان واضح به نظر نرسد، اما API-های قدرتمند OkHttp و Okio می‌توانند این کار را به خوبی انجام می‌دهند. راه‌حلی که در این راهنما معرفی کردیم، می‌تواند برای هر درخواست وب اجرا شود، چون شمارش پیشروی را می‌توان درون هر RequestBody که لازم است در درخواست ارسال شود قرار داد.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
proandroiddev
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *