Android 网络请求:多功能网络请求库

news/2025/10/18 14:31:34/文章来源:https://www.cnblogs.com/laujiangtao/p/19149681

Android 网络请求:多功能网络请求库

介绍

这是一个基于现代Android技术栈的网络请求库示例项目,集成了OkHttp、Retrofit和Kotlin Flow,提供了一套完整的网络请求解决方案。项目展示了如何在Android应用中优雅地处理网络请求,包括基本请求、接口缓存、文件上传下载、断点续传等高级功能。

核心特性

  • 现代化架构:基于OkHttp + Retrofit + Kotlin Flow构建
  • 双重API风格:支持Flow响应式编程和传统挂起函数两种方式
  • 统一错误处理:提供一致的错误处理机制
  • 文件操作支持:完整的文件上传、下载和断点续传功能
  • 模块化设计:网络层高度封装,便于复用和维护
  • 多种数据格式:支持JSON对象、原始字符串等多种响应格式
  • 多个服务器支持:支持配置多个服务器地址,请求不同服务器数据

软件架构

app
├── base          # 基础组件
├── bean          # 数据模型
├── net           # 网络层核心
│   ├── api       # API接口定义
│   ├── base      # 基础响应类
│   └── ext.kt    # 扩展函数
├── ui            # 界面层
└── vm            # ViewModel层

功能演示

项目包含三个主要功能演示页面:

1. Flow风格请求 (FlowRequestActivity)

  • 基础响应式请求(BaseResponse格式)
  • 对象响应式请求(直接解析为对象)
  • 字符串响应式请求(原始数据)
  • 组合多个API请求

2. 挂起函数风格请求 (SuspendRequestActivity)

  • 传统挂起函数方式请求
  • 同步风格的数据获取
  • 多请求组合处理

3. 文件操作 (FileOperationActivity)

  • 文件选择和上传
  • 普通文件下载
  • 断点续传下载
  • 文件信息获取

使用说明

0.快速引用

repositories {...maven(url = "https://gitee.com/laujiangtao/maven-repo/raw/main/")...
}
dependencies {...implementation("me.laujiangtao.net:easynet:1.0.0")...
}

1. 初始化网络库

在Application中初始化网络模块:
多服务器配置

