flutter_web 實戰之文章列表與詳情

SwiftOldBird發表於2019-08-22

在上一篇 【flutter_web 初體驗】中,簡單的介紹了下 flutter_web 建立一個專案和一些踩坑的解決方案,本篇將進一步講解搭建 flutter_web 專案的基本過程。

本文將講解以下要點:

  1. 專案結構介紹
  2. 佈局
  3. 響應式佈局
  4. 頁面路由
  5. 網路請求
  6. markdown 渲染

專案目錄結構

/
├── README.md
├── analysis_options.yaml
├── build # webdev build 編譯生成的目錄,用於部署
├── lib  # 工作區
│   ├── components  # 元件(Widgets)
│   ├── kit # 工具、父類
│   ├── main.dart # 入口
│   ├── models # 資料模型
│   ├── network # 網路
│   ├── pages # 頁面
│   │   ├── detail # 詳情頁
│   │   │   ├── bloc
│   │   │   ├── detail.dart
│   │   │   ├── model
│   │   │   └── page
│   │   ├── index # 首頁
│   │   ├── pages.dart  
│   │   └── user # 使用者
│   └── router # 頁面路由
├── pubspec.lock
├── pubspec.yaml  # 依賴
└── web
    ├── assets # 資源區
    │   ├── FontManifest.json  # 字型
    │   └── images # 圖片
    │       └── swift_logo.png
    ├── index.html
    └── main.dart
複製程式碼

主要劃分了 6 大部分:

  • network: 網路
  • models: 模型
  • router: 路由
  • pages: 頁面
  • components:元件
  • kit: 工具、常量、基類等

佈局

接下來,將要實現 2 個頁面, 效果分別如下:

flutter_web 實戰之文章列表與詳情

flutter_web 實戰之文章列表與詳情

列表和詳情,頁面還是比較簡單的。

flutter_web 實戰之文章列表與詳情

整體佈局就是頭部、內容、尾部。比如尾部在首頁和詳情頁的底部都是一樣的,把它領出來作為一個公共元件。個人的開發習慣就是,相同的東西往上冒泡,讓檔案目錄層次上浮。

首頁佈局

因為頁面不存在懸浮情況,所以首頁佈局還是比較簡單的:

// pages/index/index_pages.dart

// 新增頭部
List<Widget> lists = [_buildHeader(context)];

// 新增列表內容
lists.addAll(rows.map((item) {
  return _buildCell(item);
}).toList());

// 新增尾部
lists.add(Container(
      margin: EdgeInsets.only(top: 100),
      child: FooterView(),
));

/// 整體內容用 SingleChildScrollView 進行包裝
SingleChildScrollView(
        child: Container(
      color: Colors.white,
      child: Column(
        children: lists,
      ),
    ))
複製程式碼

詳情頁佈局

詳情頁頭部是懸浮的,且文章採用 markdown,也是一個 ListView。 然後底部是通用的底部欄。

那麼懸浮的話就採用了 AppBar :

Scaffold(
    backgroundColor: Colors.white,
    appBar: PreferredSize(
        child: HeaderView(), preferredSize: Size.fromHeight(50)),
    body: body)
複製程式碼

上面的 body 是通過 SingleChildScrollView 包裝:

return SingleChildScrollView(
  child: Column(
    children: <Widget>[mdView, FooterView()],
  ),
);
複製程式碼

響應式佈局

在有使用過站點的初版的時候,當你改變瀏覽器的大小的時候,佈局會比較醜陋,且會傳送一些佈局警告。造成的原因是沒有適配各種螢幕的大小。如果做前端的,會有一些比較通用的解決辦法:

  • 媒體查詢
  • 百分比
  • rem
  • vw/vh

如果對此感興趣可深入閱讀 《響應式佈局的常用解決方案對比(媒體查詢、百分比、rem和vw/vh)》

那麼如果 flutter_web 要做響應是佈局,該怎麼辦?

  • 尺寸大小和位置不使用硬編碼
  • 使用 MediaQuery 獲取當前的視窗的大小
  • 使用 FlexibleExpanded 去佈局介面,使用百分比而不是硬編碼。
  • 使用 LayoutBuilder 獲取父 widget 的 ConstraintBox
  • 使用 MediaQueryOrientationBuilder 獲取裝置的方向。
  • AspectRatioFractionallySizedBox 是常用的百分比相關的 Widget。

響應式佈局有兩篇文章推薦閱讀:

頁面路由

// main.dart
main() {
  Static.storage = Storage();
  runApp(SwiftClub());
}

