作者:騰訊NOW直播 - narutosun (孫帥)
前言
Flutter是Google使用Dart語言開發的移動應用開發框架,使用一套Dart程式碼就能構建高效能、高保真的iOS和Android應用程式,並且在排版、圖示、滾動、點選等方面實現零差異。騰訊Now直播App中使用Flutter實現了動態搜尋列表頁。本文主要介紹動態搜尋列表頁實現相關步驟,總體來看主要分為UI,資料解析和資料通訊三個部分。
1. 動態搜尋列表頁UI
Now直播動態搜尋列表頁在Native程式碼實現的UI如下圖所示。
從iOS開發的角度看,可以將這個頁面元素進行拆解。頁面的父檢視就是普通的一個UITableview。每一行的元素就是一個cell,cell內部頭像是一個UIImageview,暱稱是UILabel,時間也是UILabel,動態的內容通過UILabel展示,動態圖片通過UIImageview顯示,右上角更多的按鈕是一個UIButton。
從Flutter開發的角度看,我們可以將介面元素拆解成Row,Column,ListView,itemWidget這些UI元素。下面看下具體這些元素如何定義及使用。
- Row (行佈局)
行佈局,顧名思義,佈局就是以行為基準,將子檢視,以行的形式去排列,它可以擁有多個子widget,在這裡也說一下,Flutter秉承的原則就是“一切皆為控制元件”,每個元素都是一個widget。那這裡的子widget可以理解為子檢視,也就是說Row可以包含多個子檢視。由於Row的屬性網上有好多介紹的,這裡就不一一去介紹了,主要介紹下在Now動態搜尋列表頁裡用到Row幾個屬性:
- mainAxisAlignment: 在水平方向上,子widget的對齊方式(有這幾種方式spaceEvenly,center,left,right)。
- verticalDirection: 表示垂直方向上,從哪個方向為起始位置(比如從上向下佈局還是從下向上佈局)
- textDirector:表示在橫軸上,佈局從哪個方向開始(比如從左右向還是從右向左佈局)
- Column (列布局)
和行佈局不一樣的,列布局就是在縱軸上對子widget進行排列。同樣也是可以包含多個子檢視。同樣介紹下Now動態搜尋列表頁裡涉及到的簡單的屬性:
- mainAxisAlignment:和Row不一樣的是,這裡指的變成了在垂直方向上,子widget的對齊方式(有這幾種方式spaceEvenly,center,left,right)。
- verticalDirection: 同樣和Row有相同的效果
- Container (容器)
Container在Flutter中太常見了。官方給出的簡介,是一個結合了繪製(painting)、定位(positioning)以及尺寸(sizing)widget的widget。可以得出幾個資訊,它是一個組合的widget,內部有繪製widget、定位widget、尺寸widget。後續看到的不少widget,都是通過一些更基礎的widget組合而成的。只包含一個子widget,container的組成可以由下面這張官方的圖體現出來。
由於Container屬性有很多,下面同樣只展示下Now動態搜尋列表頁用到的屬性。
- padding: decoration內部的空白區域,如果有child的話,child位於padding內部。可以通過設定padding,來改變content在container的位置,同樣也能改變container的大小。
- alignment: 設定child在Container的對齊方式(有topLeft,topCenter,topRight,centerLeft,center,centerRight,bottomLeft,bottomCenter,bottomRight這幾種系統提供的型別)。可以通過Alignment的方法來自定義child的位置
- margin:從上圖能夠看出,margin其實表示的是decoration外面的區域,不是Container的內容區域。可以設定與父widget的間隔
- ListView
ListView控制元件是APP開發中最為常見的控制元件之一,類似iOS中的UITableView,可以用來展示列表式的資訊。它的內容對於其渲染框太長時會自動提供滾動。可以擁有多個child,可以水平或者垂直放置。在APP中扮演著重要的角色,listview屬性詳細介紹可以看下listView文件,Now這裡用的是Flutter第三庫的元件。Now動態搜尋列表頁用到的基本屬性簡單介紹一下:
- scrollDirection:預設child是垂直方向上進行佈局(Axis.vertical),可滑動方向對應的也是垂直方向上,horizontal表示的是水平方向。
- reverse:是個bool型別,預設是false,在水平方向則child是從左向右排列,垂直方向上child是從上向下排列。反之ture的話,水平方向的child是從右向左排列,垂直方向是從下向上排列。
從上面來看,我們能分析出,通過Flutter實現這個UI,用的無非就是Row,Column,Container,listView這些基礎widget。動態搜尋頁基礎UI我們就已經搭建出來了。
2. 資料格式及解析
Now App在請求資料與解析資料,都是通過谷歌的protobuf來實現的,所以Flutter頁面的資料依舊通過protobuf來實現。Flutter怎麼整合進來protobuf的外掛呢,這裡我使用的是AndroidStudio的IDE,安裝了Dart環境後,開啟Flutter工程,會有一個pubspec.yaml的配置檔案,在這裡可以像Xcode的cocopods一樣,將protobuf的版本號輸入這裡,然後update一下Dart,就會自動將protobuf整合進來。具體的protobuf是什麼以及如何使用,可以參考這篇文章
在上面我們也看到了搜尋頁有很多的檢視元素,還有一些點選跳轉的事件也需要傳參,所以這裡設計資料格式的話,給當前類建立了這些屬性。
class AnchorInfo {
String anchorHeadUrl; //頭像
String anchorName; //暱稱
String uin; //uin
String content; //內容
FEED_TYPE feedType; //Feed型別
String feedsId; //FeedID
String coverImageUrl; //封面url
double imgWidth = 200.0; //圖片寬度
double imgHeight = 200.0; //圖片高度
String jumpUrl; //跳轉url
}
複製程式碼
從這個資料格式上來看,已經包含了動態搜尋列表頁面Cell的基本資料。下面就是如何給這些資料初始化,以及怎麼拿到這些資料。
3. 資料通訊
資料通訊主要有兩種情況,一種是有Flutter頁面主動觸發的,另一種是由Native主動觸發呼叫到Flutter的。我們分別來看這兩種情況在動態搜尋頁中的應用。
3.1 Flutter呼叫Native
這種情況應用於動態搜尋列表頁feeds拉取的場景。由於Now工程專案的特殊性,客戶端在發包的時候使用的是WNS平臺,所以這塊發包不能通過Flutter頁面來實現,只能通過客戶端來發包,客戶端回包會取到一個二進位制流的資料,會將這個二進位制流的資料塞給Flutter,因為發包同樣是遵照protobuf的格式。所以這裡Flutter拿到二進位制流的資料後,就可以通過protobuf來解包,實現了客戶端發包,Flutter解包的一個過程。具體流程如下圖所示。
針對這個場景,Flutter提供了MethodChannel來實現。具體步驟如下:
1)Flutter內部註冊MethodChannel
class DynamicListViewState extends State<DynamicState> {
...
static platform = const MethodChannel('now.qq.com/flutter');
...
}
複製程式碼
在類初始化的時候,就將platform賦值給了一個MethodChannel,我們看到傳了一個字串的引數進去。那這個引數其實就是一個呼叫iOS的標識。
2)Native程式碼註冊相同名稱的MethodChannel iOS客戶端同樣會註冊一個帶有相同標識的channel。
self.methodChannel = [FlutterMethodChannel methodChannelWithName:
@"now.qq.com/flutter" binaryMessenger:self];
複製程式碼
3)Flutter呼叫MethodChannel 上面完成的步驟,可以理解為在iOS上已經注入了一個外掛,那接下來就需要通過外掛來呼叫iOS的相關的API,如何來呼叫具體的iOS的API呢。看下我們這邊載入動態資料的程式碼塊。
void moreAnchorInfoDataRequest() async {
...
List<int> data = await platform.invokeMethod("anchorInfoDataRequest", pageIndex);
...
}
複製程式碼
主要看platform.invokeMethod的方法,發現又傳入了一個字串,不同的是還傳入了一個int型的pageIndex。字串同樣是一個標識,是Flutter呼叫客戶端的一個約定。客戶端在取到這個標識後,會去呼叫相應的API。至於pageIndex,是客戶端在需要呼叫這個方法時,所需的引數。來看下客戶端的程式碼是如何響應並執行這個操作的。
4)Native接收到呼叫處理完成後回包
[self.methodChannel setMethodCallHandler:^(FlutterMethodCall* call,
FlutterResult result) {
...
if([call.method isEqualToString:@"anchorInfoDataRequest"]){
wself.result = result;
[wself anchorInfoRequest:((NSNumber*)call.arguments).unsignedIntegerValue];
}
...
}];
複製程式碼
從程式碼塊中可以看出,客戶端在閉包裡,可以取到call的例項,通過method的屬性來判斷出需要呼叫哪種API。這樣就已經完成了Flutter呼叫客戶端的步驟。那現在還有一步是怎麼通過客戶端呼叫到Flutter呢。
3.2 Native呼叫Flutter
這種情況應用於Native動態詳情頁進行刪除的場景。在Flutter頁面點選頭像->進入主人態個人中心->刪除了動態->告知Flutter動態頁資料更新。Native主動觸發事件去告訴Flutter,Flutter需要去響應Native觸發的動作。具體流程如下圖所示:
針對這個場景,Flutter提供了EventChannel來實現。具體步驟如下:1)Flutter註冊EventChannel
static const EventChannel eventChannel = const EventChannel("now.qq.com/event");
複製程式碼
重寫當前類的initState方法,在這個方法裡,註冊一個監聽。用來監聽從客戶端呼叫來的事件,在_onEvent方法裡執行監聽到之後所需要做的操作。
void initState() {
super.initState();
...
eventChannel.receiveBroadcastStream().listen(_onEvent,onError: _onEventError);
...
}
複製程式碼
在Now裡這個事件是個刪除動態的操作,所以呼叫了刪除的操作後,告知Flutter頁面的資料改變,UI需要重新整理。
void _onEvent(Object event) {
...
if(deleteData != null){
setState(() {
dynamicDataList.remove(deleteData);
if(dynamicDataList.length > 0) {
showType = show_normalView;
}else{
showType = show_notingView;
}
});
}
...
}
複製程式碼
2)Native程式碼註冊相同名稱的EventChannel
看下在Now工程中,這個方法是在什麼時機呼叫的。程式碼塊如下:
self.eventChannel = [FlutterEventChannel eventChannelWithName:@"now.qq.com/event" binaryMessenger:self];
[self.eventChannel setStreamHandler:self];
複製程式碼
同樣客戶端也建立了一個FlutterEventChannel的例項,通過類方法來建立的。setStreamHandler的方法就是注入一個工具類,作用將需要呼叫的事件流通知給Flutter。設定了這個方法後,我們需要實現它提供的代理方法,在實現代理方法前,我們還需要一個FlutterEventSink的閉包函式。我們把這個閉包函式宣告成了一個屬性。
@property (nonatomic, strong) FlutterEventSink eventSink;
複製程式碼
3)Native呼叫到Flutter
下面看怎麼使用這個屬性以及怎麼回撥到Flutter頁面上。首先需要實現FlutterStreamHandler的代理方法,在onListenWithArguments代理方法裡,我們儲存了FlutterEventSink型別的閉包函式。方便後續在呼叫的地方去使用。
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(FlutterEventSink)events {
self.eventSink = events;
return nil;
}
複製程式碼
呼叫的時機如下,在我們刪除動態成功後,我們會回撥給Flutter。傳入一個feedsId的引數
- (void)onDeleteFeedsRequestSucceed:(NSArray *)feedsArray {
...
if(self.eventSink){
self.eventSink(model.feedsId);
}
...
}
複製程式碼
總結
以上就是Now直播使用Flutter實現動態搜尋列表頁的一些步驟細節,歡迎大家探討。Now直播終端團隊致力於為Flutter生態作出一點自己的貢獻,期待Flutter越來越好!