Gitme 是Flutter中文網 https://flutterchina.club/ 開發的一款github客戶端,本文和你們分享一下咱們使用flutter從開始設計 Gitme到動手開發,再到最後上線的整個過程當中的一些思考、經驗、以及趟過的坑。在閱讀本文前,您能夠先去咱們的官網安裝一下 Gitme ,而後再對比本文中提到的點,纔會有一個清晰的認識。
首先咱們先來看幾張gime軟件截圖:javascript
咱們的目標是用flutter作一個高性能的,同時支持Android和iOS的github客戶端。可是,Github資源、功能比較多,並不是全部功能咱們都要在APP支持,在支持計劃中的功能也必須劃出優先級,首個版本應具有一些核心功能,一些優先級不高的功能隨着往後版本迭代一點一點來完善。通過整理、討論,咱們列出了1.0中要支持的功能列表:php
肯定目標後,就要對功能可能用到的技術作一個分析整理,肯定出哪些能夠在flutter中完成,哪些須要插件。html
因爲咱們使用的是flutter, 那麼UI天然是在flutter來實現,主要熟悉一下Flutter經常使用widget.前端
github中絕大多數內容是源代碼文件及markdown文本,還有一些就是圖片等其它元數據。vue
Github API是開放的,v3是restful風格的,v4是graphQL風格。咱們最終選擇了v3版本,由於graphQL雖然靈活,能夠作到按需取數據,綠色無浪費,但在咱們進行選型時,有兩個因素讓咱們不得不放棄:java
肯定選用v3版本的api後咱們須要一個合適的http庫,咱們但願http庫具有:node
content-type
, 這意味着不一樣的請求可能須要不一樣的請求配置。固然,一個優秀的Http庫可能還包括cookie管理、文件下載/上傳等功能,可是這兩個功能在咱們的需求場景中暫未用到,因此就根據這5個指標去篩選。當時通過一圈查找,發現dart社區竟無一個同時知足這五點的(甚至同時知足前四點的也沒有),這也是flutter社區剛起步生態還很差的尷尬,多但願有一個dart版的okhttp! 在這種時候,我通常都會找一個知足需求最高的開源項目,fork下來,而後定製。可是看了一些庫的源碼,發現實在是和需求相差較大,設計思路也相差太遠,發現該輪子的成本已經大於從頭造輪子的成本,沒辦法,歷史上不少時候,就須要有一些人可以敢爲人先,自告奮勇,而後留下驚才絕豔的一筆.... 因而也便有了dio:react
Dio is a powerful Http client for Dart, which supports Interceptors, FormData, Request Cancellation, File Downloading, Timeout etc.
值得一提的是,dio是flutter中文網開源項目之一,它主要借鑑了okhttp、axios、request、fly 四個開源庫,因此不管是android開發者、仍是前端、node開發者,相信都能很快上手dio。 目前dio在pub上得分是96分,github dart語言下項目排名22(正在快速上升中),在此,強烈向你推薦dio。android
Flutter的優點是在開發UI上,但因爲Flutter使用自繪引擎,並不能無縫集成原生控件(Android 原生控件及iOS UIToolkit), 而原生控件有一個比較大的優點就是能夠集成系統能力,好比能夠調用相機(如surfaceView)、支持瀏覽網頁(如webview),但在flutter中,因爲繪製引擎skia只支持二維圖形繪製,並不能直接結合原生功能,因此當咱們要到這類原生相關的控件時,咱們只能經過flutter插件來調用原生控件來實現,在gitme中,主要涉及的是如何打開github文檔、issue、評論裏的url連接內容。ios
如今咱們須要一個webview控件,能在應用內顯示h5網頁,而要實現這個,咱們只能經過flutter插件!
不少人問過我flutter中有沒有相似於webview這樣的widget,答案是如今沒有,未來極大可能也不會有,緣由很簡單,若是在flutter中加一個webkit和v8你以爲flutter應用的安裝包有多大?
好了,如今看看有沒有現成的輪子,pub中搜到的flutter_webview倒很多,但大多數都不能直接來用,緣由有兩個:
但目前沒有同時知足這兩點的插件,因此,咱們的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。
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語言最主要的特色就是結合了編譯性語言與腳本語言之所長,特色不少,在實際動手以前,我比較關注它最受詬病的一點:在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組件庫,但咱們並無直接使用 Scaffold
、 AppBar
這些基本每一個頁面都要用的組件,而是在其上包裝了一層,目的是程序風格發生變化時,咱們只須要在包裝組件中統一修改便可全部頁面生效,而避免全局去替換(也許你會說能夠設置主題,可是主題的精細粒度是不夠的,有些須要自定義的點主題並不支持)。除此以外,咱們也封裝了一些通用的自定義組件,如支持上拉加載、下拉刷新的無限列表。
在想清楚上述問題後,咱們對咱們APP總體也就有了一個輪廓。接下來就是去逐一解決這些技術點便可。
佈局主要涉及Flutter中widget的使用,這一步能夠結合google官方 Gallery 中的示例先摸索,等本身動手寫上幾個頁面後,佈局就會輕鬆不少,flutter組件很是多,但經常使用的也很固定。flutter sdk中的註釋很詳細,示例都在註釋裏(Flutter文檔就是經過註釋生成的), 在IDE中能夠很是方便的跳轉查看源碼。總之,瞭解Flutter widget的第一資料就是源碼。
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支持是在markdown解析過程當中完成的,將對應的emoji標記符先轉換成markdown語法,而後再解析markdown。
因爲gitme中使用的網絡庫是dio, 而dio的開發與迭代基本與gitme是同時的,咱們也花了很多的時間在dio庫的迭代上。
在開發測試時,咱們測試數據放在了一個git項目中,讓後push到github,App訪問git數據時就從github上的測試項目拉取,可是有一個問題就是每次打開頁面時都要等待幾秒,直到數據獲取完成,這極大的影響了咱們的開發效率。爲了解決這個問題,咱們在dio請求攔截器中作了一層mock: 若是請求的是測試項目的數據,咱們直接將本地工程對應的數據返回。這樣一來有兩個好處:
因爲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的目標基本都已完成,但仍有幾個已知問題:
對於第一個問題,上文已經談過了,待往後優化。而代碼染色問題比較棘手,這主要是由於編程語言種類繁多,而靠譜的染色方式都是須要經過將代碼轉化爲抽象語法樹(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開發過程當中讓咱們花費了較多時間的問題:
context
保存爲全局變量(多是爲了後續使用方便),build中傳入的context會變,而且widget樹不一樣部分構建時的context都不一樣,若是使用保存的全局context,將會出現不可預期的錯誤。好比沒法經過context正常獲取local及主題信息(偶現);因爲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)或全局變量中。
ListView
結合RefreshIndicator
實現下拉刷新時, 列表項若是不滿一屏,下拉刷新無效,此時須要將ListView
的primary
屬性設置true
,但設置後就不能給ListView
設置controller,這是由於primary
屬性設置爲true
的ListView
會從他父輩widget中的 PrimaryScrollController
獲取它的controller(每一個Scaffold
都會默認設置一個PrimaryScrollController
) 因此此時再設置controller時,flutter會報錯,解決辦法是本身手動設置一個PrimaryScrollController
。AppBar
)的返回按鈕時,iOS下右滑關閉手勢會失效。這和iOS原生導航欄自定義返回按鈕會致使右滑手勢失效是同樣的。flutter clean
清除緩存,不然有些時候,新的改動不會生效。除上面所述,關於Flutter, 還有一些問題多是你們比較關心的。例如:
咱們之因此作gitme,最初是想作一個flutter範例,用戶能夠直接下載,能直觀感覺flutter。同時也是想作一款可以給開發者帶來真正價值的APP。 咱們(Flutter中文網)會繼續迭代gitme,若是你們有什麼好的建議或發現了bug,歡迎反饋,請在gitme issue中反饋。
下個版本咱們主要會在代碼染色和緩存方面來優化用戶體驗。對於前者,上文已經仔細說過,不在贅述;對於後者,主要是由於github在牆外,在國內較慢,有時還會不穩定,因此咱們考慮在APP中作一些適當的緩存策略。固然若是您有其它好的功能建議,歡迎反饋。
咱們歡迎您使用Gitme ,若是您以爲好,歡迎把它推薦給您的朋友、同事(菜單>分享), 也歡迎您的建議。最後再次貼出gitme官網https://flutterchina.club/app/gm.html 。
咱們有一個APP體驗羣,您能夠掃描下面二維碼加入,如二維碼已過時,能夠添加管理員微信Demons-du(添加時請備註"gitme用戶"), 他會將你拉進羣。