Android AI解放生产力(五)实战:解放写API接口的繁琐工作

232 阅读9分钟

公司当前项目使用的是Apifox作为API调试/文档工具,所以以这个为例。市面上的工具应该都逐步开放了AI能力,需要关注一下,只要能提供原始数据就行,不能的话那也没办法喂给AI。

上一篇已经让AI写UI了,这一篇让AI写接口请求。首先思考一下可能碰到的问题。

一、可能遇到的问题

默认网络请求用的retrofit,其它也是一通百通。

1、问题一:适配请求头

项目中网络请求的拦截器已经定义了请求头,读取的原始数据如何关联?答案就是在skill中告诉AI,并让它模仿你的代码写。

2、问题二:适配请求参数中有一些公共部分

项目中网络请求的拦截器已经定义了一些公共请求参数,读取的原始数据如何关联?例如:

{
    "app": {
        "locale": "{{locale}}",
        "tid": "{{terminal_id}}",
        "platform": "{{platform}}"
    },
    "data": {
        //每个接口只有这里有差异
    }
}

只有data块中有差异。答案其实同上,skill中告诉AI,并让它模仿你的代码。

3、问题三:返回数据脱壳

例如项目中的壳是这样:

data class ApiResponse<T>(
    val code: Int,
    val msg: String,
    val data: T
) : BaseResponse<T>() {...}

那我们需要的解析的Data Class可能只需要脱壳的部分,如何让AI知道?答案也同上,skill中告诉AI,并让它模仿你的代码。

4、问题四:viewmodel中网络请求如何写

道理同上。

5、问题五:代码放在哪个文件

这个问题的解答要看情况了,我们先看下面只给代码的示例,最后再探讨一下这个问题。

二、获取接口原始数据

这一步依赖MCP的开放能力,后端兄弟接口怎么定义的,字段怎么写的,Android如何封装Data Class都依赖这些原始数据。接下来看通过Apifox如何办到的。

Apifox的MCP现阶段还不能集成到Codex中使用,启动的时候会报错:

⚠ MCP client for `apifox_api_docs` failed to start: MCP startup failed: handshaking with MCP server failed: connection closed: initialize response
⚠ MCP startup incomplete (failed: apifox_api_docs)

原因是Apifox的MCP返回不符合Codex定义的规范。

那换其他方式,打开Apifox终端(当前V版本2.7.58 (2.7.58)),选择需要生成代码的接口:

image.png

如上操作,可以得到复制的一段信息如下:

请访问以下链接获取接口“天气信息V3”的接口定义信息:https://api.apifox.cn/temp-links/api/%E5%A4%A9%E6%B0%6%81%AFv3-253061517?t=e0be3ab5-e8bf-4ec2-be81-f1489

这样我们就拿到了接口的原始数据链接。

三、如何编辑skills

这一步跟项目强关联,因为每个项目写法都不一样,文件不一样,所以skill的写法因项目而异。

先自己手动写一份粗糙的,然后给AI帮你改一改。下面贴一份修改后的skill:

---
name: api_to_request
description: 请访问用户提供的接口文档链接,解析请求/响应结构,并按项目约定生成:Retrofit 接口、@Body(仅 data 节点)、剥壳后的 Response data class、以及优化后的 ViewModel 调用代码(全部用 Markdown 代码块输出)。
---

访问链接生成 Retrofit / Request / Response / ViewModel 代码

何时使用

当用户提供一个接口定义链接(Swagger/Apifox/Postman 文档/自研文档页面等),并要求按项目既定规范生成 Android(Kotlin) 网络层代码时使用。

输入
   •  必须:接口文档链接(包含请求方式、path、请求体、返回体说明)
   •  可选:项目约束/示例(已有 api service、已有 response/request 命名、字段命名风格、是否可空、是否用 @SerializedName 等)

⸻

