精講Flutter官網的第一個例子

WLDNick發表於2019-01-29

原文連結

前言

學習Flutter你一定會看到官網的第一個例子:中文版英文版。但是作為新手,或許你看的會很費勁,這篇文章的目的是幫助你更好的理解這個例子。

最終的效果圖:

最終效果

我們先分析一下如何實現上圖中的效果:

Android開發者

1. 準備資料:列表資料和選中的資料可以分別使用兩個List或者陣列儲存。 2. 介面列表:使用ListView或RecyclerView 3. 介面跳轉:可以使用Intent攜帶資料到新的列表頁

iOS開發者

1. 準備資料:列表資料和選中的資料可以分別使用兩個陣列儲存。 2. 介面列表:使用TableView或CollectionView 3. 介面跳轉:使用NavigationController,可以把值直接賦值給新的頁面物件

結論

我們發現,無論是原生的Android還是iOS開發,都需要做的步驟是:

  1. 儲存要展示的資料,儲存選中的資料
  2. 展示列表,並把資料展示出來
  3. 設定跳轉到新頁面

所以在Flutter開發中,也遵照這幾個步驟會更好的理解

Flutter開發

* 準備資料:列表資料使用陣列儲存,選中的資料可以使用Set儲存(因為set可以自動去重)。 * 介面列表:使用ListView * 介面跳轉:可以使用Navigator

拆解分析官方程式碼,帶你快速理解

官網上使用大概110行程式碼實現上面的例子,我們把這些程式碼拆解成主要的三部分來幫助我們學習:

前提:你首先應該會用Android studio或者其他開發工具建立一個Flutter的工程,如果你需要學習關於這個步驟,可以在 這裡快速學習

當你建立一個全新的Flutter工程並執行,介面上會出現熟悉的“Hello world”。 為了更容易的理解Flutter的程式碼,我們先分析一下建立初始的程式碼,至少要知道我們需要從哪裡開始動手:

Snip20190104_19.png

我們要編輯的就是這裡的 main.dart 檔案,跟其他語言一樣,Flutter的入口函式是main函式:

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());   //分析 1

class MyApp extends StatelessWidget {  //分析 2 (StatelessWidget)
  @override
  Widget build(BuildContext context) {   //分析 3
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(      //分析 4
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(  //分析 5
          child: new Text('Hello World'),
        ),
      ),
    );
  }
}

複製程式碼

分析

  1. 這裡的 => 是Dart中單行函式的簡寫,等價於:
void main() {
  runApp(new MyApp());
} 
複製程式碼
  1. StatelessWidget 代表只有一種狀態的元件,與之對應的是StatefulWidget(表示可能有多種狀態)。這裡先不用深究其原理,只需知道這個跟flutter的重新整理等相關。

  2. 在Widget元件中都是通過build方法來描述自己的內部結構。這裡的build表示構建MyApp中使用的是MaterialApp的系統元件。

  3. home標籤的值:Scaffold是Material library 中提供的一個元件,我們可以在裡面設定導航欄、標題和包含主螢幕widget樹的body屬性。可以看到這裡是在頁面上新增了AppBar和一個Text。

  4. Center是一個可以把子元件放在中心的元件

開始改造

我們的目標是把頁面中顯示hello_world的TextView換成一個ListView。由上面的分析可知,將上面第4點的home標籤的值,換成一個ListView就能改變頁面顯示的內容。不過在此之前,需要先準備一下要顯示的資料,這裡是使用一個叫 english_words 的三方包,可以幫助我們生成顯示的單詞資料。先學習一下如何新增依賴包:

精講Flutter官網的第一個例子

  1. 開啟pubspec.yaml檔案新增三方庫:
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.0
  english_words: ^3.1.0
複製程式碼
  1. 點選 Packages get 獲取剛新增的包。

新增english_words庫之後,可以這樣使用這個庫創造資料:

//創造5個隨機片語,並返回片語的迭代器
generateWordPairs().take(5)
複製程式碼

學習使用可變狀態的元件 StatefulWidget

檢視ListView的原始碼,發現其最終是繼承自 StatelessWidget,所以它的狀態是唯一的。但是要實現的ListView中的資料是動態變化的,所以需要使用StatefulWidget來動態改變ListView中的資料。

使用StatefulWidget元件需要自己控制在不同情況下的顯示狀態,所以需要實現State類來告訴StatefulWidget類不同情況下如何展示。

建立一個動態變化的元件類,用於表示要顯示的ListView:

class RandomWords extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {  //分析1
    return new RandomWordsState();
  }
}
複製程式碼

分析:

  1. 建立State類的方法,這裡繼承系統的State創造一個名叫RandomWordsState的類,來控制ListView各個狀態下的展示。
