前言
Flutter推出來已經有一段時間了,前一陣Google IO大會後釋出了Beta3。基於Flutter的 app可以一次編寫,同時在Android和iOS平臺上跑,並且能給使用者帶來完全原生的體驗。我們都知道跨平臺開發還有Hybrid,React Native以及Weex等方案,這些解決方案都是從Web開發的角度向Native開發演進,其技術基礎都是HTML、CSS和Javascript等Web技術,對於沒有接觸過Web開發的Native app程式設計師來講,門檻是比較高的。而Flutter給我的感覺是從Native開發向Web開發演進,Native app程式設計師應該能比較舒服的入門。
作為一名Android開發者,我始終認為跨平臺是移動端開發的發展趨勢,但是哪一種技術方案會最終勝出,還有待時間的檢驗。Flutter對Native開發者友好,並且吸納了React等Web開發的前沿技術,可以作為Native程式設計師學習跨平臺開發的很好的路徑。
為了學習Flutter, 我試著開發了一個簡單的新聞app,涵蓋了一些移動端app比較基礎的功能。接下來我會對照這個app來給大家介紹一下Flutter開發的一些知識。整個工程原始碼大家可以從Github獲取。如有任何問題或建議,歡迎大家提issue。
本文是Android開發者的Flutter入門的第一部分,有一些技術細節放在了第二部分介紹,戳這裡檢視 Android開發者的Flutter入門(二)。
語言
Flutter是用Dart語言開發的。所以在開發Flutter app之前,需要我們對Dart語言有一定的掌握。對於Android程式設計師來講,學習Dart是比較快的一個過程,和Java一樣,Dart也是物件導向的語言。很多地方都是相通的。需要注意的是對於Dart裡的類(各種建構函式,getter
,setter
),函式(函式也是物件,函式內部可以定義函式,函式可以作為引數和返回值, 閉包),以及非同步(Future
,async
和await
)等地方要反覆揣摩,仔細體會。
有了Dart的基礎,那麼我們就可以開始嘗試開發個Flutter app了。
預備
首先你要配置Flutter的開發環境。對於我們Android程式設計師來講,那就是再熟悉不過的Android Studio了。整個配置過程是比較簡單的,大家照文件走就是了。不過要注意一點,如果你沒有穿牆的的話,需要看一下這裡。
開始
好了,環境已經弄好了,可能你已經把Hello World
也跑起來了。那麼我們就用Flutter來開發一個稍微像樣點的app吧。
我們開發的是一個簡單新聞app。主要包含兩個頁面,一個首頁,顯示一個頭條新聞的列表,點選裡面的某個頭條,就跳轉到那條新聞的詳情頁面。這個簡單的app包含了一些比較基礎的功能:
如何通過網路從伺服器請求資料?
Android程式設計師:我用OkHttp。
如何解析返回資料?
Android程式設計師:我用Gson。
返回的資料如何在介面上顯示出來?
Android程式設計師:我用RecylerView。
如何顯示網路圖片?
Android程式設計師:我用Glide。
頁面之間如何跳轉?
Android程式設計師:我用Intent。
如何加入下拉重新整理?
Android程式設計師:我用SwipeRefreshLayout。
接下來我們就說說以上這些功能如何在Flutter裡實現,先來兩張截圖感受一下:
新聞源我們使用的是newsapi.org。你只要申請一個apiKey就能從他家獲取json格式的頭條新聞資料。至於詳情的話需要用webview直接開啟對應的新聞url。
JSON解析
網路返回的JSON資料格式如圖所示:
這裡面"articles"欄位的值是個jsonArray,內容是頭條新聞的列表。在Android中我們可以用Gson來把json資料反序列化為物件。那再Flutter中如何來做反序列化呢?
首先我們引入必要的庫: 在pubspec.yaml加入以下內容
dependencies:
json_annotation: ^0.2.3
dev_dependencies:
build_runner: ^0.8.0
json_serializable: ^0.5.0
複製程式碼
然後在終端中執行flutter packages get
(或者點選"Packages Get"的提示,類似你更改.gradle檔案以後Android Studio顯示的同步提示)
接下來就是model類了
import 'package:json_annotation/json_annotation.dart';
part "news.g.dart";
@JsonSerializable()
class News extends Object with _$NewsSerializerMixin {
final String author;
final String title;
final String description;
final String url;
final String urlToImage;
final String publishedAt;
final Source source;
News(this.author,
this.title,
this.description,
this.url,
this.urlToImage,
this.publishedAt,
this.source);
factory News.fromJson(Map<String, dynamic> json) => _$NewsFromJson(json);
}
@JsonSerializable()
class Source extends Object with _$SourceSerializerMixin {
final String id;
final String name;
Source(this.id, this.name);
factory Source.fromJson(Map<String, dynamic> json) => _$SourceFromJson(json);
}
@JsonSerializable()
class NewsList extends Object with _$NewsListSerializerMixin {
final String status;
final int totalResults;
final List<News> articles;
final code;
final message;
NewsList(this.status, this.totalResults, this.articles, this.code, this.message);
factory NewsList.fromJson(Map<String, dynamic> json) => _$NewsListFromJson(json);
}
複製程式碼
看起來既有熟悉的欄位,又有陌生的註解和程式碼?沒關係,只要你按照這裡的要求來做就行了。可以看出反序列化是在_$NewsListFromJson(json);
裡完成的。那麼這個函式從何而來呢?這需要我們執行命令flutter packages pub run build_runner build
來生成對應的程式碼。生成的程式碼存放在news.g.dart中。
至此model類以及反序列化我們就已經做完了,那麼下面就看看網路請求怎麼來實現。
網路請求
對應於Android中的OkHttp, Flutter中的網路請求庫是http.dart。如下所示,程式碼比較簡單
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_news/model/news.dart';
class NewsApi {
static Future<NewsList> getHeadLines({String category: "general", int page: 0}) async {
final response = await http.get(
"https://newsapi.org/v2/top-headlines?country=us&apiKey=efaf5fb66d104385ad40c73d4fd4acb1&page=$page&category=$category");
return compute(parseResult, response.body);
}
static NewsList parseResult(String respond) {
return NewsList.fromJson(json.decode(respond));
}
}
複製程式碼
我們都知道在Android中網路請求需要在子執行緒來做,否則會阻塞主執行緒;請求的結果通過callback來返回給主執行緒。
而在Flutter中則更加簡潔,通過async
和await
,避免了難看的callback程式碼巢狀。
函式getHeadLines
用來做http請求,在走到await
的時候會"等待"後面的http.get
函式執行完畢,返回值賦給response
,之後繼續執行函式體中的後續程式碼。注意,這裡的"等待"並不是阻塞在那裡,而只是告訴系統,後續的程式碼需要在await
後面的表示式結束之後執行。你可以把await
那一行以下的程式碼理解為Android網路呼叫中的callback。實際的執行機制其實是比較複雜的,需要另寫文章詳細說明。
在請求得到返回值response
以後就要做json反序列化了。因為反序列化也有可能是個耗時任務,有可能會阻塞ui. 這裡我們用過Flutter提供的compute
函式把反序列化放在另外的isolate
去完成。這裡你可以先把isolate
當成是Java裡的執行緒。compute
函式的第一個引數parseResult
是真正進行反序列化操作的函式。大家可以感受一下,函式作為引數還是比較方便的。
Model層我們已經有了,那麼接下來就看下View層怎麼來搭建吧。
介面
在做Android原生開發的時候。我們一般會用XML來搭建介面,裡面是一個一個的View。而在Flutter中,和View等同的是Widget。Flutter app的介面就是由一個個Widget拼接起來的。而且Widget都是寫在程式碼中的,目前沒有用xml等其他搭建UI的方式,這也是目前Flutter開發被吐槽的點,程式碼中各種巢狀的Widget還是比較令人酸爽的。
Widget分為StatelessWidget
(無狀態的)和StatefulWidget
(有狀態的)。無狀態是指這個Widget的狀態會發生改變,類比如Android中顯示固定字串的TextView或者顯示固定圖示的ImageView。反之有狀態則是指這個Widget在顯示期間內狀態會發生改變,就比如我們在做網路請求的時候會顯示一個Progress圖示,請求回來資料以後會顯示一個列表。這就是狀態發生了變化。當需要變更狀態的時候,只要呼叫setState
。StatefulWidget的build
函式會被呼叫,根據新的state來重建UI,是不是聽起來和Android中的notifyDataSetChanged有點像?
讓我們自上而下的看一下main.dart的程式碼吧
// 我是入口,類似於java中的 static main()
void main() => runApp(new MyApp());
// 我是最外層的容器,我不關心裡面內容的變化,所以是無狀態的。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//返回給你一個MaterialApp,至於內部還有啥,看引數
return MaterialApp(
title: 'Headlines',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 這個Widget是我們自定義的
home: HeadLinePage(title: 'Headlines'),
);
}
複製程式碼
入口的這些程式碼都是常規操作。不細說了。
這裡順便說一句,一個.dart檔案中是可以包含多個在最外層的類的,這點和Java是不一樣的,需要習慣一下。
接下來我們再實現自定義的Widget: HeadLineList
。因為其狀態會發生改變(有網路請求),所以這是個StatefulWidget。
class HeadLineList extends StatefulWidget {
@override
_HeadLineListState createState() => new _HeadLineListState();
}
複製程式碼
Emmm....... 自定義一個Widget只需要一行程式碼嗎?答案是否定的,乾貨都在_HeadLineListState
裡......
class _HeadLineListState extends State<HeadLineList> {
List<News> _articles;
Future _getNews() async {
NewsList news = await NewsApi.getHeadLines();
_articles = news?.articles;
//有資料了 觸發ui更新
setState(() {
});
}
@override
void initState() {
super.initState();
//初始化 開始載入
_getNews();
}
@override
Widget build(BuildContext context) {
switch (_status) {
case IDLE:
//有資料了,返回列表
return ListView.builder(
itemCount: _articles.length,
itemBuilder: (context, index) {return NewsItem(news: _articles[index])};
case LOADING:
//載入中,返回個載入框
return Center(child: CircularProgressIndicator());
}
}
}
複製程式碼
這裡HeadLineList是包含載入進度框和新聞列表的容器Widget。而_HeadLineListState
是和其關聯的狀態。真正建立Widget是在build
函式內。這裡會根據不同的狀態返回不同的Widget。List<News> _articles;
儲存出來的新聞列表,在initState
初始化的時候開始呼叫網路請求。
在狀態變為載入完成時,build
函式內會用ListView.builder
來建立顯示列表。這裡不需要像Android裡的ListView那樣需要一個Adapter,給itemBuilder傳個函式引數就行了,這個函式引數返回我們自定義的無狀態Widget, NewsItem
, 作為列表顯示項。
自定義的NewsItem
會有一個充滿控制元件的背景圖片,這個圖片需要從網路載入。有一個placeHolder並且載入完有淡入淡出的效果,在Android中我們可能會用Glide來實現,而在Flutter中,僅需幾行程式碼也可以做到
FadeInImage.assetNetwork(
//圖片url
image: '${news.urlToImage}',
// 圖片scale方式
fit: BoxFit.fitWidth,
// 佔點陣圖,從assets 中獲取
placeholder: 'images/news_cover.png',
)
複製程式碼
總體流程基本上走完了,未涉及到的下拉重新整理,最底載入,WebView等技術點 可以戳這裡Android開發者的Flutter入門(二)檢視,或者大家可以參考原始碼自行理解。
工程
最後我們再看一下整個工程的目錄結構:
專案下會有三個主要的目錄,android
, ios
和lib
。android
, ios
目錄分別是存放兩個平臺的相關程式碼。所有的Flutter程式碼都存放在lib
目錄下。pubspec.yaml
檔案專案的配置檔案,類似於Android工程中的build.gradle。我們再看看Android開發者比較關心的android
目錄,這裡只有一個MainActivity, 程式碼如下:
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}
複製程式碼
可見這唯一的一個Activity就是個空殼,只是用來給Flutter app提供一個容器。
打包
打apk只需要一條命令:
flutter build apk
當然,這之可能需要做一些配置,具體可參考這個文件
總結
移動端跨平臺開發是大勢所趨,Flutter是一個比較強大的跨平臺解決方案,雖然現在還是在Beta階段,並沒有完全成熟。但是相對於其他跨平臺解決方案,其對Native app開發者友好,同時又吸收了一些先進的Web開發技術理念,是一個比較順一些的學習跨平臺開發的路徑。另外對於一些未涉及的技術細節大家可以到這裡檢視Android開發者的Flutter入門(二)。