Flutter 開發一個 GitHub 客戶端 | 掘金技術徵文

wendux發表於2018-07-19

Gitme 是Flutter中文網flutterchina.club/ 開發的一款github客戶端,本文和大家分享一下我們使用flutter從開始設計Gitme到動手開發,再到最後上線的整個過程中的一些思考、經驗、以及趟過的坑。在閱讀本文前,您可以先去我們的官網安裝一下Gitme ,然後再對比本文中提到的點,才會有一個清晰的認識。

首先我們先來看幾張gime軟體截圖:

開屏頁
首頁
issue頁
user.jpg

目標

我們的目標是用flutter做一個高效能的,同時支援Android和iOS的github客戶端。但是,Github資源、功能比較多,並非所有功能我們都要在APP支援,在支援計劃中的功能也必須劃出優先順序,首個版本應具備一些核心功能,一些優先順序不高的功能隨著日後版本迭代一點一點來完善。經過整理、討論,我們列出了1.0中要支援的功能列表:

  1. 支援github賬號登入、登出。

  2. 登入後使用者可以檢視自己專案、動態等資訊;支援編輯個人資訊。

  3. 搜尋;1.0支援搜尋專案、使用者、issue;支援github搜尋語法。

  4. 專案:支援對專案進行star/unstar、watch/unwatch,可以檢視專案issue列表、更新動態、分支原始碼等資訊。

  5. 使用者:支援檢視使用者詳情;支援follow/unfollow使用者;如果使用者公開了郵箱,支援給使用者傳送email。

  6. Issue: 支援瀏覽、開啟、關閉、編輯、評論issue;支援給issue新增label。

  7. Label: 支援瀏覽、建立、刪除label; 支援通過label篩選issue。

  8. 書籤:關注內容可以加入書籤收藏,以便下次可以快速開啟。

  9. 國際化:支援中文簡體與美國英語。

  10. 個性化:提供多套APP主題;提供深、淺兩種程式碼主題。

技術點分析

確定目標後,就要對功能可能用到的技術做一個分析整理,確定出哪些可以在flutter中完成,哪些需要外掛。

UI

由於我們使用的是flutter, 那麼UI自然是在flutter來實現,主要熟悉一下Flutter常用widget.

資料與內容

github中絕大多數內容是原始碼檔案及markdown文字,還有一些就是圖片等其它後設資料。

  1. 對於原始碼檔案,需要渲染為等寬字型,並且排版時不能強制換行。

  2. 對於markdown文字(主要是issue、評論、文件),這是大多數使用者主要瀏覽的內容。為此必須有一個markdown解析器,這如果是在web端,沒什麼好擔心的,成熟的輪子很多,但在flutter社群,情況卻不容樂觀,在pub倉庫找到了一個flutter_markdown的包,經過測試發現坑很多,主要表現在markdown語法支援不足、樣式自定義困難、不支援tabel、不能自動識別url等,離可用相差甚遠。

  3. 對於github中的圖片,主要是一般的圖片(專案中的圖片檔案和網站的使用者頭像等)和github的私有emoji。這裡主要關注一下github emoji,它們有些特別,因為這些emoji在文字中只是一些標記,所以在渲染之前必須對文件進行解析,提取出emoji標記,然後轉化為對應的圖片,最後再進行渲染。而emoji會出現在很多地方,比如markdown中,所以這在解析markdown時也是應該考慮的點。

網路請求

Github API是開放的,v3是restful風格的,v4是graphQL風格。我們最終選擇了v3版本,因為graphQL雖然靈活,可以做到按需取資料,綠色無浪費,但在我們進行選型時,有兩個因素讓我們不得不放棄:

  1. 需要客戶端開發者自己去彙總所需資料然後寫出請求體;這非常耗時,剛開始時,我們根據github的API文件,在彙總時效率極低,一個小時才能完整的請求出兩個業務介面。

  2. 返回資料巢狀層次太深;這讓我們在將json資料轉化成dart類(類似於java bean)時非常為難,如果把返回資料當成json資料,在開發時便不能獲得ide的提示會降低開發效率;在編譯時會犧牲掉靜態型別檢查會增加潛在出錯可能性(比如欄位名輸錯了)。

確定選用v3版本的api後我們需要一個合適的http庫,我們希望http庫具備:

  1. 良好的restful介面

  2. 請求響應攔截器;這很重要,這意味著我們可以在底層統一對請求/響應進行預處理。

  3. 靈活的請求配置;比如可以統一配置請求基地址、公共header等,還有就是github 很多API在請求時都會涉及私有的content-type, 這意味著不同的請求可能需要不同的請求配置。

  4. 支援超時; 由於重所周知的原因,在國內訪問github時,有時可能需要較長的響應時間(有時甚至無法訪問),所以支援超時是非常重要的。

  5. 最好支援請求取消;主要還是因為眾所周知的原因,導致有時頁面載入過慢,當使用者沒有耐心繼續等待下去返回時,能夠將之前請求取消,避免在後臺佔用資源、浪費流量。

