Flutter學習指南:UI佈局和控件

這是一個系列,經過8篇文章幫助你們創建起 Flutter 的知識體系,建議你們好好閱讀並收藏起來。本篇文章咱們先介紹 Flutter 裏一些經常使用的 UI 控件,而後藉助官網提供的兩個 demo 把所學的控件知識實際使用起來。html

基本控件

Widget

在 Flutter 裏,UI 控件就是所謂的 Widget。經過組合不一樣的 Widget,來實現咱們用戶交互界面。git

Widget 分爲兩種,一種是無狀態的,叫 StatelessWidget,它只能用來展現信息,不能有動做(用戶交互);另外一種是有狀態的,叫 StatefulWidget,這種 Widget 能夠經過改變狀態使得 UI 發生變化,它能夠包含用戶交互。github

StatelessWidget 的使用很是簡單,咱們只須要繼承 StatelessWidget,而後實現 build 方法就能夠了:shell

class FooWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // ...
  }
}
複製代碼

關於 build 方法的實現,在後面咱們學習具體的控件時讀者就會了解的,這裏暫時忽略掉。編程

StatefulWidget 用起來麻煩一些,他還須要一個 State:bash

class BarWidget extends StatefulWidget {
  @override
  State createState() {
    return _BarWidgetState();
  }
}

class _BarWidgetState extends State<BarWidget{
  @override
  Widget build(BuildContext context) {
    // ...
  }
}
複製代碼

這裏看起來可能有些繞,BarWidget 依賴了 _BarWidgetState,而 _BarWidgetState 又繼承了 State< BarWidget>。若是讀者不太理解,其實也沒有什麼關係,這只是一個樣板代碼,照着寫就好了。網絡

從 BarWidget 的實現來看,好像跟前面使用 StatelessWidget 沒有什麼區別,都是在 build 方法裏面返回一個 Widget,只是 stateful widget 把這個方法挪到了 State 裏面。實際上,二者的區別很是大。stateless widget 整個生命週期裏都不會改變,因此 build 方法只會執行一次。而 stateful widget 只要狀態改變,就會調用 build 方法從新建立 UI。app

爲了觸發 UI 的重建,咱們能夠調用 setState 方法。下面的代碼讀者留意一下便可,在後面咱們學習了相關的控件後再回過頭來看。less

class BarWidget extends StatefulWidget {
  @override
  State createState() {
    return _BarWidgetState();
  }
}

class _BarWidgetState extends State<BarWidget{
  var i = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Text('i = $i'),
        RaisedButton(
          onPressed: () {
            setState(() {
              ++i;
            });
          },
          child: Text('click'),
        )
      ],
    );
  }
}
複製代碼

下面咱們開始學習一些具體的控件。ide

文本

爲了展現文本,咱們使用 Text:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text("Put your text here");
  }
}
複製代碼

這就是最簡單的文本了,它使用的是默認的樣式。不少狀況下,咱們都須要對文本的樣式進行修改,這個時候,可使用 TextStyle:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      "Put your text here",
      style: TextStyle(
        color: Colors.blue,
        fontSize: 16.0,
        fontWeight: FontWeight.bold
      ),
    );
  }
}
複製代碼

圖片

使用 Image,可讓咱們向用戶展現一張圖片。圖片的來源能夠是網絡、文件、資源和內存,它們對應的構造函數分別是:

Image.asset(name);
Image.file(file);
Image.memory(bytes);
Image.network(src);
複製代碼

比方說,爲了展現一張來自網絡的圖片,咱們能夠這樣:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Image.network(
      "http://www.example.com/xxx.png",
      width: 200.0,
      height: 150.0,
    );
  }
}
複製代碼

按鈕

Flutter 提供了兩個基本的按鈕控件:FlatButton 和 RaisedButton,它們的使用方法是相似的:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var flatBtn = FlatButton(
      onPressed: () => print('FlatButton pressed'),
      child: Text('BUTTON'),
    );
    var raisedButton = RaisedButton(
      onPressed: () => print('RaisedButton pressed'),
      child: Text('BUTTON'),
    );
    return raisedButton;
  }
}
複製代碼

