Kotlin第五章: android网络编程

1. Android网络编程

  • OkHttp

OkHttp是一个高效的HTTP客户端,它的横空出世,让其他的网络请求框架都变得黯然失色。

  • Retrofit

Retrofit是一个基于OkHttp的RESTful网络请求框架,功能强大、简洁易用及高可拓展性。Retrofit说起来相当简单,简单到源码只有37个文件,其中22个文件是注解,还都和HTTP有关,真正暴露给用户的类并不多。

  • 封装

Retrofit其实就是一个基于OKHttp的网络请求框架的封装。使请求接口和数据解析更加简洁明了。为什么需要封装呢?说白了,就是为了解耦,为了方便日后切换到不同框架实现,而无需到处修改调用的地方。

比如我们项目当中经常会用到一些之前比较流行的的网络框架,后期这个框架停止维护或者功能无法满足业务需求,我们想切换到新的框架,可能调用的地方会非常多,如果不做封装,直接切换的话,改动量将非常非常大,而且还很有可能会有遗漏,风险度非常高。

OkHttp是一个HTTP骑牛引擎 ,负责任何底层网络操作,缓存,请求和响应操作等

Retrofit是在OkHttp之上构建的高级REST抽象。使请求接口和数据解析更加简洁明了

2. OkHttp

1. 出现背景

在okhttp出现以前,android上发起网络请求要么使用系统自带的HttpClientHttpURLConnection、要么使用google开源的Volley、要么使用第三方开源的AsyncHttpClient, 随着互联网的发展,APP的业务发展也越来越复杂,APP的网络请求数量急剧增加,但是上述的网络请求框架均存在难以性能和并发数量的限制

OkHttp流行得益于它的良好的架构设计,强大的拦截器(intercepts)使得操纵网络十分方便;OkHttp现在已经得到Google官方认可,大量的app都采用OkHttp做网络请求,其源码详见OkHttp Github

也得益于强大的生态,大量的流行库都以OkHttp作为底层网络框架或提供支持,比如RetrofitGlideFrescoMoshiPicasso等。

当OKhttp面世之后,瞬间成为各个公司的开发者的新宠,常年霸占github star榜单,okhttp可以说是为高效而生,迎合了互联网高速发展的需要

2. 特点

1.同时支持HTTP1.1与支持HTTP2.0

2.同时支持同步与异步请求;

3.同时具备HTTP与WebSocket功能;

4.拥有自动维护的socket连接池,减少握手次数;

5.拥有队列线程池,轻松写并发;

6.拥有Interceptors(拦截器),轻松处理请求与响应额外需求(例:请求失败重试、响应内容重定向等等);
01_okhttp.png

3. 使用

1. 添加网络访问权限

在AndroidManifest.xml 中添加网络访问权限

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

2. 添加相关的依赖

app/build.gradledependencies下添加依赖

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

// 网络请求日志打印
implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")

3. Get请求

1. 同步Get请求

创建一个OkHttpDemoTest.kt文件,采用object关键字使本类在整个程序运行期间只有一个示例,相当于是单例模式,然后安卓规定网络请求不能在主线程,所以我们的get请求需要新起一个线程运行

  1. 创建kt文件
package com.example.myapplication.http

import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit

// 程序运行的时候只有一份,相当于是单例模式
// 可以直接使用 OkHttpDemoTest1.get()调用
object OkHttpDemoTest1{
    private val client = OkHttpClient.Builder()
        .connectTimeout(10,TimeUnit.SECONDS) // 连接超时时间
        .readTimeout(10,TimeUnit.SECONDS) //读取超时
        .writeTimeout(10,TimeUnit.SECONDS) // 请求超时
        .build()

    fun get(url: String){
        Thread(Runnable {
            val request = Request.Builder()
                .url(url)
                .build()
            // 构造请求对象
            val call = client.newCall(request)
            // 发起同步请求
            val response = call.execute()
            // 获取请求的返回信息
            val body = response.body?.string()
            // Log.e是安卓自带的一个打印日志的方法,日志信息会打印到logcat里
            Log.e("OkHttp $this","get response: $body")
        }).start()

    }
}
  1. MainActivity中调用