class SwiftClub extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'swiftclub',
      theme: ThemeData(fontFamily: "Montserrat"),
      onGenerateRoute: (setting) => buildRouters(setting),
      initialRoute: "/",
    );
  }
}
複製程式碼
// router/router.dart

Route<dynamic> buildRouters(RouteSettings settings) {
  dynamic args = settings.arguments;
  switch (settings.name) {
    case "/login":
      return SimpleRoute(
          name: "/login", title: "login", builder: (context) => LoginPage());

    case "/detail":
      if (args == null) {
        // 直接重新整理當前,引數可能不存在,返回首頁
        return defaultRoute();
      }
      // 解析頁面傳遞的引數
      final topicId = SafeValue.toInt(args['topicId']);
      return SimpleRoute(
          name: "detail",
          title: "detaila",
          builder: (context) => DetailPage(
                topicId: topicId,
              ));

    case "/":
      return defaultRoute();

    default:
      return defaultRoute();
  }
}

SimpleRoute defaultRoute() {
  return SimpleRoute(
      name: '/', title: 'swiftclub', builder: (context) => IndexPage());
}
複製程式碼

flutter_web 的路由,跟 flutter 的路由管理是一樣的,主要是注意兩點:

  • 如果重新整理當前頁面,之前其他頁面傳遞過來的引數就沒有了,重新整理後,頁面獲取不到傳遞過來的引數,進行網路請求,報錯。所以這裡做了判斷,如果引數不存在了,則返回到預設的首頁。

  • 路由引數取值,嘗試多次,發現

    ModalRoute.of(context).settings.arguments;
    複製程式碼

獲取不到 arguments,但在判斷路由的時候可以獲取。

網路請求

由於 dart:io 在 flutter_web 中還不支援,所以 dio 是不能使用的,官方建議使用 package:http

Flutter for web: Frequently Asked Questions

為此做了個 http 請求的封裝,可參考使用:

import 'dart:convert';
import 'package:flutter_web/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:swiftclub/kit/macro/macro.dart';

class Network {
  static getReq(String url, {Map params, Map headers}) async {
    var fullUrl = Macro.URL_base + url;
    return await _getReq(fullUrl, params: params, headers: headers);
  }

  static _getReq(String url, {Map params, Map headers}) async {
    var reqUri = _uriWith(url, queryParameters: params);
    http.Response response = await http.get(reqUri, headers: headers);
    var responseBody = json.decode(response.body);
    return responseBody;
  }

  static _postReq(String url, {Map headers, Map params}) async {
    var fullUrl = Macro.URL_base + url;
    if (headers != null && headers.isNotEmpty) {
      http.Response response =
          await http.post(Uri.parse(fullUrl), headers: headers, body: params);
      var responseBody = json.decode(response.body);
      return responseBody;
    } else {
      http.Response response =
          await http.post(Uri.parse(fullUrl), body: params);
      var responseBody = json.decode(response.body);
      return responseBody;
    }
  }

  static Uri _uriWith(String url, {Map queryParameters}) {
    String _url = url;
    String query = _urlEncodeMap(queryParameters);
    if (query.isNotEmpty) {
      _url += (_url.contains("?") ? "&" : "?") + query;
    }
    // Normalize the url.
    return Uri.parse(_url).normalizePath();
  }

  static String _urlEncodeMap(data) {
    StringBuffer urlData = StringBuffer("");
    bool first = true;
    void urlEncode(dynamic sub, String path) {
      if (sub is List) {
        for (int i = 0; i < sub.length; i++) {
          urlEncode(sub[i],
              "$path%5B${(sub[i] is Map || sub[i] is List) ? i : ''}%5D");
        }
      } else if (sub is Map) {
        sub.forEach((k, v) {
          if (path == "") {
            urlEncode(v, "${Uri.encodeQueryComponent(k)}");
          } else {
            urlEncode(v, "$path%5B${Uri.encodeQueryComponent(k)}%5D");
          }
        });
      } else {
        if (!first) {
          urlData.write("&");
        }
        first = false;
        urlData.write("$path=${Uri.encodeQueryComponent(sub.toString())}");
      }
    }

    urlEncode(data, "");
    return urlData.toString();
  }
}
複製程式碼

markdown 渲染

在 github 上巡遊一番,應該找不到針對 flutter_web 的 markdown 的支援庫。筆者在參考

封裝了在 flutter_web 上可用的 markdown 元件,具體實現可參考 swiftclub/site

flutter_web 實戰之文章列表與詳情

語法高亮現在只支援 dart 語言的。

效果

flutter_web 實戰之文章列表與詳情

更多閱讀,請關注 SwiftOldBird 官方微信公眾號

微信公眾號

原文:swiftoldbird.loveli.site/2019/08/22/…

相關文章