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.
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().partsThe 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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.