當然,一個優秀的Http庫可能還包括cookie管理、檔案下載/上傳等功能,但是這兩個功能在我們的需求場景中暫未用到,所以就根據這5個指標去篩選。當時經過一圈查詢,發現dart社群竟無一個同時滿足這五點的(甚至同時滿足前四點的也沒有),這也是flutter社群剛起步生態還不好的尷尬,多希望有一個dart版的okhttp! 在這種時候,我一般都會找一個滿足需求最高的開源專案,fork下來,然後定製。但是看了一些庫的原始碼,發現實在是和需求相差較大,設計思路也相差太遠,發現該輪子的成本已經大於從頭造輪子的成本,沒辦法,歷史上很多時候,就需要有一些人能夠敢為人先,挺身而出,然後留下驚才絕豔的一筆.... 於是也便有了dio

Dio is a powerful Http client for Dart, which supports Interceptors, FormData, Request Cancellation, File Downloading, Timeout etc.

值得一提的是,dio是flutter中文網開源專案之一,它主要借鑑了okhttpaxiosrequestfly 四個開源庫,所以無論是android開發者、還是前端、node開發者,相信都能很快上手dio。 目前dio在pub上得分是96分,github dart語言下專案排名22(正在快速上升中),在此,強烈向你推薦dio

外掛

Flutter的優勢是在開發UI上,但由於Flutter使用自繪引擎,並不能無縫整合原生控制元件(Android 原生控制元件及iOS UIToolkit), 而原生控制元件有一個比較大的優勢就是可以整合系統能力,比如可以呼叫相機(如surfaceView)、支援瀏覽網頁(如webview),但在flutter中,由於繪製引擎skia只支援二維圖形繪製,並不能直接結合原生功能,所以當我們要到這類原生相關的控制元件時,我們只能通過flutter外掛來呼叫原生控制元件來實現,在gitme中,主要涉及的是如何開啟github文件、issue、評論裡的url連結內容。

Webview

現在我們需要一個webview控制元件,能在應用內顯示h5網頁,而要實現這個,我們只能通過flutter外掛!

很多人問過我flutter中有沒有類似於webview這樣的widget,答案是現在沒有,將來極大可能也不會有,原因很簡單,如果在flutter中加一個webkit和v8你覺得flutter應用的安裝包有多大?

好了,現在看看有沒有現成的輪子,pub中搜到的flutter_webview倒不少,但大多數都不能直接來用,原因有兩個:

  1. 我們需要對webview所在路由(android中的activity, iOS中的controller)的導航頭進行一些自定義,比如當頁內跳轉過多時給導航欄右側加一個直接關閉當前路由的button以避免要連續多次點選返回才能退出。

  2. 我們需要webview支援一套javascript bridge協議,已備日後方便整合h5功能。

但目前沒有同時滿足這兩點的外掛,所以,我們的webview外掛還是得自己來寫,最終我們通過:

Android: Webview + DSBridge-Andriod

iOS:WKWebview + DSBridge-IOS

實現了自己的webview外掛。

其它外掛

我們還用到了fluttertoast 和 shared_preferences 外掛,前者用於一些需要提示的場景來彈toast, 後者主要用於應用配置持久化。

設計模式及架構

遵循合適的設計模式,會讓我們的程式碼邏輯清晰且易維護,一般來說不同端上都會有一套成熟的設計模式,如iOS上的mvc、android上的mvp、前端的mvvm等,那麼我們的flutter程式碼中應該遵循怎樣的設計模式?要回答這個問題,我們得先看一下flutter官方給出的程式設計正規化(Flutter框架程式設計正規化)以及google團隊創造flutter時的靈感起源React-Native。

React-Native和flutter

RN最大的特點就是狀態驅動的響應式程式設計,簡而言之就是應用程式維護一套狀態(state),並提供一個UI模板,而模板可以繫結狀態,然後當狀態發生改變時框架根據狀態的變化重新構建UI介面。可見,而整個過程中使用者不會直接操作UI控制元件樹,構建過程(包括底層優化邏輯,幾React中的diff演算法)由框架完成。

