1 课程简介
1.1 为什么学习 flutter 项目
- 市场需要 flutter—— android 和 ios 使用一套设计图,却需要两批人来开发。目前 flutter 开始支持 web 和 桌面开发。
- Flutter 使用更好的语言——Dart语言。 Javascript 方便调试,有时性能不能满足需求,语言特性更新依赖浏览器厂商的支持;java 和 oc 性能好,但是调试不如 Javascript 方便。Dart 语言同时支持 JIT(just in time)和 AOT(ahead of time)两种模式。开发时使用Dart的JIT运行模式,可以做到,修改的效果及时展现到页面上;产品发布后,使用Dart的AOT运行模式,保证APP的性能最优。从而极大的提高 APP 的开发效率。
- Flutter 开箱即用—— flutter 提供丰富的组件,能够快速的开发出 原生 app。
- 以项目的方式学习—— 学到更实用的知识。
1.2 课程内容
项目知识点:
-
使用第三方组件
-
通用组件封装
-
使用静态资源
- 本地图片
- 网络图片
- 使用自带 icon
- 使用字体 icon
- 网络图片缓存超时处理
-
本地存储及 store 封装
-
数据管理 scoped_model
-
网络请求 及 dio_http 封装
-
序列号及反序列化半自动生成实体类
-
图片上传
-
app icon 及 启动页
-
面向群体:
-
有一定编程基础—— 会一门其他编程语言
-
有一定 flutter 基础—— 学完 Flutter 框架入门,或者通过官网可以安装 flutter 并运行demo。
-
希望通过项目来强化 flutter 相关技术
目标:
能够独立完成常规的 flutter 项目
1.3 项目简介
项目路径
-
基础回顾
-
项目框架
-
静态页面
-
前后端联调
-
构建打包
包含的页面
- 登陆页
- 注册页
- 首页
- 首页tab
- 搜索tab
- 咨询tab
- 我的tab
- 搜索页
- 房屋管理
-
- 添加房屋
- 房屋详情
- 设置
2 基础回顾
-
无状态组件 vs 有状态组件
//无状态组件 class MyText extends StatelessWidget { // 组件的参数 final String text; // 组件的构造函数 MyText(this.text); //组件的实现部分 Widget build(context) { return new Text( text, textStyle: new TextStyle(fontSize: 40.0), ); } } /////////////////////////////////////////////////////////////////////// //有状态组件 class Counter extends StatefulWidget { // 组件的参数 final String title; Counter({Key key, this.title}) : super(key: key); // 没有 build 方法,但有 createState() 方法。 @override _MyHomePageState createState() => new _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { //状态 int counter = 0; void increaseCount() { setState(() { this.counter++; } } //build 方法 Widget build(context) { return new RaisedButton( onPressed: increaseCount, child: new Text('点击+1'), ); } } -
Material 组件 和 Cupertino 组件
flutter 是开箱即用的
Material 组件 就是 android 风格的组件
Cupertino 组件 就是 ios 风格的组件
-
常用组件
Text - 用于显示文字的组件 Image -用于显示图片的组件 Icon - 用于显示图标,有内置的 Material 和 Cupertino 风格的图标 Container - 类似 html 中的 div。可以很方便的添加 内外边距,对齐,背景,边角的特性。 Row, Column - 用于水平和垂直方向的多组件展示。使用 flex 布局。 Stack - 用于z轴方向的多组件展示。可以把一个组件堆叠到另外一个组件上面。类似 css 中的 Position。 Scaffold - 页面的基本组件, 提供了基本的页面结构。包括 顶部 title 及功能按钮,顶部tab,底部tab,导航按钮等。
3 项目框架
3.1 初始化项目
效果:

步骤:
-
打开vscode
-
初始化项目【菜单】— 【查看】—【命令面板】— 【Flutter:New Project】
-
demo 文件夹分析

-
打开模拟器【菜单】— 【查看】—【命令面板】— 【Flutter:Launch Emulator】
-
在 ios 模拟器上运行 demo【菜单】— 【调试】—【启动调试】
-
在 android 模拟器上运行 demo
-
demo 代码分析
- 引入 flutter 依赖
- 程序入口
- 无状态组件
- 有状态组件
注意:
- 如果没有命令Flutter:New Project,则说明 flutter 安装有问题
- 如果没有命令 Flutter:Launch Emulator,则说明模拟器安装有问题
-
3.2 编写一个简单页面-准备
知识点:
MaterialApp: 封装了应用程序实现 Material Design 所需要的一些 widget。
Scaffold:Material Design 布局结构的基本实现。
Appbar:一个Material Design应用程序栏,由工具栏和其他可能的widget组成。
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
),
)
功能拆分:

3.3 编写一个简单页面-实现
效果:
步骤:
- 添加 PageContent 组件
- 新建文件 /widgets/page_content.dart
- 添加 material 依赖
- 编写无状态组件
- 添加 name 参数
- 使用 Scaffold
- 添加 home 页面
- 新建文件 /pages/home/index.dart
- 添加 material, page_content依赖
- 编写无状态组件
- 使用 PageContent
- 添加 Application 应用根组件
- 新建文件 /application.dart
- 添加依赖
- 使用 MaterialApp
- 测试
3.4 安装 fluro 并添加登陆页面
效果:
步骤:
-
了解 fluro
- 简单
- 支持参数通配符 /room/:id
- 简化自定义动画
-
添加依赖
dependencies: fluro: "^1.5.1" -
添加 /pages/login.dart
-
参考 /pages/home/index.dart 完善登陆页。
3.5 如何配置fluro
问题:
如何配置fluro?
分析:
- 看官方文档
- 声明 路由
- 配置 路由
- 关联 路由 和 materialApp
- 使用 路由
- 分析示例代码
- 编写路由配置文件
- 在 Application 中配置路由—声明/配置/关联 路由
- 测试路由
结论:
- 编写路由配置文件
- 创建 routes.dart 文件 并编写Routes类的基本结构
- 定义路由名称
- 定义路由处理函数
- 编写函数 configureRoutes 关联路由名称和处理函数
- 在 Application 中配置路由
- 定义 router
- 通过调用configureRoutes 配置 router
- 在 MaterialApp 中使用 router
- 测试路由
- 在 PageContent 中添加跳转按钮
3.6 配置fluro
效果:
步骤:
- 编写路由配置文件
- 创建 routes.dart 文件 并编写Routes类的基本结构
- 定义路由名称
- 定义路由处理函数
- 编写函数 configureRoutes 关联路由名称和处理函数
- 在 Application 中配置路由
- 定义 router
- 通过调用configureRoutes 配置 router
- 在 MaterialApp 中使用 router
- 测试路由
- 在 PageContent 中添加跳转按钮
参考官网代码:
//定义路由处理函数
var usersHandler = Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return UsersScreen(params["id"][0]);
});
//关联路由和处理函数
void defineRoutes(Router router) {
router.define("/users/:id", handler: usersHandler);
}
3.7 优化路由配置
问题:
错误页面如何处理?带参数的页面如何处理?
效果:

步骤:
- 错误页面处理
- 在 /pages 目录添加 not_found.dart 文件
- 实现 NotFoundPage
- 在 /routes.dart 添加 _notFoundHandler
- 在 /routes.dart 的 configureRoutes 中添加 router.notFoundHandler=_notFoundHandler;
- 修改 PageContent 测试
- 带参数页面处理
- 在 /pages 目录添加 room_detail/index.dart 文件
- 实现 RoomDetailPage
- 在 /routes.dart 添加 _notFoundHandler
- 在 /routes.dart 的 configureRoutes 中添加 RoomDetailPage;
- 修改 PageContent 测试
补充:
实现页面步骤
- 添加依赖
- 编写组件模板 有状态组件/无状态组件
- 如果有参数添加组件参数
- 完善 build 方法
4 静态页面
4.1 登陆页-页面分析
效果:

页面拆分:

- scafford
- appBar
- title— Text
- body
- 用户名— TextField
- 密码— TextField
- 登陆按钮— RaisedButton
- 注册链接— Row[Text,FlatButton]
- appBar
4.2 登陆页-主体结构
效果:

步骤:
- 添加 Scaffold
- 完成 appBar 部分
- 完成 body 部分
- 用户名
- 密码
- 登陆按钮
- 注册链接
- 主体颜色 — theme
- 测试
4.3 登陆页-密码显示隐藏
效果:
步骤:
- 将无状态组件改成有状态组件— 右键 重构
- 添加可点击的图标— IconButton
- 添加状态— showPassword
- 根据状态展示不同内容
- 给图标添加点击事件
- 测试
4.4 登陆页-细节优化
效果:
问题及解决方案:
-
【去注册】颜色问题
添加style
-
上下间距问题?
添加 Padding
-
边距/异形屏幕问题?
使用 SafeArea
-
垂直高度不足问题?
使用 ListView 替代 Column
-
登陆按钮宽度和颜色问题?
宽度:SizedBox 或者父级固定宽度
颜色:手动设置
-
使用 ListView 替代 Column
4.5 注册页-添加
效果:
步骤:
- 添加文件 /pages/register.dart
- 将login.dart 文件拷贝到 register.dart
- 修改类名称
- 修改 title
- 在路由中添加 register
- 添加 route name
- 添加 route handler
- 在 configureRoutes 中关联 name 和router
- 修改了组件类型,需要重启app后测试
4.6 注册页-完善
效果:
步骤:
- 删除密码显示逻辑
- 添加确认密码
- 修改按钮及下方链接到文案
- 优化登陆注册跳转,使用 Navigator.pushReplacementNamed
4.7 首页-tab-分析
设计图分析:
结论:
-
首页共用 tab 按钮区域
-
tab 内容区的 appBar 不一样
-
4 个 tab 内容区不一样
-
可以使用 flutter 自带组件 BottomNavigationBar 实现
-
需要准备 4 个 tab 内容区(tabView)
-
需要准备 4 个 BottomNavigationBarItem
-
4.8 首页-tab-编码
效果:
步骤:
- 将 HomePage 改成有状态组件
- 使用准备好的数据
- 使用官网 demo 代码
- 删除 appBar
- 修改 Scaffold.body
- 修改 Scaffold.bottomNavigationBar
参考:官网 tab 代码
import '../../includes.dart';
class SliceBottomNavigationBar extends StatefulWidget implements SliceExample {
@override
String get name => 'SliceBottomNavigationBar';
static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
static const List<Widget> _widgetOptions = <Widget>[
Text(
'Index 0: Home',
style: optionStyle,
),
Text(
'Index 1: Business',
style: optionStyle,
),
Text(
'Index 2: School',
style: optionStyle,
),
];
@override
_SliceBottomNavigationBarState createState() => _SliceBottomNavigationBarState();
}
class _SliceBottomNavigationBarState extends State<SliceBottomNavigationBar> {
int _selectedIndex = 0;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Sample'),
),
body: Center(
child: SliceBottomNavigationBar._widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Business'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('School'),
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
4.9 首页-tabIndex-分析
设计图分析:
结论:
首页第一个tab(tabIndex)共5个区域,对应5个不同的模块。
- 顶部区域—— searchBar
- 轮播图区域—— IndexSwipper
- 导航区域—— IndexNavigator
- 房屋推荐区域—— IndexRecommend
- 资讯区域—— Info
4.10 首页-tabIndex-页面结构
效果:
步骤:
- 新建文件 /pages/home/tab_index/index.dart
- 添加依赖,编写无状态组件
- 简化实现顶部区域--appBar
- body 部分包含多个组件且可以滚动—使用 ListView
- 在 HomePage 中使用 TabIndex
4.11 首页-tabIndex-轮播图-准备
问题:
是否有第三方组件满足轮播图的需求?
分析:
用法:
//1. 安装依赖
// flutter_swiper : ^1.1.6
//2. 引入依赖
import 'package:flutter_swiper/flutter_swiper.dart';
//3. 配置Swiper
Swiper(
itemBuilder: (BuildContext context,int index){
return new Image.network("http://via.placeholder.com/350x150",fit: BoxFit.fill,);
},
itemCount: 3,
pagination: new SwiperPagination(),
control: new SwiperControl(),
)
图片资源:
'ww3.sinaimg.cn/large/006y8…',
'ww3.sinaimg.cn/large/006y8…',
'ww3.sinaimg.cn/large/006y8…',
图片宽750px,高424px;
注意:flutter 如何使用网络图片
4.12 首页-tabIndex-轮播图-实现
效果:
步骤:
- 准备组件框架代码
- 新建文件 /widgets/common_swipper.dart
- 添加依赖 material 和 flutter_swiper
- 准备图片数据
- 编写无状态组件
- 添加 images 参数 并在构造函数中赋值
- 编写 swiper 核心代码
- 参照官网使用 swipper
- 修改 itemBuilder 和 itemCount
- Swiper 父组件指定高度
- 删除 Swiper.control
- 测试
- 在 tabIndex 中使用 CommonSwiper
4.13 首页-tabIndex-导航-准备
结构分析:
- indexNavigator--Row
- item--Column
- Image—Image.asset
- 整组— Text
- item--Column
- Image—Image.asset
- 合租— Text
- item--Column
- Image
- 地图找房— Text
- item--Column
- Image
- 去出租— Text
- item--Column
资源准备:
-
本地图片准备
-
将图片拷贝到 /static/images/ 目录
-
在 pubspec.yaml 中引入图片
assets: # 首页——第一个tab-导航图标 - static/images/home_index_navigator_total.png - static/images/home_index_navigator_map.png - static/images/home_index_navigator_share.png - static/images/home_index_navigator_rent.png
-
-
数据准备
// /pages/home/tab_index/index_navigator_item.dart import 'package:flutter/material.dart'; class IndexNavigatorItem { final String title; final String imageUri; final Function(BuildContext contenxt) onTap; IndexNavigatorItem(this.title, this.imageUri, this.onTap); } List<IndexNavigatorItem> indexNavigatorItemList = [ IndexNavigatorItem('整组', 'static/images/home_index_navigator_total.png', (BuildContext context) { Navigator.of(context).pushReplacementNamed('login'); }), IndexNavigatorItem('合租', 'static/images/home_index_navigator_share.png', (BuildContext context) { Navigator.of(context).pushReplacementNamed('login'); }), IndexNavigatorItem('地图找房', 'static/images/home_index_navigator_map.png', (BuildContext context) { Navigator.of(context).pushReplacementNamed('login'); }), IndexNavigatorItem('去出租', 'static/images/home_index_navigator_rent.png', (BuildContext context) { Navigator.pushNamed(context, 'login'); }), ];
4.14 首页-tabIndex-导航-实现
效果:
步骤:
- 添加文件 /pages/home/tab_index/index_navigator.dart
- 添加依赖 material 和 index_navigator_item
- 编写无状态组件
- 完成页面结构
- 测试
- 调整细节
4.15 组件 CommonImage 封装-分析
问题:
轮播图的网络图片偶尔出现超时,怎么解决?
如果涉及图片方面的优化,我们是不是需要修改很多处?
结论:
自己封装一个图片组件!
细化方案:
-
根据资源地址是可以区分本地资源和网络的资源的(网络图片地址以 http 开头,本地图片地址以 static 开头),所以可以共用一个图片组件
-
网络图片添加本地缓存,延长网络请求超时时间!可以使用第三方组件 AdvancedNetworkImage
// 1.安装依赖 // flutter_advanced_networkimage: ^0.5.0 // 2. 导入依赖 import 'package:flutter_advanced_networkimage/provider.dart'; // 2.用法 Image( image: AdvancedNetworkImage( url, header: header, useDiskCache: true, cacheRule: CacheRule(maxAge: const Duration(days: 7)), ), fit: BoxFit.cover, ) -
图片组件前期只用支持4个参数。
- src — 图片地址 String 必须按参数,可以是网络图片地址或者本地图片地址
- width — 图片宽度 double 可选参数
- height — 图片高度 double 可选参数
- fit— 图片填充方式 BoxFit 可选参数
4.16 组件 CommonImage 封装-实现
效果:
不影响之前图片展示。
步骤:
- 准备
- 安装 flutter_advanced_networkimage: ^0.5.0 依赖
- 添加文件 /widgets/common_image.dart
- 引入依赖
- 编写 正则 根据图片地址判断是网络图片还是本地图片
- 编写框架代码
- 编写无状态组件
- 完善组件参数 src width height fit
- 完成核心逻辑
- 如果是网络图片,使用 flutter_advanced_networkimage
- 如果是本地图片,使用 Image.asset
- 返回 Container
- 使用 CommonImage
4.17 首页-tabIndex-推荐-准备
分析:
组件结构:
- recommond—container 【背景色,左右边距】,column
- header—row,
- 房屋推荐
- 更多
- body— wrap
- Container width— (屏幕宽度-10*3)/2
- item— Row(spacebeteen)
- content— column
- 文案--Text
- 文案--Text
- 图片— CommonImage
- content— column
- item— Row(spacebeteen)
- Container width— (屏幕宽度-10*3)/2
- header—row,
数据准备:
-
准备静态图片
- static/images/home_index_recommend_1.png
- static/images/home_index_recommend_2.png
- static/images/home_index_recommend_3.png
- static/images/home_index_recommend_4.png
-
准备数据代码
class IndexRecommendItem {
String title;
String subTitle;
String imageUri;
String navigateUri;
IndexRecommendItem(this.title,this.subTitle, this.imageUri, this.navigateUri);
}
List<IndexRecommendItem> indexRecommendData = [
IndexRecommendItem(
'家住回龙观','归属的感觉', 'static/images/home_index_recommend_1.png', 'login'),
IndexRecommendItem(
'宜居四五环', '大都市生活','static/images/home_index_recommend_2.png', 'login'),
IndexRecommendItem(
'喧嚣三里屯', '繁华的背后','static/images/home_index_recommend_3.png', 'login'),
IndexRecommendItem(
'比邻十号线','地铁心连心', 'static/images/home_index_recommend_4.png', 'login'),
];
4.18 首页-tabIndex-推荐-编码-主体结构部分
效果:
步骤:
- 准备
- 新建文件 pages/home/tab_index/index_recommond_data.dart
- 使用上一节准备好的数据
- 新建文件 pages/home/tab_index/index_recommond.dart
- 编写核心代码
- 添加依赖,无状态组件,dataList 参数,indexRecommendData 改成常量
- 添加背景色及边距
- 完善 header 部分
- 添加 wrap
- 测试
4.19 首页-tabIndex-推荐-编码-item 部分
效果:
步骤:
- 新建文件 pages/home/tab_index/index_recommond_item_widget.dart
- 添加依赖,无状态组件,data 参数,
- 编写主体结构
- 使用并测试
- 完善细节
4.20 首页-tabIndex-资讯-准备
分析:
组件结构:
-
info— Column
- header - container
- title— Text
- body
- InfoItemWidget
- InfoItemWidget
- InfoItemWidget
- ...
- header - container
-
InfoItemWidget —container
- 内容区-- Row
- 图片— CommonImage
- 文字区—Expand, Column
- titile— container
- title 内容— Text
- 信息区— row
- 来源— Text
- 时间— Text
- titile— container
- 内容区-- Row
注意:
- 文字区域可能随屏幕宽度而改变
- title 自动换行问题
数据准备:
- 准备数据代码
class InfoItem {
final String title;
final String imageUri;
final String source;
final String time;
final String navigateUri;
const InfoItem({this.title, this.imageUri,this.source,this.time, this.navigateUri});
}
const List<InfoItem> infoData = [
const InfoItem(
title:'置业选择 | 安贞西里 三室一厅 河间的古雅别院',
imageUri:'https://wx2.sinaimg.cn/mw1024/005SQLxwly1g6f89l4obbj305v04fjsw.jpg',
source:"新华网" ,
time:"两天前",
navigateUri:'login'
),
const InfoItem(
title:'置业佳选 | 大理王宫 苍山洱海间的古雅别院',
imageUri:'https://wx2.sinaimg.cn/mw1024/005SQLxwly1g6f89l6hnsj305v04fab7.jpg',
source:"新华网" ,
time:"一周前",
navigateUri:'login'
),
const InfoItem(
title:'置业选择 | 安居小屋 花园洋房 清新别野',
imageUri:'https://wx4.sinaimg.cn/mw1024/005SQLxwly1g6f89l5jlyj305v04f75q.jpg',
source:"新华网" ,
time:"一周前",
navigateUri:'login'
),
];
4.21 首页-tabIndex-资讯-编码-主体结构
效果:
步骤:
- 准备
- 添加文件 /pages/home/info/data.dart
- 添加上一节数据到 data.dart
- 添加文件 /pages/home/info/index.dart
- 在 index.dart 添加 material 依赖
- 添加无状态组件
- 编写核心代码
- 完善 title 部分—注意 title 部分根据参数显示或隐藏
- 完善 body 部分
- 测试
注意:
- Container alignment 的使用
- 可以通过以下方式修改 dart 版本

4.22 首页-tabIndex-资讯-编码-item部分
效果:
步骤:
- 创建文件 /pages/home/info/item_widget.dart
- 引入依赖,编写无状态组件,添加 data 参数
- 完成主体结构
- 测试代码
- 完善细节
4.23 首页-tabInfo
效果:
tabInfo 主体部分可以直接使用 tabIndex 的最新资讯
步骤:
- 新建文件 /pages/home/tab_info/index.dart
- 添加 material 依赖,编写有状态组件
- 完善 header 部分
- 完善body 部分
- 测试
4.24 首页-tabSearch-分析
效果:

结构拆分:
-
searchBar — 后续实现
-
filterBar — 后续实现
-
RoomListItemWidget— container,row
-
commonImage
-
Expanded,column
-
title- container,Text
-
subtitle- container,Text
-
tags - SizedBox,Wrap(CommonTag)
-
price- Text
-
-
数据准备:
- 数据文件 /pages/home/tab_search/dataList.dart
class RoomListItemData {
final String id;
final String title;
final String subTitle;
final String imageUri;
final List<String> tags;
final int price;
const RoomListItemData(
{this.title,
this.subTitle,
this.imageUri,
this.tags,
this.price,
this.id});
}
const List<RoomListItemData> dataList = [
RoomListItemData(
title: '朝阳门南大街 2室1厅 8300元',
subTitle: "二室/114/东|北/朝阳门南大街",
imageUri:
"https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu9t1kxj30lo0c7796.jpg",
price: 1200,
id: 'roomDetail/1',
tags: ["近地铁", "集中供暖", "新上", "随时看房"]),
RoomListItemData(
title: '整租 · CBD总部公寓二期 临近国贸 精装修 随时拎包入住',
subTitle: "一室/110/西/CBD总部公寓二期",
imageUri:
"https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu5s7gcj30lo0c7myq.jpg",
price: 6000,
id: 'roomDetail/1',
tags: ["近地铁", "随时看房"]),
RoomListItemData(
title: '朝阳门南大街 2室1厅 8300元',
subTitle: "二室/114/东|北/朝阳门南大街",
imageUri:
"https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu5s7gcj30lo0c7myq.jpg",
price: 1200,
id: 'roomDetail/1',
tags: ["近地铁", "集中供暖", "新上", "随时看房"]),
RoomListItemData(
title: '整租 · CBD总部公寓二期 临近国贸 精装修 随时拎包入住',
subTitle: "一室/110/西/CBD总部公寓二期",
imageUri:
"https://tva1.sinaimg.cn/large/006y8mN6ly1g6wtu9t1kxj30lo0c7796.jpg",
price: 6000,
id: 'roomDetail/1',
tags: ["近地铁", "随时看房"]),
];
4.25 首页-tabSearch-主体结构
效果:
步骤:
- 创建文件 /pages/home/tab_search/dataList.dart 使用上一节准备的数据
- 创建文件 /pages/home/tab_search/index.dart
- 引入依赖,创建有状态组件
- 编写主体结构
- 测试
4.26 首页-tabSearch-item 部分
效果:
步骤:
- 创建文件 /widgets/room_list_item_widget.dart
- 引入依赖,创建无状态组件,添加参数 data
- 完成主体结构
- 添加测试
- 完善细节
要点:
文本 … 的问题
4.27 首页-tabSearch-tag 部分
效果:
步骤:
- 新建文件 widgtes/common_tag.dart
- 引入 material 依赖,添加无状态组件,添加参数 title,color,backgroundColor
- 完成展示效果
- 测试
- 优化参数 使用 factory 工厂构造函数
要点:
圆角设置,factory 使用
4.28 组件 SearchBar 封装-分析
结构分析:
结论:

-
结构
- SearchBar-Container,Row
- 位置选择按钮
- 返回图标按钮
- 输入框—弹性宽度
- 取消文字按钮
- 地图标志按钮
- SearchBar-Container,Row
-
参数
final bool showLoaction;//展示位置按钮 final Function goBackCallback;//回退按钮函数 final String inputValue;// 搜索框输入值 final String defaultInputValue;// 搜索框默认值 final Function onCancel;//取消按钮 final bool showMap;//展示地图按钮 final Function onSearch; //用户点击搜索框触发 final ValueChanged<String> onSearchSubmit;// 用户输入搜索词后,点击键盘的搜索键触发 -
静态资源
- 资源地址 /resource/chapter3/01静态资源/static/icons/widget_search_bar_map.png
- 拷贝 目标地址 /resource/chapter3/02课堂代码/hook_up_rent/static/icons/widget_search_bar_map.png
- 在项目配置文件 pubspec.yaml 添加
-
- static/icons/widget_search_bar_map.png
-
4.29 组件 SearchBar 封装-主体结构开发
效果:

步骤:
- 创建文件 /widgets/search_bar/index.dart
- 引入 material 依赖, 创建有状态组件,添加参数
- 编写界面代码
- 测试
- 完善代码
要点:
在有状态组件中使用参数(titile)方式 widget.title
4.30 组件 SearchBar 封装-完善搜索框
效果:

步骤:
- 解决背景色,圆角— Container,TextField.InputDecoration.border=InputBorder.none
- 前置图标 InputDecoration.prefixIcon
- 后置图标 InputDecoration.suffixIcon
- 搜索提示样式 InputDecoration.hintStyle
- 剧中 InputDecoration.contentPadding
- 间距问题 使用 InputDecoration.Icon 替代 InputDecoration.prefixIcon
4.31 组件 SearchBar 封装-细节完善
效果:

步骤:
- 使用参数 defaultInputValue
- 使用参数 inputValue
- 清除按钮逻辑实现
- 新增状态 _searchWord
- 添加处理函数 _onClean
- 添加并实例化 _controller
- 根据 _searchWord 隐藏清除按钮
- 添加 TextField.onTap
- 添加 TextField.onSearchSubmit
- 添加 TextField.textInputAction: TextInputAction.search
- 删除
|| true
4.32 组件 SearchBar 封装-使用及优化
效果:
步骤:
- 在 tabIndex 使用
- 在 tabSearch 使用
- 在 tabInfo 使用
- 优化焦点问题
- 创建焦点对象 FocusNode _focus,并实例化
- 在 TextField 使用焦点对象
- 在 TextField onTap 回掉函数中 通过_focus.unfocus() 方法 是去焦点。
4.33 首页-tabProfile-分析
结构分析:
结论:
- tabProfile— scaffold
- appBar
- AppBar,Text
- actions
- IconButton
- body— ListView
- Header— 已登陆视图/未登陆视图
- FunctionButton— 按钮需要使用本地图片
- Advertisement
- Info— 已完成组件
- appBar
数据准备:
-
网络图片
- 未登陆图片 "tva1.sinaimg.cn/large/006y8…"
- 已登陆占位图片 "tva1.sinaimg.cn/large/006y8…"
- 广告图片 'tva1.sinaimg.cn/large/006y8…'
-
静态资源
- 资源地址 /resource/chapter3/01静态资源/static/images/home_profile_xxx.png
-
配置文件
- static/images/home_profile_record.png - static/images/home_profile_order.png - static/images/home_profile_favor.png - static/images/home_profile_id.png - static/images/home_profile_message.png - static/images/home_profile_contract.png - static/images/home_profile_house.png - static/images/home_profile_wallet.png -
列表数据
import 'package:flutter/material.dart'; class FunctionButtonItem { final String imageUri; final String title; final Function onTapHandle; FunctionButtonItem(this.imageUri, this.title, this.onTapHandle); } final List<FunctionButtonItem> list = [ FunctionButtonItem( 'static/images/home_profile_record.png', '看房记录', null), FunctionButtonItem( 'static/images/home_profile_order.png', '我的订单', null), FunctionButtonItem( 'static/images/home_profile_favor.png', '我的收藏', null), FunctionButtonItem( 'static/images/home_profile_id.png', '身份认证', null), FunctionButtonItem( 'static/images/home_profile_message.png', '联系我们', null), FunctionButtonItem( 'static/images/home_profile_contract.png', '电子合同', null), FunctionButtonItem('static/images/home_profile_house.png', '房屋管理', (context) { bool isLogin = true;//todo: if (isLogin) { Navigator.pushNamed(context, 'roomManage'); return; } Navigator.pushNamed(context, 'login'); }), FunctionButtonItem( 'static/images/home_profile_wallet.png', '钱包', null), ];
4.34 首页-tabProfile-主体结构
效果:
步骤:
- 准备
- 添加文件 pages/home/tab_profile/index.dart
- 添加 material 依赖,添加无状态组件
- 主体结构
- 使用 Scaffold 搭建主体结构
- 测试效果
- 完善
- 完善 appBar 部分
- 完善 body 部分
4.35 首页-tabProfile-登陆注册-未登陆视图
效果:
步骤:
- 准备
- 添加文件 pages/home/tab_profile/header.dart
- 添加 material 依赖,添加无状态组件
- 核心编码
- 添加背景色和高度
- 添加图片 "tva1.sinaimg.cn/large/006y8…
- 添加右侧文字
- 测试和完善
- 测试效果
- 完善细节
4.36 首页-tabProfile-登陆注册-已登陆视图
效果:
步骤:
- 把内容显示部分抽提成函数
- 根据登陆状态使用函数
- 修改已登陆状态函数 "tva1.sinaimg.cn/large/006y8…"
- 测试
4.37 首页-tabProfile-功能按钮-主体结构
效果:
步骤:
- 准备
- 准备静态图片
- 准备数据文件 pages/home/tab_profile/function_button_data.dart
- 核心编码
- 新建 pages/home/tab_profile/function_button.dart
- 使用 Wrap 完成主体结构部分
- 使用 Container 替代 item
- 测试
4.38 首页-tabProfile-功能按钮-item
效果:
步骤:
- 新建 pages/home/tab_profile/function_button_widget.dart
- 引入依赖,添加无状态组件,添加 data 参数
- 完成 Item 主体结构
- 测试
- 完善细节
4.39 首页-tabProfile-广告及资讯
效果:
步骤:
- 创建文件 pages/home/tab_profile/advertisement.dart
- 引入 material 依赖,添加无状态组件
- 完善主体结构 'tva1.sinaimg.cn/large/006y8…'
- 测试
- 完善细节
4.40 设置页
效果:
分析:
需要 toast 弹窗,使用 fluttertoast
用法:
// 1. 安装依赖
//fluttertoast: ^3.1.3
// 2. 引入依赖
import 'package:fluttertoast/fluttertoast.dart';
// 3. 使用
Fluttertoast.showToast(
msg: "This is Center Short Toast",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0
);
步骤:
-
准备
- 按照依赖 fluttertoast: ^3.1.3
- utils/common_toast.dart 新建文件,封装 CommonToast
- 新建文件 pages/setting.dart,引入依赖,添加无状态组件
-
核心编码
- 完成页面主体结构
- 在路由系统注册当前页面
- 添加
退出登陆按钮 - 实现退出登陆逻辑
-
测试
4.41 房屋管理页-主体结构
效果:

分析:
class MyDemo extends StatelessWidget {
final List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: myTabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: myTabs,
),
),
body: TabBarView(
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
),
);
}
}
步骤:
- 准备
- 添加文件 pages/room_manage/index.dart
- 添加 material 依赖,创建无状态组件
- 核心编码
- 使用 Scaffold 添加页面主体结构
- 在路由系统注册当前页面
- 添加 tab AppBar.bottom : TabBar()
- 添加 body: TabBarView()
- 在 Scaffold 通过 DefaultTabController 关联 TabBar 和 TabBarView
- 测试
4.42 房屋管理页-发布按钮
效果:
分析:
因为【房源发布】页面也有同样的样式,所以需要封装成组件。组件参数包含 title 和 onTap。
步骤:
- 添加 floatingActionButton 属性
- 完善发布按钮主体结构 GestureDetector>Container>Center>Text
- 修改 floatingActionButtonLocation
- 完善细节
- 封装组件
4.43 发布房源页-分析
结构分析:
- CommonTitle
- CommonFormItem
- CommonRadioFormItem
- CommonSelectFormItem
- CommonPicker
- CommonImagePicker
- CommonCheckButton
- RoomAppliance
4.44 发布房源页-主体结构
效果:

步骤:
- 创建文件 pages/room_add/index.dart
- 添加 material 依赖,添加有状态组件
- 使用 Scaffold 完成页面主体结构
- 添加 提交按钮
- 在路由中注册该页面
4.45 发布房源页-CommonTitle
效果:

步骤:
- 创建文件 widgets/common_title.dart
- 添加 material 依赖,添加无状态组件,添加 title 参数
- 完成组件主体内容
- 使用组件
- 完善组件
4.46 发布房源页-CommonFormItem-分析
结构分析:
结论:
- CommonFormItem
- label
- Content
- 文本框
- hintText
- onChanged
- controller
- 尾缀
- suffix
- suffixText
- 文本框
组件参数:
final String label;
final Widget Function(BuildContext context) contentBuilder;
final Widget suffix;
final String suffixText;
final String hintText;
final ValueChanged onChanged;
final TextEditingController controller;
使用:
CommonFormItem(
label: '租金',
hintText: '请输入租金',
suffixText: '元/月',
controller: TextEditingController(),
),
4.47 发布房源页-CommonFormItem-实现
效果:

实现:
- 准备 2. 创建文件 widgets/common_form_item.dart 3. 添加依赖,编写无状态组件,添加并初始化参数 4. 使用组件
- 核心代码
- 主体结构 实现
- 底线 实现
- label 实现
- 文本框实现
- 尾缀实现
- 测试
4.48 发布房源页-CommonFormItem-实现小区选择
效果:
步骤:
- 添加 CommonFormItem
- 完善 contentBuilder
- 完善 点击事件处理
- 测试
- 优化 CommonFormItem,调整Expanded 位置
4.49 发布房源页-CommonRadioFormItem
效果:
分析:
//参数
final String label;
final List<String> options;
final int value;
final ValueChanged<int> onChange;
//使用
CommonRadioFormItem(
label: '租赁方式',
options: ['合租', '整租'],
value: 0,
onChange: (index) {}),
CommonRadioFormItem(
label: '装修',
options: ['精装', '简装'],
value: 0,
onChange: (index) { }),
步骤:
-
准备
- 新建文件 widgets/common_radio_form_item.dart
- 添加 material 依赖,添加无状态组件,添加并初始化参数
-
核心代码
- 使用 CommonFormItem 完成基本结构 label 和 contentBuilder 属性
- 在 roomAdd 页面使用 CommonRadioFormItem
- 完善 contentBuilder
- 处理选项及点击事件 使用 Radio
- 完善细节
-
测试
- 让按钮可点击
4.50 发布房源页-CommonSelectFormItem
效果:

分析:
//参数
final String label;
final int value;
final List<String> options;
final Function(int) onChange;
//用法
CommonSelectFormItem(
label: '户型',
value: 0,
onChange: (val) {},
options: ['一室','二室','三室','四室',],
),
CommonSelectFormItem(
label: '楼层',
value: 0,
onChange: (val) {},
options: ['高楼层','中楼层','低楼层',],
),
CommonSelectFormItem(
label: '朝向',
value: 0,
onChange: (val) {},
options: ['东','南','西','北京',],
),
步骤:
- 准备
- 新建文件 widgets/common_select_form_item.dart
- 添加 material 依赖,添加无状态组件,添加并初始化参数
- 核心代码
- 使用 CommonSelectFormItem 完成基本结构
- 在 roomAdd 页面使用 CommonSelectFormItem
- 完善细节
- 测试
4.51 发布房源页-CommonPicker-分析
结构分析:

结论:
CommonPicker.showPicker 是一个 class 的静态方法
半屏弹窗— showCupertinoModalPopup
选择区域— CupertinoPicker
使用:
//参数
BuildContext context,
List<String> options,
int value,
//返回值
Future<int> result;
//使用
var result = CommonPicker.showPicker(
context: context, options: options, value: value);
result.then((selectedValue) {
if (value != selectedValue &&
selectedValue != null &&
onChange != null) {
onChange(selectedValue);
}
});
4.52 发布房源页-CommonPicker-主体结构
效果:
步骤:
- 准备
- 新建文件 utils/common_picker/index.dart
- 引入 material 和 cupertino 依赖
- 完善类及静态方法的主体结构
- 在 CommonSelectFormItem 中使用 CommonPicker
- 核心编码
- 使用 showCupertinoModalPopup 返回 Future
- 完善内容区 header 部分
- 使用 CupertinoPicker 完善内容区 body 部分
- 测试及完善
- 使用 CommonPicker
- 完善细节
4.53 发布房源页-CommonPicker-细节和事件
目的:
细节:半屏高度
事件: roomAdd — CommonSelectFormItem — CommonPicker 关联
步骤:
- 细节
- 半屏高度
- 事件
- 在 roomAdd 页面添加状态
- 添加事件处理函数
- 完善 CommonSelectFormItem 的事件处理部分
- 完善 CommonPicker 的事件处理部分
- 测试
4.54 发布房源页-房屋图像-主体结构
效果:
分析:
-
common_image_picker 使用
//1. 参数 final ValueChanged<List<File>> onChange; //2.用法 CommonImagePicker( onChange: (List<File> files) {}, ) -
测试图片数据
const List<String> defautImages = [ 'http://ww3.sinaimg.cn/large/006y8mN6ly1g6e2tdgve1j30ku0bsn75.jpg', 'http://ww3.sinaimg.cn/large/006y8mN6ly1g6e2whp87sj30ku0bstec.jpg', 'http://ww3.sinaimg.cn/large/006y8mN6ly1g6e2tl1v3bj30ku0bs77z.jpg', ]; // 图片宽750px,高424px; var imageWidth=750.0; var imageHeight=424.0; var imageWidthHeightRatio = imageWidth / imageHeight;
步骤:
- 准备
- 添加文件 widgets/common_image_picker.dart
- 添加 material 依赖,
- 准备测试图片数据
- 创建有状态组件
- 在 roomAdd 页面使用 CommonImagePicker
- 核心编码
- 准备图片宽高
- 准备添加图片按钮
- 准备图片 wrapper 函数
- 展示图片及添加按钮
- 测试
4.55 发布房源页-房屋图像-添加删除
效果:

分析:
-
添加— 使用第三方组件 image_picker 读取本地图片
//1.安装依赖 image_picker: ^0.6.1+4 //2. iOS 配置 <project root>/ios/Runner/Info.plist: <key>NSCameraUsageDescription</key> <string>Use to get room image </string> <key>NSMicrophoneUsageDescription</key> <string>Use to capture audio for room</string> <key>NSPhotoLibraryUsageDescription</key> <string>Use to get room image</string> //3. 引入依赖 import 'package:image_picker/image_picker.dart'; //4. 使用 _pickImage() async { var image = await ImagePicker.pickImage(source: ImageSource.gallery); } -
删除 — 图标叠加 使用 Stack
步骤:
- 准备
- 安装 image_picker 依赖
- 引入 image_picker 依赖
- 核心代码
- 实现添加图片逻辑
- 添加状态 files
- 图片数据使用 files
- 添加方法 pickImage
- addButton 添加事件
- 测试
- 实现图片删除逻辑
- 添加删除图标
- 实现删除事件
- 测试
- 实现添加图片逻辑
4.56 发布房源页-房屋标题描述
效果:
步骤:
- 添加房屋标题
- 提示文案:请输入标题(例如:整组,小区名 2室 2000元)
- 添加房屋描述
- 提示文案: 请输入房屋描述信息
- 添加 controller
- 测试及细节完善
4.57 发布房源页-房屋配置-分析
设计图分析:
结论:
-
结构 Wrap
-
图标,使用字体文件
-
静态文件 /static/fonts/iconfont.ttf
-
配置文件
fonts: - family: CommonIcon fonts: - asset: static/fonts/iconfont.ttf -
字体使用
//字体的使用 Icon( IconData( item.iconPoint, fontFamily: Config.CommonIcon, ), ) //config.dart class Config { static const CommonIcon = 'CommonIcon'; }
-
-
数据
class RoomApplianceItem { final String title; final int iconPoint; final bool isChecked; const RoomApplianceItem(this.title, this.iconPoint, this.isChecked); } const List<RoomApplianceItem> _dataList = [ RoomApplianceItem('衣柜', 0xe918, false), RoomApplianceItem('洗衣机', 0xe917, false), RoomApplianceItem('空调', 0xe90d, false), RoomApplianceItem('天然气', 0xe90f, false), RoomApplianceItem('冰箱', 0xe907, false), RoomApplianceItem('暖气', 0xe910, false), RoomApplianceItem('电视', 0xe908, false), RoomApplianceItem('热水器', 0xe912, false), RoomApplianceItem('宽带', 0xe90e, false), RoomApplianceItem('沙发', 0xe913, false), ]; //组件参数 final ValueChanged<List<RoomApplianceItem>> onChange; -
CommonCheckButton 的实现
import 'package:flutter/material.dart'; class CommonCheckButton extends StatelessWidget { final bool isChecked; const CommonCheckButton( this.isChecked, { Key key, }) : super(key: key); @override Widget build(BuildContext context) { return isChecked ? Icon( Icons.check_circle, color: Colors.green, ) : Icon( Icons.radio_button_unchecked, color: Colors.green, ); } }
操作:
- 准备图标
- 创建文件 widgets/room_appliance.dart
- 准备数据
- 准备config 文件
- CommonCheckButton 实现
4.58 发布房源页-房屋配置-实现
效果:
步骤:
- 准备
- 添加 material 依赖
- 添加有状态组件
- 添加并初始化参数
- 核心编码
- 完成主体结构
- 使用 RoomApplicance
- 添加点击事件
- 测试&完善
4.59 房屋详情页-分析
设计图分析:
结论:
-
页面结构
- 主体结构— Scaffold
- 顶部部分 — appBar
- 内容部分— Stack
- ListView
- 房屋图片
- Swiper
- 房屋基本信息
- 标题
- 价格
- 标签
- 房屋详细信息
- 面积
- 楼层
- 房型
- 装修
- 房屋配置
- 房屋概况
- 猜你喜欢
- 房屋图片
- 浮动操作区
- 收藏
- 联系房东按钮
- 预约看房按钮
- ListView
- 主体结构— Scaffold
-
房屋数据结构
class RoomDetailData { String id; String title; String community; String subTitle; int size; String floor; int price; String roomType; List<String> houseImgs; List<String> tags; List<String> oriented; List<String> applicances; RoomDetailData({ this.id, this.title, this.community, this.subTitle, this.size, this.roomType, this.houseImgs, this.tags, this.price, this.floor, this.oriented, this.applicances, }); } var defaultData=RoomDetailData(id:'1111',title: '整租 中山路 历史最低价',community: '中山花园',subTitle: '近地铁,附近有商场!',size: 100,floor: '高楼层',price: 3000,oriented: ['南'],roomType: '三室',applicances:['衣柜','洗衣机'],tags:["近地铁", "集中供暖", "新上", "随时看房"],houseImgs: [ 'http://ww3.sinaimg.cn/large/006y8mN6ly1g6e2tdgve1j30ku0bsn75.jpg', 'http://ww3.sinaimg.cn/large/006y8mN6ly1g6e2whp87sj30ku0bstec.jpg', 'http://ww3.sinaimg.cn/large/006y8mN6ly1g6e2tl1v3bj30ku0bs77z.jpg', ]); -
分享按钮 share
//1. 安装依赖 share: ^0.6.3+1 //2. 引入依赖 import 'package:share/share.dart'; //3. 使用 Share.share('check out my website https://example.com');
4.60 房屋详情页面-主体结构
效果:
步骤:
- 准备
- 将RoomDetailPage 转成 有状态组件
- 安装 share 依赖
- 添加 data.dart,并粘贴之前准备的代码
- 核心编码
- 完善 appBar 部分主体结构
- 添加分享按钮
- 使用 Stack 完善body的主体结构
- 完善 底部按钮区域
- 测试&完善细节
- 修改 widgets/room_list_item_widget.dart 添加点击事件
- 修改 pages/home/tab_search/dataList.dart 修改id
- 测试
4.61房屋详情页面-底部按钮
效果:
步骤:
- 添加收藏按钮
- 添加‘联系房东’ 按钮
- 添加’预约看房’按钮
- 添加收藏事件及展示
- 测试
4.62 房屋详情页面-房屋图片&房屋基本信息
效果:
步骤:
- 添加 图片
- 添加 价格
- 添加 标签
- 添加 分割线
- 测试
4.63 房屋详情页面-房屋详细信息
效果:
步骤:
- 添加 Container Wrap 结构
- 实现【面积】部分
- 重构组件
- 实现 【楼层】【房型】【装修】
- 测试
4.64 房屋详情页面-房屋配置
效果:
步骤:
- 打开文件 /widgets/room_appliance.dart
- 创建 无状态组件 RoomApplianceList
- 添加并初始化参数 final List list;
- 完善内容部分
- 在 /pages/room_detail/index.dart 使用 RoomApplianceList
- 完善细节
- 测试
4.65 房屋详情页面-房屋概况&猜你喜欢
效果:
步骤:
- 添加 container 和 container.padding,Column
- 添加 Text 用于显示 房屋概况
- 添加按钮区域(收起/举报)
- 添加状态 showAllText=false 和 showTextTool 变量。
- 根据 showAllText 和 showTextTool 显示相应内容
- 添加事件控制 showAllText;
- 添加 Info()
4.66 filterBar-分析
结构分析:
结论:
-
结构
- filterBar— Row
- 区域--Item
- 方式--item
- 租金--item
- 筛选--item
- Picker选择器 — CommonPicker
- 筛选选择器 — Drawer
- filterBar— Row
-
组件参数
ValueChanged<FilterBarResult> onChange
数据及结构:
// pages/home/tab_search/filter_bar/data.dart
//结果数据类型
class FilterBarResult {
final String areaId ;
final String priceId ;
final String rentTypeId;
final List<String> moreIds;
FilterBarResult({this.areaId, this.priceId, this.rentTypeId, this.moreIds});
}
//通用类型
class GeneralType{
final String name;
final String id;
GeneralType(this.name, this.id);
}
List<GeneralType> areaList = [
GeneralType('区域1', '11'),
GeneralType('区域2', '22'),
];
List<GeneralType> priceList = [
GeneralType('价格1', '11'),
GeneralType('价格2', '22'),
];
List<GeneralType> rentTypeList = [
GeneralType('出租类型1', '11'),
GeneralType('出租类型2', '22'),
];
List<GeneralType> roomTypeList = [
GeneralType('房屋类型1', '11'),
GeneralType('房屋类型2', '22'),
];
List<GeneralType> orientedList = [
GeneralType('方向1', '11'),
GeneralType('方向2', '22'),
];
List<GeneralType> floorList = [
GeneralType('楼层1', '11'),
GeneralType('楼层2', '22'),
];
4.67 filterBar-展示区域
效果:

步骤:
- 准备
- 新建文件 pages/home/tab_search/filter_bar/data.dart,填充数据
- 新建文件 pages/home/tab_search/filter_bar/index.dart
- 引入 material 依赖, 创建有状态组件,添加并初始化参数
- 核心编码
- 编写基本结构,解决外边框样式问题
- 在 tabSearch页面中使用 Filter_bar
- 编写 filterBar 的 item 组件
- 使用 item 组件
4.68 filterBar-picker 部分
效果:
步骤:
- 添加 4 个按钮的激活状态
- 添加 4 个结果值
- 添加 4 响应事件
- 添加 _onChange 方法用于通知 FilterBar 的父组件
- 完成【区域】选择的 picker 部分
- 参照【区域】完成 【方式】和【租金】的 picker 部分
- 【筛选】先使用空白函数
4.69 filterBar-drawer 部分-展示部分
效果:
步骤:
- 准备
- 创建文件 filter_bar/filter_drawer.dart
- 添加 material 依赖 添加无状态组件
- 核心编码
- 实现页面基本结构
- 在 tabSearch页面使用 FiterDrawer
- 实现选项按钮
- 将选项按钮提取成组件 FilterDrawerItem
- 完善细节
- 隐藏 Drawer 按钮
- 打开 Drawer
4.70 filterBar-drawer 部分-数据分析
问题:
如何将 Drawer 的数据传递到 FilterBar中来?
分析:
结论:
使用全局状态,基于 scoped_model 实现
细节:
//1. 安装依赖
scoped_model: ^1.0.1
//2. 引入依赖
import 'package:scoped_model/scoped_model.dart';
//3.使用
//3.1 创建 model
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
//3.2 在根组件外使用 model
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new ScopedModel<CounterModel>(
model: new CounterModel(),
child: new Column(children: [
Widget(
//...
),
Widget(),
])
);
}
}
//3.3 在子组件中获取 model 中的数据/方法
var counter =
ScopedModel.of<CounterModel>(context, rebuildOnChange: true).counter;
ScopedModel.of<CounterModel>(context, rebuildOnChange: true).increment();
4.71 filterBar-drawer 部分-ScopedModel-model
分析:
-
数据分析
List<GeneralType> _roomTypeList = []; List<GeneralType> _orientedList = []; List<GeneralType> _floorList = []; Set<String> _selectedList = new Set<String>(); -
使用分析
- FilterBar 使用
- 添加可选数据列表 set dataList(Map<String, List> data)
- 获取选中数据列表 Set get selectedList
- FilterDrawer 使用
- 获取可选数据列表 Map<String, List> get dataList
- 添加/删除选中数据列表 selectedListToggleItem(GeneralType data)
实现:
- 创建文件 scoped_model/room_filter.dart
- 添加 scoped_model 依赖
- 编写 model 基本结构
- 实现 model 数据结构
- 实现 model 方法
4.72 filterBar-drawer 部分-ScopedModelHelper
问题:
操作 model 首先得获取 model,官方提供获取的方法需要注意 rebuildOnChange: true 参数!
能够封装一下?
解决:
能,自己编写一个 getModel 方法
//官方
ScopedModel.of<T>(context, rebuildOnChange: true)
////////vs/////////
//自己封装
getModel<T>(context)
实现:
- 添加文件 utils/scopoed_model_helper.dart
- 添加 scopedModel 和 material 依赖
- 实现 类的基本结构
- 实现静态方法 getModel
4.73 filterBar-drawer 部分-ScopedModel-使用
效果:
知识回顾:
-
有状态组件生命周期 initState— 只执行一次,没有context,或者 context 不完整
-
有状态组件生命周期 didChangeDependencies 依赖变更后就会执行,有context,会执行多次
-
一次执行,并且需要 context
@override void initState() { Timer.run(_getData); super.initState(); }
步骤:
- application 中添加 Model
- 在 FilterBar 的 initState 生命周期 set dataList(Map<String, List> data)
- 在 FilterBar 的 didChangeDependencies 生命周期 Set get selectedList
- 在 FilterDrawer 的 build 函数中 Map<String, List> get dataList
- 在 FilterDrawer 的 Item 点击事件中 selectedListToggleItem(String data)
- 测试
- 优化
5 前后端联调
5.1 介绍 Dio
问题:
flutter 中如何发送网络请求?
分析:
// 通过 httpClient 发送请求
get() async {
var httpClient = new HttpClient();
var uri = new Uri.https(
'itcast.cn', '/path1/path2', {'param1': '42', 'param2': 'foo'});
var request = await httpClient.getUrl(uri);
var response = await request.close();
var responseBody = await response.transform(UTF8.decoder).join();
return responseBody;
}
////////////////////vs/////////////////////
//通过 dio
get() async {
Response response = await Dio().get(
"https://itcast.cn/path1/path2?param1=42¶m2=foo");
return response;
}
结论:
使用 dio 作为 app 的网络请求工具。
Dio 对 HttpClient 的封装,增加了一些功能如 拦截器,全局配置,请求取消等。
使用:
// 安装依赖
dio: ^2.1.6
// 引入依赖
import "package:dio/dio.dart";
//使用
Dio().get("https://itcast.cn/test");
//其他用法
BaseOptions options = new BaseOptions(
baseUrl: "https://itcast.cn",
connectTimeout: 1000 * 10,
receiveTimeout: 3000,
extra: {'context': context});
Dio dio = new Dio(options);
dio.get('/test');
5.2 封装 DioHttp
问题:
dio 的 api 变了,我们需要修改的代码较多;或者后续使用其他组件替代 dio?
解决:
封装 DioHttp
详细分析:
-
内部状态
- Dio _client;
- BuildContext context;
-
初始化
- static DioHttp of(BuildContext context)
-
方法
-
get
Future<Response<Map<String, dynamic>>> get(String path, [Map<String, dynamic> params, String token]) async {} -
post
Future<Response<Map<String, dynamic>>> post(String path, [Map<String, dynamic> params, String token]) async {} -
postData
Future<Response<Map<String, dynamic>>> postFormData(String path, [Map<String, dynamic> params, String token]) async {}
-
编码:
- 创建文件 utils/dio_http.dart
- 添加 dio,material 依赖
- 完成 DioHttp 的基本结构
- 完成 初始化部分
- 完成 get 方法
- 完成 post 方法
- 完成 postData 方法
5.3 注册页联调
效果:
步骤:
- 接口文档分析
- 打开页面 pages/register.dart
- 在 _RegisterPageState 添加 3 个 TextEditingController,并赋给 3个 TextField
- 添加处理函数 _registerHandle() async {},并在注册按钮事件中调用
- 处理注册逻辑
- 获取 TextFeild 值
- 处理 输入异常
- 发送网络请求
- 处理异常返回
- 跳转到 登陆页面
- 测试
5.4 登陆页联调--分析
问题:
- 登陆状态怎么和其他页面同步?
- 再次打开 app ,是否需要二次登陆?
- 登陆过期如何处理?
分析:
- 使用 ScopedModel 同步登陆信息(token)。
- 登陆成功后,将 token 写入本地存储;添加一个app 启动页,app启动的时候,加载token!
- 在 DioHttp 中添加 Dio 拦截器,当登陆过期是,打开登陆页。
添加和使用本地缓存:
//安装依赖
shared_preferences: ^0.5.4+3
//引入依赖
import 'package:shared_preferences/shared_preferences.dart';
//使用
_incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
print('Pressed $counter times.');
await prefs.setInt('counter', counter);
}
5.5 封装 Store
问题:
本地缓存 shared_preferences 的api 可能会变?
结论:
将 shared_preferences 封装成 store
详细分析:
- 内部数据 final SharedPreferences storage;
- 方法
- getString(StoreKeys key) async{}
- setString(StoreKeys key,String value) async{}
- getStringList(StoreKeys key) async{}
- setStringList(StoreKeys key,List value) async{}
实现步骤:
- 新建文件 /utils/store.dart
- 添加 shared_preferences 依赖
- 编写类的基本结构
- 实现初始化方法
- 实现 getString setString getStringList setStringList
- 添加 storeKeys
5.6 实现 AuthModel
分析:
- 内部数据 token
- 方法
- String get token
- bool get isLogin
- void initApp(BuildContext context) async {}
- void login(String token,BuildContext context) {}
- void logout()
步骤:
- 新建文件 scoped_model/auth.dart
- 定义 内部数据 _token
- 实现分析的5个方法/属性
- 在 Application 中使用 AuthModel
5.7 登陆页联调
效果:
步骤:
- 打开页面 pages/login.dart
- 在 _LoginPageState 添加 2 个 TextEditingController,并赋给 2 个 TextField
- 添加处理函数 _loginHandle() async {},并在登陆按钮事件中调用
- 处理登陆逻辑
- 获取 TextFeild 值
- 处理 输入异常
- 发送网络请求
- 处理异常返回
- 回到上一个路由
- 测试
5.8 使用 AuthModel 及退出登陆
效果:
步骤:
- 首页-tabProfile-header 部分使用登陆态
- 首页-tabProfile-房屋管理部分使用登陆态
- 退出登陆逻辑实现
- 测试
5.9 完善个人信息
效果:
分析:
可以在登陆成功后,发送网络请求,获取登陆用户的个人信息!
将登陆用户的个人信息保存到 AuthModel 中,在 App 内共享。
获取个人信息,参考接口文档。
Get /user 需要token
步骤:
-
添加 UserInfo
-
添加 models/user_info.dart
-
完善 UserInfo 属性
final String avatar; final String gender; final String nickname; final String phone; final int id; -
添加工厂构造函数 factory UserInfo.fromJson(Map<String, dynamic> json)
-
-
在 AuthModel 添加 以下属性/方法
- UserInfo _userInfo;
- UserInfo get userInfo => _userInfo;
- _getUserInfo(BuildContext context) async {}
- 在 login/logout 方法中 维护 userInfo
-
在 pages/home/tab_profile/header.dart 中使用 UserInfo
-
测试
5.10 model 生成半自动化
问题:
工厂构造函数代码能否自动生成?

