Android開發者的Flutter入門(一)

前言

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程序員應該能比較舒服的入門。java

做爲一名Android開發者,我始終認爲跨平臺是移動端開發的發展趨勢,可是哪種技術方案會最終勝出,還有待時間的檢驗。Flutter對Native開發者友好,而且吸納了React等Web開發的前沿技術,能夠做爲Native程序員學習跨平臺開發的很好的路徑。android

爲了學習Flutter, 我試着開發了一個簡單的新聞app,涵蓋了一些移動端app比較基礎的功能。接下來我會對照這個app來給你們介紹一下Flutter開發的一些知識。整個工程源碼你們能夠從Github獲取。若有任何問題或建議,歡迎你們提issue。ios

本文是Android開發者的Flutter入門的第一部分,有一些技術細節放在了第二部分介紹,戳這裏查看 Android開發者的Flutter入門(二)git

語言

Flutter是用Dart語言開發的。因此在開發Flutter app以前,須要咱們對Dart語言有必定的掌握。對於Android程序員來說,學習Dart是比較快的一個過程,和Java同樣,Dart也是面向對象的語言。不少地方都是相通的。須要注意的是對於Dart裏的類(各類構造函數,getter,setter),函數(函數也是對象,函數內部能夠定義函數,函數能夠做爲參數和返回值, 閉包),以及異步(Future,asyncawait)等地方要反覆揣摩,仔細體會。程序員

有了Dart的基礎,那麼咱們就能夠開始嘗試開發個Flutter app了。github

預備

首先你要配置Flutter的開發環境。對於咱們Android程序員來說,那就是再熟悉不過的Android Studio了。整個配置過程是比較簡單的,你們照文檔走就是了。不過要注意一點,若是你沒有穿牆的的話,須要看一下這裏web

開始

好了,環境已經弄好了,可能你已經把Hello World也跑起來了。那麼咱們就用Flutter來開發一個稍微像樣點的app吧。json

咱們開發的是一個簡單新聞app。主要包含兩個頁面,一個首頁,顯示一個頭條新聞的列表,點擊裏面的某個頭條,就跳轉到那條新聞的詳情頁面。這個簡單的app包含了一些比較基礎的功能:api

如何經過網絡從服務器請求數據? Android程序員:我用OkHttp。bash

如何解析返回數據? 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入門(二)

相關文章
相關標籤/搜索