package com.example.myapplication

import android.os.Bundle
import com.example.myapplication.databinding.ActivityMainBinding
import com.example.myapplication.http.OkHttpDemoTest1

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
		// 中间代码省略
        val url = "http://123.56.232.18:8080/serverdemo/user/query?userId=1"
        OkHttpDemoTest1.get(url)
    }
}
  1. 如果创建应用的时候是高版本的安卓的话是不支持http请求的,必须要使用https,在AndroidManifest.xmlapplication标签中添加相应开关
<application
    android:usesCleartextTraffic="true">
</application>
  1. 运行模拟器,运行应用
    02_okhttp_response.png
2. 异步Get请求

异步请求是没有返回值的,需要实现callback方法来操作

  1. 创建方法
 // 异步请求
fun getAsync(url: String){
    val request = Request.Builder()
    .url(url)
    .build()
    // 构造请求对象
    val call = client.newCall(request)
    // 使用enqueue 发起异步请求
    val response = call.enqueue(object: Callback{
        // 失败的话会回调此方法
        override fun onFailure(call: Call, e: IOException) {
            Log.e("OkHttp Get Async  $this","")
        }
		// 接口调用成功之后调用此方法
        override fun onResponse(call: Call, response: Response) {
            // 获取请求的返回信息
            val body = response.body?.string()
            Log.e("OkHttp Get Async $this","get response: $body")
        }

    })
}
  1. MainActivityonViewCreated中测试
val url = "http://123.56.232.18:8080/serverdemo/user/query?userId=1"
//OkHttpDemoTest1.get(url)
OkHttpDemoTest1.getAsync(url)
  1. 当然也可以将获取call对象的方法封装一下
fun getClientCall(url: String): Call{
    val request = Request.Builder()
    .url(url)
    .build()
    // 构造请求对象
    return client.newCall(request)
}
3. Get请求总结

异步请求的步骤和同步请求类似,只是调用了Callenqueue方法异步请求,结果通过回调CallbackonResponse方法及onFailure方法处理。

看了两种不同的Get请求,基本流程都是先创建一个OkHttpClient对象,然后通过Request.Builder()创建一个Request对象,OkHttpClient对象调用newCall()并传入Request对象就能获得一个Call对象。

而同步和异步不同的地方在于execute()enqueue()方法的调用,

调用execute()为同步请求并返回Response对象;

调用enqueue()方法测试通过callback的形式返回Response对象。

注意:无论是同步还是异步请求,接收到Response对象时均在子线程中,onFailureonResponse的回调是在子线程中的,我们需要切换到主线程才能操作UI控件

4. Post请求

POST请求与GET请求不同的地方在于Request.Builderpost()方法,post()方法需要一个RequestBody的对象作为参数

1. 同步Post请求
  1. 书写请求
fun post(url: String): Unit{
    val body = FormBody.Builder()
    .add("userId","123")
    .add("key","value")
    .add("tagId","71")
    .build()

    val request = Request.Builder()
    .url(url)
    .post(body)
    .build()

    val call = client.newCall(request)
    // 因为传入的是一个函数,直接简略写法写到大括号里完事
    Thread {
        val response = call.execute()
        Log.e("OkHttp Post formData ${body.toString()}","")
        Log.e("OkHttp Post response","response $response")
    }.start()
}
  1. MainActivity调用
val baseUrl = "http://123.56.232.18:8080/serverdemo"
OkHttpDemoTest1.post("$baseUrl/tag/toggleTagFollow")
2. 异步Post表单提交

与Get请求一样,只需要将execute()换成enqueue()即可

  1. 书写方法
fun postAsyncForm(url: String){
    val body = FormBody.Builder()
    .add("userId","123")
    .add("key","value")
    .add("tagId","71")
    .build()

    val request = Request.Builder()
    .url(url)
    .post(body)
    .build()

    val call = client.newCall(request)
    call.enqueue(object :Callback{
        override fun onFailure(call: Call, e: IOException) {
            Log.e("Post异步提交表单数据失败")
        }

        override fun onResponse(call: Call, response: Response) {
            Log.i("Post异步提交表单成功","response $response")
        }
    })
}
  1. 调用
