Flutter 的那一兩件事 — Layout

Jast Lai
Dec 28, 2018 · 23 min read

前言

嚴格說起來我對 Flutter 的掌握度其實還沒有很高,目前還在上手的階段,現在一直在想,弄一套 Flutter + Kotlin 的結構,不過看來要實現的路還很漫長。

官網的文章其實蠻詳盡的(單論新手入門方面),

基於我看一遍做一遍教一遍的學習原則,我還是把目前學會的東西先寫成一篇文章。

因為我原則上是一個 Android Developer ,所以寫的文章應該會比較偏向從 Android 角度出發吧。

Hello World

安裝的步驟有點可以參考這裡,感覺起來跟安裝 Android 很像,那我也直接用 Android Studio 安裝 Flutter + Dart 的 Plugin 直接用了,這邊就不多做解釋,直接開始寫個 Hello World。

私心覺得 Hello World 的步驟就有點難度了,因為把 Project new 起來,並不是預設 Hello World (被毆)

一開始把專案建起來,看到的會是這個漏漏長,還略帶點波動拳的程式碼。

我第一次看到其實被嚇傻了,因為依照 Android 習慣,一開始 New 一個 Project 起來,會是一個 Hello World,就算是 Java Code + Xml 也不會超過 20 行程式,不過冷靜下來仔細看一下會發現大概八成是註解…。

什麼 Stateless 的,State 、 AppBar 、Scaffold 、Center 之類的,看起來好像才是程式碼…。

總之不管惹,實際跑一次程式看看,出來的畫面長這樣:

這 App ,具體有什麼功用,其實就是點擊下面藍色的按鈕,上頭的數字會根據你點擊的次數顯示點擊的次數。

那就解釋一下這邊究竟在寫啥:

import 'package:flutter/material.dart';void main() => runApp(MyApp());

這就很字面的意思,import 我要用的東西,然後把我要跑我要跑的東西。

那具體要跑什麼呢?用下面這段 Code 做解釋:

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Todo App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

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

可以看到 MyApp 繼承了 StatelessWidget,下邊的註解也說了,這其實就是整個 Application 的 root 。而 MyHomePage 則繼承了 StatefulWidget

StatelessWidget 字面意思就是無狀態的部件,沒有就會有,所以還有另一個 class 是 StatefulWidget ,字面意思就是有狀態的部件,基本上 Flutter 寫成的 app 就是以這兩個 class 為根開始的。

PS:即便是 App 本身,實際上也是一個 Widget

有跟無不太能解釋兩者的差異,StatelessWidget 和 StatefulWidget 最具體的差異是:StatelessWidget 絕對不會更改自己狀態而 StatefulWidget 則會但,並不意味著 StatelessWidget 底下的 Widget 不能改變自己,也不意味著來自外部的操作無法改變 StatelessWidget 的狀態。

雖然我是說具體的差異,但實際上還是很抽象,可以這樣想,一個純顯示的 Text 會是繼承自 StatelessWidget ;而一個提供使用者輸入的 Text 則會是繼承自 StatefulWidget 。

那在這邊有趣的地方是,為什麼非得在 Home Page 外包一層 StatelessWidget 呢?實際上可以直接寫成:

void main() => runApp(MaterialApp(
      title: 'Flutter Todo App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    ));

我猜是為了用程式碼表示:App 本身並不是一個時常需要變更(設計上並不會時常有 A App 切換成 B App 的情況),所以繼承了 StatelessWidget ,而 App 裡頭的 HomePage 則是時常需要改變的(設計上時常會有 A page 切換成 B Page 的情況) ,所以繼承了 StatefulWidget 。

另外我猜也有可能跟效能配置有關,只能說現在的我不清楚為什麼 new 出來的 Sample Code 需要這麼做。

先別管這個了,繼續看下去。

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

State<StatefulWidget> ,代表 State 是 StatefulWidget 的狀態(總感覺跟我認知有點顛倒)

class _MyHomePageState extends State<MyHomePage> { // ignore onClick event at here...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      // ignore the float button at here...
    );
  }
}

這裡我把註解拿掉了,會發現其實 Code 也沒想像這麼多。

Flutter 的佈局不像 Android 一樣,還有分 xml 和 java code,原則上是全部集中在 dart 之中。

