Flutter实战 从头撸一个「孤岛」APP(No.3、书单、搜索框、Dio初探)

5,379 阅读9分钟

前言

阅读建议

  • 适宜人群:从事前端开发以及移动端开发的大佬们&着手去了解Flutter 新技术的上进人士
  • 本篇词数: 2328词(也不知道怎么计算出来的)
  • 时长:内容基础、并不费脑,所以一会儿就看完
  • 场景:拥挤的地铁、沙发等

读读评论

Flutter实战 从头撸一个「孤岛」APP(No.2、闪屏Splash Page、引导页) 这篇发表之后呢,并没有收到很多鼓励,只有大佬,牛皮啊这一条评论,是来自掘友平贺才人,那可能大家并不太清楚,这一系列的Flutter,是要做成什么样子 应用到什么Flutter技术,其中我们会使用到

  • widget 小部件使用、有状态``无状态
  • 常用的第三方插件
  • Model Json 两者藕断丝连的关系转化
  • Provider 全局状态管理
  • Fluro 路由管理库
  • Dio 网络请求
  • ……

总结反省

那本篇我就先画一下这个APP,会长什么样,UI并不是我原创,其中会应用到一套后台的API

  • 完整的开发文档 后台API 这是[林间有风团队]小程序相关的接口,暂时用这个先

  • APP完整的样子

  • 回顾项目目录

以后咱们尽量以

  • UI视图部分
  • 后台交互处理
  • 小BUG修复

这样的三部分来构建正篇文章的结构,那我们这段旅程就来一起开发【书单】这部分,do it,

1. UI界面

1.1 顶部搜索栏、搜索框、搜索条

  • 功能:实现搜索功能,能够对书籍进行搜索
  • 包含:历史搜索、热门搜索、搜索的取消等

我们还是秉承面向部件、面向组件开发的中心思想,二话不说 ,我还是打算在widgwts文件夹下新建搜索这个部件,并命名widget_search_bar.dart使用第三方的插件

1.2 search_widget

  • 当前版本 ^1.0.0

  • 使用:软件包提供了一个搜索小部件,用于从数据列表中选择一个选项。提供基于搜索文本的项目过滤。

     // import 'package:loader_search_bar/loader_search_bar.dart';
     
    

在书写本篇的时候,由于大意把引包这部分写错了,写成了另一个用于搜索的包,感谢掘友 辉太郎2019友情提出,深表感谢,以下是正确的引包路径

----------------------更正-------------


import 'package:search_widget/search_widget.dart';

在这里贴上包文件地址search_widget

main函数中初始化一下兼容配置

// 兼容异常处理
void enablePlatformOverrideForDesktop() {
  if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
    debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
  }
}

那其实是main中主要是进行一些初始化操作

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

初始化list 数据

 List<LeaderBoard> list = <LeaderBoard>[
    LeaderBoard("Flutter", 54),
    LeaderBoard("React", 22.5),
    LeaderBoard("Vue", 24.7),
    LeaderBoard("小程序", 22.1),
  ];

这些数据是显示在列表这儿的

核心代码便是

     SearchWidget<LeaderBoard>(
            dataList: list, // 数据源
            hideSearchBoxWhenItemSelected: false,
            listContainerHeight: MediaQuery.of(context).size.height / 4,
            // 基于搜索查询过滤数据列表的选项
            queryBuilder: (query, list) {
              return list
                  .where((item) =>
                      item.username.toLowerCase().contains(query.toLowerCase()))
                  .toList();
            },
            // 弹出列表项构建器的选项。这基本上返回一个小部件以在弹出窗口中显示为列表项
            popupListItemBuilder: (item) {
              return PopupListItemWidget(item);
            },
            // 为选定的列表项构建器提供的选项,当用户从弹出列表中选择一个项时启用
            selectedItemBuilder: (selectedItem, deleteSelectedItem) {
              return SelectedItemWidget(selectedItem, deleteSelectedItem);
            },
            // 定制小部件
            noItemsFoundWidget: NoItemsFound(),
            // 提供自定义TextField的选项。接受TextEditingController和FocusNode作为参数
            textFieldBuilder: (controller, focusNode) {
              return MyTextField(controller, focusNode);
            },
            // 用于获取所选择的选项。返回所选项目;如果删除了该项,则返回null
            onItemSelected: (item) {
              setState(() {
                _selectedItem = item;
              });
            },
          ),