输出(必须全部提供,且全部用 Markdown 代码块输出)
   1. 代码一:Retrofit 接口定义(放入 java/api 对应 service)
   2. 代码二:请求 @Body(仅 data 节点)(Request data class 或 Map 形态)
   3. 代码三:剥壳后的 Response data class(放入 java/data4. 代码四:ViewModel 中实际请求代码(优化版)(基于现有 requestWithHttpException 机制)
   5. 代码五:View 层 StateFlow 收集示例(统一回调)

如需说明,仅在代码块外给简短要点,不要发散。

⸻

核心约定(严格遵守)

A. Retrofit 接口风格
   •  按接口文档请求类型使用:@GET/@POST/@PUT/@DELETE/...
   •  路径按文档给出,例如:@POST("/v1/iot/camera/discoverEvents")
   •  返回统一:ApiResponse<T?>
   •  必须写 KDoc 注释,说明「做什么」
   •  形参规则:
   •  若接口使用 body:@Body request: XxxRequest 或 @Body params: Map<String, Any?>
   •  若 data 节点为空:不要要求调用方传参
   •  优先:suspend fun xxx(): ApiResponse<Resp?>
   •  仅当必须显式 body(后端/网关要求)时:@Body params: Map<String, Any?> = emptyMap()

B. 请求体只给 data 节点

项目通过拦截器 AddPublicParamsInterceptor.kt 统一注入公共字段(例如 app.locale/tid/alias/platform 等),因此:
   •  @Body 只包含 data 的内容
   •  不要在 Request 中声明公共字段(如 app/tid 等)
   •  若 data 为空,调用方不传参(或由默认 {} 兜底)

C. Response 必须“剥壳后”

网络返回会被 ApiResponse.kt 统一剥壳,因此输出的 Response 类只描述真正业务数据结构:
   •  输出为 data class XxxResponse(...)
   •  字段名遵循服务端返回字段(通常 snake_case)
   •  是否用 @SerializedName:按项目既有写法决定
   •  可空性:以文档标注为准;没标注时宁可保守为可空
   •  嵌套结构需拆分成内部 data class(或独立 data class,按项目习惯)

D. ViewModel 请求代码要更“干净”

关注`BaseViewModelApiResult.kt`类,里面包含示例:
   •  requestWithHttpException(...) 获取结果,返回值使用when 分支解析
   •  参数构造清晰(优先 buildMap {} 或 request data class)
   •  方法命名与注释统一清晰

E. View层使用StateFlow回调
   •  注意使用View层的协程

⸻

严格流程(每次都按此执行)
   1. 打开链接并提取接口定义
      •  请求方式、path
      •  query/path/header/body 结构(如有)
      •  data 节点字段清单:类型、必填/可选、枚举/限制(如有)
      •  返回体结构:识别 wrapper,并提取业务 data(用于“剥壳后”Response)
   2. 生成代码一:Retrofit 接口定义
      •  HTTP 方法与 path 必须与文档一致
      •  返回类型固定 ApiResponse<T?>
   3. 生成代码二:@Body(仅 data)
      •  字段少且动态:给 buildMap { put(...) } 示例
      •  字段多/有嵌套:建 XxxRequest data class(只含 data 字段)
      •  data 为空:不要求调用方传参(或默认 {})
   4. 生成代码三:Response data class(剥壳后)
      •  仅业务字段,按返回体 data 的结构拆分嵌套类
   5. 生成代码四:ViewModel 调用(优化版)
      •  统一用私有封装减少重复
      •  StateFlow 输出统一 UiState
   6. 生成代码五:View 层收集示例
      •  使用 repeatOnLifecycle + collect
   7. 最终输出格式固定
      •  依次输出:代码一/二/三/四/五
      •  所有代码必须放在 Markdown 代码块中

⸻

推荐写法模板(用于生成 ViewModel 优化版)

private val _deviceState = MutableStateFlow<UiState<TripHistoryResponse?>>(UiState.Idle)
val deviceState: StateFlow<UiState<TripHistoryResponse?>> = _deviceState.asStateFlow()

fun discoverTrips() {
    viewModelScope.launch {
        _deviceState.value = UiState.Loading
        val response = requestWithHttpException<TripHistoryResponse?, Any?> { cameraDeviceApiService.discoverTrips(tripsRequest) }
        when (response) {
            is ApiResult.Success -> {
                _deviceState.value = UiState.Success(response.data)
            }
            is ApiResult.Fail -> {
                _deviceState.value = UiState.Fail(code = response.body.code, msg = response.body.msg)
            }
            is ApiResult.Error -> {
                _deviceState.value = UiState.Error(errorCode = response.e.errCode, errorMsg = response.e.errorMsg)
            }
        }
    }
}


View 层模板(输出时作为“代码五”)

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.deviceState.collect { state ->
            when (state) {
                UiState.Idle -> Unit
                UiState.Loading -> {
                    // show loading
                }
                is UiState.Success -> {
                    // TODO handle success
                }
                is UiState.Fail -> {
                    // TODO handle fail
                }
                is UiState.Error -> {
                    // TODO handle error
                }
            }
        }
    }
}

⸻

你输出时的固定结构(必须遵守)

代码一:Retrofit 接口定义

// 放在对应 ApiService 中