經過設置 onPressed 回調,咱們能夠在按鈕被點擊的時候獲得回調。child 參數用於設置按鈕的內容。雖然咱們給 child 傳遞的是 Text,但這不是必需的,它能夠接受任意的 Widget,比方說,Image。

注意,因爲咱們只是在按鈕點擊的時候打印一個字符串,這裏使用 StatelessWidget 是沒有問題的。但若是有其餘 UI 動做(好比彈出一個 dialog,則必須使用 StatefulWidget)。

它們的區別只是樣式不一樣而已的:

FlatButton:

flat-button
flat-button

RaiseButton:

raised-button
raised-button

文本輸入框

Flutter 的文本輸入框叫 TextField。爲了獲取用戶輸入的文本,咱們須要給他設置一個 controller。經過這個 controller,就能夠拿到文本框裏的內容:

class MessageForm extends StatefulWidget {
  @override
  State createState() {
    return _MessageFormState();
  }
}

class _MessageFormState extends State<MessageForm{
  var editController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    // Row、Expand 都是用於佈局的控件,這裏能夠先忽略它們
    return Row(
      children: <Widget>[
        Expanded(
          child: TextField(
            controller: editController,
          ),
        ),
        RaisedButton(
          child: Text("click"),
          onPressed: () => print('text inputted: ${editController.text}'),
        )
      ],
    );
  }

  @override
  void dispose() {
    super.dispose();
    // 手動調用 controller 的 dispose 方法以釋放資源
    editController.dispose();
  }
}
複製代碼

顯示彈框

在前面的 TextField 例子中,咱們只是把用戶的輸入經過 print 打印出來,這未免也太無趣了。在這一小節,咱們要把它顯示在 dialog 裏。爲了彈出一個 dialog,咱們須要調用 showDialog 方法並傳遞一個 builder:

class _MessageFormState extends State<MessageForm{
  var editController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(
          child: TextField(
            controller: editController,
          ),
        ),
        RaisedButton(
          child: Text("click"),
          onPressed: () {
            showDialog(
                // 第一個 context 是參數名,第二個 context 是 State 的成員變量
                context: context,
                builder: (_) {
                  return AlertDialog(
                    // dialog 的內容
                    content: Text(editController.text),
                    // actions 設置 dialog 的按鈕
                    actions: <Widget>[
                      FlatButton(
                        child: Text('OK'),
                        // 用戶點擊按鈕後,關閉彈框
                        onPressed: () => Navigator.pop(context),
                      )
                    ],
                  );
                }
            );
          }
        )
      ],
    );
  }

  @override
  void dispose() {
    super.dispose();
    editController.dispose();
  }
}
複製代碼

最簡單的佈局——Container、Padding 和 Center:

咱們常常說,Flutter 裏面全部的東西都是 Widget,因此,佈局也是 Widget。

控件 Container 可讓咱們設置一個控件的尺寸、背景、margin 等:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text('text'),
      padding: EdgeInsets.all(8.0),
      margin: EdgeInsets.all(4.0),
      width: 80.0,
      decoration: BoxDecoration(
        // 背景色
        color: Colors.grey,
        // 圓角
        borderRadius: BorderRadius.circular(5.0),
      ),
    );
  }
}
複製代碼

若是咱們只須要 padding,可使用控件 Padding:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(8.0),
      child: Text('text'),
    );
  }
}
複製代碼

Center 就跟它的名字同樣,把一個控件放在中間:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(8.0),
      margin: EdgeInsets.all(4.0),
      width: 200.0,
      height: 200.0,
      decoration: BoxDecoration(
        // 背景色
        color: Colors.grey,
        // 圓角
        borderRadius: BorderRadius.circular(5.0),
      ),

      // 把文本放在 Container 的中間
      child: Center(
        child: Text('text'),
      ),
    );
  }
}
複製代碼

