Flutter ListView 實戰快速上手

本文微信公衆號「AndroidTraveler」首發。git

背景

本篇主要講述如何快速在 Flutter 中實現 ListView。github

效果圖

先上效果圖感覺一下: json

基本實現

1. 肯定 Item 項佈局

首先咱們要先肯定咱們列表項的佈局,咱們按照咱們效果圖上面所顯示的,能夠寫出以下代碼:bash

import 'package:flutter/material.dart';

class ItemWidget extends StatefulWidget {
  @override
  _ItemWidgetState createState() => _ItemWidgetState();
}

class _ItemWidgetState extends State<ItemWidget> {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text('title'),
        SizedBox(height: 6,),
        Text('description')
      ],
    );
  }
}
複製代碼

顯示效果以下: 微信

固然這裏的 titledescription 目前是 hard code,咱們第二步肯定 Bean 以後會作相應的處理。網絡

2. 肯定數據源

咱們根據列表項的顯示狀況能夠獲得以下 Bean:less

class ItemBean {
  final String title;
  final String description;

  ItemBean(this.title, this.description);
}
複製代碼

能夠看到就是標題和描述而已。ide

同時咱們第一步的列表項能夠更新以下:佈局

import 'package:flutter/material.dart';
import 'package:my_flutter/item_bean.dart';

class ItemWidget extends StatefulWidget {

  final ItemBean itemBean;

  ItemWidget(this.itemBean);

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

class _ItemWidgetState extends State<ItemWidget> {
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(widget?.itemBean?.title ?? ''),
        SizedBox(height: 6,),
        Text(widget?.itemBean?.description ?? '')
      ],
    );
  }
}
複製代碼

再也不 hard code 了。post

另外若是你對於 ?. 和 ?? 不熟悉,能夠看下我以前的文章 Dart 如何優雅的避空

3. 顯示

有了數據源和顯示的 Widget,那麼顯示也就水到渠成了。

以下:

import 'package:flutter/material.dart';
import 'package:my_flutter/item_bean.dart';
import 'package:my_flutter/item_widget.dart';

class ListViewWidget extends StatefulWidget {
  @override
  _ListViewWidgetState createState() => _ListViewWidgetState();
}

class _ListViewWidgetState extends State<ListViewWidget> {
  final List<ItemBean> itemBeans = [];

  @override
  void initState() {
    super.initState();

    _initData();
  }

  /// 實際場景多是從網絡拉取,這裏演示就直接填充數據源了
  void _initData() {
    itemBeans.add(ItemBean('第一句', '關注微信公衆號「AndroidTraveler」'));
    itemBeans.add(ItemBean('第二句', '星河滾燙,你是人間理想'));
    itemBeans.add(ItemBean('第三句', '我明白你會來,因此我等。'));
    itemBeans.add(ItemBean('第四句', '家人閒坐,燈火可親。'));
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: ListView.builder(
        itemCount: itemBeans.length,
        itemBuilder: (context, index) {
          return ItemWidget(itemBeans[index]);
        },
      ),
    );
  }
}
複製代碼

列表的關鍵代碼在於:

ListView.builder(
    itemCount: itemBeans.length,
    itemBuilder: (context, index) {
        return ItemWidget(itemBeans[index]);
    },
)
複製代碼

仍是比較固定的。

最後咱們把這個 ListViewWidget 加載到主頁面,主頁面代碼以下:

import 'package:flutter/material.dart';
import 'package:my_flutter/listview_widget.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: _buildWidget(),
        ),
      ),
    );
  }

  Widget _buildWidget() {
    return ListViewWidget();
  }
}
複製代碼

運行效果以下:

添加分隔線

看起來仍是怪怪的,咱們增長下分隔線看看效果。

Flutter 官方 sdk 裏面自帶了分隔線 Widget,爲 Divider

具體每一個屬性能夠在代碼裏面看到詳細註釋,這裏就不展開了。

咱們的 Divider 代碼以下:

Divider(color: Colors.grey,),
複製代碼

很簡單,就是指定分隔線的顏色。

由於咱們的 Item 自己就是一個 Column,咱們直接追加就能夠了。

ItemWidget 修改後以下:

···

class _ItemWidgetState extends State<ItemWidget> {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(left: 16.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(widget?.itemBean?.title ?? ''),
          SizedBox(
            height: 6,
          ),
          Text(widget?.itemBean?.description ?? ''),
          Divider(color: Colors.grey),
        ],
      ),
    );
  }
}
複製代碼

效果以下:

可能有小夥伴會說,你這個是恰好 item 佈局是 Column,若是不是 Column 的話呢?

方法多種多樣,這裏就說其中的一種方法吧,好比你能夠利用 Stack 來實現。

代碼位置:github.com/nesger/Flut…

添加點擊回調

咱們知道,列表成功顯示只是第一步而已,點擊可以實現咱們指望的效果纔是常規操做。