1.3 MediaQuery

listContainerHeight: MediaQuery.of(context).size.height / 4,

在这部分,有用到MediaQuery,它便是适配屏幕的一种方式,在之前第一段旅程的时候,也有提到屏幕适配

在搜索的时候,我并不想出它出现搜索的显示

 selectedItemBuilder: (selectedItem, deleteSelectedItem) {
              return SelectedItemWidget(selectedItem, deleteSelectedItem);
            },

看一下源码,它还是要求咱们必须传入的,那咱们就把这部分返回一个空的Container

class SelectedItemWidget extends StatelessWidget {
  const SelectedItemWidget(this.selectedItem, this.deleteSelectedItem);

  final LeaderBoard selectedItem;
  final VoidCallback deleteSelectedItem;

  @override
  Widget build(BuildContext context) {
    return Container();
	// 这Container()便是返回的选中后的值
    // Container(
    //   padding: const EdgeInsets.symmetric(
    //     vertical: 2,
    //     horizontal: 4,
    //   ),
    //   child: Row(
    //     children: <Widget>[
    //       Expanded(
    //         child: Padding(
    //           padding: const EdgeInsets.only(
    //             left: 16,
    //             right: 16,
    //             top: 8,
    //             bottom: 8,
    //           ),
    //           child: Text(
    //             selectedItem.username,
    //             style: const TextStyle(fontSize: 14),
    //           ),
    //         ),
    //       ),
    //       IconButton(
    //         icon: Icon(Icons.delete_outline, size: 22),
    //         color: Colors.grey[700],
    //         onPressed: deleteSelectedItem,
    //       ),
    //     ],
    //   ),
    // );
  }
}

1.4 数据模型

在初始化数据的时候LeaderBoard,我们填充了LeaderBoard("Flutter", 54), 这每一个数据其实是一个数据模型,这里咱们先不说JSON 与Model之间的转换

2. 后台通信

2.1 Dio 初探

在之后的旅程与后台通信的过程中,我们就选取dio这个库来辅助请求,这就类似于React、Vue 项目中的axios,

dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...意思是总得有个与后台通信的工具吧,不是吗?当前的版本是3.0.7

  • 添加依赖
dependencies:
  dio: ^3.0.7


  • 在需要的文件引入
import 'package:dio/dio.dart';

让我们先来发送个请求试一下,那么后台的接口选哪个呢,我记得之前有写个在线模拟的接口,是用的淘宝开源的

RAP2

  • 接口地址 在线接口模拟

  • 响应数据

    {
      "code": 200,
      "data": [
        {
          "name": "洋小洋同学",
          "age": 18,
          "flag": "up"
        }
      ]
    }
    
    
  • 请求方式 GET 也就是说可以直接在浏览器地址栏 输入便会返回结果

那么我们怎么利用前文所提到的dio 去发送请求呢?既然是一个网络请求,那还是老样子,少不了是要封装的,那本章节的咱们先试试水,初探一下。

这个请求的方法暂且写在utils这个公共方法的文件夹里,并命名my_http.dart,写上一段简单的代码

import 'package:dio/dio.dart';

void getHttp() async {
  try {
    Response response = await Dio().get(
        "http://rap2api.taobao.org/app/mock/236998/isolated/island/api/v1/test");
    print('后台返回的结果是 ${response}');
  } catch (e) {
    print(e);
  }
}