要理解其實也不難,把東西全部拿掉,留下一個 Text(“Hello World”) 就好,因為我本意也就是要寫個 Hello World:

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
      title: 'Hello World App',
      theme: ThemeData(
        primarySwatch: Colors.grey,
      ),
      home: Text("Hello World"),
    ));

那結果看來就會像這樣:

其實有種手機當掉進工程模式的 Feel

如果先前有 bulid 過,會發現這個 build 起來的速度真的爆炸快的,大約兩到三秒,這是 Flutter 的 feature : Hot reload 。不過在有圖片或新增檔案得狀態下還是得重新 build 就是了,得看當下的情況。

這看起來有點恐怖,所以還是得上個底,那這邊就會用到 Scaffold。

Scaffold 字面意思是鷹架,我個人比較喜歡另一個讓人印象深刻的翻譯:絞刑架。其實指得就是 App layout 的架子,上頭要放什麼東西取決於設計者。

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
      title: 'Hello World App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home:Scaffold(
        body:  Text('Hello World'),
      ),
    ));

看起來就會是這樣:

可以看到 Hello World 被蓋在 Status bar 底下,這樣似乎有點不妥,通常會有一個 AppBar,那就把 App bar 加上去:

void main() => runApp(MaterialApp(
      title: 'Hello World App',
      theme: ThemeData(
        primarySwatch: Colors.grey,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Title bar'),
        ),
        body: Text('Hello World'),
      ),
    ));

這裡加了一個參數,叫 appBar ,指的當然是上頭的條,在這邊預設是藍色的 bar,這裡裝的會是 AppBar,那 AppBar 裡頭可以放的東西很多,可以設定 title 不用說,也可以在 bar 的左邊放一個 menu button (leading),也可以在 bar 的右邊放上一整排的按鈕(actions),看設計。

看起來就會像這樣:

這樣很像 Android 剛 new 出來的 Project ,但是不同的是 ,字沒有在畫面中間,那這也好處理,直接在 Text 外頭包上一層 Center。

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
      title: 'Hello World App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
          appBar: AppBar(
            title: Text('Title bar'),
          ),
          body: Center(
            child: Text('Hello World'),
          )),
    ));

Center 指的即是中間,他的 child 會被放在中間,所以 Text 放進去裡頭很就會至中了。

那麼結果看起來就像這樣:

這樣 Hello World 就完成了。

因為這個頁面之後可能要置換的動作,不包裝起來要改實在比較麻煩一點,所以把 Scaffold 包成一個 HelloWorldPage ,更動起來會比較省事一點點。

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
    title: 'Hello World App',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: HelloWorldPage()));

class HelloWorldPage extends StatefulWidget {
  HelloWorldPage({Key key}) : super(key: key);

  @override
  HelloWorldState createState() => HelloWorldState();
}

class HelloWorldState extends State<HelloWorldPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Title bar'),
        ),
        body: Center(
          child: Text('Hello World'),
        ));
  }
}

文字和排版

這時我在想替文字換個顏色,然後把他弄大一點,這狀況可以使用 TextStyle()

Text(
  'Hello World',
  style: TextStyle(color: Colors.cyan, fontSize: 32.0),
),

這時想要替文字加上背景色,好彰顯它佔據的位置,不過 Text 本身並沒有覆蓋背景的能力,那可以在 Text 的外層包一個 Container 。

          child: Container(
            child: Text(
              'Hello World',
              style: TextStyle(color: Colors.cyan, fontSize: 32.0),
            ),
            color: Colors.orange,
          ),

那樣子看起來就會是:

旁邊的同事表示對我的配色品味黑人問號

此外 Container 還可以處理 Padding 還有 margin ,這邊就不一一示範了。

PS: Colors 是 flutter/material.dart 預載的顏色工具,此外還有 Icons ,也同樣預載的很多 icon

那我現在想要在上頭放上一些 icon ,在 Text 的下面,或者是多加字之類的,該怎麼做呢?

這時就得用上 Column 這個 Widget 了。

body: Center(
  child: Column(children: <Widget>[
    Text(
      'Hello World',
      style: TextStyle(color: Colors.cyan, fontSize: 32.0),
    ),
    Text("I've seeing things you people wouldn't believe."),
    Icon(Icons.near_me)
  ]),
));

