Android開發者的Flutter入門(一)

ad6623發表於2019-09-20

前言

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,asyncawait)等地方要反覆揣摩,仔細體會。

有了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資料格式如圖所示:

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中則更加簡潔,通過asyncawait,避免了難看的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入門(二)檢視,或者大家可以參考原始碼自行理解。

工程

最後我們再看一下整個工程的目錄結構:

image
專案下會有三個主要的目錄,android , ioslibandroid , 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入門(二)

相關文章