val baseUrl = "http://123.56.232.18:8080/serverdemo"        
OkHttpDemoTest1.postAsyncForm("$baseUrl/tag/toggleTagFollow")
3. Post请求文件上传

读取存储卡的文件需要在清单文件中声明权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
/**
     * 异步post上传文件
     * 在android6.0以后,读取外部存储卡的文件需要动态申请权限
     * 即时声明了权限也需要动态授权的
     */
fun postAsyncMultipart(context: Context,url: String){
    val file = File(Environment.getExternalStorageDirectory(),"1.png")
    if(!file.exists()){
        Toast.makeText(context, "文件不存在", Toast.LENGTH_SHORT).show()
        return
    }
    val multipartBody = MultipartBody.Builder()
    .addFormDataPart("key", "value")
    .addFormDataPart(
        "file", "file.png",
        RequestBody.create("application/octet-stream".toMediaType(), file)
    )
    .build()
    val request = Request.Builder().url(url)
    .post(multipartBody)
    .build()
    val call = client.newCall(request)
    call.enqueue(object :Callback{
        override fun onFailure(call: Call, e: IOException) {
            Log.e("异步post请求上传文件失败","$e")
        }

        override fun onResponse(call: Call, response: Response) {
            Log.e("异步post请求上传文件成功","response $response")
        }

    })
}
4. Post提交字符串
fun postAsyncString(url: String){
    val jsonObject = JSONObject()
    jsonObject.put("'key1","value1")
    jsonObject.put("'key2","value2")
    // 这里如果想要提交纯文本的话需要指定的请求头为 text/plain;charset=utf-8
    val body = RequestBody.create(
        "application/json;charset=utf-8".toMediaType(),
        jsonObject.toString()
    )
    val request = Request.Builder().url(url)
    .post(body)
    .build()
    val call = client.newCall(request)
    call.enqueue(object :Callback{
        override fun onFailure(call: Call, e: IOException) {
            Log.e("OkHttp Post发送json参数失败","错误信息 $e")
        }

        override fun onResponse(call: Call, response: Response) {
            Log.e("OkHttp Post发送json参数成功","返回结果 $response")
        }

    })
}

5. 拦截器

拦截器是OkHttp当中的一个比较强大的机制,可以监视,重写和重试调用请求.

本次例子书写一个拦截请求记录并日志信息输出的拦截器

  1. 创建拦截器
package com.example.myapplication.interceptor

import android.util.Log
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.ResponseBody
import okio.Buffer

// 自定义拦截器需要实现 okhttp中的接口
class LogInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        // 获取请求执行时的时间戳
        val timeStart = System.nanoTime()
        // 获取调用链中的request对象
        val request = chain.request()
        var buffer = Buffer()
        request.body?.writeTo(buffer)
        val requestBodyStr = buffer.readUtf8()
        Log.e("OkHttp",
            String.format("Sending request %s with params 5s",request.url,requestBodyStr))
        val response = chain.proceed(request)
        // response 只能读取一次,后续再次读取body的时候就会报错
        val responseData = response.body?.string()?: "response body null"
        // 构建新的responseBody
        val responseBody = ResponseBody.create(response.body?.contentType(), responseData)
        val endTime = System.nanoTime()
        Log.e("OkHttp","接口请求地址为 ${request.url},接口返回的数据是 $responseData,用时${(endTime - timeStart) / 1e6}ms ")
        // 返回新的 response
        return response.newBuilder().body(responseBody).build()
    }
}
  1. 使用拦截器

在上边创建client的时候添加上拦截器即可

private val client = OkHttpClient.Builder()
        .connectTimeout(10,TimeUnit.SECONDS) // 连接超时时间
        .readTimeout(10,TimeUnit.SECONDS) //读取超时
        .writeTimeout(10,TimeUnit.SECONDS) // 请求超时
        .addInterceptor(LogInterceptor())
        .build()

03_interceptor_1.png

  1. 优化日志输出