可以看到結果:

那 column 就是欄,給他的是 Children ,意思就是要塞陣列(複數),塞進去的 Widgets 自然就會由上到下排列,類似 Android 的 orientation 設定為 vertical 的 LinearLayout。

只是因為 Column 沒給他其他的參數,很自然就會從最上頭開始排,所以給要給他一個參數,叫 mainAxisAlignment,可以讓 Column 裡頭的 Children 在 Column 之中置中:

Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text(
        'Hello World',
        style: TextStyle(color: Colors.cyan, fontSize: 32.0),
      ),
      Text("I've seeing things you people wouldn't believe."),,
    ]),

那樣子就像這樣:

那有垂直,自然也會有水平,這時我想在 Hello World 旁邊加上一個星星,藉此彰顯我 Hello World 的閃耀,該怎麼做呢?

這時可以用 Row 這個 Widget。

Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'Hello World',
            style: TextStyle(color: Colors.cyan, fontSize: 32.0),
          ),
          Icon(Icons.star, color: Colors.yellow)
        ],
      ),
      Text("I've seeing things you people wouldn't believe."),
    ]),

一樣的,要加 mainAxisALignment ,不然一樣會從左邊開始。

載入圖片

如果我想在 Hello World 上頭放上一個圖片就下面這張:

Robot.png

那就在專案底下直接建一個資料夾叫 images

要載入本地圖片的話需要改一下專案底下的 yaml 檔,那可以在專案底下找到 pubspec.yaml ,在裡頭看到這段:

照著他註解上的去寫,改成:

(寫 yaml 煩人的是得對齊)

然後在程式碼中加上 Image.asset:

body: Center(
  child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Image.asset('images/robot.png',
          width: 200,
          height: 200,),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[

            Text(
              'Hello World',
              style: TextStyle(color: Colors.cyan, fontSize: 32.0),
            ),
            Icon(Icons.star, color: Colors.yellow)
          ],
        ),
        Text("I've seeing things you people wouldn't believe."),
      ]),
));

結果看起來是這樣:

那我想從網路抓圖呢?

在 Android 這個步驟我可能會打算直接開個另一篇了,因為要弄上一堆快取記憶體控制有的沒有的,最後花了很長一段時間發現還是寫不贏人家寫好的,最後還是選擇用 Glide 或者是 ImageLoader 之類網路上比較著名的第三方元件。不過 Flutter 畢竟是踩著別人踩過的坑過來了,這樣的步驟相對簡單不少。

一樣也是用 Image 這個 widget,裡面的 network

Image.network('https://raw.githubusercontent.com/flutter/website/master/src/_includes/code/layout/lakes/images/lake.jpg'),

就醬(咦?),怎麼感覺 Load local 端的圖比較麻煩…。

PS: 這張圖是 Flutter 官網上提供的圖。

更棒的是,他支援 gif

Image.network('https://github.com/flutter/plugins/raw/master/packages/video_player/doc/demo_ipod.gif?raw=true'),

結果是這樣:

那這張圖 6Mb 多,這邊也反映出一個問題,網路抓起來肯定會需要時間,那會有一段時間通常會放 placeholder ,那要怎麼放呢?

這時可以用 FadeImage 這個 widget。

那我在我的 images 裡頭另外放一張 gif ,結果會寫成這樣:

FadeInImage.assetNetwork(
    placeholder: 'images/animated_gun_turret.gif',
    image: 'https://github.com/flutter/plugins/raw/master/packages/video_player/doc/demo_ipod.gif?raw=true')

結果會類似這樣:

簡單到的讓我有點怕怕der

弄個列表

學會怎麼排列文字,載入圖片之後,就該開始學怎麼弄出一個列表了。

在 Android 裡頭,弄一個 List ,會需要生成一個 Adapter ,可以用它來產生 View ,那他會藉此控制產生 View 的數量,好讓記憶體或電池的耗用不會炸掉。

那 Flutter 很潮,會生成一個 Widget List ,然後,剩下的 Flutter 的 ListView Widget 會幫你處理掉(官網說的)。

ListView

那總之,要先決定 List 上的單一 item 要怎麼建立:

Widget createItem(String contentText) {
  return Container(
    padding: EdgeInsets.all(20.0),
    child: Text(contentText),
  );
}

很簡單,放一個容器,裡頭一個 Text ,Text 的四周 padding 20 ,好讓 list 上頭的 text 之間有個

我這邊直接把建立 Widget 的動作獨立成一個 method ,省的程式越來越波動拳。

再來建立 Widget List:

List<Widget> createList() {
  List<Widget> widgets = [];

  for (int index = 1; index <= 100; index++) {
    widgets.add(createItem('Text $index'));
  }

  return widgets;
}

我想讓畫面可以呈現 1 ~ 100 的 Text ,那就用迴圈去製作,最後把列表回傳回去。

最後我要把這個 list 實作在上去:

Scaffold(
      appBar: AppBar(
        title: Text('Title bar'),
      ),
      body: ListView(
        children: createList(),
      ));
}

使用 ListView 這個 Widget ,直接把 list (即是一個 List<Widget>) 丟進去,這樣就行了,完整的程式碼如下:

出來的結果是這個樣子:

改變一下 ListView 的 item 樣式

我熊熊想在 item 的左邊加上一張圖,該怎麼做呢?

(其實每次弄 Demo 列表最煩人的往往是要怎麼把那一堆資訊弄出來)

藉由 Flutter 官網這邊提供的資源,可以得到 30 張左右的圖片,放進資料夾,參考這邊去修改 yaml。

稍微小改一下 createItem 這個 method:

Widget createItem(int imageIndex) {
  return Row(
    children: <Widget>[
      Container(
        padding: EdgeInsets.all(5),
        child: CircleAvatar(
          backgroundImage: AssetImage(
            "images/pic$imageIndex.jpg",
          ),
          radius: 50.0,
        ),
      ),
      Container(
        padding: EdgeInsets.all(20),
        child: Text('Image $imageIndex'),
      )
    ],
  );
}

用 Row 把 Image 和 Text 包起來,兩者再用 Container 包裝裝飾,那這邊因為我不喜歡圖片方方正正的,所以我用了 CircleAvatar 這個 Widget 對圖做圓形遮罩裝飾。

那根據 createItem 的更動去更改 createList():

List<Widget> createList() {
  List<Widget> widgets = [];

  for (int index = 0; index < 100; index++) {
    widgets.add(createItem((index % 30) + 1));
  }

  return widgets;
}

其餘維持原樣,因為實際上變更其實只有產生 item 的方式而已。

效果如下:

完整的程式碼如下:

結語

這邊簡單寫了一下 Flutter 的一些關於 Layout 的基本,寫的東西當然是 Flutter 可用 Widget 的冰山一角而已,如果一步一步地看、一步一步地寫,會發現它能做的事情遠遠比想像中更多。

而我這邊沒提到 GridView 、Stack 等官網上說是 Standard 的 Layout ,是因為我不想讓這篇文章太長,這篇文章比較偏向是我開始練習寫 Flutter 的一個心得文吧,不得不說 Dart 這語言,一開始看很像 Java ,寫一寫發現有 Kotlin 的味道,看一看也有很像 python 的寫法,然後,原先在 Android 當中做起來算麻煩的事情,到了 Flutter 就變得省事很多(例如那個 image load),畢竟誠如我文章前面提到的,Flutter 是踩著別人踩過的坑過來的(不排除他創造了新的坑),使用起來當然會相對簡單一點。

下一篇,應該會開始寫互動的部分,不知道何時會出來就是了,再來沒幾天後,就要邁入 2019 年了。

如果有任何意見、問題、或者是對我的論點有質疑想要反駁,希望各位不要吝嗇提出疑惑對我多多指教。

參考資料

https://pub.dartlang.org/packages/http#-installing-tab-

https://docs.flutter.io/flutter/material/CircleAvatar-class.htmlhttps://docs.flutter.io/flutter/material/CircleAvatar-class.html

https://github.com/flutter/flutter

Jastzeonic

It’s not just. It’s Jast!

Jast Lai

Written by

Jast Lai

一個迷途在塵世之中、邊緣、喜歡電玩、動漫、還有有趣科技新資訊、通常一開口就會讓對話句點的的小小 Android 工程師。

Jastzeonic

It’s not just. It’s Jast!