在Flutter中,和RN非常相似,使用者可以建立有狀態(stateful)無狀態(stateless) 的widget。 然後在build方法中宣告UI模板,當狀態改變時,通過setState方法通知flutter, flutter會在下一個frame中呼叫使用者提供的build方法來重建UI, 而底層的優化,如對比狀態更新前後widget樹的變化,只渲染變化部分的最小集,這些工作由flutter框架來完成,正如RN中的diff演算法也是由框架完成一樣。

所以很明顯, Flutter是一個響應式框架,忘記mvxx這一套吧,如果你非要在flutter中套用mvxx這一套設計模式,很可能就會變成過度設計。

Dart語言正規化

Dart語言最主要的特點就是結合了編譯性語言與指令碼語言之所長,特點很多,在實際動手之前,我比較關注它最受詬病的一點:在flutter中,對於複雜一點的UI,巢狀層次太深!

這一點確實無法反駁,過多的巢狀確實讓程式碼看起來很難維護,尤其是web前端開發者,早就受夠javaScript “回撥地獄 ”(callback hell)之苦,沒想到現在到了flutter還是逃不掉。但其實,問題並沒有那麼糟糕,flutter中的巢狀和javascript中的回撥巢狀是不同的,javascript中的回撥巢狀一般是非同步任務的回撥,需要在回撥中處理之前回撥的邏輯, 而flutter中的巢狀一般來說並不是回撥,而是UI widget的宣告結構,它不需要再回撥中再處理邏輯,所以,flutter中也就是巢狀層次深一些,但不會發生處理邏輯混亂。目前比較好的建議就是對於複雜的ui,最好將各個部分拆分成單獨函式。

架構

其實flutter本身就是響應式的框架,我們只需遵循響應式程式設計的規範就行,但在程式邏輯結構上,我們也要多考慮一下。由於gitme主要是通過網路從github獲取資料,然後再渲染UI. 我們可以在邏輯上對業務程式碼簡單分成兩層:底層資料IO+上層UI渲染

資料層

關於資料請求的配置、邏輯等不要在UI層去控制,而由資料層自己完成。這也就是為什麼我們隊http庫的要求中一定要包含“支援請求/響應攔截器”,因為只有支援攔截器,我們才能將io邏輯更好分離。

UI渲染層

UI層我們主要使用的事是material元件庫,但我們並沒有直接使用 ScaffoldAppBar 這些基本每個頁面都要用的元件,而是在其上包裝了一層,目的是程式風格發生變化時,我們只需要在包裝元件中統一修改即可所有頁面生效,而避免全域性去替換(也許你會說可以設定主題,但是主題的精細粒度是不夠的,有些需要自定義的點主題並不支援)。除此之外,我們也封裝了一些通用的自定義元件,如支援上拉載入、下拉重新整理的無限列表。

編碼

在想清楚上述問題後,我們對我們APP整體也就有了一個輪廓。接下來就是去逐一解決這些技術點即可。

UI佈局

佈局主要涉及Flutter中widget的使用,這一步可以結合google官方 Gallery 中的示例先摸索,等自己動手寫上幾個頁面後,佈局就會輕鬆很多,flutter元件非常多,但常用的也很固定。flutter sdk中的註釋很詳細,示例都在註釋裡(Flutter文件就是通過註釋生成的), 在IDE中可以非常方便的跳轉檢視原始碼。總之,瞭解Flutter widget的第一資料就是原始碼。

Markdown支援

dart官方有一個markdown包,它可以將markdown文字解析成html。但是我們需要的是將markdown文字直接轉化成flutter widget樹,所以這個包是不能直接用的,但是,如果我們要自己實現一個markdown到flutter的解析器,也並非易事。於是,我們想到了markdown包,看能否把它將markdown語法轉化為html這一步替換為從markdown到flutter的widget,順著這個思路,我們實現了最終的markdown解析器,並且工作良好。但是有一個問題就是:markdown包只支援純粹markdown語法解析,如果在markdown文字中嵌入html程式碼,html程式碼是不支援的,所以現在我們的markdown解析器只支援markdown語法,對內嵌html程式碼不支援。這個我們希望markdown包作者能在後續版本中支援內嵌html語法,或者等我這邊騰出手再去給它提pr。

Emoji支援

Emoji支援是在markdown解析過程中完成的,將對應的emoji標記符先轉換成markdown語法,然後再解析markdown。

Mock與快取

由於gitme中使用的網路庫是dio, 而dio的開發與迭代基本與gitme是同時的,我們也花了不少的時間在dio庫的迭代上。

Mock

