Flutter開始干係列-完整列表與Provider3實戰

直接開始幹,沒有爲何~android


在上一篇一個完整的登陸實踐,使用 connectivityshared_preferencesDio 實現一個完整的登陸操做,包含網絡鏈接檢測、請求、數據存儲等~git

列表在實際開發中必不可少,所以今天在以前的基礎上,實現一個"完整"列表界。項目主要功能包含 基礎Widget、Dio簡單封裝、Provider3實踐、列表下拉刷新與自動加載等github

固然,接口依舊不是我實現的,是由玩Android友情提供。依舊先看效果:json

效果圖

請求接口圖

Gif看不出請求了幾回數據,上面是請求接口圖,能夠看到請求了1~5頁。1頁請求了2次,由於最後又進行了一次刷新。後端

Dio 簡單封裝

由於登陸和列表都須要網絡請求,仍是像登陸那樣作的話,就有點繁瑣了。這裏使用單例對 Dio 進行封裝。bash

  1. 單例

網絡請求通常都會封裝爲單例,減小資源消耗。服務器

class Net {
  Dio _dio;

  factory Net() => instance;

  static final Net instance = Net._();

  // 私有構造器
  Net._() {
    ...
  }
  
  ...

  Dio get dio {
    return _dio;
  }
}
複製代碼
  1. Dio 統一配置

在私有構造器中配置基本參數和攔截器等,由於登陸後接口須要 Cookie ,因此攔截器裏面也作了關於 Cookie 的處理。markdown

Net._() {
    _dio = Dio();

    // 基本配置
    _dio.options.baseUrl = 'https://www.wanandroid.com/';
    _dio.options.connectTimeout = 5000;
    _dio.options.receiveTimeout = 5000;

    // 攔截器
    _dio.interceptors
      ..add(
        InterceptorsWrapper(
          onRequest: (RequestOptions options) async {
            var prefs = await SharedPreferences.getInstance();
            var userJson = prefs.getString('user');
            if (userJson != null && userJson.isNotEmpty) {
              UserData user = UserData.fromJson(jsonDecode(userJson));
              options.headers
                ..addAll({
                  'userId': user.id ?? '',
                  'token': user.token ?? '',
                });
            }
            // 添加cookie
            var cookie = prefs.getString("login_cookies");
            if (cookie != null) {
              options.headers.addAll({"Cookie": cookie.toString()});
            }
            return options;
          },
          onResponse: (Response res) async {
            // 保存cookie
            var cookies = res.headers['Set-Cookie'];
            var prefs = await SharedPreferences.getInstance();
            if (cookies != null && cookies.isNotEmpty) {
              prefs.setString("login_cookies", cookies.toString());
            }
          },
        ),
      )
      ..add(LogInterceptor(requestBody: true, responseBody: true));

//    // 設置代理
//    var clientAdapter = (dio.httpClientAdapter as DefaultHttpClientAdapter);
//
//    clientAdapter.onHttpClientCreate = (HttpClient client) {
//      client.findProxy = (uri) {
//        //proxy all request to localhost:8888
//        return 'PROXY 192.168.10.82:8888';
//      };
//      client.badCertificateCallback =
//          (X509Certificate cert, String host, int port) => true;
//    };
  }
複製代碼
  1. post 請求

封裝請求,Flutter 就不必用回調形式了,由於我們有 Future,好處是能夠連續處理,避免嵌套。而後在真正發起請求前仍是須要判斷一下網絡鏈接是否正常,請求完成後作一個簡單預判(實際能夠將後端返回 json 作下處理,通常返回格式都是{ "data": *** , "code": 0, "message": "" })。get請求大體相同,看源碼就好。cookie

Future post(String path, {data}) async {
    // 檢測網絡鏈接
    var connectivityResult = await (Connectivity().checkConnectivity());
    if (connectivityResult == ConnectivityResult.none) {
      throw Exception('網絡錯誤~');
    }
    // 發起請求
    Response response = await dio.post(path, data: data);
    if (response.statusCode == 200) {
      return response.data;
    } else {
      throw Exception('服務器錯誤~');
    }
  }
複製代碼

最後看一下調用網絡

_doLogin() {
    LoadingDialog.show(context);
    Net.instance
        .post('user/login',
            data: FormData.fromMap({
              "username": _accountController.text.trim(),
              "password": _pwdController.text.trim(),
            }))
        .then((data) async {
      UserEntity user = UserEntity.fromJson(data);
      if (user.errorCode == 0) {
        //登陸成功後 保存信息
        LoadingDialog.hide(context);
        ...
      } else {
        LoadingDialog.hide(context);
        ...
      }
    }).catchError((e) {
      print(e.toString());
    });
  }
複製代碼

實現列表

代碼地址

class ProjectListPage extends StatefulWidget {
  @override
  _ProjectListPageState createState() => _ProjectListPageState();
}

