news 2026/9/3 2:06:34

用Android+Kotlin+Compose打造轻量股票行情应用:从免费接口到实时刷新

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Android+Kotlin+Compose打造轻量股票行情应用:从免费接口到实时刷新

简介:基于Android平台的股票行情小软件完整工程包,数据源采用新浪股票接口,适合Android初学者或对证券数据可视化感兴趣的开发者学习参考。项目实现了自选股的添加与删除,支持输入股票代码和简称,并覆盖三大股票的实时行情,同时提供个股日K、周K、月K等历史K线展示以及盘口交易数据,功能脉络清晰,代码结构紧凑。资源包共116个文件,大小约3.79MB,以Java源码、编译生成的class文件、Android界面布局XML、界面图标PNG以及第三方JAR依赖为主,另附可直接安装的APK,便于对照运行和验证功能。目前已有848人学习下载,可作为课程设计、毕业设计或入门实战的参考项目。通过阅读源码可掌握网络数据请求解析、列表增删交互、K线绘制及多界面跳转等常见Android开发要点,具备较强的动手演练价值。 我印象很深,有一次在地铁上打开手机想看一眼自选股,结果被某炒股App的开屏广告、持仓弹窗、资讯红点轮番轰炸,等真正看到股价已经过去半分钟了。我就想,自己用Android做一个简单的股票行情小软件,打开就是列表,刷新就是价格,不搞社区不看资讯,干净利落。这个想法落地之后用了大半年,启动快、不卡顿、没广告,成了我手机里打开频率最高的行情工具。这篇就聊聊我怎么做出来的,包括数据源选择、UI刷新方案、轮询省电的那些事,以及真机联调时踩过的坑。如果你也是Android开发者,想给自己或者身边人写一个够用且不复杂的行情软件,这篇应该能帮你少走不少弯路。

1. 动手之前先定边界:什么才算"简单"的行情软件

1.1 我把产品需求收敛成了三件事

市面上的行情App功能很多,但绝大多数人日常就三件事:看自选列表、看实时价格、看涨跌幅。所以这个软件我一开始就划定了功能红线——只有三个页面状态:自选列表页、行情详情页(只展示价格相关字段)、搜索添加页。不做K线、不做分时图、不接资讯流、不做模拟交易。

"简单"在不同人眼里定义不同,但对软件来说,少即是多。每多一个功能,就多一条刷新链路、多一类异常处理。一个只做行情的App,其实真正要解决的核心问题只有两个:数据从哪来、界面怎么刷。把这两件事做好,这个软件就立住了。

我给自己定的验收标准是:从点击图标到看见行情不超过2秒,自选列表滑动不掉帧,切到后台就不耗电,打开App看到的永远是最近一次成功的行情数据。

1.2 技术选型:原生Android + Kotlin + Jetpack Compose

技术栈我选的是原生Kotlin + Jetpack Compose,维护起来顺手。之前有人问我为什么不用Flutter或者WebView套壳。我的理由很简单:行情类应用对刷新流畅度和内存占用敏感,原生方案在列表滚动和局部刷新上更有底气;同时这个项目不涉及多端复用、不需要复杂原生插件,原生开发反而是最省事的路径。

Compose在这个场景下有个天然优势,数据变化驱动UI重组,价格更新直接更新到某个Text组件,不需要手动notifyDataSetChanged。列表用LazyColumn配稳定key,局部更新非常丝滑。

版本方面,我的配置是minSdk 24、targetSdk 34,兼容绝大多数市面上还在用的Android手机。JDK用的17,Android Studio版本建议至少保持Hedgehog之后的版本,对Compose编译支持更好。

如果你熟悉XML + RecyclerView那一套,同样可以完成这个项目,核心思路大同小异,只不过UI更新的代码量会多一些。我这里直接用Compose讲,因为它确实是目前新项目的默认选择。

2. 行情数据从哪来:免费接口的选择和解析细节

2.1 免费A股行情接口

做股票行情软件第一道坎就是数据源。正规的券商行情接口需要License,个人开发者基本拿不到;第三方聚合API大多收费,免费额度也很寒酸。实际上最靠谱的免费方案是新浪和腾讯的HTTP行情接口,它们本来是为自家网页版服务的,返回的是纯文本或简单格式,直接用就行。

我用得最多的是新浪接口,请求格式:

https://hq.sinajs.cn/list=sh600519,sz000001,sh000001

这个接口支持一次请求多个股票代码,用逗号分隔,返回内容是一个JS变量赋值,每一行代表一只股票的行情字段。返回样例:

var hq_str_sh600519="贵州茅台,1700.000,1690.000,1720.000,1735.000,1688.000,1719.000,1720.500,2023-12-01,15:00:00,00,";

