1. Introduction

File upload is a common functionality in many web applications. Presently, several libraries can handle file uploads in Kotlin.

This tutorial explores how to upload files using four popular libraries: Fuel, OkHttp, Ktor, and Retrofit.

2. Uploading a File Using Fuel

Fuel is a lightweight HTTP networking library for Kotlin. It provides a simple and intuitive way to perform HTTP requests, including file uploads. Specifically, we can use httpUpload() and add() a FileDataPart containing our file to upload a file with Fuel:

fun uploadFileFuel(filePath: String, uploadUrl: String) {
    val file = File(filePath)
    uploadUrl.httpUpload()
      .add(FileDataPart(file, name = "file"))
      .response { request, response, result ->
          println(response)
      }.get()
}

Furthermore, to test these uploads, we’ll use WireMock to verify outbound network calls made by our app. Explicitly, we’ll use JUnit‘s WireMockRule to make our server run:

@Rule
@JvmField
val wireMockRule = WireMockRule(8080)

@Test
fun `Should upload file using Fuel`() {
    stubFor(post(urlEqualTo("/upload")).willReturn(aResponse().withStatus(200)))

    uploadFileFuel("testfile.txt", "http://localhost:8080/upload")

    verify(
        postRequestedFor(urlEqualTo("/upload"))
          .withHeader("Content-Type", containing("multipart/form-data"))
          .withRequestBody(matching(".*testfile.txt.*"))
    )
}

Additionally, we configure WireMock to respond to our upload request with a 200 status.

3. Uploading a File Using Ktor

Ktor is a Kotlin framework for building asynchronous servers and clients in connected systems. It’s highly customizable and suitable for a variety of use cases, including file uploads:

suspend fun uploadFileKtor(filePath: String, uploadUrl: String) {
    val client = HttpClient(CIO) {}

    val statement: HttpStatement = client.submitFormWithBinaryData(
        url = uploadUrl,
        formData = formData {
            append("file", File(filePath).readBytes(), Headers.build {
                append(HttpHeaders.ContentType, "application/octet-stream")
                append(HttpHeaders.ContentDisposition, "filename=${File(filePath).name}")
            })
        }
    )

    println(statement.execute().readText())
    client.close()
}

This code snippet demonstrates how to use the submitFormWithBinaryData() method to upload a file. Additionally, the HttpClient from Ktor is configured with the CIO engine although different engines would also work. The submitFormWithBinaryData() method also creates a multipart form-data request with the file’s content and metadata.

Finally, let’s test our upload with Ktor with the same WireMock setup to verify our upload:

@Test
fun `Should upload file using Ktor`() = runBlocking {
    stubFor(post(urlEqualTo("/upload")).willReturn(aResponse().withStatus(200)))

    uploadFileKtor("testfile.txt", "http://localhost:8080/upload")

    verify(
        postRequestedFor(urlEqualTo("/upload"))
          .withRequestBody(matching(".*testfile.txt.*"))
          .withHeader("Content-Type", containing("multipart/form-data"))
    )
}

4. Uploading a File Using OkHttp

OkHttp is a powerful HTTP client for Kotlin and Java applications. It also supports synchronous and asynchronous requests and is highly configurable.

Although it’s not as simple as Fuel, let’s explore a file upload with OkHttp:

fun uploadFileOkHttp(filePath: String, uploadUrl: String) {
    val client = OkHttpClient()
    val file = File(filePath)
    val requestBody = MultipartBody.Builder()
      .setType(MultipartBody.FORM)
      .addFormDataPart("file", file.name, file.asRequestBody("application/octet-stream".toMediaTypeOrNull()))
      .build()

    val request = Request.Builder()
      .url(uploadUrl)
      .post(requestBody)
      .build()

    println(client.newCall(request).execute())
}

This function uses OkHttp to upload a file by creating an instance of OkHttpClient to handle the HTTP request. A File object is created from the provided file path and wrapped in a RequestBody with the media type application/octet-stream.

Then, the MultipartBody.Builder constructs a multipart form-data request body, and the addFormDataPart() method adds the file part to the request with its name. The Request.Builder builds the HTTP request, setting the URL and the POST method with the constructed request body. The execute() method synchronously sends the request and handles the response or throws an exception on failure.

Similarly, we’ll test our OkHttp upload:

@Test
fun `Should upload file using OkHttp`() {
    stubFor(post(urlEqualTo("/upload")).willReturn(aResponse().withStatus(200)))

    uploadFileOkHttp("testfile.txt", "http://localhost:8080/upload")

    verify(
        postRequestedFor(urlEqualTo("/upload"))
          .withHeader("Content-Type", containing("multipart/form-data"))
          .withRequestBody(matching(".*testfile.txt.*"))
    )
}

5. Uploading a File Using Retrofit

Retrofit is a type-safe HTTP client for Android and Kotlin developed by Square. Specifically, it’s commonly used to build on top of OkHttp.

5.1. Create Interface

First, we must define an interface that Retrofit uses to make network requests. Mainly, this interface declares a method for uploading the file:

interface UploadService {
    @Multipart
    @POST("upload")
    fun uploadFile(@Part file: MultipartBody.Part): Call
}

In this interface, the @Multipart annotation indicates that the request is a multipart request. The uploadFile() method also uses the @Part annotation to specify the file part in the request.

5.2. Create Service

Next, we create a Retrofit instance and the service using the defined interface. This sets up the network client and prepares it for making requests:

fun createUploadService(url: String): UploadService {
    val retrofit = Retrofit.Builder()
      .baseUrl(url)
      .client(OkHttpClient())
      .addConverterFactory(GsonConverterFactory.create())
      .build()

    return retrofit.create(UploadService::class.java)
}

In this function, we build a Retrofit instance using Retrofit.Builder(). We set the base URL, attach the OkHttp client, and add a converter factory for JSON. We can then create an instance of the UploadService interface.

5.3. Upload File

Finally, we’ll use the service to upload a file. This involves creating a MultipartBody.Part object and calling the uploadFile() method on the service:

fun uploadFileRetrofit(filePath: String, uploadUrl: String) {
    val file = File(filePath)
    val requestBody = file.asRequestBody("application/octet-stream".toMediaTypeOrNull())
    val multipartBody = MultipartBody.Part.createFormData("file", file.name, requestBody)

    val service = createUploadService(uploadUrl)
    val call = service.uploadFile(multipartBody)
    println(call.execute().body()?.string())
}

In this function, we first create a File object from the file path and then wrap it in a RequestBody and MultipartBody.Part. We call the uploadFile() method on the service and execute() the request to be executed synchronously. The response is returned or any errors are thrown as exceptions and we print the response body to the console.

Finally, let’s test our upload with Retrofit to ensure we can upload our file:

@Test
fun `Should upload file using Retrofit`() {
    stubFor(post(urlEqualTo("/upload")).willReturn(aResponse().withStatus(200)))

    uploadFileRetrofit("testfile.txt", "http://localhost:8080/upload")

    verify(
        postRequestedFor(urlEqualTo("/upload"))
          .withHeader("Content-Type", containing("multipart/form-data"))
          .withRequestBody(matching(".*testfile.txt.*"))
    )
}

6. Conclusion

This article explored several ways to upload files in Kotlin using Fuel, OkHttp, KTor, and Retrofit. Each library has its own strengths, and the choice of which to use depends on the specific requirements of our project. By understanding these approaches, we can effectively handle file uploads in our Kotlin applications.

As always, the code used in this article is available over on GitHub.