可以看到,上边的例子输出的日志中间有很多的无用信息,所以可以优化一下日志输出,将interceptor的级别设置为Body,这样输出的日志就会好看一点

// 将client对象提取到外边
private val client : OkHttpClient
// 初始化类的时候加载这个方法
init {
    // 使用okhttp自带的拦截器
    val httpLoggingInterceptor = HttpLoggingInterceptor()
    // 设置拦截器级别为 Body
    httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
    // 给client添加 interceptor
    client = OkHttpClient.Builder()
    .connectTimeout(10,TimeUnit.SECONDS) // 连接超时时间
    .readTimeout(10,TimeUnit.SECONDS) //读取超时
    .writeTimeout(10,TimeUnit.SECONDS) // 请求超时
    .addInterceptor(httpLoggingInterceptor)
    .build()
}

04_interceptor_2.png

3. 使用Gson

1. 添加依赖

添加在app/build.gradle

dependencies{
    implementation 'com.google.code.gson:gson:2.8.6'
}

2. 解析json到对象

这里类对象需要用 Account::class.java而不是Account.class

package com.example.myapplication.http

import com.google.gson.Gson


class Account {
    var uid: String = ""
    var userName: String = "Freeman"
    var password: String = "pwd"
    var phone: String = "17663333333"

    override fun toString(): String {
        return "Account(uid='$uid', userName='$userName', password='$password', phone='$phone')"
    }


}

fun main() {
    val jsonStr = """
            {
              "uid": "123",
              "userName": "test",
              "password": "pwd",
              "phone": "16666666666"
            }
        """.trimIndent()
    var gson = Gson()
    var fromJson = gson.fromJson<Account>(jsonStr, Account::class.java)
    println(fromJson.toString())
}

3. 对象转Json

var toJson = gson.toJson(fromJson)
println(toJson)

4. 集合转json

val jsonArrayStr = """
        [{
          "uid": "123",
          "userName": "test",
          "password": "pwd",
          "phone": "16666666666"
        }]
    """.trimIndent()
var fromJsonArray: List<Account> =
        gson.fromJson(jsonArrayStr, object : TypeToken<List<Account>>(){}.type)
println(fromJsonArray)

5. 优化实体类

使用class 定义类的时候,类里边的字段需要有初始值,不是很方便使用,所以这个时候可以用data class,而且还不用写toString()方法

data class Account2(
    val uid: String = "111",
    val userName: String,
    val password: String,
    val phone: String
)
fun main(){
    var fromJson2 = gson.fromJson(jsonStr, Account2::class.java)
    println(fromJson2)
}

6. JsonToKotlinClass插件

File --> plugins–> JsonToKotlinClass插件下载,快捷键是 alt + K,或者右键找到 generate然后选择kotlinm data class from JSON

{
  "status": 200,
  "message": "成功",
  "data": {
    "data": {
      "id": 3117,
      "userId": 160093269,
      "name": "qvelychubby",
      "avatar": "https://pipijoke.oss-cn-hangzhou.aliyuncs.com//ajsdfksjakfjasklfjkasfas_54757db023a4c2fE5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20200214204431.jpg",
      "description": "更多android进阶课程请在慕课授索lovelychubby",
      "likeCount": 985,
      "topCommentCount": 200,
      "followCount": 100,
      "followerCount": 10,
      "qqOpenId": "A8747C32A5D614281E65DA5B473D1F31",
      "expires_time": 1640266383000,
      "score": 1000,
      "historyCount": 10,
      "commentCount": 3,
      "favoriteCount": 0,
      "feedCount": 10,
      "hasFollow": false
    }
  }
}
  • 生成实体类
// 自动生成的是没有泛型的,这里可以直接添加上泛型
// 后续直接改泛型就行了,就不需要改实体类了
data class Result<T>(
    val data: Data<T>,
    val message: String,
    val status: Int
)

data class Data<T>(
    val data: T
)