水平、豎直佈局和 Expand

咱們常常說,Flutter 裏面全部的東西都是 Widget,因此,佈局也是 Widget。水平佈局咱們可使用 Row,豎直佈局使用 Column。

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      // 只有一個子元素的 widget,通常使用 child 參數來設置;Row 能夠包含多個子控件,
      // 對應的則是 children。
      children: <Widget>[
        Text('text1'),
        Text('text2'),
        Text('text3'),
        Text('text4'),
      ],
    );
  }
}
複製代碼

Column 的使用是同樣的:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Text('text1'),
        Text('text2'),
        Text('text3'),
        Text('text4'),
      ],
    );
  }
}
複製代碼

關於 Expand 控件,咱們來看看 TextField 的那個例子:

class MessageForm extends StatefulWidget {
  @override
  State createState() {
    return _MessageFormState();
  }
}

class _MessageFormState extends State<MessageForm{
  var editController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        // 佔滿一行裏除 RaisedButton 外的全部空間
        Expanded(
          child: TextField(
            controller: editController,
          ),
        ),
        RaisedButton(
          child: Text("click"),
          onPressed: () => print('text inputted: ${editController.text}'),
        )
      ],
    );
  }

  @override
  void dispose() {
    super.dispose();
    editController.dispose();
  }
}
複製代碼

這裏經過使用 Expand,TextField 纔可以佔滿一行裏除按鈕外的全部空間。此外,當一行/列裏有多個 Expand 時,咱們還能夠經過設置它的 flex 參數,在多個 Expand 之間按比例劃分可用空間。

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(
          // 佔一行的 2/3
          flex: 2,
          child: RaisedButton(child: Text('btn1'),),
        ),
        Expanded(
          // 佔一行的 1/3
          flex: 1,
          child: RaisedButton(child: Text('btn2'),),
        ),
      ],
    );
  }
}
複製代碼

Stack 佈局

有些時候,咱們可能會但願一個控件疊在另外一個控件的上面。因而,Stack 應運而生:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Text('foobar'),
        Text('barfoo'),
      ],
    );
  }
}
複製代碼

默認狀況下,子控件都按 Stack 的左上角對齊,因而,上面的兩個文本徹底一上一下堆疊在一塊兒。咱們還能夠經過設置 alignment 參數來改變這個對齊的位置:

class TestWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      // Aligment 的取值範圍爲 [-1, 1],Stack 中心爲 (0, 0),
      // 這裏設置爲 (-0.5, -0.5) 後,可讓文本對齊到 Container 的 1/4 處
      alignment: const Alignment(-0.5-0.5),
      children: <Widget>[
        Container(
          width: 200.0,
          height: 200.0,
          color: Colors.blue,
        ),
        Text('foobar'),
      ],
    );
  }
}
複製代碼

效果以下:

screenshot-stack
screenshot-stack

經過組合 Row/Column 和 Stack,已經可以完成絕大部分的佈局了,因此 Flutter 裏沒有相對佈局之類的東西。更多的 Flutter 控件,讀者能夠參考 flutter.io/widgets/

示例一

在這一節裏,咱們綜合前面所學的知識,來實現下面這個界面。

lakes-diagram
lakes-diagram

