十六年全栈开发者的Android开发踩坑实录

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这是一个完完全全马后炮的故事。身为拥有差不多十六年开发经验的全栈 web 开发者,作者对构建 web 应用所需要的各种技术可谓是了如指掌。而在最近几年的工作项目中,作者第一次成为了一名安卓开发者。在经过一段时间的磨合之后,作者才意识到,从 web 开发转型到安卓、移动端应用开发,开发者的思维也需要一定转换。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安卓开发的萌新们走错的路大多数都可以在项目后期通过重构或修改构建流程解决,不断打磨直到单元测试完美覆盖需要的所有情况也能处理一些小错误。但剩下的漏网之鱼就不是那么好解决了,这些足以在 app 的生命历程中造成持久影响、令人想要将整个项目推翻重来的错误中,有些甚至让作者羞于启齿自己曾经犯过它们。以下将提供一些防止你想要穿越回过去重做项目导致时间悖论(笑)的小 tips,希望能够帮助大家预防那些难以摆脱的糟糕麻烦。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"添加应用内更新"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"立刻、马上。一直到出炉一年后,我们才把更新通知功能塞进我们的 app 里。内置的更新提醒功能在项目初始就添加的话,那么过程就还算简单,但如果拖到后期才做的话,难免会造成不少的问题,其中包括:必须手动搭建自定义流程,以及用户自行尝试跳过更新。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"kotlin"},"content":[{"type":"text","text":"\/\/ Creates instance of the manager.\nval appUpdateManager = AppUpdateManagerFactory.create(context)\n\/\/ Returns an intent object that you use to check for an update.\nval appUpdateInfoTask = appUpdateManager.appUpdateInfo\n\/\/ Checks that the platform will allow the specified type of update.\nappUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->\n if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE\n \/\/ For a flexible update, use AppUpdateType.FLEXIBLE\n && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)\n ) {\n \/\/ Request the update.\n appUpdateManager.startUpdateFlowForResult(\n \/\/ Pass the intent that is returned by 'getAppUpdateInfo()'.\n appUpdateInfo,\n \/\/ Or 'AppUpdateType.FLEXIBLE' for flexible updates.\n AppUpdateType.IMMEDIATE,\n \/\/ The current activity making the update request.\n this,\n \/\/ Include a request code to later monitor this update request.\n MY_REQUEST_CODE)\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相信我,这项功能将会是你的 app 的突破式改变。app 的现有用户可能已经通过其他的 app 习惯了应用内更新功能,甚至会理所应当地认为这其实应该是移动端平台的一项特点之一。但实际上,直到我亲身经历了安卓开发,才知道原来这项功能还要开发者手动添加。当你的 app 不幸停止运行之后,用户并不会去找软件更新包,他们只会卸载再安装,甚至更糟的是,他们会在应用商城留下评论。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"限制 API 密钥"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先让程序跑起来,出了问题再去打补丁。或许你也有这个习惯,但请不要继续拖延了。指路一篇关于谷歌云平台上 API 密钥 的文章,但对于其他平台,这一点同样适用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于 GCP(谷歌云平台)来说,我们只需要在登录谷歌账号,选择要设置限制的 API 密钥后,系统便会跳转到密钥的属性界面。在“应用限制”里选择安卓应用,点击“+”添加软件包名称到需要添加限制的 API 密钥下即可。至于添加证书指纹,可以直接复制页面中的命令后,按照网页右侧的指示,只需要几分钟就可以完成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们在 app 出厂两年后才开始限制 API 密钥。然而在限制之后,app 的一个地图功能罢工了。回滚更改之后,我们费了好大一番功夫才找到问题所在。app 所使用的大部分谷歌官方软件包都可以完美适配限制 API 密钥后的代码,唯独其中一张地图需要重写另一套 API 调用代码。如果在项目初始我们能考虑到 API 密钥的限制问题,并将其写入源码,这无疑会增加开发时间,但到了后期我们就可以不用再担心限制的问题了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"故事并没有在这里结束。为了能在保证地图的正常运行并限制 API 密钥,我们不得不进行强制更新。我们有后台的统计数据可以监控用户的更新流程,而数据表明,有 90% 的用户在收到更新通知的几周后才进行更新,而另外 10% 的用户则在地图几乎彻底罢工的情况下依旧选择不更新,完全不晓得他们是怎么忍受这种 bug 的。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"内部 API 版本控制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当我还在主攻 web 开发时,我一直都搞不太明白为什么有人会想这么做。在更新前端代码后,为什么还要留着旧版本的 API?怎么想都是无用的浪费。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但用户使用的软件版本不同时,API 的大更新可能会导致软件大范围的崩溃。应用内更新的方法可以帮忙缓解这种问题,但过程将会无比漫长。划分 API 版本更像是一种针对这类软件崩溃的,快捷简单的解决方案,而非是我曾经以为的过度工程。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"万事先离线"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们的 app 是有实用目标的。当我们收到用户反馈的 app 反应卡顿、响应超时时,我还只是移动端应用开发的小白,刚刚接触到一个新的名词:优先离线(Offline First)。如果用户联网失败,所有未上传、未保存的东西都会丢失,等到连接恢复,他们将不得不重新输入所有的内容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"优先离线的结构会将更改内容写入本地数据库,等有网络连接时再进行同步。这样一来,用户得以在离线下使用 app,联网时响应也会更快,用户不用再干巴巴地等着服务器传回响应才能进行下一步操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/9b\/9bb43d65c10ec6f4e3e96dbc6e0c4eea.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"离线优先的功能在项目后期可能会更难实现,难易度取决于 app 的数据的复杂程度。所以还请尽快决定 app 是否需要它。我们至今还在研究要如何在我们的“高龄”app 中更好地实现这项功能…"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"谨慎选择导航项"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你的安卓 app 结构复杂、有很多界面的话,开发进程到后期再去修改导航项麻烦程度将超乎你的想象。我们的 app 在后期是直接改为了底部导航的形式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一些情境下,安卓开发中的 Activity 可以被看做是 app 中某块屏幕的代码;安卓 3.0 才有的 Fragments 则可以被理解子视图代码或是 app 中的部分代码。二者的 layout 都是通过 XML 定义的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们的导航指向的是 app 不同区域中的主要功能,这些导航小卡片又各自导向不同的子功能,一共连接起了三十余个 Activity。这些也不过是这款基于 Activity 的 app 中的四个 fragment。导航抽屉则是另一种常见导航形式,主要服务对象是 Activity 对接 Activity 形式的导航需求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"底部导航因为 app 的底边栏一直都是可见状态,所以它的设计对象是 fragment 式导航。在将底边栏添加到 Activity 后,接下来我们只需要它相关的代码敲进该 Activity,并把它的 view 添加到 Activity 的 layout 中。这样,通过点击底边栏的按钮,我们就可以把 fragment 加载到 Activity 中了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,为了在 app 中添加底部导航栏,我试图将 Activity 转换为 fragment。结果很悲惨,过量的 bug 直接导致软件崩溃,浪费了我一个月的时间。如果我们只有五六个 Activity,那么解决起来可能还不算太难,但事实上我们的 app 足足有三十多个 Activity!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这直接导致了我在这一个月了放弃了其他工作,专注为每一个 Activity 添加导航功能。我还尝试过创建一个 helper 函数,但这并不能帮我省多少麻烦,到头来还是要一个个地为 Activity 写代码。同时,我还需要把底边栏添加到所有的 layout 中,并且在已有的 layout 中为这个小家伙腾地方。再加上还要对 Activity 栈进行编程操作,防止出现竞赛条件。虽然过程繁琐,但最后好歹还是成功了,并且效果还不错。只不过如果在项目最初我就能把底部导航栏加上去,并且从基于 fragment 的方向开始设计,那么将轻松很多。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这只是份不完全清单……"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当然,在开始你的第一份安卓应用时,还有很多其他的事情需要考虑的,比如添加单元测试、确定一个 app 的模式后不要更改等等。但如果你之前有接触过其他类型的开发模式,这些应该都不陌生。或许你并不会遇到与文中提到的一模一样的问题,但恐怕不会相差太多。希望这些小 tip 能够帮你意识到安卓开发与其他的类型的开发是不甚相同的,这些开发决定的影响或许能持续相当长一段时间。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文链接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/triplebyte.com\/blog\/everything-id-do-differently-if-i-could-go-back-and-rewrite-my-android-app-today","title":"","type":null},"content":[{"type":"text","text":"https:\/\/triplebyte.com\/blog\/everything-id-do-differently-if-i-could-go-back-and-rewrite-my-android-app-today"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章