做一个计数小工具的封面图
上次更新于

做一个计数小工具

关于独立开发一个跨平台应用的种种


应用首页截图
Web
应用计数页面截图
iOS
应用设置截图
Android

试一试这个应用

起因

为了一碟醋,包了一盘饺子。

一般来说,如果一个应用既要有苹果设备的版本,也要有安卓设备的版本,那就要写两套代码,因为这两个平台所使用的开发工具和编程语言是不一样的。而网页版、电脑客户端版等也都不尽相同。所以为了减少开发跨平台应用的工作量,就诞生了许多“写一遍代码、运行在多个平台”的技术。React Native 就是其中之一,通过编写 JavaScript 代码来使用它定义的通用组件,比如按钮、文本框,就能生成出不同平台的版本,分别在对应的平台运行。出于对它的底层技术 React 的喜爱,在想尝试的跨平台开发技术中,它是我首要的目标。但我还差一个足够小且至少对我自己来说有点用的需求,使我有信心,也有动力独自一人把它完成。

饺子

某天,出于某个原因,我需要统计某物在一段时间内出现的次数。由于眼睛要盯着目标,自然就不方便使用纸笔来记录,纯靠记忆又容易搞混或是遗忘。这种情况常见于火车、飞机上,乘务员在统计人数的时候,会拿一个机械的计数器,按一下它的按钮,上面显示的计数数就涨一。但为了一次偶然且突发的需要,买一个计数器实在有些浪费,换成一个手机上的计数小工具就正好合适。虽说这个需求小众,这样的应用也肯定早就有了,毕竟连定价极高且毫无功能的纯粹炫富应用都有人开发、有人买,不过我正好可以拿它来试一试开发跨平台应用。于是一晃三个月过去了,这个应用的开发告一段落,我可以来聊一聊这段经历,它没有我想的那么简单,却比我预期的更加有趣。

经过

有道理的设计

设计是一门做选择的学问,坦诚地说我学的不多。但用过的产品多了,至少有些感性的认识和自己琢磨的道理。虽说难免会有偏颇和遗漏,但我觉得有道理总比没道理强。

比如用户需要盯着目标这件事,就需要这个应用能提供非视觉的反馈,来让用户清晰地感知到每次的计数增长。首当其冲的就是每次按下计数按钮时的振动,但并不是所有的设备都有这个功能,所以语音播报当前计数就是很好的补充,而且还可以让用户无需看屏幕就能对计数进行确认。再进一步尝试就能发现,按的较快时,需要最新的语音能打断之前的,因为用户只会关心最新的数字。而一旦考虑到用户和使用场景的多样性,这些功能就会很自然地变为可选项,供用户在设置中按喜好开关、调整。另一方面,为了减少盲按时误触的可能性,屏幕上的计数按钮要越大越好,其他按钮所占的面积要越少越好。于是计数相关的功能按钮(复制、删除等)要能在不用的时候收拢起来。

我觉得这些逻辑对于大部分人来说并不难想到,所涉及到的功能也常见于各种应用之中,但没道理的设计在市场上还是一抓一大把。我认为开发的难度是客观摆在那的,但设计的难度是一种人为的选择。如果有心麻烦自己,不妥协于“能用就行”,抓住每个细节的可能性,通过换位思考和亲自尝试来做出更有利用户的选择,这样诞生的应用怎么会那么不好用呢?不知道你有没有像我一样忍无可忍的时候,通过产品中提供的联系方式反馈自己使用时的困惑,但比起告知我问题暂时无法解决,更令我沮丧的是不止一次的毫无回应。我猜想那些不够有道理的设计,多半不来自于专业性的欠缺,而来自于对这件事的漠不关心吧。

无处安放的小游戏

开发期间,我无意间探索出了一个有些幼稚的玩法——比手速。只要把对应人数的计数器合并成组,每人点一个计数器。既可以限时比谁点的多,也可以比谁先点到一定数量。做起来也不难,只要在应用中增加一个开始游戏的按钮、计时器和游戏结果的显示。但这有必要吗?

也许它可以作为一个聚会破冰小游戏,但也许只能获得众人尴尬且不失礼貌的微笑。又或者这世上会有一个电竞选手,因为某些原因无法使用电脑但能使用手机,而且 TA 玩的那款游戏不是手游,如果这样奇葩的情景能够存在的话,那这个小游戏是不是能成为他锻炼手速的好帮手呢?

