flutter 项目实战
本文最后更新于:2022年4月22日 上午
豆瓣小项目实战
项目入口
通过 theme
将全局主题色修改为绿色
import 'package:flutter/material.dart';
import 'package:learn_fluuter_00/douban/main/main.dart';
main() => runApp(
MaterialApp(
theme: ThemeData(
primaryColor: Colors.green,
primarySwatch: Colors.green,
splashColor: Colors.transparent,
),
home: MainPage(),
),
);
MainPage
indexdStack
可以通过索引来控制子元素的展示,类似于前端中修改定位元素的 z-index
来让子元素展示
import 'package:flutter/material.dart';
import 'package:learn_fluuter_00/douban/main/config.dart';
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(index: _currentIndex, children: pages),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
items: tabbar,
onTap: (index) => setState(() {
_currentIndex = index;
}),
unselectedFontSize: 14, // 将选中的字体和未选中的字体设为一致大小
type: BottomNavigationBarType.fixed,
),
);
}
}
bottomNavigationBar
底部的 tabbar
,小程序底部的tabbar
,也是通过 index
来控制当前展示的 item,这里需要注意的是如果 item 有 3 个以上后它底部的展示会是安卓风格的展示,底部的文字展示不全
(widget.items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting)
需要配置 type
为 BottomNavigationBarType.fixed
来展示成 ios
风格的样式
config
import 'package:flutter/material.dart';
import 'package:learn_fluuter_00/douban/profile/profile.dart';
import '../home/home.dart';
List<Widget> pages = [HomePage(), ProfilePage(), ProfilePage(), ProfilePage(), ProfilePage()];
List<BottomNavigationBarItem> tabbar = [
BottomNavigationBarItem(
label: '首页',
icon: Image.asset('lib/assets/images/tabbar/home.png', width: 30),
activeIcon: Image.asset('lib/assets/images/tabbar/home_active.png', width: 30),
),
BottomNavigationBarItem(
label: '书影音',
icon: Image.asset('lib/assets/images/tabbar/subject.png', width: 30),
activeIcon: Image.asset('lib/assets/images/tabbar/subject_active.png', width: 30),
),
BottomNavigationBarItem(
label: '小组',
icon: Image.asset('lib/assets/images/tabbar/group.png', width: 30),
activeIcon: Image.asset('lib/assets/images/tabbar/group_active.png', width: 30),
),
BottomNavigationBarItem(
label: '市集',
icon: Image.asset('lib/assets/images/tabbar/mall.png', width: 30),
activeIcon: Image.asset('lib/assets/images/tabbar/mall_active.png', width: 30),
),
BottomNavigationBarItem(
label: '我的',
icon: Image.asset('lib/assets/images/tabbar/profile.png', width: 30),
activeIcon: Image.asset('lib/assets/images/tabbar/profile_active.png', width: 30),
),
];
网络请求
根目录建立 services
文件夹,同事新建 config.dart
和 request.dart
文件
config.dart
存放 网络请求配置
class Config {
static const baseUrl = 'https://m.douban.com/rexxar/api/v2/';
static const timeout = 10000;
static const method = 'GET';
static const Map<String, dynamic> headers = {
'Referer': 'https://m.douban.com/subject_collection/ECWY6B2GQ?dt_dapp=1',
};
}
request.dart
import 'package:dio/dio.dart';
import './config.dart';
class Request {
static final dio = Dio(
BaseOptions(
baseUrl: Config.baseUrl,
connectTimeout: Config.timeout,
method: Config.method,
headers: Config.headers,
),
);
static Future request(String url, {Map<String, dynamic>? queryParameters, String? method}) async {
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handle) {
print('请求拦截: ${options.uri} ${options.headers}');
return handle.next(options);
// return options;
},
onResponse: (options, handle) {
print('响应拦截');
return handle.next(options);
},
onError: (options, handle) {
print('错误拦截:$options');
return handle.next(options);
},
));
Options options = Options(method: method);
Response<dynamic> res;
try {
res = await dio.request(url, queryParameters: queryParameters, options: options);
} catch (err) {
print('请求发生错误$err');
return Future.error(err);
}
return res.data;
}
}
HomePage
这里用到了之前封装的 评分组件 和 虚线分割线组件
import 'package:flutter/material.dart';
import 'package:learn_fluuter_00/douban/model/home_model.dart';
import 'package:learn_fluuter_00/douban/services/home_request.dart';
import 'package:learn_fluuter_00/widgets/star/dash_line.dart';
import 'package:learn_fluuter_00/widgets/star/star.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<HotMoiveItem> moiveList = [];
@override
void initState() {
super.initState();
HomeRequest.getHotMoive().then((res) {
setState(() => moiveList.addAll(res));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('首页')),
body: ListView.builder(
itemCount: moiveList.length,
itemBuilder: (ctx, index) {
final moive = moiveList[index];
return Container(
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey[200]!, width: 15))),
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildRanking(index + 1),
SizedBox(height: 8),
buildContent(moive),
buildFooter(moive),
],
),
),
);
},
),
);
}
// 构建底部
Container buildFooter(HotMoiveItem moive) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(6),
margin: EdgeInsets.symmetric(vertical: 15),
decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(2)),
child: Text('${moive.rating.count}评价', style: TextStyle(color: Colors.grey[700])),
);
}
// 构建内容
Widget buildContent(HotMoiveItem moive) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面图
ClipRRect(borderRadius: BorderRadius.circular(6), child: Image.network(moive.coverUrl, width: 85)),
SizedBox(width: 10),
buildContentInfo(moive), // 中间的内容
// 虚线分割线
Container(
margin: EdgeInsets.symmetric(horizontal: 10),
child: DashLine(axis: Axis.vertical, count: 20, height: 2, width: 1)),
// 想看
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('lib/assets/images/douban/icons/love.png', width: 20),
SizedBox(height: 5),
Text('想看', style: TextStyle(color: Colors.orange, fontSize: 12)),
],
),
],
),
);
}
// 构建内容 info
Expanded buildContentInfo(HotMoiveItem moive) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text.rich(TextSpan(children: [
WidgetSpan(child: Icon(Icons.play_circle_outline_rounded, color: Colors.red[500])),
WidgetSpan(child: SizedBox(width: 3)),
TextSpan(text: moive.title, style: TextStyle(fontSize: 18))
])),
SizedBox(height: 3),
// 评分星星
Row(
children: [
Star(
score: moive.rating.value,
totalScore: moive.rating.max,
size: 17,
selectedColor: Colors.orange[400]!),
SizedBox(width: 5),
Text('${moive.rating.value}', style: TextStyle(color: Colors.orange[500], fontSize: 16))
],
),
SizedBox(height: 2),
// card标题
Text(moive.cardSubtitle, style: TextStyle(color: Colors.grey[600]))
],
),
);
}
// 构建头部排名
Widget buildRanking(int ranking) {
Color getColor() {
switch (ranking) {
case 1:
return Colors.red[400]!;
case 2:
return Colors.orange[700]!;
case 3:
return Colors.orange[300]!;
default:
return Colors.grey;
}
}
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: getColor()),
child: Text('No.$ranking', style: TextStyle(color: Colors.white)),
);
}
}
项目注意
CLipRRect
widget
矩形裁剪,如上,给封面图进行了圆角裁剪
IntrinsicHeight
该组件会使其内部的子组件都占满高度,如上代码中,给 Row
包裹 IntrinsicHeight
,则 row
的所有子组件的高度会将Row
占满
换行问题
可以使用富文本,或者将文字分割成数组,然后再一个个构建(文字太多可能会影响性能),
...title.runes.map((rune) {
return WidgetSpan(child: Text(new String.formCharCode(rune)))
}).toList(),
富文本居中对齐
alignment: Alignment.middle
如何抓取接口
可在 豆瓣app
仅需需要抓取的页面,点击右上角分享当前页的链接,然后在 chrome 中查看请求接口
底部 bottomBar 首次点击图片闪烁问题
给每个 item
的 icon
都添加gaplessPlayback: true;
json 转 model
在线网址 https://app.quicktype.io/
弊端:在线网站会错误的转换成 double
-> int
如 4.0 -> 4 JSON.parse
转换的问题,包括 apifox
也存在
vscode
插件 Paste JSON as Code
弊端:插件无法生成 formMap
当前实践是将 错误的类型手动修复
查看打印所在的行数
手动封装,或者查看是否有第三方库
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处。