腾讯接口格式类似:

https://qt.gtimg.cn/q=sh600519

两个接口返回的都是GBK编码的中文文本,这一点后面专门讲坑。

2.2 各类字段的含义和解析逻辑

新浪接口返回的字符串以逗号分隔,关键字段索引如下:

索引含义示例值
0股票名称贵州茅台
1今开价1700.000
2昨收价1690.000
3当前价1720.000
4今日最高价1735.000
5今日最低价1688.000
6买一报价1719.000
7卖一报价1720.500
8日期2023-12-01
9时间15:00:00

涨跌幅需要自己算:(当前价 - 昨收价) / 昨收价 * 100。所以数据模型里我存的是昨收和当前价,UI层统一计算涨跌幅,这样不管来源是哪家接口都能算出正确结果。

2.3 请求时的两个关键配置

新浪接口有个防盗链限制,直接不带Referer请求会返回403或者提示信息。所以OkHttp请求时必须带一个浏览器的Referer头:

val request = Request.Builder() .url("https://hq.sinajs.cn/list=sh600519") .header("Referer", "https://finance.sina.com.cn") .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") .build()

另一个关键点是编码。OkHttp默认用UTF-8解析响应体,但接口返回的是GBK,直接用response.body?.string()会乱码。正确做法是先拿到原始字节流,再手动指定GBK解码:

val bytes = response.body?.bytes() ?: return val text = String(bytes, Charset.forName("GBK"))

这个细节不处理,你看到的股票名称就是一堆乱码。

注意:新浪接口的个股代码前必须带市场前缀,sh代表上海,sz代表深圳。不带前缀或前缀错了会返回空数据。指数也一样,上证指数是sh000001,深证成指是sz399001。

3. UI层怎么做:列表刷新不闪烁、不卡顿的方案

3.1 数据模型和状态设计

行情数据每秒钟都在变,但UI不能整个列表全部重建,否则滑动时会有明显的闪烁和卡顿。我用Compose的状态管理解决了这个问题。

先定义数据模型:

data class StockQuote( val code: String, val name: String, val currentPrice: Double, val preClosePrice: Double, val openPrice: Double, val highPrice: Double, val lowPrice: Double ) { val changePercent: Double get() = if (preClosePrice > 0) { (currentPrice - preClosePrice) / preClosePrice * 100 } else 0.0 }

页面里维护一个自选股列表状态:

var quoteList by remember { mutableStateOf<List<StockQuote>>(emptyList()) }

请求完成后更新这个状态,Compose只会在数据变化时重组依赖了数据的UI组件。关键是LazyColumn里每一项要设置稳定的key,用股票代码做key:

LazyColumn { items(quoteList, key = { it.code }) { quote -> StockListItem(quote = quote) } }

这样当某一只股票价格变化时,只有那一行会被重组,其他行不受影响。

3.2 颜色变红变绿的规则和防闪烁处理

A股的涨跌颜色规则是红涨绿跌、平盘灰色,跟美股刚好相反。价格变色逻辑我抽成了一个独立函数:

@Composable fun StockListItem(quote: StockQuote) { val color = when { quote.currentPrice > quote.preClosePrice -> Color(0xFFE53935) quote.currentPrice < quote.preClosePrice -> Color(0xFF43A047) else -> Color(0xFF757575) } Text( text = String.format("%.2f", quote.currentPrice), color = color, style = MaterialTheme.typography.titleMedium ) }

这里有个细节值得注意:红色和绿色必须在深色和浅色背景下都清晰可读。我最初用的纯红Color.Red和纯绿Color.Green,在深色模式下对比度差、看着费劲,后来换成了Material Design的红色和绿色才能看清楚。

还有一个体验优化是"价格变化提示"。A股股票价格每次跳动至少0.01元,如果只是颜色变了,眼睛不太容易捕捉到变化。我加了一个轻量的高亮动画:价格变动时数字背景短暂闪一下颜色,再变回透明。这个效果在Compose里用Animatable就能实现:

val background by animateColorAsState( targetValue = if (isChanged) flashColor else Color.Transparent, animationSpec = tween(durationMillis = 400) )

实测这个效果对"扫一眼就知道哪只股票在动"的帮助非常大,而且实现成本很低。

3.3 格式化显示的坑

股票价格显示成几位小数,不同价格区间规则不一样。我的做法简单粗暴:

  • 低价股(<10元)显示4位小数
  • 中价股(10~100元)显示2位小数
  • 高价股(>=100元)显示2位小数

但A股最低变动单位是0.01元,所以正常情况2位小数就够了,遇到某些特殊股票(比如可转债)会显示3位。我最后统一保留到3位小数再格式化,避免出现"因为精度问题显示"这种低级bug。