但最终使我搁置这个小游戏的,不是它本身那微妙的实用性,而是它无处安放的事实。毕竟这本来就只是一个小工具应用。小游戏的组件无论放在界面中的哪个位置,都会挤占本就不多的空间,混淆原有的功能逻辑。最可行的方案也就是在设置界面中,设置一个彩蛋选项来打开独立的小游戏界面,但这就使得它的优先级更要往后放放。所以很遗憾,如果你是一个身陷囹圄的电竞选手,还要再等等,等我哪天有空了再来做这个小游戏。

当然,你其实也可以另拿一个秒表之类的计时器来做这件事。

UI:要不要重复造轮子?

UI 直译过来叫用户界面,一般指的是用户能在屏幕上看到、操作的一切。UI 上要显示哪些文字和图像、要有哪些交互逻辑、要怎么排版所有的视觉元素等等,这些设计最终都要转化为代码。而编写 UI 代码的过程对我来说,就像是搭积木,只不过拼搭的是 UI 组件,比如按钮、文本框等等。虽说只要组合基本款的组件,就能搭出任意的效果,但也会稍显繁琐。比如 React Native 本身提供的组件,种类就相对单一。再加上秉持着“不要重复造轮子”的理念,我一开始就选择了第三方的 UI 库,没想到这反而使我绕了很多路。

首先就是学习成本,其大小取决于 UI 库的设计是否简洁明了、文档是否健全。但可能由于 UI 库要考虑的东西实在太多,再加上跨平台额外增添的复杂性,设计难度非常高,所以我试过的 React Native 的 UI 库用起来都十分别扭。不是出现莫名其妙的 bug,就是文档如同一团浆糊,连一个属性适用于哪些平台也没写清楚。进而诞生的还有整合成本。当一个 UI 库无法解决所有的问题时,就需要整合其他的 UI 库或是零散的 UI 组件。而这些 UI 库都有一套自己的相对封闭的逻辑,用以实现相对一致的表现,却也使得整合成本变得很高。

除了没找到满意的 UI 库之外,一些固有的问题也使我焦虑。比如选用的 UI 库日后能否持续稳定地更新。因为一旦它停止维护,我需要的不止是重写代码,还要丢掉和这个库绑定的使用经验。同时这也束缚了我未来可能开展的别的项目。而且 UI 库往往关注的是通用的解决方案。对效率的提升和一致性的保障也只在大型项目中比较有价值。但这个应用对我来说,只是一个小而美的东西。许多复杂的组件根本用不到,用到的也不会很频繁。而我对各种细节的个性化的要求又很难被完全满足。当认清了这些情况后,我还是回到了 React Native 提供的 UI 组件上,开始按需设计、组合、实现我自己的组件。

而这件事实际做起来远比我想象的简单和顺畅。因为只给自己一个人使用,就无需担心组件库的完整性,可以根据需要一点点添砖加瓦,工作量并不大。而且自己实现的组件能完全掌控,实现的过程也加深了对更底层逻辑的理解,写代码的效率提高了许多。更关键的是,我由此理解了我在用第三方 UI 库时觉得别扭的底层原因:在我自己的项目里,设计与实现由我一人负责,而 UI 的代码是直接为我的作品表达服务的,是直接和我的用户产生连接的地方。这就使得“高效完成代码的编写”远没有“代码准确地实现了我的设计诉求”来得重要。在没有幸运地遇到一套极其适配的“成品积木”的情况下,自己动手就会更快更好。

React Native 的 Modal 组件不仅被我用来实现确认、编辑弹窗,还被用来实现下拉菜单,这样就可以轻易做到“点击菜单外的地方来关闭菜单”的操作。加上我还做了 toast(应用内的通知消息),一个奇怪的场景就出现了:下拉菜单会遮挡通知消息!

通常来说,通知消息应该是置顶的,那么理论上可以把它也放到 Modal 中。但同时,通知消息的出现不应该使得别的组件无法交互,而 Modal 虽然可以设置成透明的,但无法使用 pointerEvents: "box-none" 来使底下的组件可交互。难道要在显示 Modal 的时候,把通知消息挪进去,不显示的时候再挪出来吗?这样绕的逻辑容易产生复杂且不好懂的代码,进而容易产生错误,我希望尽量避免。

于是,为了使 Modal 和 toast 并存,我尝试自己写一个 Modal 的替代品。这并不难,表面上的效果也没什么问题,只是在无障碍上出了问题。原本的 Modal 的无障碍实现是很完善的,每当弹窗显示的时候,读屏器的焦点就会自动地移动到其中,并且不会意外地跑出去。理论上我可以用 React Native 的 AccessibilityInfo 来实现这一点时,但实际做的时候,却怎么也无法正确地移动读屏器的焦点。