class _ProjectListPageState extends State<ProjectListPage> {
  List<ProjectListDataData> list = List();

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

  /// 構建界面
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: ...,
      body: ListView.builder(
        itemBuilder: (context, index) {
          var item = list[index];
          return _buildItem(item);
        },
        itemCount: list.length,
      ),
    );
  }

  /// 構建 item
  Widget _buildItem(ProjectListDataData item) {
    return Card(
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Row(
          children: <Widget>[
            Image.network(
              ...
            ),
            SizedBox(
            ...
            ),
            Expanded(
                child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(...),
                Text(...),
                Text(...),
              ],
            ))
          ],
        ),
      ),
    );
  }

  /// 請求列表數據
  loadData() async {
    Net.instance
        .get('project/list/1/json', queryParameters: {'cid': 294}).then((res) {
      ProjectListEntity entity = ProjectListEntity.fromJson(res);
      if (entity.errorCode == 0) {
        setState(() {
          //登陸成功後 保存信息
          list = entity.data.datas;
        });
      } else {
        throw Exception('響應錯誤');
      }
    }).catchError((e) {
      print(e.toString());
    }).whenComplete(() {});
  }
}
複製代碼

在 initState 中調用了 loadData 獲取數據,當數據獲取完畢後,調用 setState 更新界面數據,這樣一個簡單的列表就實現了,效果以下。

列表效果圖

目前爲止咱們用到了新的 Widget 有:ListView、Card、Row、Expanded,接下來簡單介紹下:

  1. ListView ListView 是一個線性排列的可滾動部件,構建 ListView 有如下幾種方式,
  • ListView() 經過構建 children 中全部 widget 實現列表,無論可見與否
  • ListView.builder 經過 itembuilder 構建可見 widget 實現列表
  • ListView.separated 實現分割線列表
  • ListView.custom 自定義 childrenDelegate 實現列表

比較經常使用的是前3種方式,少許列表用第一種,大量列表用第二種,須要分割線能夠用第三種,。

ListView.builder({
    Key key,
    
    Axis scrollDirection = Axis.vertical, // 滾動方向
    bool reverse = false, // 是否反向
    ScrollController controller, // 滾動控制和監聽
    bool primary,
    ScrollPhysics physics, // 滾動響應操做; ClampingScrollPhysics【Android 越界水波紋】、 BouncingScrollPhysics【iOS 越界回彈】
    bool shrinkWrap = false, // 滾動視圖長度是否只包含內容
    EdgeInsetsGeometry padding,
    this.itemExtent, // item長度,豎向是高度 橫向是寬度 ,若是非空則強制 child 使用,不作測量
    @required IndexedWidgetBuilder itemBuilder, // item 構建
    int itemCount, // item 數
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  })
複製代碼
  1. Card Card 卡片效果,能夠設置設置圓角和陰影
Card({
    Key key,
    this.color, // 背景色
    this.elevation, // z 座標,簡單理解控制陰影顯示效果
    this.shape, // 卡片形狀
    this.borderOnForeground = true,
    this.margin, // 外邊距
    this.clipBehavior,
    this.child,
    this.semanticContainer = true,
  })
複製代碼
  1. Row Row 和 Column 相似,不一樣的是 Row 是橫向排列,Column 是豎向排列。
  2. Expanded

Expanded 應用在 Flex 佈局中,如Row 和 Column 。使包裹的子 Widget 儘量多的佔用空間。

下拉刷新和加載更多

代碼地址

上面已經完成基礎了列表,接下來就是添加下拉刷新和加載更多。爲何叫加載更多,而不是上拉加載,由於是根據距離自動觸發。下面是加入下拉刷新和加載更多後的代碼(包含分頁):

...

 /// 構建界面
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        centerTitle: true,
        title: Text('項目'),
        backgroundColor: Colors.white,
      ),
      body: NotificationListener(
        child: RefreshIndicator(
            child: ListView.builder(
              itemBuilder: (context, index) {
                var item = list[index];
                return _buildItem(item);
              },
              itemCount: list.length,
            ),
            onRefresh: () {
              // 下拉刷新
              _pageIndex = 1;
              return loadData(_pageIndex);
            }),
        onNotification: (ScrollNotification notify) {
          /// 判斷滑動距離【小於等於400 】和 滾動方向
          if (notify.metrics.pixels >= (notify.metrics.maxScrollExtent - 400) &&
              notify.metrics.axis == Axis.vertical) {
            // 加載更多
            _pageIndex += 1;
            loadData(_pageIndex);
          }
          return true;
        },
      ),
    );
  }
  
  ...
  
  /// 請求列表數據
  loadData(int pageIndex) async {
    await Net.instance.get('project/list/$pageIndex/json',
        queryParameters: {'cid': 294}).then((res) {
      ProjectListEntity entity = ProjectListEntity.fromJson(res);
      if (entity.errorCode == 0) {
        setState(() {
         if(pageIndex == 1){
           list = entity.data.datas;
         }else{
           list.addAll(entity.data.datas);
         }
        });
      } else {
        throw Exception('響應錯誤');
      }
    }).catchError((e) {
      print(e.toString());
    }).whenComplete(() {});
  }