代码二:@Body(仅 data// XxxRequest data class 或 buildMap 示例(二选一或都给,看接口复杂度)

代码三:Response data class(剥壳后)

// 放在 java/data 下,参考项目现有 response 写法

代码四:ViewModel 请求代码(优化版)

// 给出可直接粘贴的 ViewModel 方法(包含必要的私有封装 + StateFlow)

代码五:View 层 StateFlow 收集示例

// repeatOnLifecycle + collect 模板

⸻

质量检查清单(输出前自检)
   •  Retrofit 注解的 HTTP 方法与 path 与文档一致
   •  @Body 只包含 data 节点;data 为空时不强迫传参(除非必须显式 body)
   •  Response 已剥壳:不再包含 ApiResponse 外层结构
   •  ViewModel 已减少重复样板,参数构造清晰,回调透传简洁一致
   •  所有代码均在 Markdown 代码块中输出

Apifox复制链接的内容因为匹配skill的description,所以给出链接就可以触发上面的skill。Codex生成的代码如下(剔除了敏感信息):

 代码一:Retrofit 接口定义

  // 放在对应 ApiService 中
  import com.foxxusa.beaver.deps.network_use.ApiResponse
  import retrofit2.http.Body
  import retrofit2.http.POST

  interface HomeManagementApiService {

      /**
       * 获取天气信息V3
       */
      @POST("/v1/iot/weather/getCurrentInfo")
      suspend fun getWeatherInfoV3(
          @Body request: WeatherInfoV3Request
      ): ApiResponse<WeatherInfoV3Response?>
  }

  代码二:@Body(仅 data// XxxRequest data class
  data class WeatherInfoV3Request(
      val lon: Double,
      val lat: Double,
      val unit: WeatherUnit = WeatherUnit.IMPERIAL
  )

  enum class WeatherUnit {
      METRIC,
      IMPERIAL
  }


  代码三:Response data class(剥壳后)

  // 放在 java/data 下,参考项目现有 response 写法
  data class WeatherInfoV3Response(
      val day: Boolean,
      val weather: WeatherV3,
      val temperature: Double,
      val wind: Double,
      val pressure: Double,
      val humidity: Double,
      val aqi: String
  )

  data class WeatherV3(
      val condition: String,
      val icon: String? = null
  )


  代码四:ViewModel 请求代码(优化版)

  private val homeManagementApiService = MainNetworkApi.homeManagementApiService

  private val _weatherInfoState =
      MutableStateFlow<UiState<WeatherInfoV3Response?>>(UiState.Idle)
  val weatherInfoState: StateFlow<UiState<WeatherInfoV3Response?>> =
      _weatherInfoState.asStateFlow()

  fun fetchWeatherInfoV3(lon: Double, lat: Double, unit: WeatherUnit = WeatherUnit.IMPERIAL) {
      viewModelScope.launch {
          _weatherInfoState.value = UiState.Loading

          val request = WeatherInfoV3Request(lon = lon, lat = lat, unit = unit)
          val result = requestWithHttpException<WeatherInfoV3Response?, Any?> {
              homeManagementApiService.getWeatherInfoV3(request)
          }

          when (result) {
              is ApiResult.Success -> {
                  _weatherInfoState.value = UiState.Success(result.data)
              }
              is ApiResult.Fail -> {
                  _weatherInfoState.value =
                      UiState.Fail(code = result.body.code, msg = result.body.msg)
              }
              is ApiResult.Error -> {
                  _weatherInfoState.value =
                      UiState.Error(errorCode = result.e.errCode, errorMsg = result.e.errorMsg)
              }
          }
      }
  }

  代码五:View 层 StateFlow 收集示例

  viewLifecycleOwner.lifecycleScope.launch {
      viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
          viewModel.weatherInfoState.collect { state ->
              when (state) {
                  UiState.Idle -> Unit
                  UiState.Loading -> {
                      // show loading
                  }
                  is UiState.Success -> {
                      // TODO handle success
                  }
                  is UiState.Fail -> {
                      // TODO handle fail
                  }
                  is UiState.Error -> {
                      // TODO handle error
                  }
              }
          }
      }
  }

生成的代码可用度极高,很大程度上解放了生产力。

这个是单个网络请求,还可以编写分页网络请求并发网络请求的skill等。

四、AI的放权

AI生成的代码可用度这么高,那是不是应该放权直接让AI写文件呢?

1、如果是一个新项目,定义好项目架构后就大胆的让AI生成文件自己写代码吧,能”摸鱼"就别动手,人生苦短我用AI。

2、如果是一个成熟度高的项目,建议以小颗粒度的形式让AI生成或修改文件代码,这样影响的范围可控,git提交的时候请仔细审查AI生成的代码。

3、如果是一个成熟度高的项目,修改的需求复杂,牵扯面广,或者公司规定不让AI写代码只让辅助参考,那还是开只读模式自己写吧。

回到最上面的问题五:代码放哪个文件?

答案就简单了。

  • 项目架构清晰,让AI干活,可以完全让AI自主决策,后续自己调整。
  • 可以在skill中添加提示加以引导,让AI放哪个文件/目录。
  • 只读模式,自己处理AI生成的代码,前期可以开只读熟悉AI的使用,等熟练后就可以让AI自己写文件了。