class MyApplication : Application() {override fun onCreate() {super.onCreate()// 初始化网络模块val server1Config = HttpConfig(serverUrl = server1Url, cacheDir = cacheDir)HttpClient.init(server1Url, server1Config)// 初始化网络模块val server2Config = HttpConfig(serverUrl = server2Url)HttpClient.init(server2Config)// 初始化网络模块HttpClient.init(server3Url)}
}

2. 定义API接口

/*** @author jiangtao on 2025/9/20* 网络请求API接口定义* 定义了应用程序所需的各种网络请求方法*/
interface ApiService {/*** 查询域名的Whois信息* @param domain 域名参数* @return 返回封装了Whois信息的BaseResponse对象*/@GET("/api/whois")@Cacheable(ttl = 30 * 1000,strategy = CacheStrategy.CACHE_FIRST,includeQueryParams = true)suspend fun whois(@Query("domain") domain: String): BaseResponse<Whois?>?/*** 查询城市天气信息* @param city 城市名称参数* @return 返回天气信息对象*/@GET("/api/weather")suspend fun tianqi(@Query("city") city: String): Tianqi/*** 获取美女图片信息(示例接口)* @return 返回任意类型的数据*/@GET("/api/pcmeinvpic")suspend fun pcmeinv(): Any?
}

2.1 动态添加请求头

interface ApiService {@Headers("X-Force-Network: true")@GET("users")suspend fun getUsersForceNetwork(): BaseResponse<List<User>>
}
interface ApiService {@GET("users")suspend fun getUsers(@Header("X-Force-Network") forceNetwork: Boolean = false): BaseResponse<List<User>>
}// 使用时强制从网络获取
apiService.getUsers("true")

3. 创建Repository

/*** @author jiangtao on 2025/9/20* 网络请求仓库类* 继承自NetRepository,提供应用程序所需的网络请求方法* 包含Flow风格和普通挂起函数风格的网络请求方法*/
class MyNetworkRepository : NetworkRepository {/*** 创建单个实例*/private val service = RetrofitClient.create(server1Url)private val apiService1 = service.createService(ApiService::class.java)private val apiService2 = service.createService<ApiService>()//请求返回非json数据private val apiService3 = service.createService<ApiService>(false)/*** 通过 HttpClient 从 Application创建的实例获取*/// 注入具体的API服务private val apiService: ApiService = HttpClient.createService(server1Url)//用于请求返回非json数据private val rawApiService = HttpClient.createService<RawApiService>(server1Url, false)private val fileApiService = HttpClient.createService(server1Url, FileApiService::class.java)// ===================================================================// Flow 风格的网络请求方法// ===================================================================/*** 查询域名信息* @param domain 域名* @return 返回包含Whois信息的Flow*/fun whois(domain: String): Flow<FlowResult<BaseResponse<Whois?>?>> = apiCall {apiService.whois(domain)}/*** 查询天气信息* @param city 城市名* @return 返回包含天气信息的Flow*/fun tianqi(city: String): Flow<FlowResult<Tianqi?>> {return apiCall {apiService.tianqi(city)}}/*** 获取美女图片接口(示例接口)* @return 返回图片数据的Flow*/fun pcmeinv(): Flow<FlowResult<String?>> {return apiCall {rawApiService.pcmeinv()}}// ===================================================================// 普通挂起函数风格的网络请求方法// ===================================================================/*** 同步风格的挂起函数 - 查询域名信息* @param domain 域名* @return 返回Whois信息的BaseResponse包装对象*/suspend fun getWhoIsInfo(domain: String): BaseResponse<Whois?>? {// 这里应该是实际的 API 调用return apiService.whois(domain)}/*** 同步风格的挂起函数 - 查询天气信息* @param city 城市名* @return 返回天气信息对象*/suspend fun getTianqiInfo(city: String): Tianqi? {// 这里应该是实际的 API 调用return apiService.tianqi(city)}/*** 同步风格的挂起函数 - 获取美女图片接口* @return 返回图片数据字符串*/suspend fun getPcmeinvInfo(): String? {// 这里应该是实际的 API 调用return rawApiService.pcmeinv()}suspend fun fetchMultipleDataDirect(domain: String, city: String): MultipleDataResult {// 并行执行所有API调用val whoisResult = apiService.whois(domain)val tianqiResult = apiService.tianqi(city)val pcmeinvResult = rawApiService.pcmeinv()// 等待所有结果val whoisData = whoisResult?.dataval tianqiData = tianqiResultval pcmeinvData = pcmeinvResult// 组合结果并返回return MultipleDataResult(whoisData, tianqiData, pcmeinvData)}/*** 合并多个网络请求结果的Flow方法*/fun fetchMultipleData(domain: String, city: String): Flow<FlowResult<MultipleDataResult?>> =combineApiCalls({ getWhoIsInfo(domain) },{ getTianqiInfo(city) },{ getPcmeinvInfo() },// ... 可以继续添加更多API调用) { results ->// 在这里处理所有结果并组合成最终数据val data1 = results[0] as? BaseResponse<Whois?>?val data2 = results[1] as? Tianqival data3 = results[2]// ... 处理其他结果MultipleDataResult(data1?.data, data2, data3)}/*** 使用Flow方式合并多个网络请求结果*/fun fetchMultipleDataWithFlow(domain: String,city: String): Flow<FlowResult<MultipleDataResult>> =combineFlows(whois(domain),tianqi(city),pcmeinv(),// ... 添加更多Flow) { results ->// 在这里处理所有结果并组合成最终数据val data1 = results[0] as? BaseResponse<Whois?>?val data2 = results[1] as? Tianqival data3 = results[2]// ... 处理其他结果MultipleDataResult(data1?.data, data2, data3)}fun uploadFile(request: FileUploadBean): Flow<FlowResult<String?>> {return apiCall {fileApiService.upload(request.build())}}/*** 普通文件下载* @param fileUrl 文件下载地址* @return Flow<FlowResult<ResponseBody>> 返回文件流的Flow*/fun downloadFile(fileUrl: String): Flow<FlowResult<ResponseBody?>> {return apiCall {fileApiService.downloadFile(fileUrl)}}/*** 带参数的文件下载* @param fileUrl 文件下载地址* @param params 下载参数* @return Flow<FlowResult<ResponseBody>> 返回文件流的Flow*/fun downloadFileWithParams(fileUrl: String,params: Map<String, String>): Flow<FlowResult<ResponseBody?>> {return apiCall {fileApiService.downloadFileWithParams(fileUrl, params)}}/*** 断点续传下载* @param fileUrl 文件下载地址* @param startByte 开始下载的字节位置* @return Flow<FlowResult<ResponseBody>> 返回文件流的Flow*/fun downloadFileWithResume(fileUrl: String,startByte: Long = 0): Flow<FlowResult<ResponseBody?>> {return if (startByte > 0) {// 断点续传val rangeHeader = "bytes=$startByte-"apiCall {fileApiService.downloadFileWithRange(fileUrl, rangeHeader)}} else {// 普通下载apiCall {fileApiService.downloadFile(fileUrl)}}}/*** 获取文件信息(用于断点续传前检查)* @param fileUrl 文件地址* @return Flow<FlowResult<Response>> 返回响应信息的Flow*/fun getFileInfo(fileUrl: String): Flow<FlowResult<Response?>> {return apiCall {fileApiService.getFileInfo(fileUrl)}}
}

4. 在ViewModel中使用

/*** @author jiangtao on 2025/9/20*/
class MyViewModel : ViewModel() {private val repository = MyNetworkRepository()// ===================================================================// Flow 风格的网络请求方法// ===================================================================fun getWhoIs(domain: String, cb: ((resp: FlowResult<BaseResponse<Whois?>?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.whois(domain).collect { result ->cb?.invoke(result)}}}fun getTianqi(city: String, cb: ((resp: FlowResult<Tianqi?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.tianqi(city).collect { result ->cb?.invoke(result)}}}fun getPcmeinvpic(cb: ((resp: FlowResult<String?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.pcmeinv().collect { result ->cb?.invoke(result)}}}// ===================================================================// 普通挂起函数风格的网络请求方法// ===================================================================// 提供同步风格的挂起函数suspend fun getWhoIsSuspend(domain: String): BaseResponse<Whois?>? {return try {// 这里使用 withContext 确保在 IO 线程执行网络请求withContext(Dispatchers.IO) {// 实际调用 API 获取 whois 信息repository.getWhoIsInfo(domain)}} catch (e: Exception) {// 记录错误日志Log.e("UserViewModel", "获取WhoIs信息失败", e)null // 返回 null 表示失败}}suspend fun getTianqiSuspend(city: String): Tianqi? {return try {withContext(Dispatchers.IO) {repository.getTianqiInfo(city)}} catch (e: Exception) {Log.e("UserViewModel", "获取天气信息失败", e)null}}suspend fun getPcmeinvpicSuspend(): String? {return try {withContext(Dispatchers.IO) {repository.getPcmeinvInfo()}} catch (e: Exception) {Log.e("UserViewModel", "获取PC妹纸图片失败", e)null}}// ===================================================================// Flow 风格的多个网络请求合并// ===================================================================suspend fun fetchMultipleDataDirect(domain: String, city: String): MultipleDataResult? {return try {withContext(Dispatchers.IO) {repository.fetchMultipleDataDirect(domain, city)}} catch (e: Exception) {Log.e("UserViewModel", "获取PC妹纸图片失败", e)null}}// 使用 combineResults 方法合并多个网络请求fun fetchMultipleData(domain: String,city: String,cb: ((resp: FlowResult<MultipleDataResult?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.fetchMultipleData(domain, city).collect { result ->cb?.invoke(result)}}}// 使用 combineResultsWithFlow 方法合并多个网络请求fun fetchMultipleDataWithFlow(domain: String,city: String,cb: ((resp: FlowResult<MultipleDataResult?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.fetchMultipleDataWithFlow(domain, city).collect { result ->cb?.invoke(result)}}}fun uploadFile(file: File, params: Map<String, String>, cb: ((resp: FlowResult<String?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {val uploadBean = FileUploadBean(file, params)repository.uploadFile(uploadBean).collect { result ->cb?.invoke(result)}}}fun downloadFile(fileUrl: String, cb: ((resp: FlowResult<ResponseBody?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.downloadFile(fileUrl).collect { result ->cb?.invoke(result)}}}fun downloadFileWithParams(fileUrl: String,params: Map<String, String>,cb: ((resp: FlowResult<ResponseBody?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.downloadFileWithParams(fileUrl, params).collect { result ->cb?.invoke(result)}}}fun downloadFileWithResume(fileUrl: String,startByte: Long = 0,cb: ((resp: FlowResult<ResponseBody?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.downloadFileWithResume(fileUrl, startByte).collect { result ->cb?.invoke(result)}}}
}

5. 在Activity中调用

Flow风格网络请求

/*** @author jiangtao on 2025/9/20*/
class FlowRequestActivity : MyBaseActivity<ActivityFlowRequestBinding>() {private val TAG = "FlowRequestActivity"private lateinit var viewModel: MyViewModelprivate var time: Long = 0private var cost: Long = 0override fun setup(savedInstanceState: Bundle?) {viewModel = ViewModelProvider(this)[MyViewModel::class.java]binding.result.movementMethod = ScrollingMovementMethod.getInstance()setOnClick()}private fun setOnClick() {// ===================================================================// Flow 风格的网络请求方法// ===================================================================binding.flowRequestBaseResponse.setOnClickListener {viewModel.getWhoIs("xxhzm.cn") {handlerData(it as FlowResult)}}binding.flowRequestObject.setOnClickListener {viewModel.getTianqi("上海") {handlerData(it as FlowResult)}}binding.flowRequestString.setOnClickListener {viewModel.getPcmeinvpic() {handlerData(it as FlowResult)}}binding.flowRequestCombinedData1.setOnClickListener {viewModel.fetchMultipleData("xxhzm.cn", "上海") {handlerData(it as FlowResult)}}binding.flowRequestCombinedData2.setOnClickListener {viewModel.fetchMultipleDataWithFlow("xxhzm.cn", "上海") {handlerData(it as FlowResult)}}}/*** 简便起见,返回数据统一处理*/override fun <T> handlerData(result: FlowResult<T?>) {result.handle(onSuccess = {hideProgress()showData(it.toString())},onError = { code, message ->hideProgress()showError("错误: $code, $message")},onException = { e ->hideProgress()showError("异常: ${e.message}")},onPrepare = {showProgress()})}private fun showProgress() {time = System.currentTimeMillis()Log.i(TAG, "showProgress")Toast.makeText(this, "showProgress", Toast.LENGTH_SHORT).show()}private fun hideProgress() {cost = System.currentTimeMillis() - timeLog.i(TAG, "hideProgress")Toast.makeText(this, "hideProgress", Toast.LENGTH_SHORT).show()}private fun showData(str: String?) {Log.i(TAG, "showData: $str")binding.result.text = "耗时: $cost ms\n"val jsonObject: JSONObject = JSONObject(str)binding.result.append(jsonObject.toString(4).toString())}private fun showError(message: String) {Log.e(TAG, "showError: $message")Toast.makeText(this, message, Toast.LENGTH_SHORT).show()}
}

挂起函数风格网络请求

/*** @author jiangtao on 2025/9/20*/
class SuspendRequestActivity : MyBaseActivity<ActivitySuspendRequestBinding>() {private val TAG = "SuspendRequestActivity"private lateinit var viewModel: MyViewModelprivate var time: Long = 0private var cost: Long = 0override fun setup(savedInstanceState: Bundle?) {viewModel = ViewModelProvider(this)[MyViewModel::class.java]binding.result.movementMethod = ScrollingMovementMethod.getInstance()setOnClick()}private fun setOnClick() {// ===================================================================// 普通挂起函数风格的网络请求方法// ===================================================================binding.requestBaseResponse.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.getWhoIsSuspend("xxhzm.cn")showData(response.toString())}}binding.requestObject.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.getTianqiSuspend("上海")showData(response.toString())}}binding.requestString.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.getPcmeinvpicSuspend()showData(response)}}binding.requestCombinedData.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.fetchMultipleDataDirect("xxhzm.cn", "上海")showData(response.toString())}}}private fun showProgress() {time = System.currentTimeMillis()Log.i(TAG, "showProgress")Toast.makeText(this, "showProgress", Toast.LENGTH_SHORT).show()}private fun hideProgress() {cost = System.currentTimeMillis() - timeLog.i(TAG, "hideProgress")Toast.makeText(this, "hideProgress", Toast.LENGTH_SHORT).show()}private fun showData(str: String?) {hideProgress()Log.i(TAG, "showData: $str")binding.result.text = "耗时: $cost ms\n"val jsonObject: JSONObject = JSONObject(str)binding.result.append(jsonObject.toString(4).toString())}
}

文件操作功能

文件上传

val uploadBean = FileUploadBean(file, params)
repository.uploadFile(uploadBean).collect { result ->// 处理上传结果
}

文件下载

/*** @author jiangtao on 2025/9/20*/
class FileOperationActivity : MyBaseActivity<ActivityFileOperationBinding>() {private val TAG = "FileOperationActivity"private lateinit var viewModel: MyViewModelprivate var selectedFileUri: Uri? = nullprivate lateinit var filePickerLauncher: ActivityResultLauncher<Intent>private var time: Long = 0private var cost: Long = 0override fun setup(savedInstanceState: Bundle?) {viewModel = ViewModelProvider(this)[MyViewModel::class.java]binding.result.movementMethod = ScrollingMovementMethod.getInstance()registerFilePickerLauncher()setOnClick()}/*** 注册文件选择器的回调*/private fun registerFilePickerLauncher() {filePickerLauncher =registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->if (result.resultCode == RESULT_OK) {val data = result.dataif (data != null && data.data != null) {selectedFileUri = data.datadisplaySelectedFileInfo(selectedFileUri!!)binding.uploadFileButton.isEnabled = true}}}}private fun setOnClick() {binding.selectFileButton.setOnClickListener { v -> openFilePicker() }binding.uploadFileButton.setOnClickListener { v -> uploadFile(selectedFileUri) }// 添加下载按钮的点击事件binding.downloadFileButton.setOnClickListener {startDownload(false) // 普通下载}binding.resumeDownloadButton.setOnClickListener {startDownload(true) // 断点续传下载}}private fun openFilePicker() {val intent = Intent(Intent.ACTION_GET_CONTENT)intent.type = "*/*" // 可以选择所有类型的文件intent.addCategory(Intent.CATEGORY_OPENABLE)filePickerLauncher.launch(intent)}private fun displaySelectedFileInfo(fileUri: Uri) {try {val cursor = contentResolver.query(fileUri, null, null, null, null)cursor?.use {val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)if (it.moveToFirst()) {val fileName = it.getString(nameIndex)val fileSize = it.getLong(sizeIndex)binding.selectedFileInfo.text ="文件名: $fileName\n大小: ${formatFileSize(this, fileSize)}"}}} catch (e: Exception) {// 如果无法获取文件信息,则只显示 URIbinding.selectedFileInfo.text = "已选择文件: $fileUri"}}/*** 通过 Uri 获取文件对象*/private fun getFileFromUri(uri: Uri): File? {return try {val cursor = contentResolver.query(uri, null, null, null, null)val fileName = cursor?.use {if (it.moveToFirst()) {val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)it.getString(nameIndex)} else {"unknown_file"}} ?: "unknown_file"// 创建临时文件val tempFile = File(cacheDir, fileName)// 将 Uri 指向的内容复制到临时文件contentResolver.openInputStream(uri)?.use { inputStream ->FileOutputStream(tempFile).use { outputStream ->inputStream.copyTo(outputStream)}}tempFile} catch (e: Exception) {e.printStackTrace()null}}private fun uploadFile(fileUri: Uri?) {if (fileUri == null) {binding.result.text = "请先选择文件\n"return}binding.result.text = "开始处理文件...\n"try {// 获取文件对象val file = getFileFromUri(fileUri)if (file != null && file.exists()) {binding.result.append("文件获取成功!\n")binding.result.append("文件名: ${file.name}\n")binding.result.append("文件路径: ${file.absolutePath}\n")binding.result.append("文件大小: ${android.text.format.Formatter.formatFileSize(this,file.length())}\n")// 在这里可以使用 file 对象进行实际的上传操作// 例如使用 OkHttp、Retrofit 等网络库上传文件binding.result.append("文件准备就绪,可以进行上传操作\n")val params = mutableMapOf<String, String>()params["param1"] = "value1"params["param2"] = "value2"viewModel.uploadFile(file, params) {handlerData(it as FlowResult)}} else {binding.result.append("文件获取失败\n")}} catch (e: Exception) {binding.result.append("文件处理失败: ${e.message}\n")}}/*** 开始下载文件* @param isResume 是否断点续传下载*/private fun startDownload(isResume: Boolean) {val fileUrl = "https://example.com/file.zip"if (isResume) {// 断点续传下载viewModel.downloadFileWithResume(fileUrl, 128) {handlerDownload(it as FlowResult)}} else {// 普通下载viewModel.downloadFile(fileUrl) {handlerDownload(it as FlowResult)}}}private fun handlerDownload(result: FlowResult<ResponseBody?>) {result.handle(onSuccess = {binding.result.text = "下载成功!\n"val responseBody = itresponseBody?.saveFile(".", "file.zip")},onError = { code, message ->binding.result.text = "下载失败!\n${code}, ${message}"},onPrepare = {binding.result.text = "正在下载...\n"},onException = { e -> binding.result.text = "下载异常!\n$e" })}/*** 简便起见,返回数据统一处理*/override fun <T> handlerData(result: FlowResult<T?>) {result.handle(onSuccess = {hideProgress()showData(it.toString())},onError = { code, message ->hideProgress()showError("错误: $code, $message")},onException = { e ->hideProgress()showError("异常: ${e.message}")},onPrepare = {showProgress()})}private fun showProgress() {Log.i(TAG, "showProgress")Toast.makeText(this, "showProgress", Toast.LENGTH_SHORT).show()}private fun hideProgress() {Log.i(TAG, "hideProgress")Toast.makeText(this, "hideProgress", Toast.LENGTH_SHORT).show()}private fun showData(str: String?) {Log.i(TAG, "showData: $str")val jsonObject: JSONObject = JSONObject(str)binding.result.text = jsonObject.toString(4)}private fun showError(message: String) {Log.e(TAG, "showError: $message")Toast.makeText(this, message, Toast.LENGTH_SHORT).show()}
}

项目结构说明