戏剧性的是,在我找到解决办法前,这个问题本身就被设计的迭代给消灭了,因为在布局调整之后,下拉菜单和通知消息几乎不可能同时出现。我可以换回简单且好用的 Modal 组件,也不用担心我的通知消息被遮挡。或许我再坚持一下办法就有了,但谁说不相往来、各过各的不是一种共存呢?

不完美是常态,尤其是跨平台

我很后悔没有早点开始跨平台测试。在发布前夕,我才开始在安卓真机上进行测试,而这一下就发现了许多在 iOS 上没有的问题。我当然可以找借口说,在开发的过程中我还没有一台安卓设备,但事实上我连 Android 模拟器都没怎么测。我也可以说,React Native 乃至 Expo 理应填平所有的平台间差异,但考虑到任何工具都不是完美的,更何况是解决跨平台这样极其复杂需求的工具,平台特定甚至是设备特定和版本特定的问题很难避免。拖到最后才进行全平台测试的代价就是,我不得不重写一些逻辑、重新设计一些细节、为安卓版本单独写一些解决方案。所幸这个应用不大,可以快速搞定这一切,但我想下次我会把这种问题扼杀在摇篮里,避免积重难返的窘境。以下是两个出错的例子。

版本问题和版本无法自动升级的问题

Android 15 版本开始强制推行 edge-to-edge,也就是说一个应用的内容可以延伸到顶端的状态栏和底端的导航栏,这和目前的 iOS 是统一的。但 Expo 在我开发时仍未默认支持 React Native 0.77,也就是支持 Android 15 的版本。这就导致在 iOS 中从上到下一致的背景色,在 Android 上被割裂成了三个部分,需要单独设置颜色,而且有时不能同步地变化颜色,视觉体验很糟糕。更令人沮丧的是,在 React Native、Expo 和 Expo 的一些插件里,有多个重复的针对这个问题的功能,让人迷惑不解不说,而且实际上没一个好使的。但还记得我前面说的是 Expo “仍未默认支持” React Native 0.77 吗?在它文档的某个角落里,我找到了手动更新的方式,使项目的 Android 目标版本提升到了 15,问题迎刃而解。但在更老版本的 Android 系统中,暂时还没有很好的解决办法。

千呼万唤不出来的键盘

React Native 的 Modal 中的 TextInput 如果设置了 autoFocus 属性,在 Android 上,能自动聚焦,但无法唤起键盘。没有任何报错,找不到任何靠谱的解决方案。最终我只能尝试绕过它,所幸 Expo Router 提供的 Modal 可以很好地在 Android 上显示,而且不会有无法唤起键盘的问题,但它在 iOS 上完全是另一个样子,所以在其他平台我仍沿用了原本的代码。

结果

是时候对 React Native 发表看法了

尽管有这样那样的问题,我仍然觉得它是一个很棒的技术,因为我本要花至少两倍的时间来完成这件事。当然只靠它本身可能也不够,所幸在丰富的第三方支持的情况下(其中 Expo 几乎是必须的),我基本无需去了解、使用平台原生的开发技术。当然适配海量的不同型号、不同系统的设备仍然是一个难点,更何况软硬件都在持续且快速地迭代。但在应用平台和技术如此丰富的当下,做减法显然是有意义的事情,它让更多的个体和小团队能开发出跨平台应用,触达更多的用户,提供更多的可能。在完成这个应用前,我有数不清的“烂尾楼”,完美主义是其中的罪魁祸首,但跟这篇文章的主题无关,就暂且按下不表。但 React Native 无疑起到了助力,它使我更少地和工具、技术本身周旋,而是把重心放在解决问题上,无论是应用开发的还是我自身的问题。

独立开发不只是写代码

虽说这个应用只是我的一个尝试,我也很乐意结束在这里。但若想用户能长久且稳定地使用它,这个应用就不只是一个应用,更应该被视作一项服务。无论是测试、宣发、更新还是解决用户遇到的问题,都需要持续的进行。这就使得独立开发者的工作远不止是写代码,也不只是要做产品的设计,还要承担后续宣发、运维的角色。而在跨平台开发的情况下,为了保证每个平台的用户都能有相似且可靠的体验,任务只会更加艰巨,而这些都是 React Native 本身爱莫能助的。在当下,我只能说我会去尽力尝试,但其结果仍不可知。

延伸阅读