在開發測試時,我們測試資料放在了一個git專案中,讓後push到github,App訪問git資料時就從github上的測試專案拉取,但是有一個問題就是每次開啟頁面時都要等待幾秒,直到資料獲取完成,這極大的影響了我們的開發效率。為了解決這個問題,我們在dio請求攔截器中做了一層mock: 如果請求的是測試專案的資料,我們直接將本地工程對應的資料返回。這樣一來有兩個好處:

  1. 需要新增、改動測試資料時無需push到github遠端倉庫,本地該了就立即生效。

  2. 節省了網路請求時間。

快取

由於github在牆外,國內訪問有時可能會在速度和穩定性上存在一些問題,為了提高使用者體驗,我們需要一個合理的快取策略。一般來說,http協議有一套完整的策略,需要伺服器與客戶端配合(通過header來傳遞快取策略資訊),但是我們呼叫的是github的介面,所以伺服器對於我們來說是不可控的,所以我們不能使用http協議本身的快取策略,這確實比較遺憾,但是現在我們又有了一種新的思路,這還是多虧dio支援攔截器,這讓我們也可以在請求前/後來定製我們的快取策略,值得一提的是,1.0中還沒有加入快取功能,這在我們後續版本迭代時會被支援。

連結攔截

如果在markdown中點選url連結時,會進行統一的預處理,比如:檢查如果是github連結的話,將其轉換為App內路由,這樣就可以在APP內開啟,避免跳到網頁中去,如果是郵箱地址,則呼叫系統郵箱APP開啟。

全域性事件匯流排

gitme中有些場景需要全域性狀態共享,這和react中的redux或vue中的vux很相似,不過gitme中需要共享的狀態並不多,所以我們採用了事件匯流排的方式來同步狀態。

外掛

正如上文所說,我們需要實現一個支援一種javascript bridge協議的webview外掛,這個需要會原生開發,本身難度不大,就是gitme中實現了狀態列自動變色功能,會根據背景顏色自動調整前景文字、圖示顏色,這使得我們的webview外掛樣式比較智慧,並且非常容易自定義主題。同時也實現了幾個API,以供javascript呼叫。

我們實現的另一個外掛是版本更新外掛,在其中我們也整合了mta統計sdk.

修輪子

在gitme中引入了一些第三方包,而其中近乎一半的第三方包無法直接使用,對於這些包,我們的做法是fork其原始碼,然後修復、定製,然後在gitme中依賴我們fork的repo(flutter支援直接依賴git專案)。在開發gitme的過程中,我們深深的體會到了生態的重要性。

總結

在1.0開發完成後,首先根據之前設定的目標,check一下完成度, 然後在談談開發過程中躺過的坑。

目標完成度

1.0的目標基本都已完成,但仍有幾個已知問題:

  1. 不支援markdown中巢狀的html程式碼。
  2. SVG暫不支援;原因是flutter目前不支援svg,而第三方的包質量太差,所以初始版本暫不支援。
  3. 程式碼染色能力不足。

對於第一個問題,上文已經談過了,待日後優化。而程式碼染色問題比較棘手,這主要是因為程式語言種類繁多,而靠譜的染色方式都是需要通過將程式碼轉化為抽象語法樹(AST,Abstract Syntax Tree),然後再進行關鍵字、方法名、類名等提取,然後應用不同樣式渲染。如果是在web端,直接引入highlight.js,但dart中目前並沒有這樣的庫,為此我們自己實現了一個簡單的分析器,我們主要測試了Dart、Javascript、Java、php四種語言的成功率,gitme 1.0.0 結果如下:

語言 成功率
Dart > 95%
Javascript > 90%
Java > 90%
php 50%

其它語言在1.0.0中染色成功率可能會非常低,由於良好的程式碼染色對gitme的使用者體驗非常重要,因此,我們的下個版本主要的任務就是優化程式碼染色,根據目前1.0.1的開發進度,我們的分析器已經足夠強大,就目前的測試結果,已經支援絕大多數程式語言,並且染色成功率都在90%以上,當然,在1.0.1上線前,我們還要進行更加全面的測試,最終的結果,敬請期待!

趟過的坑