class RandomWordsState extends State<RandomWords> {
}
複製程式碼

要完成展示資料和儲存點選後的資料,這裡分別用陣列和set來儲存(用set儲存點選後的資料是因為set可以去重,你也可以選擇其他的儲存方式)

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];   //分析 1
  final _saved = new Set<WordPair>();
  final _biggerFont = const TextStyle(fontSize: 18.0);   //分析 2
}
複製程式碼

分析:

  1. Dart中加上 _表示私有化
  2. 表示字型大小的常量

創造資料集和構建ListView

新增如下兩個方法到RandomWordsState類中,表示創造資料集和構建ListView:

Widget _buildSuggestions() {   
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),
      // 對於每個建議的單詞對都會呼叫一次itemBuilder,然後將單詞對新增到ListTile行中
      // 在偶數行,該函式會為單詞對新增一個ListTile row.
      // 在奇數行,該函式會新增一個分割線widget,來分隔相鄰的詞對。
      // 注意,在小螢幕上,分割線看起來可能比較吃力。
      itemBuilder: (context, i) {          
        // 在每一列之前,新增一個1畫素高的分隔線widget
        if (i.isOdd) return new Divider();

        // 語法 "i ~/ 2" 表示i除以2,但返回值是整形(向下取整),比如i為:1, 2, 3, 4, 5
        // 時,結果為0, 1, 1, 2, 2, 這可以計算出ListView中減去分隔線後的實際單詞對數量
        final index = i ~/ 2;
        // 如果是建議列表中最後一個單詞對
        if (index >= _suggestions.length) {
          // ...接著再生成10個單詞對,然後新增到建議列表
          _suggestions.addAll(generateWordPairs().take(10));   
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);

    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: new Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _saved.remove(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }
複製程式碼

程式碼中有詳細的註釋,但是為了方便理解,這裡還是給出一點解釋:

  1. _buildSuggestions方法就是返回一個ListView
  2. _buildRow方法就是返回ListView中的一行(ListTile)如何展示

_buildSuggestions方法中:

  • 程式碼中的itemBuilder就是如何顯示一行(ListTile)的配置,其返回值是_buildRow方法
  • _suggestions.addAll(generateWordPairs().take(10));就是每次新增10個資料到顯示陣列中

_buildRow方法中:

  • 設定了一行(ListTile代表一行內容,在iOS和android中叫cell)如何展示
  • ListTile設定了title、trailing(右邊的圖示)和點選事件onTap()
  • 會根據有沒有被儲存過,決定右邊顯示什麼圖示
  • 當點選時,會把沒有點選過的內容儲存到_saved容器中。

新增跳轉邏輯

新增_pushSaved方法表示如何跳轉到新的頁面並展示選中的資料:

main.dart

void _pushSaved() {
    Navigator.of(context).push(  // 分析 1
      new MaterialPageRoute(  // 分析 2
        builder: (context) {
          final tiles = _saved.map(  //資料
            (pair) {
              return new ListTile(
                title: new Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();

          return new Scaffold(  // 分析 3
            appBar: new AppBar(
              title: new Text('Saved Suggestions'),
            ),
            body: new ListView(children: divided),
          );
        },
      ),
    );
  }
複製程式碼

分析: 1.使用Navigator.of(context).push的方式來處理跳轉,需要的引數是一個Route 2.建立頁面Route 3.返回一個新的裡面,裡面的body內容是一個ListView,展示的是_saved中讀取出來的資料

最終,所有程式碼整合是如下的樣子:


import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      theme: new ThemeData(
        primaryColor: Colors.red,
      ),
      home: RandomWords(),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new RandomWordsState();
  }
}

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _saved = new Set<WordPair>();
  final _biggerFont = const TextStyle(fontSize: 18.0);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
        actions: <Widget>[
          new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggestions(),
    );
  }

  void _pushSaved() {
    Navigator.of(context).push(
      new MaterialPageRoute(
        builder: (context) {
          final tiles = _saved.map(
            (pair) {
              return new ListTile(
                title: new Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();

          return new Scaffold(
            appBar: new AppBar(
              title: new Text('Saved Suggestions'),
            ),
            body: new ListView(children: divided),
          );
        },
      ),
    );
  }

  Widget _buildSuggestions() {
    return new ListView.builder(
        padding: const EdgeInsets.all(16.0),
        itemBuilder: (context, i) {
          if (i.isOdd) return new Divider();
          final index = i ~/ 2;
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        });
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);

    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: new Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _saved.remove(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }
}

複製程式碼

執行吧,就能看到最上方的效果。

謝謝觀看這篇文章,如果讓您發現了錯誤或者有好的建議,歡迎在下方評論給我留言。

相關文章