data class UserInfo(
    val avatar: String,
    val commentCount: Int,
    val description: String,
    val expires_time: Long,
    val favoriteCount: Int,
    val feedCount: Int,
    val followCount: Int,
    val followerCount: Int,
    val qqOpenId: String,
    val hasFollow: Boolean,
    val historyCount: Int,
    val id: Int,
    val likeCount: Int,
    val name: String,
    val score: Int,
    val topCommentCount: Int,
    val userId: Int
)
fun main(){
    val responseJson = """ 上边的json """
    var result = gson.fromJson<Result<UserInfo>>(responseStr, Result::class.java)
    println(result)
}

4. Retrofit

Retrofit是一个高质量高效率的HTTP请求库,是一个restful的请求库,和OkHttp同样出自Square公司。Retrofit内部依赖于OkHttp,它将OKHttp底层的代码和细节都封装了起来,功能上做了更多的扩展,比如返回结果的自动解析,网络引擎的切换,拦截器…

有了Retrofit之后对于一些请求我们就只需要一行代码或者一个注解、大大简化了网络请求的代码量。

1. 注解

etrofit注解驱动型上层网络请求框架,使用注解来简化请求,大体分为以下几类:

  • 用于标注网络请求方式的注解
  • 标记网络请求参数的注解
  • 用于标记网络请求和响应格式的注解
interface ApiService{
    @GET("user/query")
    Call<User> queryUser(@Query("userId") String userId);
}

val mApi = retrofit.create(ApiService.class);
val response = mApi.queryUser("100086").execute()

05_retrofit_type.png

1. 请求方法注解

序号注解说明
1@GETget请求
2@POSTpost请求
3@PUTput请求
4@DELETEdelete请求
5@PATCHpatch请求,该请求是对put请求的补充,用于更新局部资源
6@HEADhead请求
7@OPTIONSoption请求
8@HTTP通用注解,可以替换以上所有的注解,其拥有三个属性:method,path,hasBody

2. 请求头注解

注解说明
@Headers用于添加固定请求头,可以同时添加多个。通过该注解添加的请求头不会相互覆盖,而是共同存在
@Header作为方法的参数传入,用于添加不固定值的Header,该注解会更新已有的请求头

3. 请求参数注解

名称说明
@Body多用于post请求发送非表单数据,比如想要以post方式传递json格式数据
@Filed多用于post请求中表单字段,Filed和FieldMap需要FormUrlEncoded结合使用
@FiledMap和@Filed作用一致,用于不确定表单参数
@Part用于表单字段,Part和PartMap与Multipart注解结合使用,适合文件上传的情况
@PartMap用于表单字段,默认接受的类型是Map,可用于实现多文件上传
@Path用于url中的占位符
@Query用于Get中指定参数
@QueryMap和Query使用类似
@Url指定请求路径

4. 请求和响应格式注解

名称说明
@FormUrlEncoded表示请求发送编码表单数据,每个键值对需要使用@Field注解
@Multipart表示请求发送multipart数据,需要配合使用@Part
@Streaming表示响应用字节流的形式返回.如果没使用该注解,默认会把数据全部载入到内存中.该注解在在下载大文件的特别有用

2. 使用

1. 引入依赖

app/build.gradle中添加

// 引入 retrofit框架
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'

2. 初始化

  • 创建工具类

baseUrl必须以 / 结尾,否则会报错

package com.example.myapplication.http

import com.example.myapplication.interceptor.LogInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object RetrofitUtil {
    // 创建 okhttp的client,可以在这里添加拦截器等
    private val client = OkHttpClient.Builder()
        .connectTimeout(60,TimeUnit.SECONDS)
        .readTimeout(60,TimeUnit.SECONDS)
        .writeTimeout(60,TimeUnit.SECONDS)
        .addInterceptor(LogInterceptor())
        .build()
    
    // 使用okhttp自带的日志输出
    private var clientWithHttpLoggingInterceptor = OkHttpClient()
    init {
        val httpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        clientWithHttpLoggingInterceptor = OkHttpClient.Builder()
            .connectTimeout(10,TimeUnit.SECONDS) // 连接超时时间
            .readTimeout(10,TimeUnit.SECONDS) //读取超时
            .writeTimeout(10,TimeUnit.SECONDS) // 请求超时
            .addInterceptor(httpLoggingInterceptor)
            .build()
    }

    // 创建出来 retrofit的对象
    private var retrofit = Retrofit.Builder()
    	// 这里可以使用自己定义的client
        .client(clientWithHttpLoggingInterceptor)
    	// 注意,这里必须以 / 结尾,否则会报错
        .baseUrl("http://123.56.232.18:8080/serverdemo/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    fun <T> create(clazz: Class<T>): T{
        return retrofit.create(clazz)
    }
}
  1. 创建接口
