برنامه نویسی 278 بازدید

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

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

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

نقطه انتهایی

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

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

@Multipart
@POST("file")
fun attachFile(
    @Part("name") filename: RequestBody,
    @Part("type") mimeType: RequestBody,
    @Part("size") fileSize: RequestBody,
    @Part filePart: MultipartBody.Part
): Single<AttachmentUploadedRemoteDto>

شمارش پیشروی

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

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

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

class CountingRequestBody(
    private val requestBody: RequestBody,
    private val onProgressUpdate: CountingRequestListener
) : RequestBody() {
    override fun contentType() = requestBody.contentType()

    @Throws(IOException::class)
    override fun contentLength() = requestBody.contentLength()

    ...
}

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

typealias CountingRequestListener = (bytesWritten: Long, contentLength: Long) -> Unit

class CountingSink(
    sink: Sink,
    private val requestBody: RequestBody,
    private val onProgressUpdate: CountingRequestListener
) : ForwardingSink(sink) {
    private var bytesWritten = 0L

    override fun write(source: Buffer, byteCount: Long) {
        super.write(source, byteCount)

        bytesWritten += byteCount
        onProgressUpdate(bytesWritten, requestBody.contentLength())
    }
}

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

class CountingRequestBody(...) : RequestBody() {
    ...

    @Throws(IOException::class)
    override fun writeTo(sink: BufferedSink) {
        val countingSink = CountingSink(sink, this, onProgressUpdate)
        val bufferedSink = countingSink.buffer()

        requestBody.writeTo(bufferedSink)

        bufferedSink.flush()
    }
}

نتیجه

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

sealed class CountingRequestResult<ResultT> {
    data class Progress<ResultT>(
        val progressFraction: Double
    ) : CountingRequestResult<ResultT>()

    data class Completed<ResultT>(
        val result: ResultT
    ) : CountingRequestResult<ResultT>()
}

اجرای آپلود

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

private fun createUploadRequestBody(
    file: File,
    mimeType: String,
    progressEmitter: PublishSubject<Double>
): RequestBody {
    val fileRequestBody = file.asRequestBody(mimeType.toMediaType())
    return CountingRequestBody(fileRequestBody) { bytesWritten, contentLength ->
        val progress = 1.0 * bytesWritten / contentLength
        progressEmitter.onNext(progress)

        if (progress >= 1.0) {
            progressEmitter.onComplete()
        }
    }
}

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

private fun createUploadRequest(
    filename: String,
    file: File,
    mimeType: String,
    progressEmitter: PublishSubject<Double>
): Single<AttachmentUploadedRemoteDto> {
    val requestBody = createUploadRequestBody(file, mimeType, progressEmitter)
    return remoteApi.attachFile(
        filename = filename.toPlainTextBody(),
        mimeType = mimeType.toPlainTextBody(),
        fileSize = file.length().toString().toPlainTextBody(),
        filePart = MultipartBody.Part.createFormData(
            name = "files[]",
            filename = filename,
            body = requestBody
        )
    )
}

private fun String.toPlainTextBody() = toRequestBody("text/plain".toMediaType())

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

fun uploadAttachment(
    filename: String, file: File, mimeType: String
): Observable<AttachmentUploadRemoteResult> {
    val progressEmitter = PublishSubject.create<Double>()
    val uploadRequest = createUploadRequest(
        filename, file, mimeType, progressEmitter
    )

    val uploadResult = uploadRequest
        .map<AttachmentUploadRemoteResult> { 
            CountingRequestResult.Completed(it.result) 
        }
        .toObservable()

    val progressResult = progressEmitter
        .map<AttachmentUploadRemoteResult> { 
            CountingRequestResult.Progress(it) 
        }

    return progressResult.mergeWith(uploadResult)
}

typealias AttachmentUploadRemoteResult = 
    CountingRequestResult<AttachmentUploadedRemoteDto>

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

uploader.uploadAttachment(request.filename, request.file, request.mimeType)
    .subscribeOn(appRxSchedulers.io)
    .observeOn(appRxSchedulers.main)
    .subscribeBy(
        onError = { error ->
            // Display error alert
        },
        onComplete = {
            // Display completed Snackbar
        },
        onNext = { progress ->
            // Update progress bar
        }
    )
    .addTo(disposeBag)

سخن پایانی

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

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

==

اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

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