Gitme 是Flutter中文网flutterchina.club/ 开发的一款github客户端,本文和你们分享一下咱们使用flutter从开始设计Gitme到动手开发,再到最后上线的整个过程当中的一些思考、经验、以及趟过的坑。在阅读本文前,您能够先去咱们的官网安装一下Gitme ,而后再对比本文中提到的点,才会有一个清晰的认识。javascript
首先咱们先来看几张gime软件截图: php
咱们的目标是用flutter作一个高性能的,同时支持Android和iOS的github客户端。可是,Github资源、功能比较多,并不是全部功能咱们都要在APP支持,在支持计划中的功能也必须划出优先级,首个版本应具有一些核心功能,一些优先级不高的功能随着往后版本迭代一点一点来完善。通过整理、讨论,咱们列出了1.0中要支持的功能列表:html
支持github帐号登陆、注销。前端
登陆后用户能够查看本身项目、动态等信息;支持编辑我的信息。vue
搜索;1.0支持搜索项目、用户、issue;支持github搜索语法。java
项目:支持对项目进行star/unstar、watch/unwatch,能够查看项目issue列表、更新动态、分支源码等信息。node
用户:支持查看用户详情;支持follow/unfollow用户;若是用户公开了邮箱,支持给用户发送email。react
Issue: 支持浏览、打开、关闭、编辑、评论issue;支持给issue添加label。android
Label: 支持浏览、建立、删除label; 支持经过label筛选issue。ios
书签:关注内容能够加入书签收藏,以便下次能够快速打开。
国际化:支持中文简体与美国英语。
个性化:提供多套APP主题;提供深、浅两种代码主题。
肯定目标后,就要对功能可能用到的技术作一个分析整理,肯定出哪些能够在flutter中完成,哪些须要插件。
因为咱们使用的是flutter, 那么UI天然是在flutter来实现,主要熟悉一下Flutter经常使用widget.
github中绝大多数内容是源代码文件及markdown文本,还有一些就是图片等其它元数据。
对于源代码文件,须要渲染为等宽字体,而且排版时不能强制换行。
对于markdown文本(主要是issue、评论、文档),这是大多数用户主要浏览的内容。为此必须有一个markdown解析器,这若是是在web端,没什么好担忧的,成熟的轮子不少,但在flutter社区,状况却不容乐观,在pub仓库找到了一个flutter_markdown的包,通过测试发现坑不少,主要表如今markdown语法支持不足、样式自定义困难、不支持tabel、不能自动识别url等,离可用相差甚远。
对于github中的图片,主要是通常的图片(项目中的图片文件和网站的用户头像等)和github的私有emoji。这里主要关注一下github emoji,它们有些特别,由于这些emoji在文本中只是一些标记,因此在渲染以前必须对文档进行解析,提取出emoji标记,而后转化为对应的图片,最后再进行渲染。而emoji会出如今不少地方,好比markdown中,因此这在解析markdown时也是应该考虑的点。
Github API是开放的,v3是restful风格的,v4是graphQL风格。咱们最终选择了v3版本,由于graphQL虽然灵活,能够作到按需取数据,绿色无浪费,但在咱们进行选型时,有两个因素让咱们不得不放弃:
须要客户端开发者本身去汇总所需数据而后写出请求体;这很是耗时,刚开始时,咱们根据github的API文档,在汇总时效率极低,一个小时才能完整的请求出两个业务接口。
返回数据嵌套层次太深;这让咱们在将json数据转化成dart类(相似于java bean)时很是为难,若是把返回数据当成json数据,在开发时便不能得到ide的提示会下降开发效率;在编译时会牺牲掉静态类型检查会增长潜在出错可能性(好比字段名输错了)。
肯定选用v3版本的api后咱们须要一个合适的http库,咱们但愿http库具有:
良好的restful接口
请求响应拦截器;这很重要,这意味着咱们能够在底层统一对请求/响应进行预处理。
灵活的请求配置;好比能够统一配置请求基地址、公共header等,还有就是github 不少API在请求时都会涉及私有的content-type
, 这意味着不一样的请求可能须要不一样的请求配置。
支持超时; 因为重所周知的缘由,在国内访问github时,有时可能须要较长的响应时间(有时甚至没法访问),因此支持超时是很是重要的。
最好支持请求取消;主要仍是由于众所周知的缘由,致使有时页面加载过慢,当用户没有耐心继续等待下去返回时,可以将以前请求取消,避免在后台占用资源、浪费流量。
固然,一个优秀的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中文网开源项目之一,它主要借鉴了okhttp、axios、request、fly 四个开源库,因此不管是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控件,能在应用内显示h5网页,而要实现这个,咱们只能经过flutter插件!
不少人问过我flutter中有没有相似于webview这样的widget,答案是如今没有,未来极大可能也不会有,缘由很简单,若是在flutter中加一个webkit和v8你以为flutter应用的安装包有多大?
好了,如今看看有没有现成的轮子,pub中搜到的flutter_webview倒很多,但大多数都不能直接来用,缘由有两个:
咱们须要对webview所在路由(android中的activity, iOS中的controller)的导航头进行一些自定义,好比当页内跳转过多时给导航栏右侧加一个直接关闭当前路由的button以免要连续屡次点击返回才能退出。
咱们须要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。
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: 若是请求的是测试项目的数据,咱们直接将本地工程对应的数据返回。这样一来有两个好处:
须要添加、改动测试数据时无需push到github远程仓库,本地该了就当即生效。
节省了网络请求时间。
因为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开发过程当中让咱们花费了较多时间的问题:
不要将build函数中传入的context
保存为全局变量(多是为了后续使用方便),build中传入的context会变,而且widget树不一样部分构建时的context都不一样,若是使用保存的全局context,将会出现不可预期的错误。好比没法经过context正常获取local及主题信息(偶现);
不要将须要缓存的数据保存在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)或全局变量中。
ListView
结合RefreshIndicator
实现下拉刷新时, 列表项若是不满一屏,下拉刷新无效,此时须要将ListView
的primary
属性设置true
,但设置后就不能给ListView
设置controller,这是由于primary
属性设置为true
的ListView
会从他父辈widget中的 PrimaryScrollController
获取它的controller(每一个Scaffold
都会默认设置一个PrimaryScrollController
) 因此此时再设置controller时,flutter会报错,解决办法是本身手动设置一个PrimaryScrollController
。
当自定义导航栏(AppBar
)的返回按钮时,iOS下右滑关闭手势会失效。这和iOS原生导航栏自定义返回按钮会致使右滑手势失效是同样的。
Android和iOS系统支持的字体不同,不要觉得flutter会本身使用一套标准字体,flutter在绘制时也会使用系统字体,因此在Text widget指定字体时必定要看看是否两个平台都支持,gitme中在设置代码的等宽字体时发现了这个问题。
在替换图片、资源后或构建release包以前要先执行flutter clean
清除缓存,不然有些时候,新的改动不会生效。
除上面所述,关于Flutter, 还有一些问题多是你们比较关心的。例如:
包大小; gitme 1.0.0 release版,Android: 11.7M, iOS AppStore上架后38M,可见android包比ios包小不少,固然,ios中各类尺寸的icon和launchImage确实会比android多占用些空间,可是这3倍的差距确实也大了一些。 笔者还没有研究flutter framework ios部分代码,至于优化空间,我想若能更好,谷歌是不会不采起行动的。
热更新; flutter release版默认是AOT,因此要实现热更新,那就只能依赖dart做为脚本语言的特性,采用JIT模式,而flutter的debug模式默认就是JIT模式,而JIT模式和AOT模式性能差距是很是大的,若是要作热更,问题瓶颈应该在性能。可是随着苹果AppStore审核策略的收紧,使用热更都会面临被拒风险,因此建议须要动态化的功能仍是经过h5或rn/weex这样的框架,固然h5的风险要比rn/weex更低。
性能; 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用户"), 他会将你拉进群。