複製代碼

這一步完成後,就是頁面文章的效果了。這一步用到了 RefreshIndicator 和 NotificationListener 兩個 Widget,老規矩來個簡單說明。

  1. RefreshIndicator
RefreshIndicator({
    Key key,
    @required this.child,
    this.displacement = 40.0, // 刷新距離
    @required this.onRefresh,  // 刷新回調 須要有 async 和 await 關鍵字,缺乏 await,刷新圖標立馬消失,缺乏 async,刷新圖標不會消失
    this.color, // 指示器前景色
    this.backgroundColor,  //指示器背景色
    this.notificationPredicate = defaultScrollNotificationPredicate,
    this.semanticsLabel,
    this.semanticsValue,
  })
複製代碼
  1. NotificationListener NotificationListener:通知(通知冒泡)監聽,這裏經過它監聽 ScrollNotification(滾動通知)。
NotificationListener({
    Key key,
    @required this.child, // 監聽的子 Widget
    this.onNotification, // 通知回調
  })
複製代碼

封裝 ProviderWidget

經過上面的代碼能夠看到,請求完成後咱們經過 setState 重繪界面。在實際使用中咱們可能只須要繪製界面的某一起。爲了方便使用 Provider 控制刷新範圍,所以使用 ChangeNotifierProvider 和 Consumer 封裝了一個 ProviderWidget 。不熟悉 Provider 能夠先看下Flutter開始干係列-狀態管理Provider3

  1. 封裝
class ProviderWidget<T extends ChangeNotifier> extends StatefulWidget {
  final T model;

  final Widget child;

  final ValueWidgetBuilder<T> builder;
  final Function(T) onReady;

  const ProviderWidget({Key key, this.model, this.builder, this.onReady, this.child})
      : super(key: key);

  @override
  _ProviderWidgetState createState() => _ProviderWidgetState<T>();
}

class _ProviderWidgetState<T extends ChangeNotifier> extends State<ProviderWidget<T>> {
  T model;

  @override
  void initState() {
    model = widget.model;
    //第一幀回調,避免build中異常,如顯示彈窗,固然根據我的實際狀況來
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (widget.onReady != null) {
        widget.onReady(model);
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      builder: (_) => model,
      child: Consumer(
        builder: widget.builder,
        child: widget.child,
      ),
    );
  }
}

複製代碼
  1. 使用
...

  @override
  Widget build(BuildContext context) {
    print('--------build page----------');
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        centerTitle: true,
        title: Text('項目'),
        backgroundColor: Colors.white,
      ),
      body: ProviderWidget<ProjectViewModel>(
        model: ProjectViewModel(),
        onReady: (model) => model.refresh(),
        builder: (context, model, _) {
          print('--------build list----------');
          return NotificationListener(
            child: RefreshIndicator(
              child: (model.list?.length ?? 0) == 0
                  ? Container()
                  : ListView.builder(
                      itemBuilder: (context, index) {
                        ProjectListDataData item = model.list[index];
                        return _buildItem(item);
                      },
                      itemCount: model.list?.length ?? 0,
                    ),
              onRefresh: () => model.refresh(),
            ),
            onNotification: (ScrollNotification notify) {
              /// 判斷滑動距離和滾動方向
              if (notify.metrics.pixels >=
                      (notify.metrics.maxScrollExtent - 400) &&
                  notify.metrics.axis == Axis.vertical) {
                model.loadMore();
              }
              return true;
            },
          );
        },
      ),
    );
  }
   
  ...

  
複製代碼

示例代碼中我使用了日誌輸出以下,能夠發現除了初次構建,都不會再輸出 build page。

2019-10-22 15:10:35.239 11686-11742/com.joker.flutter_widgets I/flutter: --------build page----------
2019-10-22 15:10:35.243 11686-11742/com.joker.flutter_widgets I/flutter: --------build list----------
2019-10-22 15:10:42.126 11686-11742/com.joker.flutter_widgets I/flutter: --------build list----------
複製代碼

可能經過日誌有的朋友仍是不能理解,這裏咱們在列表頂部再加一個妹子,列表更新的時候妹子是不須要更新的,這時就可使用 Provider ,將刷新粒度控制在列表上。這部分代碼作了在項目中作了屏蔽,能夠取消註釋看看。

使用效果圖

最後

說明幾點:

  1. 文章不是一天寫完的,因此看效果圖的數據是不同的。
  2. 只作了簡單的封裝,而且都是寫在同一個文件,方便查找。
  3. json 轉 dart 插件使用的是 FlutterJsonBeanFactory,本來上一章登陸就該說明的。

最後附上 Github地址github.com/joker-fu/fl…

相關文章
相關標籤/搜索