導語:騰訊在線教育團隊(簡稱:OED)已經將 Flutter 在 『騰訊企鵝輔導』的產品中落地了,IMWeb團隊也積極參與,共同推動產品落地和技術的提高。本文描述了最近基於 Flutter 模擬開發企鵝輔導 APP 的實踐經歷,從 0 到 1 的進行了樣板工程的落地實踐,但願可讓您近距離的瞭解和感覺 Flutter 開發的過程。前端
恰逢十一放假,年假多到用不完,索性也就多請了幾天,回了一趟我大東北,處理一些我的私事。切實的感覺到了北方的雨 + 雪 ,以及真實的冷!在假期的時候,就萌生了一個想法,趁着有整塊的時間,能夠仿照 企鵝輔導App 寫一個 Flutter 的實例工程。OED 的客戶端團隊已經用 Flutter 作了一個 iPad 版本, 所以我也想獨立嘗試一下,正如以前的文章當 Flutter 碰見 Web,會有怎樣的祕密 中提到的,光說不練假把式,實踐方可出真知。所以,必須本身動手,系統的嘗試一下。同時本身也但願迅速的求證和落地一個項目,看看在這個領域內有怎樣的機會。所以用了幾天時間,懷着忐忑的心情,作了一隻小白鼠,進行了輔導實例樣板工程的開發體驗,不用處理其餘事情,能夠長時間專一寫代碼,確實也是被爽到了!web
樣板工程的目的就是 熟悉 Flutter 的開發流程,關注如何調試、UI 佈局、事件,以及基礎能力的使用。最開始作的時候,不少東西也都沒了解清楚,一邊作一邊摸索,畢竟只有真實體驗以後,才能發現開發痛點,爲後續的工程化和動態運營作一些技術上的認知和理解。算法
【左側 改版前 RN】 、【中間 Flutter Demo】、【右側 To Web Demo】json
寫 JS 寫習慣了,再寫 Dart 確實沒有那麼爽。這個觀點沒有對錯,確實是仁者見仁,智者見智了。佈局上面,因爲能夠把 Flutter 的佈局理解爲 Css in Js ,所以,能夠簡單同理爲寫 RN 的佈局。在 Flutter 的基礎佈局中,共有組件 31 個,您熟悉它須要一點時間成本,因爲 UI 也延續了 App 的設計理念,所以 UI 定製化的靈活度,不如 Css3 那麼狂拽炫酷吊炸天,可是,知足正常的業務訴求,仍是沒問題的。瀏覽器
結論:
性能優化
Flutter To App 或 To Web, 頁面的還原度很是高,大膽一點的話,確實能夠部分場景進行商用。微信
02 開發計劃心血來潮的列了一個計劃,而後就開始幹了。因爲實踐時間的問題,這篇文章不涉及性能優化的問題,理想很豐滿,現實是,確實還沒時間開始作。本篇文章只專一體驗了 UI 層面的還原的實現,後續進行性能優化的經驗後,再跟你們一塊兒分享。網絡
開發過程當中遇到了不少不知所云的問題,致使實際的速度沒有那麼快。後面精簡了不少功能,索性差很少簡單作完 3個 一級頁面的展現,作了一個基礎版本的 Demo,若是要精細化的還原 UI,仍是須要下很多功夫的。閉包
簡單說一下作的流程,先把頁面拆了,劃分紅不一樣的區域。一個層級、一個模塊的進行組件的拆分和整理(上圖簡單的列了一個開發時的中間狀態,過模塊的時候,查看基礎組件的能力,是否知足頁面 UI)。拆完以後,學習一下基礎組件的使用(以前看過一點,確實過完節就忘記),而後分別拆分實現,最後組合安裝成頁面的 UI 和功能。單純從 UI 這個角度上,寫 Dart 跟寫 HTML 和 CSS 差很少,但確實沒有在瀏覽器開發那麼爽。app
樣板工程裏面,並無很在乎代碼規範,文件寫的亂了,才能體會到規範的重要性。前期確實什麼也不會,有一些都是網上搜索功能,而後粘代碼測試,時間長了之後,寫的多了的時候,纔開始關注如何能寫的更好。業務在落地的時候,仍是要制定一套代碼設計規範,這對團隊很重要。一套好的代碼規範,能夠提高不少開發效率。
您有好的 Flutter 開發規範的設計思路,歡迎在留言區域討論。
比較核心的幾個點就是 底部狀態欄、頂部導航欄、輪播圖切換、路由狀態維護。下面咱們分別從前端角度,介紹一下開發過程當中的體驗問題。在跨端的技術方案的進程中,大機率發生的事情就是,若是 Flutter 發展起來了,將來前端會加入進來,參與到工程化和業務開發中。而 Native 下沉到 基礎組件 和 底層核心庫 的性能優化,就相似的理解就像後臺服務把接入層交給 Nodejs 去處理,而 C++ 專一作算法和數據中臺。
從目前看客戶端作頁面短時間內是沒問題,但當技術進入深水區的時候,讓客戶端寫頁面確實有點糟蹋人力。專一作底層 框架 和 SDK 的設計纔是核心價值;而在工程化的方向上面,前端就有更大的發揮的空間了。下面咱們分別從幾個方面來看待 Flutter 開發過程的是與非。
import 'dart:convert';
import 'package:meta/meta.dart';
class SysCourse {
final String title; // 標題
final String timeArea; // 上課時間
SysCourse({
this.timeArea,
this.title,
});
static List<SysCourse> fromJson(String json) {
List<SysCourse> _sysCourseList = [];
JsonDecoder decoder = new JsonDecoder();
var mapdata = decoder.convert(json)['List'];
mapdata.forEach((item) {
SysCourse obj = new SysCourse(
timeArea: item['time_area'],
title: item['title'],
);
_sysCourseList.add(obj);
});
return _sysCourseList;
}
}
相似這樣的使用
var sys = new SysCourse(title: '高一秋季系統課', timeArea: '9月10-12月26日');
Dart 是靜態語言,如上面一段代碼就是對一個系統課的對象,進行定義和初始化。若是您寫過 C++ 或者 Java 的話,理解起來會很是簡單。構造函數能夠方便您初始化對象,函數的繼承採用單一集成的方式,不像 C++ 那樣能夠同時繼承於多個類。可是能夠採用混入 mixins (with進行擴展)。已經存在繼承,固然也確定存在 override 複寫,下面摘抄了一段剪短的官方代碼(
https://dart.dev/guides/language/language-tour#instance-variables):
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
if (emp is Person) {
emp.firstName = 'Bob';
} else {
(emp as Person).firstName = 'Bob';
}
}
// 輸出:
// flutter: in Person
// flutter: in Employee
若是您不多寫靜態語音,用 dart 開發 您可能仍是要適應一下,我也是長時間的寫了 2-3 天以後,纔開始慢慢適應的。如今寫 JS 又有點慌了,哈哈,確實尷尬了,代碼能力還不是很到家,要繼續提高。
所以,這裏也引伸出了一個問題,技術本質上仍是須要沉澱的,專注是一件很重要的事情,即便語音範疇也是同樣的。一個 JS 閉包的設計,也許一個技術專家能跟你聊一上午。從設計原理,到實現思路,以及優缺點。所以,不少時候,多而不精確實也是一個問題!但這個問題,因人而異,也因環境而異,看我的和團隊選擇了。行業人才,即須要單一方向有深度的,也須要橫向上有廣度的。由於,站在不一樣的維度,看到的世界是不同的。
不管是公司或者團隊的技術建設,仍是產品規劃,不要爲了統一而統一,到時候由於大一統了,反而少了試錯的可能,極可能致使一次錯誤的決定,就所有都玩完了。這就搞笑了!我一直都是一個多元化的倡導者,多種不一樣觀點和認知的存在,纔能有更多創新的可能。固然,多元 不等於 不聚焦!
核心組件的拆分其實和平時寫頁面同樣,先關注一下大的功能模塊;以後進行組件劃分;再而後進行頁面 UI 元素的繪製,以及邏輯的編寫,固然這些都是一些常規操做了。
但因爲第一次寫,因此,根本沒有辦法按照套路出牌。開始的時候,大量粘貼了網上的代碼。隨着熟練以後,纔開始慢慢手寫。所以工程中的代碼,有很是多的冗餘和設計不合理的地方。後續,有時間了能夠把代碼進行重構和優化。歷史包袱不少時候,都是新人搞出來的事情。你是否是似曾相識了,發現團隊裏面一個很是重要的項目,最開始的設計竟然是實習生搞的!後來,一堆所謂的高級工程師給這個項目補鍋,而後說本身是如何補鍋,痛罵前任代碼垃圾!
實習生說 —— 這個鍋,我不背~~~
親,就我如今這代碼要是合到了 APP 的發佈流裏面,不用過半年,活脫脫的就是歷史包袱了(我估計是沒時間優化這個工程的代碼,真是由於想快速測試結果,才致使了細節的丟失,看場景,我這個場景,只是爲了學習,不會任何商用,這裏就不討論對錯了)。
但不管如何 —— 規範很重要!哪怕開始的時候慢一點。可是總有產品說,這個需求必須下週三上線,這個是宇宙無敵第一需求,這是 XXX 提的。開發估計想說,XXX 你妹啊。這代碼過了半年之後,就是鍋。都要開發 Leader 本身背!開發 leader 就是背鍋俠,你不背誰背,你就是幹這個的。因此規範和流程很重要。專一於 —— 「 規矩的開發 」,是工程師和碼農之間最大的區別,也是咱們成長的必須課。要學習寫乾淨整潔的代碼!
Flutter 經常使用的 佈局組件有 如 單子 widget 的 Container、Padding、Center 能夠做爲排版佈局的基礎元素。好比上面的卡片,就能夠用 Container 進行包裹。而多子 widget 的狀況下,可使用 Row 和 Column,以及相似 Flex 佈局的 Expanded 進行處理。層疊樣式 能夠用 Stack 和 Positioned 進行處理。好比下面紅色區域,便可以用 Stack 處理,也能夠用 Row 進行排版。
@override
Widget build(BuildContext context) {
Map<String, WidgetBuilder> routes = {
'pack': (BuildContext context) => CoursePack(),
'my': (BuildContext context) => My(),
'break': (BuildContext context) => Break(),
'router': (BuildContext context) => DiscoverRouter(),
'my': (BuildContext context) => My(),
'grade': (BuildContext context) => Grade(),
'discover': (BuildContext context) => Discover()
};
return MaterialApp(
title: '企鵝輔導',
theme: ThemeData(
primaryColor: Colors.white,
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false, // 隱藏右側 BUG
home: SplashPage(), // 默認進入閃屏
routes: routes, // 路由表
style="margin: 0px; padding: 0px; font-size: inherit; line-height: inherit; color: rgb(128, 128, 128); overflow-wrap: inherit !important; word-break: inherit !important;">// 路由表找不到,在進入此路由處理
navigatorObservers: [routeObserver],
);
}
上面的代碼您能夠看到 routes 裏面的路由對照表,返回一個對應的路由頁面。
class Goto {
static Goto shared = Goto();
void goto(BuildContext context, time, String router) {
Future.delayed(Duration(milliseconds: time), () {
Navigator.pushNamed(context, router); // 路由跳轉
});
}
}
// 調用以下方式,進行頁面跳轉
Goto.shared.goto(context, 100, 'subject');
這裏沒有用到路由的傳參,傳參的案例 代碼在 example 工程裏有用例子,可移步 example 工程查看。
_navigateToProductDetail(BuildContext context, Product product) async {
this.result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
// 帶着參數,打開一個新的頁面
return new ProductDetail(product: product);
},
),
);
print('result list = ' + this.result.toString());
}
Navigator.pop(context, ['Fred Wu', product.desctiption]);
Flutter 的資源管理在 pubspec.yaml 文件中進行統一的管理。有一些相關規則,你們進行實際開發的時候,能夠詳細瞭解一下。好比 設備像素比 的匹配規則,字體庫的加載,圖片資源的管理等。在 Flutter 中也有相似 Npm 的包管理器,它用的是 pub。flutter pub get 進行能夠進行項目依賴的下載。
您看到了,頁面有一些點擊和滑動操做。Flutter 提供了強大的事件監聽能力 —— Pointer Event 和 Gesture Detector。他們的使用跟咱們在 JS 中使用事件監聽的方式差很少。下面就是輪播圖內嵌的圖片點擊事件監聽,點擊以後會打開一個 webView。
GestureDetector(
child: Container(
margin: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: Image.asset(
item,
fit: BoxFit.cover,
),
),
),
> print('輪播圖點擊');
Navigator.pushNamed(context, 'webView'); // 路由跳轉
},
);
針對於複雜的用戶交互 Flutter 引入了 Arena 的概念,在個人理解就是 battle ,來較量一下,最後,只會有一種事件進行業務處理。這裏您在實際開發中就會有所體驗,若是想多重事件共同發生,就要您定製化的實現了。
常規的組件傳遞就和 React 的開發相似了,Vue 裏面是存在事件代理的概念。固然您若是想在 React 內實現它,也不是一件複雜的事情,咱們只是在規範和靈活之間作一些取捨罷了。能夠把它簡單理解爲事件總線,進行事件的訂閱和分發,幫助您進行跨組件的事件通訊,減小多層級傳參的代碼負擔。
EventBus eventBus = new EventBus();
class TransEvent {
String text;
TransEvent(this.text);
}
觸發事件,發送通知。
eventBus.fire(TransEvent('gradeRouter'));
事件監聽,收到事件觸發以後,進行狀態處理,展現年級頁面。
eventBus.on<TransEvent>().listen((TransEvent data) => change(data.text));
在關注生命週期的時候,不要忘記 APP 的生命週期。這是做爲前端同窗比較容易忽略的。這一部份內容,在以前的一篇文章中有所說起,您能夠點擊觀看生命週期的部分。
在實際開發中,有一點值得注意的是:initState 表示當前 State 將和一個 BuildContext 產生關聯,可是此時 BuildContext 沒有徹底裝載完成!若是你須要在該方法中獲取 BuildContext ,可使用 Future.delayed(const Duration(seconds: 0, (){//context}); 進行處理。
這裏與 React 相似,防止內存泄露是很重要的。開發的時候也遇到了相似的 告警泄露的問題(https://stackoverflow.com/questions/52130648/nosuchmethoderror-the-method-ancestorstateoftype-was-called-on-null),由於沒有釋放掉對象初始化的內容。所以,要麼使用單例模式,要麼須要在生命週期函數中進行數據釋放。
class Foo extends StatefulWidget {
@override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
StreamSubscription streamSubscription;
@override
void initState() {
super.initState();
streamSubscription = Bloc.of(context).myStream.listen((value) {
print(value);
});
}
@override
void dispose() {
streamSubscription.cancel(); // 釋放對象內的變量
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
上次轉上課頁的時候,有很是多的邊距沒法看清。此次我寫的時候,稍微注意了 Flutter 的排版格式。轉換出來的還原度已經很是高了,比上一次上課頁的轉換效果還要好一些。上面兩張圖,您會發現標題的位置,最開始的時候 Web 版本是沒有居中對齊的。能夠經過更換 widget 的結構進行優化,就能夠看到,後面變成了居中顯示的版本,你能夠看下面的代碼。
從目前 Flutter to Web 的表現看,有些超出預期,在兼容方面的處理也是 小於 RN to Web 的。
打包對目前來講,意義不是特別大。業務目前不會發布 Flutter 的獨立 App 版本。而 組件化 和 工程化 是目前須要專一的部分,歡迎一塊兒討論,共建開發體驗。
總體開發體驗進行到如今,仍是很是有意思的。後面有時間把網絡請求的數據存儲都接入進來,這裏比較麻煩的事情是在 App 內都是有登陸態的,所以技術方案,仍是須要結合 App 去落地,單純的全靠 Flutter 支撐業務,效率仍是不夠高。好比:要用 Dart 獨立完成一套接入手Q 和微信的登錄體系,以及支持自由帳號體系的手機號登錄,僅僅是作完一套端的登錄體系接入,就是一件很重的體力活。所以,能力能複用的就堅定複用,能借鑑的就趕快借鑑!團隊服務好業務是核心目標,團隊的技術成長只是達成目標的手段之一。
後面要和客戶端同窗共同開發,一塊兒去完成 Flutter 的企鵝輔導的業務的體系化探索實踐。因此,有致力於開發 Flutter 的同窗,以及已經在 Flutter 的道路上前行的同窗,能夠私下@我,做爲 Flutter 萌新,能夠跟你一塊兒探討技術,共建內部開發者社區,一塊兒把更好的產品體驗,回饋咱們的用戶。安心作好產品,服務好用戶,是咱們做爲業務團隊的核心價值。
咱們專一前端領域多年,負責過 QQ 資料、QQ 註冊、QQ 羣等億級業務。目前聚焦於在線教育領域,精心打磨 騰訊課堂、企鵝輔導 及 ABCMouse 三大產品。