Android Kotlin 使用 OkHttp3 上传拍照获取的 Bitmap 图片

更新日期: 2022-04-04 阅读次数: 5201 字数: 1235 分类: Android

断断续续耗费了快一天时间,终于把 Android 拍照并使用 OkHttp3 上传图片的功能实现。 整体感受:

  • Kotlin 相关的资料还是少,即便是英文的资料也不足,特别是三方库。大部分都是 Java 的代码
  • Android 这些类库相关接口废弃得过于频繁,就算找到了示例代码,经常是已经废弃的接口。对新手很不友好
  • 无聊的概念太多,必须沉住气,要不很容易掀桌子
  • 家里有孩子需要看,不可能一直坐在电脑边,身边有本纸质参考书翻翻,即便是过时的实现方法,还是有启发的,也可以了解基础知识

OkHttp 官方文档

https://square.github.io/okhttp/

只不过文档是基于 Java 的,如果要参考 Koltin 的使用方法,还是得 Google。

OkHttp 与 OkHttp 3 的关系

其实就类似 vue3 与 vue2 的关系,版本不同罢了。还是同一个项目。

只不过当前的最新版本是 OkHttp 3 罢了。不过诡异的是 okhttp 有自己的版本号,现在居然是 4 开头,不能理解。

调试过程中经常能收到之前版本的 OkHttp 代码,完全不能用,兼容性炸裂。

添加依赖

build.gradle 里新增:

implementation("com.squareup.okhttp3:okhttp:4.9.3")

修改后,点击 Sync。

网络权限

如果不申请网络权限,会报错:

2022-04-04 10:43:10.885 3570-3647/com.sunzhongwei.androidwheatcv E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.sunzhongwei.androidwheatcv, PID: 3570
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)

在 AndroidManifest.xml 中 application 上方添加网络权限。

<uses-permission android:name="android.permission.INTERNET" />

一个简单的 OkHttp 调用服务器接口实现

先写个简单 http 请求,建立一下信心。例如在一个按钮的点击事件中:

import okhttp3.OkHttpClient
import okhttp3.Request
import kotlin.concurrent.thread

thread {
	val client = OkHttpClient()
	val request = Request.Builder()
		.url("https://www.sunzhongwei.com/test")
		.build()
	val response = client.newCall(request).execute()
	val responseData = response.body?.string()
	if (responseData != null) {
		Log.d("tag1", responseData)
	}
}

加上 try catch 更安全一点。

确认后台 fastapi 接口 content type: application/octet-stream 还是 multipart/form-data

我服务端后台接收文件上传的接口是用 python 的 fastapi 框架写的。 从官方文档看,fastapi 接收文件的方式是 form data。

https://fastapi.tiangolo.com/tutorial/request-files/

uploaded files are sent as "form data"

Data from forms is normally encoded using the "media type" application/x-www-form-urlencoded when it doesn't include files. But when the form includes files, it is encoded as multipart/form-data.

类似的 HTML 前端代码为:

<form action="/files/" enctype="multipart/form-data" method="post">
	<input name="files" type="file" multiple>
	<input type="submit">
</form>

类似的,Spring Boot 默认也是支持 form data 上传文件

OkHttp 上传 Bitmap 图片到服务器

试了差不多一天,终于拼凑出一段可以用的代码。

thread {
	try {
		val client = OkHttpClient()
		val byteArrayOutputStream = ByteArrayOutputStream()
		bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
		val byteArray = byteArrayOutputStream.toByteArray()
		val requestBody = MultipartBody.Builder()
			.setType(MultipartBody.FORM)
			.addFormDataPart("file", "image.png", byteArray.toRequestBody("multipart/form-data".toMediaTypeOrNull(), 0, byteArray.size))
			.build()
		val request = Request.Builder()
			.url("https://www.sunzhongwei.com/test")
			.post(requestBody)
			.build()
		val response = client.newCall(request).execute()
		val responseData = response.body?.string()
		Log.d("test1", "test2")
		if (responseData != null) {
			Log.d("test", responseData)
		}
	} catch (e: Exception) {
		Log.d("tag", e.toString())
	}
}

里面 byteArray.toRequestBody 那行感觉有问题,虽然能正常执行。 搞完这个 app 再回头看看,怎么改才合理。

什么是 bitmap

Bit即比特,是目前计算机系统里边数据的最小单位,8个bit即为一个Byte。一个bit的值,或者是0,或者是1;也就是说一个bit能存储的最多信息是2。Bitmap可以理解为通过一个bit数组来存储特定数据的一种数据结构;由于bit是数据的最小单位,所以这种数据结构往往是非常节省存储空间。

在 Android 系统中,可以通过 api 将图像的文件路径,或资源 ID,转换为 Bitmap 对象。然后通过 getWidth, getHeight 等函数获取图像的基本信息。

需要注意的是,为了防止内存问题,在加载为 Bitmap 之前,需要先判定图像的大小,然后根据大小进行适当的降采样。

参考代码

虽然不是 bitmap 的处理,但是也帮了大忙。

val file = File("/sdcard/image.png")
val fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file)
val requestBody = MultipartBody.Builder()
  .setType(MultipartBody.FORM)
  .addFormDataPart("uploadfile", "image.png", fileBody)
  .build()
val request = Request.Builder()
  .url(url)
  .post(requestBody)
  .build()

界面刷新

runOnUiThread { ttviewResponse.text = responseStr }

为何网上很多示例都是用 Retrofit 写的

Retrofit 为何更流行

  • 实际上 Retrofit 是基于 OkHttp 开发的,并内置了 GSON
  • 发起请求时,Retrofit 会自动开启子线程,在 Callback 里又会自动切换回主线程。省去了手动切换线程的麻烦。

但是,我感觉简单的单页 APP 用 OkHttp 加 GSON 已经足够方便了,暂时没有使用 Retrofit 的场景。

RequestBody' is deprecated

Kotlin Solution: Use the extension function content.toRequestBody(contentType); for the File type file.asRequestBody(contentType)

{"detail":[{"loc":["body","file"],"msg":"field required","type":"value_error.missing"}]}

后台 fastapi 报错,实际上是 content type 设置问题:

val requestBody = MultipartBody.Part.createFormData(
	"file", "test.png",
	byteArray.toRequestBody("image/*".toMediaTypeOrNull(), 0, byteArray.size)
).body

修改为:

val requestBody = MultipartBody.Part.createFormData(
	"file", "test.png",
	byteArray.toRequestBody("multipart/form-data".toMediaTypeOrNull(), 0, byteArray.size)
).body

{"detail":"There was an error parsing the body"}

实际上,上面那个也不对,改成最终的代码才正常执行。

java.net.UnknownServiceException: CLEARTEXT communication to 127.0.0.1 not permitted by network security policy

本想用本地开发环境的后台接口测试,发现报错,需要配置

Starting with Android 9 (API level 28), cleartext support is disabled by default.

参考

  • https://handyopinion.com/upload-file-to-server-in-android-kotlin/
  • https://cloud.tencent.com/developer/article/1735910
  • https://stackoverflow.com/questions/45828401/how-to-post-a-bitmap-to-a-server-using-retrofit-android

关于作者 🌱

我是来自山东烟台的一名开发者,有敢兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式