  • ApiService.kt - 标准API接口定义
  • RawApiService.kt - 原始数据API接口定义
  • FileApiService.kt - 文件操作相关API接口
  • MyNetworkRepository.kt - 网络请求仓库实现
  • MyViewModel.kt - ViewModel层,连接UI和数据
  • BaseResponse.kt - 统一响应数据结构
  • ext.kt - 扩展函数

依赖技术

  • OkHttp - HTTP客户端
  • Retrofit - REST API客户端
  • Kotlin Coroutines - 协程支持
  • Kotlin Flow - 响应式流处理

使用要求

  • Android API 21+
  • Kotlin 1.5+
  • Android Studio Arctic Fox或更高版本

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/939598.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

触想参与国家标准起草,助力行业规范化发展

近期,由触想作为主要起草单位之一、深度参与制定的国家标准计划——《工业过程测量控制和自动化 智能制造 第1部分:术语和定义》指导性技术文件,已通过审查进入批准阶段,预计2025年完成制定。正式发布后,该文件将…

F5 BIG-IP 16.1.6.1 - 多云安全和应用交付

F5 BIG-IP 16.1 LTS (Release 16.1.6.1) - 多云安全和应用交付F5 BIG-IP 16.1 LTS (Release 16.1.6.1) - 多云安全和应用交付 BIG-IP 是硬件平台和软件解决方案的集合,提供专注于安全性、可靠性和性能的服务 请访问原…

2025 年最新推荐!污水处理设备优质厂家排行榜,帮企业避开劣质产品选到高效靠谱设备

当前,污水处理需求随着环保要求提升而日益迫切,但市场上污水处理设备厂家良莠不齐,部分厂家缺乏核心技术,设备处理效率低、能耗高,难以达标;还有些厂家售后不完善,设备故障难以及时维修,严重影响企业污水处理工…

Burp Suite Professional 2025.10 发布 - Web 应用安全、测试和扫描

Burp Suite Professional 2025.10 (macOS, Linux, Windows) - Web 应用安全、测试和扫描Burp Suite Professional 2025.10 (macOS, Linux, Windows) - Web 应用安全、测试和扫描 Burp Suite Professional, Test, find,…

2025 年最新推荐真空炉制造厂家榜单:覆盖高温烧结 / 真空退火 / 智能铍铜炉,助力企业精准选型

引言随着工业 4.0 持续推进,半导体、5G 通信、航空航天等高端产业对真空炉的需求日益攀升,智能化、节能化、高精度成为设备核心竞争力。但当前市场品牌繁杂,部分设备存在升温慢、能耗高、操作复杂、维护贵等问题,企…

F5 安全事件:BIG-IP 源代码被窃取

F5 安全事件:BIG-IP 源代码被窃取F5 安全事件:BIG-IP 源代码被窃取 F5 Security Incident 请访问原文链接:https://sysin.org/blog/f5-security-incident/ 查看最新版。原创作品,转载请保留出处。 作者主页:sysin…

F5 BIG-IP 15.1.10.8 - 领先的应用交付与安全服务

F5 BIG-IP 15.1 (Release 15.1.10.8) - 领先的应用交付与安全服务F5 BIG-IP 15.1 (Release 15.1.10.8) - 领先的应用交付与安全服务 BIG-IP is a collection of hardware platforms and software solutions providing …

2025 测量仪器厂家最新推荐榜单:国产新锐与领军品牌深度解析,精准匹配工业科研需求

在工业制造升级与科研创新加速的双重驱动下,测量仪器作为 “精度标尺” 的核心价值愈发凸显,却也面临市场乱象:部分产品精度不足难以适配航空航天等高端场景,品牌鱼龙混杂导致企业选型困难,传统设备效率低下制约自…

用java打印Hello World

用java打印"Hello World"$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");建立一个后缀名为.java,文件名为xx的文件编写代码 例:// 文件名为code…

Ant Design:企业级 UI 设计语言与 React 组件库

Ant Design 是一套企业级 UI 设计语言和 React 组件库,提供丰富的组件、完整的 TypeScript 支持和国际化解决方案,广泛应用于中后台产品开发。Ant Design 项目描述 Ant Design 是一套企业级的 UI 设计语言和 React 组…

2025 年最新推荐钢套钢保温钢管源头厂家榜:聚焦品质与实力,精选优质厂家助力采购决策

引言当前钢套钢保温钢管市场需求持续增长,但行业乱象仍未彻底改善。部分小型厂家为追求短期利益,使用劣质原材料、简化生产工艺,导致产品保温性能差、易腐蚀,不仅增加工程后期维护成本,还埋下安全隐患;同时,行业…

2025年10月市场地位认证机构推荐榜:尚普与华信人深度对比评测

一、引言 在品牌同质化加速、流量成本走高的当下,企业若想用“销量第一”“品类领导者”一类表述进行传播,必须先拿到一张能被市场监管、渠道伙伴、投资人同时认可的“市场地位认证”。对于市场总监、融资负责人、招…

2025年10月智能体公司推荐榜单:五强对比与中立评测助您精准选型

一、引言 在人工智能从“模型能力”走向“场景落地”的2025年,智能体公司已成为政企客户、系统集成商与创新型开发者共同关注的枢纽环节。对于需要把大模型能力快速嵌入业务流的用户而言,选对智能体供应商意味着在数…

XPath索引定位深度解析://X[n]与(//X)[n]的本质区别

XPath索引定位深度解析://X[n]与(//X)[n]的本质区别 在自动化测试和网页爬取中,XPath是定位元素的利器。但许多开发者会遇到一个困惑: //div[@class="a"][1] 返回了多个元素(而非预期的1个) //div[@cla…

2025年10月波形护栏厂家推荐榜单:基于公开数据的中立对比与选购参考

一、引言 对于交通建设承包方、市政采购经办人以及需要批量更换护栏的运维单位而言,波形护栏的防撞等级、防腐年限、供货周期与售后响应速度直接关联到道路安全预算与后期维护成本。2025年四季度,国内钢材价格小幅波…

咱也是用上 claude 4.5 api 了

咱也是用上 claude 4.5 api 了claude连不了官方还买不了中转吗! 支持 claude code、cline、roo code、kilo code、Cherry Studio、酒馆 等常见工具~作为备用,不要太爽! ⭐️支持 claude-sonnet-4-5-20250929、clau…

优化电商包装的机器学习模型解析

本文介绍了一种基于线性模型的包装优化方法,通过数据增强和有序性约束,在降低24%货损率的同时减少5%运输成本。该模型处理了1亿个产品-包装组合,解决了传统机器学习方法在处理有序分类问题时的局限性。优化电商包装…

Index of /python/

https://mirrors.ustc.edu.cn/python/

2025年10月项目管理工具推荐榜:十款主流平台深度对比与选购指南

一、引言 在2025年第四季度预算锁定前夕,企业数字化负责人、PMO总监、初创团队CEO乃至政府采购中心都面临同一道选择题:如何在有限成本内为组织引入一套可用、可扩、可管的项目管理工具。工具一旦落地,将直接影响交…

2025年10月项目管理工具推荐榜单:基于真实数据的中立对比与选购指南

一、引言 在数字化交付节奏持续加快的2025年,项目经理、IT负责人以及采购决策者面临同一道考题:如何在预算可控的前提下,为团队挑选一套既能支撑复杂研发流程,又能与国产软硬件生态无缝衔接的项目管理工具。工具一…