flutter_web 实战之文章列表与详情

1,692 阅读4分钟

在上一篇 【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 个页面, 效果分别如下:

列表和详情,页面还是比较简单的。

整体布局就是头部、内容、尾部。比如尾部在首页和详情页的底部都是一样的,把它领出来作为一个公共组件。个人的开发习惯就是,相同的东西往上冒泡,让文件目录层次上浮。

首页布局

因为页面不存在悬浮情况,所以首页布局还是比较简单的:

// 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

语法高亮现在只支持 dart 语言的。

效果

更多阅读,请关注 SwiftOldBird 官方微信公众号

微信公众号

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