github地址:github.com/VipMinF/Lyc…java
本庫其餘相關文章git
Retrofit2的上傳方式相信你們都有了解,下面是百度的一個栗子github
@Multipart
@POST("upload")
Call<ResponseBody> uploadFiles(@PartMap Map<String, RequestBody> map);
RequestBody fb = RequestBody.create(MediaType.parse("text/plain"), "hello,retrofit");
RequestBody fileTwo = RequestBody.create(MediaType.parse("image/*"), new File(Environment.getExternalStorageDirectory() + file.separator + "original.png"));
Map<String, RequestBody> map = new HashMap<>();
//這裏的key必須這麼寫,不然服務端沒法識別
map.put("file\"; filename=\""+ file.getName(), fileRQ);
map.put("file\"; filename=\""+ "2.png", fileTwo);
Call<ResponseBody> uploadCall = downloadService.uploadFiles(map);
複製代碼
這個是使用Retrofit2上傳的一種方式,由上面的代碼可總結如下5個步驟數組
其餘的方式也差很少,看起來很很差看,不簡約。因此,我設計了一個庫,只爲了用更優雅的方式寫API——使用註解完成上面的步驟。app
dependencies {
implementation 'com.vecharm:lycheehttp:1.0.2'
}
複製代碼
若是你喜歡用RxJava 還須要加入框架
dependencies {
//RxJava
implementation 'com.vecharm.lycheehttp:lychee_rxjava:1.0.2'
//或者 RxJava2
implementation 'com.vecharm.lycheehttp:lychee_rxjava2:1.0.2'
}
複製代碼
Upload
進行註解@Upload
@Multipart
@POST("http://xxx/xxx")
fun upload(@Part("file") file: File): Call<ResultBean<UploadResult>>
複製代碼
FileType("png")
或者FileType("image/png")
@Multipart
@POST("http:/xxx/xxx")
fun upload(@Part("file") @FileType("png") file: File): Call<ResultBean<UploadResult>>
複製代碼
MultiFileType("png")
或者MultiFileType("image/png")
@Multipart
@MultiFileType("png")
@POST("http://xxx/xxx")
fun upload(@PartMap map: MutableMap<String, Any>): Call<ResultBean<UploadResult>>
複製代碼
三個註解能夠同時使用,優先級FileType
> MultiFileType
> Upload
,喜歡哪種就看你本身了ide
getService<API>().upload(file).upload {
onUpdateProgress = { fileName, currLen, size, speed, progress -> /*上傳進度更新*/ }
onSuccess = { }
onErrorMessage = { }
onCompleted = { }
}
複製代碼
從上面的上傳步驟可知,其實就是要建立一個帶MediaType
的RequestBody
,要實現經過註解建立其實不難,只須要寫一個Converter
,在請求的時候,獲取註解方法的值進行建立就能夠了。post
override fun requestBodyConverter(type: Type, parameterAnnotations: Array<Annotation>, methodAnnotations: Array<Annotation>, retrofit: Retrofit): Converter<*, RequestBody>? {
return when {
//參數是File的方式
type == File::class.java -> FileConverter(parameterAnnotations.findFileType(), methodAnnotations.findMultiType(), methodAnnotations.isIncludeUpload())
//參數是Map的方式
parameterAnnotations.find { it is PartMap } != null -> {
//爲map中不是File類型的參數找到合適的Coverter
var realCover: Converter<*, *>? = null
retrofit.converterFactories().filter { it != this }.find { it.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit).also { realCover = it } != null }
if (realCover == null) return null
return MapParamsConverter(parameterAnnotations, methodAnnotations, realCover as Converter<Any, RequestBody>)
}
else -> null
}
}
複製代碼
重點在FileConverter
測試
class FileConverter(private val fileType: FileType?, private val multiFileType: MultiFileType?, private val isAutoWried: Boolean) :
Converter<File, RequestBody> {
override fun convert(value: File): RequestBody {
//獲取後綴名 先判斷FileType 而後上MultiFileType
var subffix = fileType?.value ?: multiFileType?.value
//而後上面兩個都沒有設置 判斷是否設置了Upload註解
if (isAutoWried) if (subffix == null) subffix = value.name?.substringAfterLast(".", "")
//都沒有設置爲了null
if (subffix == null || subffix.isEmpty()) return create(null, value)
//判斷設置的是 image/png 仍是 png
val type = (if (subffix.contains("/")) subffix else LycheeHttp.getMediaTypeManager()?.getTypeBySuffix(subffix))
?: return create(null, value)
return create(MediaType.parse(type), value)
}
}
複製代碼
對於後綴名對應的MediaType,從http發展至今有多個,全部我整理了一些經常使用的放到了DefaultMediaTypeManager
,大約有300個。ui
"png" to "image/png",
"png" to "application/x-png",
"pot" to "application/vnd.ms-powerpoint",
"ppa" to "application/vnd.ms-powerpoint",
"ppm" to "application/x-ppm",
"pps" to "application/vnd.ms-powerpoint",
"ppt" to "application/vnd.ms-powerpoint",
"ppt" to "application/x-ppt",
"pr" to "application/x-pr",
"prf" to "application/pics-rules",
"prn" to "application/x-prn",
.......
複製代碼
若是以上不夠用,能夠繼承DefaultMediaTypeManager
進行擴展,若是以爲不須要這麼精確的需求,也能夠實現IMediaTypeManager
直接返回*/*
。
以上是上傳時實現自動建立帶MediaType的RequestBody的代碼。
上傳功能最基本的,確定要有上傳進度的交互。
class FileRequestBody(private val contentType: MediaType?, val file: File) : RequestBody() {
.....
private fun warpSource(source: Source) = object : ForwardingSource(source) {
private var currLen = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val len = super.read(sink, byteCount)
currLen += if (len != -1L) len else 0
return len
}
}
}
複製代碼
寫完了,歡快的測試一下。結果,事情通常沒有這麼簡單,踩坑了。發現第一次的進度是日誌讀取而產生的。去掉日誌吧,又感受調試不方便,調試調試着,最後發現,讀取時兩個BufferedSink
是不一樣實現的,日誌用的是Buffer
,上傳用的是RealBufferedSink
,這就好辦了。
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
var source: Source? = null
try {
source = Okio.source(file)
if (sink is Buffer) sink.writeAll(source)//處理被日誌讀取的狀況
else sink.writeAll(warpSource(source))
} finally {
Util.closeQuietly(source)
}
}
複製代碼
完美。
Callback
和誰有關係,和Call
有關係,Call從哪裏來,從CallAdapter
中生成。T adapt(Call<R> call);
Call有RequestBody。adapt
的參數
和返回值
,在經過PostStation
關聯他們。class CoreCallAdapter : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*,*>? {
var adapter: CallAdapter<*, *>? = null
//獲取真實的 adapterFactory
retrofit.callAdapterFactories().filter { it != this }
.find { adapter = it.get(returnType, annotations, retrofit);adapter != null }
return adapter?.let {
//使用動態代理
Proxy.newProxyInstance(CallAdapter::class.java.classLoader, arrayOf(CallAdapter::class.java)) { _, method, arrayOfAnys ->
val returnObj = when (arrayOfAnys?.size) { // 這裏 Retrofit調用 CallAdapter.adapt 得到返回值對象 在此截獲
null, 0 -> method.invoke(adapter)
1 -> method.invoke(adapter, arrayOfAnys[0])
else -> method.invoke(adapter, arrayOfAnys)
}
//從參數中把OkHttpCall拿出 OkHttpCall是Retrofit 封裝的一個請求call 裏面放了本次請求的全部參數
val okHttpCall = arrayOfAnys?.getOrNull(0) as? Call<*>
//由於上面咱們自定了一個FileRequestBody 經過這個識別是不是和上傳有關的請求
okHttpCall?.also { list = getFileRequestBody(okHttpCall.request()?.body())}
//將返回值對象的toString 和 UploadFile 關聯起來,由於一次可能上傳多個文件就用數組
list.forEach { UploadFilePostStation.setCallBack(returnObj.toString(), it) }
return@newProxyInstance returnObj
} as CallAdapter<*, *>
}
}
複製代碼
關聯二者關係的驛站實現
object UploadFilePostStation {
val map = WeakHashMap<String, ArrayList<UploadFile>>()
// first be executed 在CallAdapter中調用這個
fun setCallBack(callBackToken: String, callbackFile: UploadFile) {
val list = map[callBackToken] ?: ArrayList()
if (!list.contains(callbackFile)) list.add(callbackFile)
map[callBackToken] = list
}
// second 在CallBack調用這個,關聯完移除在驛站的引用
fun registerProgressCallback(callBackToken: String, listener: ProgressHelper.ProgressListener) {
map[callBackToken]?.forEach { it.progressListener = listener }
map[callBackToken]?.clear()
map.remove(callBackToken)
}
}
複製代碼
object ProgressHelper {
/** * 下載速度計算器,可初始化時改變這個值 * */
var downloadSpeedComputer: Class<out ISpeedComputer>? = DefaultSpeedComputer::class.java
/** * 上傳速度計算器,可初始化時改變這個值 * */
var uploadSpeedComputer: Class<out ISpeedComputer>? = downloadSpeedComputer
/** * 速度計算器接口 * @see FileRequestBody.warpSource 上傳 * @see CoreResponseBody.read 下載 * */
interface ISpeedComputer {
/** * 獲取速度 * */
fun computer(currLen: Long, contentLen: Long): Long
/** * 獲取進度 * */
fun progress(currLen: Long, contentLen: Long): Int
/** * 是否容許回調 * */
fun isUpdate(currLen: Long, contentLen: Long): Boolean
}
}
複製代碼
後話:第一次寫文章,寫的頭暈腦漲,寫的不太好。若是這篇文章對各位大大有用的話,能夠給我點個贊鼓勵一下我哦,感謝!