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

文章目录

    断断续续耗费了快一天时间,终于把 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 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式