package com.example.myapplication.http

import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query

interface ApiServiceKotlin  {
    @GET(value = "user/query")
    fun queryUser(
        @Query(value = "userId", encoded = true) userId: String): Call<Result<UserInfo>>
}
  1. MainActivity中使用

retrofit框架已经帮助我们完成了线程切换,在这里可以直接操作主线程了

var serviceKotlin: ApiServiceKotlin = RetrofitUtil.create(ApiServiceKotlin::class.java)
serviceKotlin.queryUser("123456")
    .enqueue(object: Callback<Result<UserInfo>> {
        override fun onResponse(
            call: retrofit2.Call<Result<UserInfo>>,
            response: retrofit2.Response<Result<UserInfo>>
        ) {
            Log.e("Retrofit Success","'$response")
        }

        override fun onFailure(call: retrofit2.Call<Result<UserInfo>>, t: Throwable) {
            Log.e("Retrofit",t.message?: "unknown reason")
        }
    })

3. 测试用例

  • java格式
public interface ApiService {

    @GET("user/query")
    Call<User> queryUser(@Query("userId") String userId);

    //使用@Headers添加多个请求头
    @Headers({"User-Agent:android", "apikey:123456789", })
    @GET("user/query")
    Call<User> queryUser(@Query("userId") String userId);

    // 多个参数的情况下可以使用@QueryMap,但只能用在GET请求上
    @GET("user/query"")
    Call<User> queryUser(@QueryMap Map<String, String> params);

    /**
     * 很多情况下,我们需要上传json格式的数据。当我们注册新用户的时候,因为用户注册时的数据相对较多
     * 并可能以后会变化,这时候,服务端可能要求我们上传json格式的数据。此时就要@Body注解来实现。
     * 直接传入实体,它会自行转化成Json, @Body只能用在POST请求上
     *
     * 字符串提交
     */
    @POST("user/update")
    Call<User> update(@Body News post);

    /**
    * 表单提交(键值对提交)
    */
    @POST()
    @FormUrlEncoded  
    Call<User> executePost(@FieldMap Map<String, Object> maps);


    /**
     * 表单上传文件(键值对提交、同时上传文件)
     */
    @Multipart
    @POST("upload/upload")
    Call<> register(@Field("openId") String openId, @PartMap Map<String, MultipartBody.Part> map);


}
  • kotlin 版本
interface ApiServiceKotlin  {
    @GET(value = "user/query")
    fun queryUser(
        @Query(value = "userId", encoded = true) userId: String): Call<Result<UserInfo>>
}

tring> params);

/**
 * 很多情况下,我们需要上传json格式的数据。当我们注册新用户的时候,因为用户注册时的数据相对较多
 * 并可能以后会变化,这时候,服务端可能要求我们上传json格式的数据。此时就要@Body注解来实现。
 * 直接传入实体,它会自行转化成Json, @Body只能用在POST请求上
 *
 * 字符串提交
 */
@POST("user/update")
Call<User> update(@Body News post);

/**
* 表单提交(键值对提交)
*/
@POST()
@FormUrlEncoded  
Call<User> executePost(@FieldMap Map<String, Object> maps);


/**
 * 表单上传文件(键值对提交、同时上传文件)
 */
@Multipart
@POST("upload/upload")
Call<> register(@Field("openId") String openId, @PartMap Map<String, MultipartBody.Part> map);

}


- kotlin 版本

```kotlin
interface ApiServiceKotlin  {
    @GET(value = "user/query")
    fun queryUser(
        @Query(value = "userId", encoded = true) userId: String): Call<Result<UserInfo>>
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值