所以,點擊回調是必不可少的。

那麼如何實現呢?

其實也很簡單,就是跟普通 Widget 同樣包裹一層 GestureDetector 就能夠了。

修改後的 ItemWidget 以下:

···

class _ItemWidgetState extends State<ItemWidget> {
  @override
  Widget build(BuildContext context) {
    Widget container = Container(
      padding: EdgeInsets.only(left: 16.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(widget?.itemBean?.title ?? ''),
          SizedBox(
            height: 6,
          ),
          Text(widget?.itemBean?.description ?? ''),
          Divider(color: Colors.grey),
        ],
      ),
    );
    return GestureDetector(
      child: container,
      onTap: (){
        print('onTap');
      },
    );
  }
}
複製代碼

點擊 Item 時控制檯確實輸出了打印日誌:

flutter: onTap
flutter: onTap
複製代碼

可是存在兩個問題。

第一個就是不知道點擊的是哪個 item,第二個就是通常回調應該是在外層而不該該直接寫在裏面。

所以咱們須要對 ItemWidget 作修改,傳入 index 和監聽回調。

咱們定義的回調接口以下:

/// 定義一個回調接口
typedef OnItemClickListener = void Function(int position, ItemBean itemBean);
複製代碼

ItemWidget 修改後代碼以下:

import 'package:flutter/material.dart';
import 'package:my_flutter/item_bean.dart';

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

class ItemWidget extends StatefulWidget {
  final int position;
  final ItemBean itemBean;
  final OnItemClickListener listener;

  ItemWidget(this.position, this.itemBean, this.listener);

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

class _ItemWidgetState extends State<ItemWidget> {
  @override
  Widget build(BuildContext context) {
    Widget container = Container(
      padding: EdgeInsets.only(left: 16.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(widget?.itemBean?.title ?? ''),
          SizedBox(
            height: 6,
          ),
          Text(widget?.itemBean?.description ?? ''),
          Divider(color: Colors.grey),
        ],
      ),
    );
    return GestureDetector(
      child: container,
      onTap: () => widget.listener(widget.position, widget.itemBean),
    );
  }
}
複製代碼

能夠看到咱們增長了 position 和 listener。

所以咱們的 ListViewWidget 也須要作相應修改:

class ListViewWidget extends StatefulWidget {

  final OnItemClickListener listener;

  ListViewWidget(this.listener);

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

class _ListViewWidgetState extends State<ListViewWidget> {

    ···
    
    @override
    Widget build(BuildContext context) {
        return Container(
          color: Colors.white,
          child: ListView.builder(
            itemCount: itemBeans.length,
            itemBuilder: (context, index) {
              return ItemWidget(index, itemBeans[index], widget.listener);
            },
          ),
        );
    }
}
複製代碼

能夠看到改動項就是傳入了 listener 而且在 itemBuilder 返回的時候對應傳入參數給 ItemWidget。

而後咱們在 main.dart 修改以下:

···

class MyApp extends StatelessWidget {

    ···
    
    Widget _buildWidget() {
        return ListViewWidget((position, itemBean){
          print('pos=$position, title='+itemBean.title+",description="+itemBean.description);
        });
    }
}
複製代碼

點擊列表,控制檯輸出指望效果以下:

flutter: pos=0, title=第一句,description=關注微信公衆號「AndroidTraveler」
flutter: pos=1, title=第二句,description=星河滾燙,你是人間理想
複製代碼

代碼位置:github.com/nesger/Flut…

添加點擊視覺反饋

點擊是實現了,可是點擊以後沒有一點點反饋,用戶怎麼知道本身是否是點擊了呢?

所以點擊後的視覺反饋也是必不可少的。

那麼這個點擊後的反饋怎麼處理呢?

其實仍是離不開 GestureDetector 的回調監聽。

當按下時,咱們更新顏色值,當擡起或取消時咱們恢復顏色值。

所以咱們能夠修改 ItemWidget 以下:

···

class _ItemWidgetState extends State<ItemWidget> {

  Color _color;

  @override
  void initState() {
    super.initState();
    _color = Colors.white;
  }

  @override
  Widget build(BuildContext context) {
    Widget container = Container(
      color: _color,
      padding: EdgeInsets.only(left: 16.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(widget?.itemBean?.title ?? ''),
          SizedBox(
            height: 6,
          ),
          Text(widget?.itemBean?.description ?? ''),
          Divider(color: Colors.grey),
        ],
      ),
    );
    return GestureDetector(
      child: container,
      onTap: () => widget.listener(widget.position, widget.itemBean),
      onTapDown: (_) => _updatePressedColor(),
      onTapUp: (_) => _updateNormalColor(),
      onTapCancel: () => _updateNormalColor(),
    );
  }

  void _updateNormalColor() {
    setState(() {
      _color = Colors.white;
    });
  }