嚴格來說,從一開始到現在遇到的問題是挺多的,但其中大部分是由於剛接觸flutter,不太熟悉,並不能說是坑,如各種widget的使用等。下面列出幾個在gitme開發過程中讓我們花費了較多時間的問題:

  1. 不要將build函式中傳入的context儲存為全域性變數(可能是為了後續使用方便),build中傳入的context會變,並且widget樹不同部分構建時的context都不同,如果使用儲存的全域性context,將會出現不可預期的錯誤。比如無法通過context正常獲取local及主題資訊(偶現);

  2. 不要將需要快取的資料儲存在widget中。

    由於Flutter響應式機制,每次狀態變化都會重新build widget樹,一般來說應該將需要快取的資料儲存在state中,由於widget和state生命週期不同,大多數情況下重新build時,state是複用的,但是發現在TabView中切換tab時,每次tab都會完全重建(包括state), 這時快取的資料就不能放在state中,有種做法是可以將資料儲存在widget中,應為widget都是你在build方法中手動建立的,只要在建立時快取一下widget(而不是每次build都重新new一個widget),這樣只要widget不重建,就可以保證儲存在widget中的資料不銷燬,但我告訴你,千萬不要這麼做,因為你快取widget的元件本身也是可能被重建的,這樣就會導致你快取的widget還是會被重建(原來儲存的資料就銷燬了); 如果你非要這麼做,那麼久必須保證從你快取widget的元件開始到widget樹根之間的所有widget都得被快取,否則,一旦flutter呼叫根widget的build方法,那麼整個widget樹都會被重新構建,之前快取的資料也就自然不復存在了。正確的做法是放在全域性狀態管理器(如redux)或全域性變數中。

  3. ListView結合RefreshIndicator 實現下拉重新整理時, 列表項如果不滿一屏,下拉重新整理無效,此時需要將ListViewprimary 屬性設定true,但設定後就不能給ListView設定controller,這是因為primary 屬性設定為trueListView 會從他父輩widget中的 PrimaryScrollController 獲取它的controller(每個Scaffold 都會預設設定一個PrimaryScrollController) 所以此時再設定controller時,flutter會報錯,解決辦法是自己手動設定一個PrimaryScrollController

  4. 當自定義導航欄(AppBar)的返回按鈕時,iOS下右滑關閉手勢會失效。這和iOS原生導航欄自定義返回按鈕會導致右滑手勢失效是一樣的。

  5. Android和iOS系統支援的字型不一樣,不要以為flutter會自己使用一套標準字型,flutter在繪製時也會使用系統字型,所以在Text widget指定字型時一定要看看是否兩個平臺都支援,gitme中在設定程式碼的等寬字型時發現了這個問題。

  6. 在替換圖片、資源後或構建release包之前要先執行flutter clean清除快取,否則有些時候,新的改動不會生效。

其它相關問題

除上面所述,關於Flutter, 還有一些問題可能是大家比較關心的。例如:

  1. 包大小; gitme 1.0.0 release版,Android: 11.7M, iOS AppStore上架後38M,可見android包比ios包小很多,當然,ios中各種尺寸的icon和launchImage確實會比android多佔用些空間,但是這3倍的差距確實也大了一些。 筆者尚未研究flutter framework ios部分程式碼,至於優化空間,我想若能更好,谷歌是不會不採取行動的。

  2. 熱更新; flutter release版預設是AOT,所以要實現熱更新,那就只能依賴dart作為指令碼語言的特性,採用JIT模式,而flutter的debug模式預設就是JIT模式,而JIT模式和AOT模式效能差距是非常大的,如果要做熱更,問題瓶頸應該在效能。但是隨著蘋果AppStore稽核策略的收緊,使用熱更都會面臨被拒風險,所以建議需要動態化的功能還是通過h5或rn/weex這樣的框架,當然h5的風險要比rn/weex更低。

  3. 效能; Flutter AOT模式下比JIT效能好很多,如果你開發時在debug模式感覺效能不佳,可以切換到AOT模式(打Release包)試試,整體來說,flutter的效能還是符合預期的,如果Release模式下效能依然不佳,那麼你就要考慮重構你的程式碼(或者換種實現方式)。

反饋和建議

我們之所以做gitme,最初是想做一個flutter範例,使用者可以直接下載,能直觀感受flutter。同時也是想做一款能夠給開發者帶來真正價值的APP。 我們(Flutter中文網)會繼續迭代gitme,如果大家有什麼好的建議或發現了bug,歡迎反饋,請在gitme issue中反饋。

下個版本計劃

下個版本我們主要會在程式碼染色和快取方面來優化使用者體驗。對於前者,上文已經仔細說過,不在贅述;對於後者,主要是因為github在牆外,在國內較慢,有時還會不穩定,所以我們考慮在APP中做一些適當的快取策略。當然如果您有其它好的功能建議,歡迎反饋。

最後

我們歡迎您使用Gitme ,如果您覺得好,歡迎把它推薦給您的朋友、同事(選單>分享), 也歡迎您的建議。最後再次貼出gitme官網flutterchina.club/app/gm.html

我們有一個APP體驗群,您可以掃描下面二維碼加入,如二維碼已過期,可以新增管理員微信Demons-du(新增時請備註"gitme使用者"), 他會將你拉進群。

gitme體驗群

從 0 到 1:我的 Flutter 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章