涨跌幅我显示的是带正负号的百分比,比如+2.35%-1.24%。正号不能丢,不然看不出方向。

4. 轮询刷新的正确姿势:既要实时也要省电

4.1 为什么不能搞一个无限循环去刷新

最直观的做法是启动一个定时器,每隔几秒请求一次接口,然后更新UI。这在App停留在前台时没问题,但切到后台之后如果还继续跑,问题就来了:

  • 耗电,网络请求和解析都是耗电大户
  • 后台请求容易被系统限制甚至直接杀掉进程
  • 造成不必要的流量消耗

在Android 12及以上版本的设备上,系统对后台应用的网络访问限制更加严格。所以在这个项目里,我的原则很简单:只有页面可见时才刷新,页面不可见立刻停止。

4.2 生命周期感知的刷新方案

我的刷新逻辑用协程实现,配合Lifecycle组件。核心思路是在repeatOnLifecycleSTARTED状态里启动刷新循环,页面进入后台就自动取消:

@Composable fun StockScreen(viewModel: StockViewModel) { val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(lifecycleOwner) { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.startAutoRefresh() } } }

startAutoRefresh在ViewModel里就是一个循环请求:

suspend fun startAutoRefresh() { while (isActive) { refreshQuotes() delay(REFRESH_INTERVAL_MS) } }

我设置的刷新间隔是30秒。A股开盘时间内的行情变化,30秒基本够用。想更激进可以改成15秒,但需要注意接口方的承受能力和自己的流量消耗,个人自用15~30秒其实都合理。

4.3 请求超时和失败重试策略

免费接口不比商业API,偶尔超时或返回异常是常态。我的请求策略是:单次请求设置5秒超时,失败后延迟3秒重试一次,再失败就放弃这次刷新,保留UI上的旧数据,并在页面顶部显示"更新失败,显示上次数据"的提示,而不是清空列表。

private suspend fun refreshQuotes() { for (attempt in 1..2) { try { val quotes = api.fetchQuotes(codes) _quoteList.value = quotes return } catch (e: Exception) { if (attempt == 2) { _errorMessage.value = "行情刷新失败,请下拉重试" } else { delay(3000) } } } }

这里我踩过一个坑,就是请求失败时如果不做处理,列表会闪着加载状态但一直不更新,用户感知就是"卡住了"。后来我改成"失败静默",不弹窗不转圈,只是顶部显示一条浅灰色提示,体验好很多。

5. 自测与真机联调时遇到的坑

5.1 模拟器突然连不上行情源

开发初期我用模拟器调试,请求新浪接口一直失败。后来发现不是接口问题,是Android默认禁止HTTP明文流量。从Android 9(API 28)开始,应用的明文HTTP请求默认会被拦截,而新浪接口当时只支持HTTP协议。我需要显式开启明文流量支持。

在AndroidManifest.xml的application标签里加:

<application android:usesCleartextTraffic="true" ...>

不过这种做法适合个人自用的工具类App。如果是要上架的应用,更规范的做法是配置networkSecurityConfig,只允许特定域名走明文HTTP,其他域名强制HTTPS:

<network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">hq.sinajs.cn</domain> </domain-config> </network-security-config>

这个配置放在res/xml/network_security_config.xml里,然后在AndroidManifest.xml中引用。

5.2 行情字段里混入了奇怪字符

有一次排查解析异常,发现某只停牌股票返回的数据里,价格字段全部是0.000,而名称字段却正常。当时我的解析代码拿到currentPrice = 0.0之后直接计算涨跌幅,算出个-100%给用户看,这显然是错的。

后来我加了过滤逻辑:当前价小于等于0视为无效行情,列表里显示为"--"而不是0.00。这个坑没有数据字典光靠看正常行情样本是发现不了的,必须在真实请求里主动检查异常字段。

5.3 真机测试遇到的HTTPS证书问题

换成真机测试后,又遇到一个问题:某些国产手机上行情接口的HTTPS证书校验失败。原因有一部分是系统时间不准,有一部分是厂商ROM自带的证书信任配置与原生Android不同。排查办法:先检查系统时间,时间不准的先去设置里校准;如果还报SSLHandshakeException,再用浏览器打开行情地址看是否正常。

如果只是自己用,可以临时在OkHttp里配置一个宽松的TrustManager,但这等于把加密链路的防护拆掉了,不建议长期保留。更稳妥的办法是尽量用HTTPS接口,并把证书校验失败当成一次普通请求失败处理,不做特殊放行。

5.4 后台被杀之后重新打开的数据恢复

