Skip to content

Commit 8149ce0

Browse files
authored
Update ktor-client.md
добавлен пример реализации загрузки файлов с multipart/formdata на android
1 parent b4b3a28 commit 8149ce0

File tree

1 file changed

+141
-0
lines changed

1 file changed

+141
-0
lines changed

learning/libraries/ktor/ktor-client.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,147 @@ val result = httpClient.post<Unit> {
169169
}
170170
```
171171

172+
### Пример реализации на Android
173+
174+
Стек: Retrofit 2.11.0, KotlinX.io, Multipart-formdata для передачи файла.
175+
Для передачи файла в Retrofit надо представить его в формате понятном ему - RequestBody. Создаем наследника данного класса:
176+
177+
```kotlin
178+
import kotlinx.io.Buffer
179+
import kotlinx.io.readByteArray
180+
import okhttp3.MediaType
181+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
182+
import okhttp3.RequestBody
183+
import okio.BufferedSink
184+
import io.example.FileSource
185+
import kotlin.math.min
186+
187+
class FileSourceRequestBody(
188+
private val fileSource: FileSource,
189+
private val onUploadCallback: (Float) -> Unit,
190+
) : RequestBody() {
191+
192+
// Определяем тип контента для передачи в заголовке MediaType
193+
override fun contentType(): MediaType? {
194+
return when {
195+
fileSource.fileName.endsWith(".png", ignoreCase = true) -> "image/png"
196+
fileSource.fileName.endsWith(".jpg", ignoreCase = true) ||
197+
fileSource.fileName.endsWith(".jpeg", ignoreCase = true) -> "image/jpeg"
198+
fileSource.fileName.endsWith(".bmp", ignoreCase = true) -> "image/bmp"
199+
fileSource.fileName.endsWith(".gif", ignoreCase = true) -> "image/gif"
200+
fileSource.fileName.endsWith(".webp", ignoreCase = true) -> "image/webp"
201+
else -> "application/octet-stream"
202+
}.toMediaTypeOrNull()
203+
}
204+
205+
override fun contentLength(): Long {
206+
return fileSource.fileSize
207+
}
208+
209+
override fun writeTo(sink: BufferedSink) {
210+
// Создаем буфер
211+
val buffer = Buffer()
212+
// Объем загруженный на сервер, для расчета прогресса загрузки
213+
var totalBytesRead = 0L
214+
215+
// Открываем поток, который будет закрыт автоматически по окончании работы с ним
216+
fileSource.source.use { source ->
217+
var readBytes: Long
218+
219+
while (totalBytesRead != fileSource.fileSize) {
220+
// Читаем файл по размеру буффера, либо по оставшемуся количеству от файла для загрузки
221+
// Чтение происходит с удалением прочитанных байтов из source
222+
readBytes = source.readAtMostTo(
223+
sink = buffer,
224+
byteCount = min(
225+
a = DEFAULT_BUFFER_SIZE,
226+
b = fileSource.fileSize - totalBytesRead
227+
)
228+
)
229+
230+
totalBytesRead += readBytes
231+
232+
// Записываем прочитанный объем в исходящий поток данных
233+
sink.write(
234+
source = buffer.readByteArray(),
235+
offset = 0,
236+
byteCount = readBytes.toInt()
237+
)
238+
239+
// Вычисляем прогресс загрузки
240+
calculateProgress(totalBytesRead, onUploadCallback)
241+
}
242+
243+
// Очищаем текущий поток
244+
sink.flush()
245+
}
246+
}
247+
248+
private fun calculateProgress(
249+
totalBytesRead: Long,
250+
onUploadCallback: (Float) -> Unit,
251+
) {
252+
val progress: Float = (totalBytesRead / contentLength().toFloat())
253+
onUploadCallback(progress)
254+
}
255+
256+
companion object {
257+
private const val DEFAULT_BUFFER_SIZE = 4096L
258+
}
259+
}
260+
```
261+
262+
В качестве входящих параметров для класса нужно передать информацию о файле, вторым параметром передаем callback для отображения прогресса загрузки на ui. Структура FileSource:
263+
264+
```kotlin
265+
import kotlinx.io.RawSource
266+
267+
data class FileSource(
268+
val fileName: String,
269+
val source: RawSource,
270+
val fileSize: Long,
271+
)
272+
```
273+
Теперь когда готовы основные структуры для загрузки файла на сервер, давайте создадим интерфейс для нашего API:
274+
275+
```kotlin
276+
import okhttp3.MultipartBody
277+
import retrofit2.Response
278+
import retrofit2.http.Multipart
279+
import retrofit2.http.POST
280+
import retrofit2.http.Part
281+
282+
interface UploadApi {
283+
@Multipart
284+
@POST("/api/images")
285+
suspend fun uploadImage(
286+
@Part image: MultipartBody.Part,
287+
): Response<SuccessDto>
288+
}
289+
```
290+
Для обозначения, что в теле запроса содержится multi-part на него нужно повесить аннотацию '@Multipart', а для параметра содержащий его '@Part'. При вызове данного запроса в репозитории необходимо будет создать MultipartBody.Part, вызывом createFormData:
291+
292+
```kotlin
293+
...
294+
suspend fun uploadImage(
295+
fileSource: FileSource,
296+
onUploadCallback: (Float) -> Unit,
297+
): ImageUploadResult {
298+
return uploadApi.uploadImage(
299+
image = MultipartBody.Part.createFormData(
300+
name = "image", // имя Multipart файла указанное в api бекенда
301+
filename = fileSource.fileName, // Имя файла передается в заголовке form-data
302+
body = FileSourceRequestBody(
303+
fileSource = fileSource,
304+
onUploadCallback = onUploadCallback
305+
)
306+
)
307+
)
308+
}.toDomain()
309+
}
310+
...
311+
```
312+
172313
### application/octet-stream
173314

174315
Данный подход используется довольно редко, но все же используется. Для данного типа запроса нет возможности передать несколько параметров или файлов, можно отправлять файл, притом только один. Для реализации подхода необходимо создать класс, унаследованный от `WriteChannelContent` ([ссылка на класс](https://www.mvndoc.com/c/io.ktor/ktor-http-iosarm64/io/ktor/http/content/OutgoingContent.WriteChannelContent.html)). Пример кода:

0 commit comments

Comments
 (0)