6.1.初識Flutter應用之實現一個計數器

codersay發表於2019-07-22

用Android Studio和VS Code建立的Flutter應用模板是一個簡單的計數器示例,本節先仔細講解一下這個計數器Demo的原始碼,讓讀者對Flutter應用程式結構有個基本瞭解,在隨後小節中,將會基於此示例,一步一步新增一些新的功能來介紹Flutter應用的其它概念與技術。對於接下來的示例,希望讀者可以跟著筆者實際動手來寫一下,這樣不僅可以加深印象,而且也會對介紹的概念與技術有一個真切的體會。如果你還不是很熟悉Dart或者沒有移動開發經驗,不用擔心,只要你熟悉物件導向和基本程式設計概念(如變數、迴圈和條件控制),則可以完成本示例。

通過Android Studio和VS Code根據前面“編輯器配置與使用”一章中介紹的建立Flutter工程的方法建立一個新的Flutter工程,命名為"first_flutter_app"。建立好後,就會得到一個計數器應用的Demo。

注意,預設Demo示例可能隨著編輯器Flutter外掛版本變化而變化,本例中會介紹計數器示例的全部程式碼,所以不會對本示例產生影響。

我們先執行此示例,效果圖如下:

avatar

該計數器示例中,每點選一次右下角帶“➕”號的懸浮按鈕,螢幕中央的數字就會加1。

在這個示例中,主要Dart程式碼是在 lib/main.dart 檔案中,下面我們看看該示例的原始碼:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: '【全棧程式設計】-onajax.com',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: '【全棧程式設計】-onajax.com'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              '【全棧程式設計】-onajax.com提示:你已經點選了--',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: '【全棧程式設計】-onajax.com',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

分析

1.匯入包

import 'package:flutter/material.dart';

此行程式碼作用是匯入了Material UI元件庫。Material是一種標準的移動端和web端的視覺設計語言, Flutter預設提供了一套豐富的Material風格的UI元件。

2.應用入口

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

與C/C++、Java類似,Flutter 應用中main函式為應用程式的入口,main函式中呼叫了,runApp 方法,它的功能是啟動Flutter應用,它接受一個Widget引數,在本示例中它是MyApp類的一個例項,該引數代表Flutter應用。
main函式使用了(=>)符號,這是Dart中單行函式或方法的簡寫。

3.應用結構。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      //應用名稱  
      title: 'Flutter Demo', 
      theme: new ThemeData(
        //藍色主題  
        primarySwatch: Colors.blue,
      ),
      //應用首頁路由  
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

MyApp類代表Flutter應用,它繼承了 StatelessWidget類,這也就意味著應用本身也是一個widget。
在Flutter中,大多數東西都是widget,包括對齊(alignment)、填充(padding)和佈局(layout)。

Flutter在構建頁面時,會呼叫元件的build方法,widget的主要工作是提供一個build()方法來描述如何構建UI介面(通常是通過組合、拼裝其它基礎widget)。

MaterialApp 是Material庫中提供的Flutter APP框架,通過它可以設定應用的名稱、主題、語言、首頁及路由列表等。MaterialApp也是一個widget。

Scaffold 是Material庫中提供的頁面腳手架,它包含導航欄和Body以及FloatingActionButton(如果需要的話)。 本書後面示例中,路由預設都是通過Scaffold建立。

home 為Flutter應用的首頁,它也是一個widget。

4.首頁

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
 ...
}

MyHomePage 是應用的首頁,它繼承自StatefulWidget類,表示它是一個有狀態的widget(Stateful widget)。現在,我們可以簡單認為Stateful widget 和Stateless widget有兩點不同:

Stateful widget可以擁有狀態,這些狀態在widget生命週期中是可以變的,而Stateless widget是不可變的。
Stateful widget至少由兩個類組成:

一個StatefulWidget類。
一個 State類; StatefulWidget類本身是不變的,但是 State類中持有的狀態在widget生命週期中可能會發生變化。
_MyHomePageState類是MyHomePage類對應的狀態類。看到這裡,細心的讀者可能已經發現,和MyApp 類不同, MyHomePage類中並沒有build方法,取而代之的是,build方法被挪到了_MyHomePageState方法中,至於為什麼這麼做,先留個疑問,在分析完完整程式碼後再來解答。

接下來,我們看看_MyHomePageState中都包含哪些東西:

1.狀態。

int _counter = 0;

_counter 為儲存螢幕右下角帶“➕”號按鈕點選次數的狀態。

2.設定狀態的自增函式。

void _incrementCounter() {
  setState(() {
     _counter++;
  });
}

