十六年全棧開發者的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"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章