我在开发过程中经常遇到的情况是:App在后台被系统回收,重新打开后列表是空的。原因是行情数据只存在内存里,没有持久化。后来我用Room数据库存了一份最近一次成功的行情快照,App启动后先读本地数据秒开页面,再异步请求最新行情覆盖。

这样用户体验就变成了:冷启动瞬间能看到上一次的行情(哪怕已经过去几小时),然后刷新最新价格。对比之前"加载中"转圈几秒才看到内容的体验,区别还是很明显的。

重要提示:自选股列表是用户的核心资产,必须持久化,我用DataStore存了自选股代码列表,启动时先读本地配置再加载行情,整个流程不依赖网络。

6. 一些让我用得很舒服的细节优化

6.1 下拉刷新要有反馈

虽然自动刷新30秒一次,但用户长时间盯着列表时还是想"手动确认一下现在的价格"。我在列表外层加了SwipeRefresh,下拉时立即触发一次刷新。痛点在于Compose里自带的pullRefresh在部分版本上反馈动画不够跟手,后来我直接用了Material 3的PullToRefreshBox组件,原生托手,效果很自然。

6.2 行情页打开时自动定位到第一只股票

很多时候用户关注的是"我持仓的第一只股票",因为平时看的次数最多。我加了一个小功能,进入自选列表时自动把第一只股票的涨幅钉在页面顶部显眼位置,这样打开App不用找,就能看到最关心的那只。这个改动虽然简单,但实际使用频率非常高。

6.3 桌面小组件值得做

后来我还给自己的软件加了一个桌面上2x2的小组件,显示自选列表前三只的实时行情。桌面组件走的是RemoteViews,数据要通过AppWidgetProvider更新。一开始觉得麻烦,但做完之后发现这才是最实用的形态——解锁手机不用打开App就能看到行情,完全符合"简单"的初衷。

不过桌面组件的刷新机制跟App内刷新完全不同,不能复用协程刷新逻辑,需要用WorkManager或者AlarmManager定时更新RemoteViews,而且系统对桌面组件的刷新频率同样有限制。如果你只想做一个工具体不打算上架,这个功能可以放到第二期再考虑。

这个项目从开始到稳定运行,前后花了一个多月。如今我手机上这类工具类应用越来越多,反而更觉得"简单"是个稀缺品质。行情软件本质是把复杂的数据变成一眼就能看懂的涨跌信息,做太多加法,反而会失去工具存在的意义。如果你也打算做类似的东西,我的建议是从最小闭环开始,先把自选列表和实时刷新跑通,再根据实际使用感受加功能,别在一开始就把App塞得太满。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 2:06:34

森海塞尔HD660S2低频实测:暖声不等于猛,前端驱动是关键

拿到森海塞尔HD 660S2之后&#xff0c;我连续听了一个多月&#xff0c;再回头看“低频很暖”这个说法&#xff0c;发现它特别容易被误会。很多人把“暖”理解成低频量感大&#xff0c;以为一戴上就能获得那种震胸的低音炮感。实际不是。660S2的“暖”更多是中低频衔接处有厚度、…

作者头像 李华
网站建设 2026/9/3 2:05:44

网络安全入门到精通:Web安全与渗透测试实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 2:04:37

51单片机驱动16x16 LED点阵屏:从硬件设计到代码实现的完整指南

简介&#xff1a;本资源是一套完整的基于51单片机的1616 LED点阵显示控制系统设计资料&#xff0c;适用于电子类课程设计、毕业设计及单片机实践入门学习者&#xff0c;解决初学者在点阵驱动、字符动态显示与人机交互控制方面的典型工程实现难题。压缩包共53个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/3 2:01:08

基于OpenCV与YOLO的台球击球路线规划系统:从视觉感知到几何决策

简介&#xff1a;本资源是一个面向高校计算机及相关专业&#xff08;如人工智能、自动化、电子信息等&#xff09;学生的课程实训项目&#xff0c;聚焦台球击球路线智能规划问题&#xff0c;融合OpenCV图像处理与YOLO目标检测技术&#xff0c;实现从台球桌图像采集、球体识别定…

作者头像 李华
网站建设 2026/9/3 2:00:24

基于OpenCV与KNN的LED数码管电表读数自动识别实战

简介&#xff1a;本资源面向计算机视觉初学者与电力自动化领域开发者&#xff0c;提供一套基于Python与OpenCV实现LED数码管电表读数自动识别的轻量级解决方案&#xff0c;解决传统人工抄表效率低、易出错的问题&#xff0c;适用于智能电表巡检、工业能源监控等实际场景。压缩包…

作者头像 李华
网站建设 2026/9/3 1:59:18

拉格朗日乘数法详解:从数学原理到Python实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华