展現圖片

  1. 把圖片 lake 放到項目根目錄的 images 文件夾下(若是沒有,你須要本身建立一個)

  2. 修改 pubspec.yaml,找到下面這個地方,而後把圖片加進來

    flutter:

      # The following line ensures that the Material Icons font is
      # included with your application, so that you can use the icons in
      # the material Icons class.
      uses-material-design: true

      # To add assets to your application, add an assets section, like this:
      # assets:
      #  - images/a_dot_burr.jpeg
      #  - images/a_dot_ham.jpeg
    複製代碼

    修改後以下:

    flutter:

      # The following line ensures that the Material Icons font is
      # included with your application, so that you can use the icons in
      # the material Icons class.
      uses-material-design: true

      # To add assets to your application, add an assets section, like this:
      assets:
        - images/lake.jpg
    複製代碼
  3. 如今,咱們能夠把這張圖片展現出來了:

    void main() {
      runApp(MyApp());
    }

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter UI basic 1',
          home: Scaffold(
            appBar: AppBar(
              title: Text('Top Lakes'),
            ),
            body: Image.asset(
              'images/lake.jpg',
              width: 600.0,
              height: 240.0,
              // cover 相似於 Android 開發中的 centerCrop,其餘一些類型,讀者能夠查看
              // https://docs.flutter.io/flutter/painting/BoxFit-class.html
              fit: BoxFit.cover,
            )
          ),
        );
      }
    }
    複製代碼

若是讀者是初學 Flutter,強烈建議在遇到不熟悉的 API 時翻一翻文檔,並在文檔中找到 demo 所使用的 API。咱們的例子不可能覆蓋全部的 API,經過這種方式熟悉文檔後,讀者就能夠根據文檔實現出本身想要的效果。不妨就從 Image 開始吧,在 docs.flutter.io/flutter/wid… 找出上面咱們使用的 Image.asset 構造函數的幾個參數的含義,還有 BoxFit 的其餘幾個枚舉值。

佈局

在這一小節,咱們來實現圖片下方的標題區域。

咱們直接來看代碼:

class _TitleSection extends StatelessWidget {
  final String title;
  final String subtitle;
  final int starCount;

  _TitleSection(this.title, this.subtitle, this.starCount);

  @override
  Widget build(BuildContext context) {
    // 爲了給 title section 加上 padding,這裏咱們給內容套一個 Container
    return Container(
      // 設置上下左右的 padding 都是 32。相似的還有 EdgeInsets.only/symmetric 等
      padding: EdgeInsets.all(32.0),
      child: Row(
        children: <Widget>[
          // 這裏爲了讓標題佔滿屏幕寬度的剩餘空間,用 Expanded 把標題包了起來
          Expanded(
            // 再次提醒讀者,Expanded 只能包含一個子元素,使用的參數名是 child。接下來,
            // 爲了在豎直方向放兩個標題,加入一個 Column。
            child: Column(
              // Column 是豎直方向的,cross 爲交叉的意思,也就是說,這裏設置的是水平方向
              // 的對齊。在水平方向,咱們讓文本對齊到 start(讀者能夠修改成 end 看看效果)
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                // 聰明的你,這個時候確定知道爲何忽然加入一個 Container 了。
                // 跟前面同樣,只是爲了設置一個 padding
                Container(
                  padding: const EdgeInsets.only(bottom: 8.0),
                  child: Text(
                    title,
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),
                Text(
                  subtitle,
                  style: TextStyle(color: Colors.grey[500]),
                )
              ],
            ),
          ),

          // 這裏是 Row 的第二個子元素,下面這兩個就沒用太多值得說的東西了。
          Icon(
            Icons.star,
            color: Colors.red[500],
          ),

          Text(starCount.toString())
        ],
      ),
    );
  }
}
複製代碼

對齊

接下來咱們要作的這一部分在佈局上所用到的知識,基本知識在上一小節咱們都已經學習了。這裏惟一的區別在於,三個按鈕是水平分佈的。

實現以下:

Widget _buildButtonColumn(BuildContext context, IconData icon, String label) {
  final color = Theme.of(context).primaryColor;

  return Column(
    // main axis 跟咱們前面提到的 cross axis 相對應,對 Column 來講,指的就是豎直方向。
    // 在放置完子控件後,屏幕上可能還會有一些剩餘的空間(free space),min 表示儘可能少佔用
    // free space;相似於 Android 的 wrap_content。
    // 對應的,還有 MainAxisSize.max
    mainAxisSize: MainAxisSize.min,
    // 沿着 main axis 居中放置
    mainAxisAlignment: MainAxisAlignment.center,

    children: <Widget>[
      Icon(icon, color: color),
      Container(
        margin: const EdgeInsets.only(top: 8.0),
        child: Text(
          label,
          style: TextStyle(
            fontSize: 12.0,
            fontWeight: FontWeight.w400,
            color: color,
          ),
        ),
      )
    ],
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //...

    Widget buttonSection = Container(
      child: Row(
        // 沿水平方向平均放置
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          _buildButtonColumn(context, Icons.call, 'CALL'),
          _buildButtonColumn(context, Icons.near_me, 'ROUTE'),
          _buildButtonColumn(context, Icons.share, 'SHARE'),
        ],
      ),
    );
  //...
}
複製代碼

關於 cross/main axis,看看下面這兩個圖就很清楚了:


MainAxisAlignment 的更多的信息,能夠查看 docs.flutter.io/flutter/ren…

所有放到一塊兒

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final titleSection = _TitleSection(
        'Oeschinen Lake Campground''Kandersteg, Switzerland'41);
    final buttonSection = ...;
    final textSection = Container(
        padding: const EdgeInsets.all(32.0),
        child: Text(
          '''
Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese Alps. Situated 1,578 meters above sea level, it is one of the larger Alpine Lakes. A gondola ride from Kandersteg, followed by a half-hour walk through pastures and pine forest, leads you to the lake, which warms to 20 degrees Celsius in the summer. Activities enjoyed here include rowing, and riding the summer toboggan run.
          '''
,
          softWrap: true,
        ),
    );

    return MaterialApp(
      title: 'Flutter UI basic 1',
      home: Scaffold(
          appBar: AppBar(
            title: Text('Top Lakes'),
          ),
          // 因爲咱們的內容可能會超出屏幕的長度,這裏把內容都放到 ListView 裏。
          // 除了這種用法,ListView 也能夠像咱們在 Android 原生開發中使用 ListView 那樣,
          // 根據數據動態生成一個個 item。這個咱們在下一節再來學習
          body: ListView(
            children: <Widget>[
              Image.asset(
                'images/lake.jpg',
                width: 600.0,
                height: 240.0,
                // cover 相似於 Android 開發中的 centerCrop,其餘一些類型,讀者能夠查看
                // https://docs.flutter.io/flutter/painting/BoxFit-class.html
                fit: BoxFit.cover,
              ),

              titleSection,
              buttonSection,
              textSection
            ],
          ),
      )
    );
  }
}
}
複製代碼

如今,若是沒有出錯的話,運行後應該就能夠看到下面這個頁面。

若是你遇到了麻煩,能夠在這裏找到全部的源碼:

git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout ui-basic1
複製代碼

更多的佈局知識,讀者還能夠參考 flutter.io/tutorials/l…

示例二

在這一小節咱們來實現一個 list view。

這裏咱們採用的仍是官網提供的例子,可是換一種方式來實現,讓它跟咱們平時使用 Java 時更像一些。

首先給數據建模:

enum BuildingType { theater, restaurant }

class Building {
  final BuildingType type;
  final String title;
  final String address;

  Building(this.type, this.title, this.address);
}
複製代碼

而後實現每一個 item 的 UI:

class ItemView extends StatelessWidget {
  final int position;
  final Building building;

  ItemView(this.position, this.building);

  @override
  Widget build(BuildContext context) {
    final icon = Icon(
        building.type == BuildingType.restaurant
            ? Icons.restaurant
            : Icons.theaters,
        color: Colors.blue[500]);

    final widget = Row(
      children: <Widget>[
        Container(
          margin: EdgeInsets.all(16.0),
          child: icon,
        ),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                building.title,
                style: TextStyle(
                  fontSize: 20.0,
                  fontWeight: FontWeight.w500,
                )
              ),
              Text(building.address)
            ],
          ),
        )
      ],
    );

    return widget;
  }
}
複製代碼

接着是 ListView。因爲渲染機制不一樣,這裏不必弄個 adapter 來管理 widget:

class BuildingListView extends StatelessWidget {
  final List<Building> buildings;

  BuildingListView(this.buildings);

  @override
  Widget build(BuildContext context) {
    // ListView.builder 能夠按需生成子控件
    return ListView.builder(
      itemCount: buildings.length,
      itemBuilder: (context, index) {
        return new ItemView(index, buildings[index]);
      }
    );
  }
}
複製代碼

如今,咱們來給 item 加上點擊事件。

// 定義一個回調接口
typedef OnItemClickListener = void Function(int position);

class ItemView extends StatelessWidget {

  final int position;
  final Building building;
  final OnItemClickListener listener;

  // 這裏的 listener 會從 ListView 那邊傳過來
  ItemView(this.position, this.building, this.listener);

  @override
  Widget build(BuildContext context) {
    final widget = ...;

    // 通常來講,爲了監聽手勢事件,咱們使用 GestureDetector。但這裏爲了在點擊的時候有個
    // 水波紋效果,使用的是 InkWell。
    return InkWell(
      onTap: () => listener(position),
      child: widget
    );
  }
}

class BuildingListView extends StatelessWidget {
  final List<Building> buildings;
  final OnItemClickListener listener;

  // 這是對外接口。外部經過構造函數傳入數據和 listener
  BuildingListView(this.buildings, this.listener);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: buildings.length,
      itemBuilder: (context, index) {
        return new ItemView(index, buildings[index], listener);
      }
    );
  }
}
複製代碼

最後加上一些腳手架代碼,咱們的列表就可以跑起來了:

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    final buildings = [
      Building(BuildingType.theater, 'CineArts at the Empire''85 W Portal Ave'),
      Building(BuildingType.theater, 'The Castro Theater''429 Castro St'),
      Building(BuildingType.theater, 'Alamo Drafthouse Cinema''2550 Mission St'),
      Building(BuildingType.theater, 'Roxie Theater''3117 16th St'),
      Building(BuildingType.theater, 'United Artists Stonestown Twin''501 Buckingham Way'),
      Building(BuildingType.theater, 'AMC Metreon 16''135 4th St #3000'),
      Building(BuildingType.restaurant, 'K\'s Kitchen''1923 Ocean Ave'),
      Building(BuildingType.restaurant, 'Chaiya Thai Restaurant''72 Claremont Blvd'),
      Building(BuildingType.restaurant, 'La Ciccia''291 30th St'),

      // double 一下
      Building(BuildingType.theater, 'CineArts at the Empire''85 W Portal Ave'),
      Building(BuildingType.theater, 'The Castro Theater''429 Castro St'),
      Building(BuildingType.theater, 'Alamo Drafthouse Cinema''2550 Mission St'),
      Building(BuildingType.theater, 'Roxie Theater''3117 16th St'),
      Building(BuildingType.theater, 'United Artists Stonestown Twin''501 Buckingham Way'),
      Building(BuildingType.theater, 'AMC Metreon 16''135 4th St #3000'),
      Building(BuildingType.restaurant, 'K\'s Kitchen''1923 Ocean Ave'),
      Building(BuildingType.restaurant, 'Chaiya Thai Restaurant''72 Claremont Blvd'),
      Building(BuildingType.restaurant, 'La Ciccia''291 30th St'),
    ];
    return MaterialApp(
      title: 'ListView demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Buildings'),
        ),
        body: BuildingListView(buildings, (index) => debugPrint('item $index clicked'))
      ),
    );
  }
}
複製代碼

這個時候你應該能夠看到像這樣的界面了:

若是你遇到了什麼麻煩,能夠查看 tag ui-basic2 的代碼:

git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout ui-basic2
複製代碼



推薦閱讀

Flutter學習指南:熟悉Dart語言
Flutter學習指南:編寫第一個應用
Flutter學習指南:開發環境搭建

編程·思惟·職場
關注下方的公衆號並回復 [禮包] 領取50元編程禮包

相關文章
相關標籤/搜索