Mobile Development 17 min read

Building a Kotlin Retrofit Network Request Framework for Android with Token Interceptors, File Upload/Download, and ViewModel Integration

This article demonstrates how to create a clean, reusable network request layer for Android using Kotlin, Retrofit, and OkHttp, covering dependency setup, custom token interceptors, Retrofit utility creation, request/result handling with ViewModel extensions, and comprehensive file upload and download implementations.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Building a Kotlin Retrofit Network Request Framework for Android with Token Interceptors, File Upload/Download, and ViewModel Integration

In Android app development, network requests are essential. This guide shows how to encapsulate network calls using Kotlin's functional programming style and Retrofit, resulting in a concise and maintainable framework.

Define Interceptors

Custom interceptors are created to handle common request fields such as tokens. The TokenInterceptor checks token expiration and refreshes it when necessary, while the TokenHeaderInterceptor adds the token to request headers globally.

class TokenInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        // current request
        val request = chain.request()
        // proceed request
        var response = chain.proceed(request)
        if (response.body == null) return response
        val mediaType = response.body!!.contentType() ?: return response
        val type = mediaType.toString()
        if (!type.contains("application/json")) return response
        val result = response.body!!.string()
        var code = ""
        try {
            val jsonObject = JSONObject(result)
            code = jsonObject.getString("code")
        } catch (e: Exception) {
            e.printStackTrace()
        }
        // rebuild response
        response = response.newBuilder().body(result.toResponseBody(null)).code(200).build()
        if (isTokenExpired(code)) {
            // token expired, get new token
            val newToken = getNewToken() ?: return response
            // rebuild request with new token
            val builder = request.url.newBuilder().setEncodedQueryParameter("token", newToken)
            val newRequest = request.newBuilder().method(request.method, request.body)
                .url(builder.build()).build()
            return chain.proceed(newRequest)
        }
        return response
    }

    private fun isTokenExpired(code: String) =
        TextUtils.equals(code, "401") || TextUtils.equals(code, "402")

    private fun getNewToken() = ServiceManager.instance.refreshToken()
}

Create Retrofit

class RetrofitUtil {
    companion object {
        private const val TIME_OUT = 20L
        private fun createRetrofit(): Retrofit {
            val interceptor = HttpLoggingInterceptor()
            interceptor.level = HttpLoggingInterceptor.Level.BODY
            val okHttpClient = OkHttpClient().newBuilder().apply {
                addInterceptor(interceptor)
                addInterceptor(TokenInterceptor())
                addInterceptor(TokenHeaderInterceptor())
                retryOnConnectionFailure(true)
                connectTimeout(TIME_OUT, TimeUnit.SECONDS)
                writeTimeout(TIME_OUT, TimeUnit.SECONDS)
                readTimeout(TIME_OUT, TimeUnit.SECONDS)
            }.build()
            return Retrofit.Builder().apply {
                addConverterFactory(GsonConverterFactory.create())
                baseUrl(ServiceManager.instance.baseHttpUrl)
                client(okHttpClient)
            }.build()
        }
        fun
getAPI(clazz: Class
): T = createRetrofit().create(clazz)
    }
}

Network Request Wrappers

A generic response data class BaseResp and helper functions for success checking are defined. RequestAction encapsulates the whole request lifecycle (start, request, success, error, finish) using suspend functions.

private const val SERVER_SUCCESS = "200"

data class BaseResp
(val code: String, val message: String, val data: T)

fun
BaseResp
?.isSuccess() = this?.code == SERVER_SUCCESS

class RequestAction
{
    var start: (() -> Unit)? = null
    var request: (suspend () -> BaseResp
)? = null
    var success: ((T?) -> Unit)? = null
    var error: ((String) -> Unit)? = null
    var finish: (() -> Unit)? = null
    fun request(block: suspend () -> BaseResp
) { request = block }
    fun start(block: () -> Unit) { start = block }
    fun success(block: (T?) -> Unit) { success = block }
    fun error(block: (String) -> Unit) { error = block }
    fun finish(block: () -> Unit) { finish = block }
}

A ViewModel extension netRequest runs the RequestAction within a coroutine, handling UI state updates based on the result.

fun
ViewModel.netRequest(block: RequestAction
.() -> Unit) {
    val action = RequestAction
().apply(block)
    viewModelScope.launch {
        try {
            action.start?.invoke()
            val result = action.request?.invoke()
            if (result?.isSuccess() == true) {
                action.success?.invoke(result.data)
            } else {
                action.error?.invoke(result?.message ?: "Unknown error")
            }
        } catch (ex: Exception) {
            action.error?.invoke(getErrorTipContent(ex))
        } finally {
            action.finish?.invoke()
        }
    }
}

Usage Example

An HttpApi interface defines endpoints with Retrofit annotations. RequestHelper provides concrete methods that delegate to the generated API.

interface HttpApi {
    @GET("/exampleA/exampleP/exampleI/exampleApi/getNetData")
    suspend fun getNetData(@QueryMap params: HashMap
): BaseResp
// other endpoints omitted for brevity
    @Multipart
    @POST("/exampleA/exampleP/exampleI/exampleApi/uploadFile")
    suspend fun uploadFile(@Part partLis: List
): BaseResp
}

class RequestHelper {
    private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)
    suspend fun getNetData(params: HashMap
) = httpApi.getNetData(params)
    suspend fun uploadFile(partList: List
) = httpApi.uploadFile(partList)
    // other methods omitted
}