方案:
详细用法:
//1.安装依赖
dependencies:
json_annotation: ^3.0.0
dev_dependencies:
build_runner: ^1.6.7
json_serializable: ^3.2.2
//2. 引入依赖
import 'package:json_annotation/json_annotation.dart';
//3. 代码准备
import 'package:json_annotation/json_annotation.dart';
part 'user_info.g.dart';
@JsonSerializable()
class UserInfo {
final String avatar;
final String gender;
final String nickname;
final String phone;
final int id;
UserInfo(this.avatar, this.gender, this.nickname, this.phone, this.id);
factory UserInfo.fromJson(Map<String, dynamic> json) =>
_$UserInfoFromJson(json);
Map<String, dynamic> toJson() => _$UserInfoToJson(this);
}
//4. 执行命令
flutter packages pub run build_runner build
操作:
- 安装依赖
- 修改 userInfo 代码
- 执行命令
- 测试 @JsonKey(name: 'value')
5.11 优化model
问题:
优化现有 model,方便后面的开发。
分析:
GeneralType 的使用场景,
从接口文档中看 GeneralType 对应真实的字段。
操作:
-
优化 GeneralType
-
优化 RoomDetailData
import 'package:json_annotation/json_annotation.dart'; part 'room_detail_data.g.dart'; @JsonSerializable() class RoomDetailData { @JsonKey(name: 'houseCode') String id; String title; String community; @JsonKey(name: 'description') String subTitle; int size; String floor; int price; String roomType; @JsonKey(name: 'houseImg') List<String> houseImgs; List<String> tags; List<String> oriented; @JsonKey(name: 'supporting') List<String> applicances; RoomDetailData({ this.id, this.title, this.community, this.subTitle, this.size, this.roomType, this.houseImgs, this.tags, this.price, this.floor, this.oriented, this.applicances, }); factory RoomDetailData.fromJson(Map<String, dynamic> json) => _$RoomDetailDataFromJson(json); Map<String, dynamic> toJson() => _$RoomDetailDataToJson(this); } -
优化 RoomListItemData
import 'package:json_annotation/json_annotation.dart'; part 'room_list_item_data.g.dart'; @JsonSerializable() class RoomListItemData { @JsonKey(name: 'houseCode') final String id; final String title; @JsonKey(name: 'desc') final String subTitle; @JsonKey(name: 'houseImg') final String imageUri; final List<String> tags; final int price; const RoomListItemData( {this.title, this.subTitle, this.imageUri, this.tags, this.price, this.id}); factory RoomListItemData.fromJson(Map<String, dynamic> json) => _$RoomListItemDataFromJson(json); Map<String, dynamic> toJson() => _$RoomListItemDataToJson(this); } -
修改引用
5.12 城市选择器-分析
效果:
分析:
- 城市选择使用 city_pickers
- 需要在本地缓存中 存储 city,需要使用 Store
- 目前仅部分城市可用,在config 中添加可用城市列表
- 多个页面共享当前 city 状态,需要使用 ScopedModel
代码准备:
-
使用 city_pickers
//1. 安装依赖 city_pickers: ^0.1.28 //2. 引入依赖 import 'package:city_pickers/city_pickers.dart'; //3. 使用 var result = await CityPickers.showCitiesSelector( context: context, theme: new ThemeData( primaryColor: Colors.green, ), ); String cityName = result?.cityName; -
配置可用 city
// config.dart static List<GeneralType> availableCitys = [ GeneralType('北京', 'AREA|88cff55c-aaa4-e2e0'), GeneralType('上海', 'AREA|dbf46d32-7e76-1196'), GeneralType('深圳', 'AREA|a6649a11-be98-b150'), GeneralType('广州', 'AREA|e4940177-c04c-383d'), ]; -
编写 CityModel
//scoped_model/city.dart import 'package:hook_up_rent/models/general_type.dart'; import 'package:scoped_model/scoped_model.dart'; class CityModel extends Model { GeneralType _city ; set city(GeneralType data) { _city=data; notifyListeners(); } GeneralType get city { return _city; } } -
在 application 中使用 CityModel
-
Store.StoreKeys 添加 city
-
在 scoped_model_helper.dart 中 添加 getAreaId 方法
static String getAreaId(context){ return ScopedModelHelper.getModel<CityModel>(context).city?.id??Config.AvailableCitys.first.id; }
5.13 城市选择器-实现
效果:
分析:
- 点击按钮
- 弹出城市选择器
- 选择城市
- 保存城市
- 判断城市合法
- 城市保存到 ScopedModel
- 城市保存到 Store
- 打开app,获取城市
- 先从 ScopedModel 获取
- 如果前者为空,使用默认值的第一个 ;并从 Store 获取(异步)
步骤:
- 打开 widgets/search_bar/index.dart
- 实现修改城市逻辑
- 使用 _changeLocation
- 实现 _changeLocation
- 实现 _saveCity
- 实现获取城市逻辑
- 使用 _getLocation
- 实现 _getLocation
- 使用 city.name
- 测试
5.14 联调 FilterBar
效果:
分析:
其实就是获取条件数据。
接口,参考接口文档。
final url = '/houses/condition?id=$areaId';
List<GeneralType> areaList = List<GeneralType>.from(data['area']['children']
.map((item) => GeneralType.fromJson(item))
.toList());
List<GeneralType> priceList = List<GeneralType>.from(
data['price'].map((item) => GeneralType.fromJson(item)).toList());
List<GeneralType> rentTypeList = List<GeneralType>.from(
data['rentType'].map((item) => GeneralType.fromJson(item)).toList());
List<GeneralType> roomTypeList = List<GeneralType>.from(
data['roomType'].map((item) => GeneralType.fromJson(item)).toList());
List<GeneralType> orientedList = List<GeneralType>.from(
data['oriented'].map((item) => GeneralType.fromJson(item)).toList());
List<GeneralType> floorList = List<GeneralType>.from(
data['floor'].map((item) => GeneralType.fromJson(item)).toList());
步骤:
- 打开文件 pages/home/tab_search/filter_bar/index.dart
- 核心编码
- 将filter_bar/data.dart 的数据剪切到 index.dart 作为 6 个 List 状态默认值为[];
- 在 _getData 中获取网上数据并 setState
- 测试及优化
- 测试
- 解决数据返回时,组件已卸载掉问题
- 解决城市变更的问题
5.15 联调找房页
效果:
分析:
-
需要一个数据列表的状态 List dataList;
-
实现 FIlterBar.onChange 的处理逻辑
-
房屋列表查询接口
//1. url String url = '/houses?cityId=' + '$cityId&area=$area&mode=$mode&price=$price&more=$more&start=1&end=20'; //2. 返回数据 resMap['body']['list'];
步骤:
- 打开文件 pages/home/tab_search/index.dart
- 核心代码
- 添加状态 List dataList;
- 展示数据使用 dataList;
- 完善 RoomListItemWidget , 图片地址需要添加 baseUri
- 给 FIlterBar 添加 onChange 参数 _onFilterBarChange(FilterBarResult data) async {}
- 实现 _onFilterBarChange 方法
- 测试
5.16 房屋详情页
效果:
分析:
进页面就获取数据
接口详情:
// url
final url = '/houses/$roomId';
// 数据
resMap['body']
步骤:
- 打开文件 pages/room_detail/index.dart
- 在列表中使用 data;
- 借助 initState 生命周期 执行 _getData
- 实现 _getData() async {} 方法,注意解决图片相对路径问题
- 测试
5.17 房屋管理页
效果:
分析:
进页面就获取数据
接口详情:
// url
String url = '/user/houses';
//数据
resMap['body'];
步骤:
- 打开文件 pages/room_manage/index.dart
- 将无状态组件转成有状态组件
- 添加状态List availableDataList
- 借助 initState 生命周期 执行 _getData,注意使用延迟执行 _getData
- 实现 _getData() async {} 方法,注意接口需要 token
- 测试
5.18 房源发布-分析
结构分析:
功能拆分:
- 基础数据准备阶段
- 户型列表
- 楼层列表
- 朝向列表
- 输入信息阶段
- 选择小区
- 数据提交阶段
- 校验数据
- 上传图片
- 提交数据
步骤:
- 条件数据准备
- 选择小区逻辑
- 图片上传逻辑
- 数据验证及提交
- 优化
5.19 房源发布-条件数据
效果:
分析:
接口及返回数据
//url
String url = '/houses/params';
//返回处理
var res = await DioHttp.of(context).get(url);
var data = json.decode(res.toString())['body'];
List<GeneralType> floorList = List<GeneralType>.from(
data['floor'].map((item) => GeneralType.fromJson(item)).toList());
List<GeneralType> orientedList = List<GeneralType>.from(
data['oriented'].map((item) => GeneralType.fromJson(item)).toList());
List<GeneralType> roomTypeList = List<GeneralType>.from(
data['roomType'].map((item) => GeneralType.fromJson(item)).toList());
步骤:
-
打开页面 page/room_add/index.dart
-
添加 3 个状态
List<GeneralType> floorList = []; List<GeneralType> orientedList = []; List<GeneralType> roomTypeList = []; -
使用 3 个状态的数据
-
借助 initState 生命周期,延迟调用 _getParams
-
实现 _getParams() async {}
-
测试
5.20 房源发布-小区选择-主流程
效果:
分析:
-
页面基本结构
- Scaffold
- AppBar
- ListView
- Scaffold
-
准备 Community Model
import 'package:json_annotation/json_annotation.dart'; part 'community.g.dart'; @JsonSerializable() class Community{ @JsonKey(name: 'community') final String id; @JsonKey(name: 'communityName') final String name; Community(this.id, this.name); factory Community.fromJson(Map<String,dynamic> json)=>_$CommunityFromJson(json); Map<String,dynamic> toJson()=>_$CommunityToJson(this); } List<Community> list=[ Community('11','小区1'), Community('22','小区2'), Community('33','小区3'), ];
步骤:
-
准备
- 新建文件 models/community.dart
- 使用数据并执行构建命令
- 新建文件 pages/community_picker.dart
- 添加依赖及有状态组件 CommunityPickerPage
-
核心编码
-
完成页面基本结构
-
路由注册
-
在 room_add 页面调用CommunityPickerPage并使用返回值
-
完善页面 Body 部分
-
5.21 房源发布-小区选择-细节完善及联调
效果:
分析:
-
添加 searchBar
-
小区选择页接口联调
//接口说明 final url = '/area/community?name=$value&id=$areaId'; var res = await DioHttp.of(context).get(url); var data = json.decode(res.toString())['body']; List<Community> dataList = List<Community>.from( data.map((item) => Community.fromJson(item)).toList());
步骤:
-
打开文件 pages/community_picker.dart
-
添加列表状态 List dataList = []; 并使用
-
添加 searchBar
-
appBar 背景色及隐藏回退按钮
-
添加并现 SearchBar.onSearchSubmit 方法
-
测试
5.22 房源发布-图片上传
效果:
点击提交,上传图片,上传成功后,把图片通过toast 展示到页面上。
分析:
- 图片上传函数
输入: List
输出: String imageString ,多张图片地址以 | 隔开
函数签名 :Future uploadImages(List files, BuildContext context) async {}
- 图片上传接口
//准备 formData
var fromData = FormData();
fromData.add("file",
files.map((file) => UploadFileInfo(file, 'picture.jpg')).toList());
//准备url
String url = '/houses/image';
//发送请求,处理返回
var res = await DioHttp.of(context).postFormData(url, fromData, token);
var data = json.decode(res.toString())['body'];
String images = List<String>.from(data).join('|');
步骤:
- 实现图片上传函数
- 添加文件 utils/upload_images.dart
- 编写函数的主体结构
- 实现函数体
- 使用图片上传函数
- 打开 room_add/index.dart
- 添加状态 List images = [];
- 在 CommonImagePicker.onChange 中 setState
- 添加并使用 _submit() async {} 方法
- 实现 _submit
- 测试
5.23 房源发布-数据校验及提交
效果:
-
数据校验
【小区】,【租金】,【大小】 不能为空
-
接口联调
接口说明:
//参数 Map<String, dynamic> params = { "title":'', "description": '', "price": '', "size": '', "oriented": '', "roomType": '', "floor":'', "community": '', "houseImg": '',//多条以 | 分割 "supporting": '',//多条以 | 分割 }; //接口url String url = '/user/houses'; //数据返回 var res = await DioHttp.of(context).post(url, params, token); var status = json.decode(res.toString())['status'];
步骤:
- 打开 room_add/index.dart
- 数据校验逻辑
- 参数准备
- 发送请求并返回
- 测试
- 优化
5.24 房源发布-优化
问题1:

解决1:
如果可选项为空,则不展示对应的选择项
问题2:
选择小区时,点击搜索框时,报错
解决2:
添加空判断!
问题3:
【房源发布】页数据提交后,【房屋管理】页列表未更新!
解决3:
通过路由传参数解决
5.25 登陆过期处理
问题:
点击提交无反应!!!
分析:
- 接口未正常返回应该给予提示
- 处理登陆过期问题。
登陆过期是所有需要登陆态接口的通用问题,
通过 Dio 的拦截器(Interceptor) 解决。
用法:
Interceptor interceptor = InterceptorsWrapper(onResponse: (Response res) {
});
client.interceptors.add(interceptor);
解决:
- 打开 room_add/index.dart 在 _submit 方法中处理未正常返回的情况
- 解决登陆态过期问题
- 打开 utils/dio_http.dart
- 添加 interceptor
- 实现 interceptor
- 使用 interceptor
- 测试
5.26 添加 flutter 启动页
问题:
重启 app 之后,需要重新登陆
方案:
在启动页处理
启动页就是一个普通的页面,延迟 3 秒跳转到首页
效果:
步骤:
-
准备
- 添加图片资源 - static/images/loading.jpg
- 创建 pages/loading.dart
-
核心编码
- 完成页面基本结构
- 实现延迟跳转和调用initApp
- 注册路由
- 在 application.dart中 修改程序入口页面
-
测试及优化
-
退出登陆 store 未清空问题 — logout 清空 store
-
loading 页直接跳转到 登陆页问题 — 特殊处理
-
6 构建打包
6.1 构建打包分析
主要包含几部分
- 准备
- 桌面图标修改
- 应用名称修改
- 启动图片修改
- 构建
资源地址:/resource/chapter6/01静态资源/package
添加启动页后仍然有明显的白屏!
6.2 构建 Android 包
效果:
生成打包文件,能在开启开发模式的手机上运行
步骤:
-
修改应用名称
/项目目录/android/app/src/main/AndroidManifest.xml文件application标签android:label属性 -
修改图标及背景图
文件地址:
/resource/chapter6/01静态资源/package/android/res目标地址:
/项目目录/android/app/src/main/res -
修改构建配置
在
android/app/build.gradle文件中android.lintOption添加属性checkReleaseBuilds false -
构建
flutter build apk
-
查看文件
部分同学构建出现版本问题,参考解决方案


6.3 构建 ios 包
效果:
生成打包文件,能在开启开发模式的手机上运行(需要 苹果开发者账号 )
步骤:
-
修改应用名称
/项目目录/ios/Runner/info.plist文件 修改key 为 CFBundleName 的值。 -
修改图标及背景图
文件地址:
/resource/chapter6/01静态资源/package/ios/Assets.xcassets目标地址:
/项目目录/Runner/Assets.xcassets -
构建
flutter build ios
-
查看文件
7 总结
-
回顾实现的功能/页面
实现了完整的登陆注册,添加房源,查找房源,查看房源的功能。
- 登陆页
- 注册页
- 首页
- 首页tab
- 搜索tab
- 咨询tab
- 我的tab
- 搜索页
- 房屋管理
- 添加房屋
- 房屋详情
- 设置
-
回顾项目知识点
项目知识点:
- 使用第三方组件
- 通用组件封装
- 使用静态资源
- 本地图片
- 网络图片
- 使用自带 icon
- 使用字体 icon
- 网络图片缓存超时处理
- 本地存储及 store 封装
- 数据管理 scoped_model
- 网络请求 及 dio_http 封装
- 序列化及反序列化半自动生成实体类
- 图片上传
- app icon 及 启动页
-
个人感悟
技术一直在更新;唯有终身学习,才能让自己保持