由于我们接下来将要请求书单的数据,那我们打算在book_list_page.dart初始化调用,在有状态的组件是有这个初始化的方法的,在其中可以做一些事情,有点类似前端的生命周期钩子,显然在调试的控制台返回的正是我们预期的结果

2.2 后台API 分析

先来看一下真实后台返回的接口数据,其返回一个列表,包含所有热门书籍的概要信息

  • 获取热门书籍
  • 请求方式 GET
  • URL :/book/hot_list
  • Response 200
  • 返回报文
[{
	"author": "[\u7f8e]\u4fdd\u7f57\u00b7\u683c\u96f7\u5384\u59c6",
	"fav_nums": 259,
	"id": 7,
	"image": "https://img3.doubanio.com/lpic/s4669554.jpg",
	"like_status": 0,
	"title": "\u9ed1\u5ba2\u4e0e\u753b\u5bb6"
}, {
	"author": "MarkPilgrim",
	"fav_nums": 145,
	"id": 65,
	"image": "https://img3.doubanio.com/lpic/s4059293.jpg",
	"like_status": 0,
	"title": "Dive Into Python 3"
}, {
	"author": "MagnusLieHetland",
	"fav_nums": 88,
	"id": 183,
	"image": "https://img3.doubanio.com/lpic/s4387251.jpg",
	"like_status": 0,
	"title": "Python\u57fa\u7840\u6559\u7a0b"
}, {
	"author": "[\u54e5\u4f26\u6bd4\u4e9a]\u52a0\u897f\u4e9a\u00b7\u9a6c\u5c14\u514b\u65af",
	"fav_nums": 99,
	"id": 1002,
	"image": "https://img3.doubanio.com/lpic/s6384944.jpg",
	"like_status": 0,
	"title": "\u767e\u5e74\u5b64\u72ec"
}, {
	"author": "[\u65e5]\u5ca9\u4e95\u4fca\u4e8c",
	"fav_nums": 78,
	"id": 1049,
	"image": "https://img1.doubanio.com/view/subject/l/public/s29775868.jpg",
	"like_status": 0,
	"title": "\u60c5\u4e66"
}, {
	"author": "[\u7f8e]\u4e54\u6cbb\u00b7R\u00b7R\u00b7\u9a6c\u4e01",
	"fav_nums": 52,
	"id": 1061,
	"image": "https://img3.doubanio.com/lpic/s1358984.jpg",
	"like_status": 0,
	"title": "\u51b0\u4e0e\u706b\u4e4b\u6b4c\uff08\u5377\u4e00\uff09"
}, {
	"author": "[\u65e5]\u4e1c\u91ce\u572d\u543e",
	"fav_nums": 81,
	"id": 1120,
	"image": "https://img3.doubanio.com/lpic/s4610502.jpg",
	"like_status": 0,
	"title": "\u767d\u591c\u884c"
}, {
	"author": "\u91d1\u5eb8",
	"fav_nums": 50,
	"id": 1166,
	"image": "https://img1.doubanio.com/lpic/s23632058.jpg",
	"like_status": 0,
	"title": "\u5929\u9f99\u516b\u90e8"
}, {
	"author": "[\u65e5]\u4e1c\u91ce\u572d\u543e",
	"fav_nums": 13,
	"id": 1308,
	"image": "https://img3.doubanio.com/lpic/s3814606.jpg",
	"like_status": 0,
	"title": "\u6076\u610f"
}, {
	"author": "[\u82f1]J\u00b7K\u00b7\u7f57\u7433",
	"fav_nums": 33,
	"id": 1339,
	"image": "https://img3.doubanio.com/lpic/s1074376.jpg",
	"like_status": 0,
	"title": "\u54c8\u5229\u00b7\u6ce2\u7279\u4e0e\u963f\u5179\u5361\u73ed\u7684\u56da\u5f92"
}, {
	"author": "\u97e9\u5bd2",
	"fav_nums": 21,
	"id": 1383,
	"image": "https://img1.doubanio.com/lpic/s3557848.jpg",
	"like_status": 0,
	"title": "\u4ed6\u7684\u56fd"
}, {
	"author": "[\u82f1]J\u00b7K\u00b7\u7f57\u7433",
	"fav_nums": 32,
	"id": 1398,
	"image": "https://img1.doubanio.com/lpic/s2752367.jpg",
	"like_status": 0,
	"title": "\u54c8\u5229\u00b7\u6ce2\u7279\u4e0e\u6b7b\u4ea1\u5723\u5668"
}, {
	"author": "\u738b\u5c0f\u6ce2",
	"fav_nums": 17,
	"id": 1560,
	"image": "https://img1.doubanio.com/lpic/s3463069.jpg",
	"like_status": 0,
	"title": "\u4e09\u5341\u800c\u7acb"
}, {
	"author": "[\u4f0a\u6717]\u739b\u8d5e\u00b7\u838e\u5854\u78a7",
	"fav_nums": 16,
	"id": 7821,
	"image": "https://img3.doubanio.com/lpic/s6144591.jpg",
	"like_status": 0,
	"title": "\u6211\u5728\u4f0a\u6717\u957f\u5927"
}, {
	"author": "[\u65e5]\u6751\u4e0a\u6625\u6811",
	"fav_nums": 10,
	"id": 8854,
	"image": "https://img1.doubanio.com/lpic/s29494718.jpg",
	"like_status": 0,
	"title": "\u8fdc\u65b9\u7684\u9f13\u58f0"
}, {
	"author": "\u4e09\u6bdb",
	"fav_nums": 18,
	"id": 8866,
	"image": "https://img3.doubanio.com/lpic/s2393243.jpg",
	"like_status": 0,
	"title": "\u68a6\u91cc\u82b1\u843d\u77e5\u591a\u5c11"
}, {
	"author": "\u97e9\u5bd2",
	"fav_nums": 21,
	"id": 15198,
	"image": "https://img1.doubanio.com/lpic/s1080179.jpg",
	"like_status": 0,
	"title": "\u50cf\u5c11\u5e74\u5566\u98de\u9a70"
}, {
	"author": "\u9c81\u8fc5",
	"fav_nums": 30,
	"id": 15984,
	"image": "https://img3.doubanio.com/lpic/s27970504.jpg",
	"like_status": 0,
	"title": "\u671d\u82b1\u5915\u62fe"
}, {
	"author": "[\u65e5]\u4e95\u4e0a\u96c4\u5f66",
	"fav_nums": 33,
	"id": 21050,
	"image": "https://img3.doubanio.com/lpic/s2853431.jpg",
	"like_status": 0,
	"title": "\u704c\u7bee\u9ad8\u624b31"
}, {
	"author": "[\u65e5]\u65b0\u4e95\u4e00\u4e8c\u4e09",
	"fav_nums": 24,
	"id": 51664,
	"image": "https://img3.doubanio.com/lpic/s29034294.jpg",
	"like_status": 0,
	"title": "\u4e1c\u4eac\u65f6\u5473\u8bb0"
}]

  • response_description

    参数字段 含义
    author 作者
    fav_nums 点赞数
    id 书籍ID
    image 书籍图片
    like_status 是否点赞
    title 书籍题目

    未完待续。。。

本篇小结

那其实本周做的东西是有点少的,只是和大家熟悉了一下界面的样子,而主要开发的书单这一块呢,现在接口返回的数据已经知晓了,也明确了应用dio这个库,不过瘾的话那就

谢谢你看到这里,如果感到有点用的话,希望点点关注、给个赞、写点个人看法在评论..自己也会穿插着写一些深刻的关于某个技能点的文章,先从Flutter 中的状态管理这个主题开始吧,那加油,我是洋小洋同学,所有的代码都会同步到github

特别感谢

掘友

  • 辉太郎2019 在评论区指出文章引包的错误,大赞··

--END--THANKS--