The ViewModel defines intents and UI states, then uses netRequest to fetch data and update the UI.

class MainViewModel : ViewModel() {
    val mainIntent = Channel
(Channel.UNLIMITED)
    private val _mainUIState = MutableStateFlow
(MainUIState.Loading)
    val mainUIState: StateFlow
get() = _mainUIState

    init {
        viewModelScope.launch {
            mainIntent.consumeAsFlow().collect {
                if (it is MainIntent.FetchData) getNetDataResult()
            }
        }
    }

    private fun getNetDataResult() = netRequest {
        start { _mainUIState.value = MainUIState.Loading }
        request { 
            val paramMap = hashMapOf("param1" to "param1", "param2" to "param2")
            RequestHelper.instance.getNetData(paramMap) 
        }
        success { _mainUIState.value = MainUIState.NetData(it) }
        error { _mainUIState.value = MainUIState.Error(it) }
    }
}

File Upload & Download

For file operations, a data class UpLoadFileBean holds the file and its key. ProgressRequestBody wraps a RequestBody to report upload progress via a coroutine on the main thread.

data class UpLoadFileBean(val file: File, val fileKey: String)

class ProgressRequestBody(
    private var requestBody: RequestBody,
    var onProgress: ((Int) -> Unit)?,
) : RequestBody() {
    private var bufferedSink: BufferedSink? = null
    override fun contentType(): MediaType? = requestBody.contentType()
    override fun contentLength(): Long = requestBody.contentLength()
    override fun writeTo(sink: BufferedSink) {
        if (bufferedSink == null) bufferedSink = createSink(sink).buffer()
        bufferedSink?.let {
            requestBody.writeTo(it)
            it.flush()
        }
    }
    private fun createSink(sink: Sink): Sink = object : ForwardingSink(sink) {
        var bytesWritten = 0L
        var contentLength = 0L
        override fun write(source: Buffer, byteCount: Long) {
            super.write(source, byteCount)
            if (contentLength == 0L) contentLength = contentLength()
            bytesWritten += byteCount
            CoroutineScope(Dispatchers.Main).launch {
                onProgress?.invoke((bytesWritten * 100 / contentLength).toInt())
            }
        }
    }
}

Utility functions create multipart parts, and UpLoadFileAction manages the whole upload flow (init, start, progress, success, error, finish).

fun
createPartList(action: UpLoadFileAction
): List
=
    MultipartBody.Builder().apply {
        addFormDataPart("token", ServiceManager.instance.getToken())
        action.params?.forEach { if (it.key.isNotBlank() && it.value.isNotBlank()) addFormDataPart(it.key, it.value) }
        action.fileData?.let {
            addFormDataPart(
                it.fileKey, it.file.name,
                ProgressRequestBody(it.file.asRequestBody("application/octet-stream".toMediaTypeOrNull()), action.progress)
            )
        }
    }.build().parts

The ViewModel extension upLoadFile executes the upload action, handling callbacks for each stage.

fun
ViewModel.upLoadFile(
    block: UpLoadFileAction
.() -> Unit,
    params: HashMap
?,
    fileData: UpLoadFileBean?
) = viewModelScope.launch {
    val action = UpLoadFileAction
().apply(block)
    try {
        action.init(params, fileData)
        action.start?.invoke()
        val result = action.request.invoke()
        if (result.isSuccess()) action.success?.invoke() else action.error?.invoke(result.message)
    } catch (ex: Exception) {
        action.error?.invoke(getErrorTipContent(ex))
    } finally {
        action.finish?.invoke()
    }
}

Downloading uses a simple OkHttp synchronous call wrapped in a coroutine on the IO dispatcher, reporting progress via an extension function on InputStream .

fun ViewModel.downLoadFile(
    downLoadUrl: String,
    dirPath: String,
    fileName: String,
    progress: ((Int) -> Unit)?,
    success: (File) -> Unit,
    failed: (String) -> Unit
) = viewModelScope.launch(Dispatchers.IO) {
    try {
        val fileDir = File(dirPath).apply { if (!exists()) mkdirs() }
        val downLoadFile = File(fileDir, fileName)
        val request = Request.Builder().url(downLoadUrl).get().build()
        val response = OkHttpClient.Builder().build().newCall(request).execute()
        if (response.isSuccessful) {
            response.body?.let {
                val total = it.contentLength().toDouble()
                val stream = it.byteStream()
                stream.copyTo(downLoadFile.outputStream()) { current ->
                    val percent = (current / total * 100).toInt()
                    progress?.invoke(percent)
                }
                success(downLoadFile)
            } ?: failed("response body is null")
        } else {
            failed("download failed: $response")
        }
    } catch (ex: Exception) {
        failed("download failed: ${getErrorTipContent(ex)}")
    }
}

private fun InputStream.copyTo(
    out: OutputStream,
    bufferSize: Int = DEFAULT_BUFFER_SIZE,
    progress: (Long) -> Unit
): Long {
    var bytesCopied = 0L
    val buffer = ByteArray(bufferSize)
    var bytes = read(buffer)
    while (bytes >= 0) {
        out.write(buffer, 0, bytes)
        bytesCopied += bytes
        progress(bytesCopied)
        bytes = read(buffer)
    }
    return bytesCopied
}

Finally, the article shows concise usage examples for both uploading and downloading files, illustrating how the framework simplifies network operations in an Android project.

ViewModelandroidNetworkKotlinFile UploadRetrofitToken Interceptor
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.