當按鈕點選時,會呼叫此函式,該函式的作用是先自增_counter,然後呼叫setState 方法。setState方法的作用是通知Flutter框架,有狀態發生了改變,Flutter框架收到通知後,會執行build方法來根據新的狀態重新構建介面, Flutter 對此方法做了優化,使重新執行變的很快,所以你可以重新構建任何需要更新的東西,而無需分別去修改各個widget。

3.構建UI介面

構建UI介面的邏輯在build方法中,當MyHomePage第一次建立時,_MyHomePageState類會被建立,當初始化完成後,Flutter框架會呼叫Widget的build方法來構建widget樹,最終將widget樹渲染到裝置螢幕上。所以,我們看看_MyHomePageState的build方法中都幹了什麼事:

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              '【全棧程式設計】-onajax.com提示:你已經點選了--',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: '【全棧程式設計】-onajax.com',
        child: new Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

Scaffold 是 Material庫中提供的一個widget, 它提供了預設的導航欄、標題和包含主螢幕widget樹的body屬性。widget樹可以很複雜。

body的widget樹中包含了一個Center widget,Center 可以將其子widget樹對齊到螢幕中心, Center 子widget是一個Column widget,Column的作用是將其所有子widget沿螢幕垂直方向依次排列, 此例中Column包含兩個 Text子widget,第一個Text widget顯示固定文字 “【全棧程式設計】-onajax.com提示:你已經點選了--”,第二個Text widget顯示_counter狀態的數值。

floatingActionButton是頁面右下角的帶“➕”的懸浮按鈕,它的onPressed屬性接受一個回撥函式,代表它被點選後的處理器,本例中直接將_incrementCounter作為其處理函式。

現在,我們將整個流程串起來:當右下角的floatingActionButton按鈕被點選之後,會呼叫_incrementCounter,在_incrementCounter中,首先會自增_counter計數器(狀態),然後setState會通知Flutter框架狀態發生變化,接著,Flutter會呼叫build方法以新的狀態重新構建UI,最終顯示在裝置螢幕上。

為什麼要將build方法放在State中,而不是放在StatefulWidget中?

現在,我們回答之前提出的問題,為什麼build()方法在State(而不是StatefulWidget)中 ?這主要是為了開發的靈活性。如果將build()方法在StatefulWidget中則會有兩個問題:

狀態訪問不便

試想一下,如果我們的Stateful widget 有很多狀態,而每次狀態改變都要呼叫build方法,由於狀態是儲存在State中的,如果將build方法放在StatefulWidget中,那麼構建時讀取狀態將會很不方便,試想一下,如果真的將build方法放在StatefulWidget中的話,由於構建使用者介面過程需要依賴State,所以build方法將必須加一個State引數,大概是下面這樣:

  Widget build(BuildContext context, State state){
      //state.counter
      ...
  }

這樣的話就只能將State的所有狀態宣告為公開的狀態,這樣才能在State類外部訪問狀態,但將狀態設定為公開後,狀態將不再具有私密性,這樣依賴,對狀態的修改將會變的不可控。將build()方法放在State中的話,構建過程則可以直接訪問狀態,這樣會很方便。

繼承StatefulWidget不便

例如,Flutter中有一個動畫widget的基類AnimatedWidget,它繼承自StatefulWidget類。AnimatedWidget中引入了一個抽象方法build(BuildContext context),繼承自AnimatedWidget的動畫widget都要實現這個build方法。現在設想一下,如果StatefulWidget 類中已經有了一個build方法,正如上面所述,此時build方法需要接收一個state物件,這就意味著AnimatedWidget必須將自己的State物件(記為_animatedWidgetState)提供給其子類,因為子類需要在其build方法中呼叫父類的build方法,程式碼可能如下:

class MyAnimationWidget extends AnimatedWidget{
    @override
    Widget build(BuildContext context, State state){
      //由於子類要用到AnimatedWidget的狀態物件_animatedWidgetState,
      //所以AnimatedWidget必須通過某種方式將其狀態物件_animatedWidgetState
      //暴露給其子類   
      super.build(context, _animatedWidgetState)
    }
}

這樣很顯然是不合理的,因為

AnimatedWidget的狀態物件是AnimatedWidget內部實現細節,不應該暴露給外部。
如果要將父類狀態暴露給子類,那麼必須得有一種傳遞機制,而做這一套傳遞機制是無意義的,因為父子類之間狀態的傳遞和子類本身邏輯是無關的。
綜上所述,可以發現,對於StatefulWidget,將build方法放在State中,可以給開發帶來很大的靈活性。

來源

相關文章