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

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

首先咱們先來看幾張gime軟件截圖: php

開屏頁
首頁
issue頁
user.jpg

目標

咱們的目標是用flutter作一個高性能的,同時支持Android和iOS的github客戶端。可是,Github資源、功能比較多,並不是全部功能咱們都要在APP支持,在支持計劃中的功能也必須劃出優先級,首個版本應具有一些核心功能,一些優先級不高的功能隨着往後版本迭代一點一點來完善。通過整理、討論,咱們列出了1.0中要支持的功能列表:html

  1. 支持github帳號登陸、註銷。前端

  2. 登陸後用戶能夠查看本身項目、動態等信息;支持編輯我的信息。vue

  3. 搜索;1.0支持搜索項目、用戶、issue;支持github搜索語法。java

  4. 項目:支持對項目進行star/unstar、watch/unwatch,能夠查看項目issue列表、更新動態、分支源碼等信息。node

  5. 用戶:支持查看用戶詳情;支持follow/unfollow用戶;若是用戶公開了郵箱,支持給用戶發送email。react

  6. Issue: 支持瀏覽、打開、關閉、編輯、評論issue;支持給issue添加label。android

  7. Label: 支持瀏覽、建立、刪除label; 支持經過label篩選issue。ios

  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 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章
相關標籤/搜索