  void _updatePressedColor() {
    setState(() {
      _color = Color(0xFFF0F1F2);
    });
  }
}
複製代碼

效果以下:

能夠看到分隔線有點問題,主要緣由是 Divider 默認高度是 16.0,因此咱們調整下,同時改下 item 的上下間隔。

修改以下:

···

class _ItemWidgetState extends State<ItemWidget> {

  ···

  @override
  Widget build(BuildContext context) {
    Widget container = Container(
      color: _color,
      padding: EdgeInsets.only(left: 16.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          SizedBox(
            height: 8,
          ),
          Text(widget?.itemBean?.title ?? ''),
          SizedBox(
            height: 6,
          ),
          Text(widget?.itemBean?.description ?? ''),
          SizedBox(
            height: 8,
          ),
          Divider(color: Colors.grey, height: 0.5,),
        ],
      ),
    );
    return GestureDetector(
      child: container,
      onTap: () => widget.listener(widget.position, widget.itemBean),
      onTapDown: (_) => _updatePressedColor(),
      onTapUp: (_) => _updateNormalColor(),
      onTapCancel: () => _updateNormalColor(),
    );
  }

  void _updateNormalColor() {
    setState(() {
      _color = Colors.white;
    });
  }

  void _updatePressedColor() {
    setState(() {
      _color = Colors.grey;
    });
  }
}
複製代碼

效果以下:

可是若是你不是長按,而是快速點擊,會發現沒有效果。

因此咱們須要給擡起恢復來個延時,修改以下:

···

void _updateNormalColor() {
    Future.delayed(Duration(milliseconds: 100), () {
      setState(() {
        _color = Colors.white;
      });
    });
}

···
複製代碼

效果以下:

代碼位置:github.com/nesger/Flut…

多種佈局處理

這個其實也不難。

咱們知道 ListView 的核心代碼是:

ListView.builder(
    itemCount: itemBeans.length,
    itemBuilder: (context, index) {
        return ItemWidget(itemBeans[index]);
    },
)
複製代碼

所以只須要在 itemBuilder 這裏作文章。

舉個例子,假設我要求要顯示一個純色塊在頂部。

那麼咱們能夠以下修改

···

class _ListViewWidgetState extends State<ListViewWidget> {

  ···

  /// 實際場景多是從網絡拉取,這裏演示就直接填充數據源了
  void _initData() {
    itemBeans.add(ItemBean('', ''));

    itemBeans.add(ItemBean('第一句', '關注微信公衆號「AndroidTraveler」'));
    itemBeans.add(ItemBean('第二句', '星河滾燙,你是人間理想'));
    itemBeans.add(ItemBean('第三句', '我明白你會來,因此我等。'));
    itemBeans.add(ItemBean('第四句', '家人閒坐,燈火可親。'));
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: ListView.builder(
        itemCount: itemBeans.length,
        itemBuilder: (context, index) {
          if (index == 0) {
            return Container(
              color: Colors.blue,
              height: 66,
            );
          } else {
            return ItemWidget(index, itemBeans[index], widget.listener);
          }
        },
      ),
    );
  }
}
複製代碼

這裏經過在一開始添加一個空 Bean,而後在 itemBuilder 作判斷返回對應佈局來實現。

固然你也能夠不在集合添加,可是 index 須要更改,而且列表長度也要修改,等價代碼以下:

···

class _ListViewWidgetState extends State<ListViewWidget> {

  ···

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: ListView.builder(
        itemCount: itemBeans.length + 1,
        itemBuilder: (context, index) {
          if (index == 0) {
            return Container(
              color: Colors.blue,
              height: 66,
            );
          } else {
            return ItemWidget(index, itemBeans[index - 1], widget.listener);
          }
        },
      ),
    );
  }
}

複製代碼

能夠看到 itemCount 和 itemBuilder 都變化了。

效果圖以下:

從這個小演示,咱們也能夠看到關鍵在於 itemCountitemBuilder 的處理。

只要處理得當,能夠實現各類各樣的佈局。

通常的方式都是經過在 Bean 添加一個 viewType 來區分加載不一樣的佈局。

也能夠考慮繼承和多態等方式,這裏就不展開講了。

相信小夥伴們都可以自行處理的。

代碼位置:github.com/nesger/Flut…

咱們一開始的效果圖就是這個代碼,不過度隔線和視覺反饋的顏色值不同而已。

說明

因爲只是演示,所以有一些地方並無作額外處理,實際使用須要注意。

  1. 代碼結構,注意按業務或者功能等劃分。
  2. 有些公用的地方能夠進行封裝,減小後續寫多個 ListView 頁面時重複代碼。
  3. 代碼裏面的數據源是直接填充的,實際狀況多是從網絡獲取。所以須要增長 Bean 相關的 json 解析邏輯。

相關文章
相關標籤/搜索