Flutter 实战
详细代码可参见 仓库 folder 40
lib/model 目录下缺少 news_response.g.dart,
lib/components/Home/ListItem.dart 中部分 UI 缺少.
一些需要注意的点
- Scaffold: 实现基本的 Material Design 可视布局结构
- SafeArea: 一种小部件,它在其子部件中插入足够的填充,以避免操作系统的入侵(适配异形屏)
- Column/Row: 按列/行布局
- SideBox/Container 都可以设置 width/height,但用 Container 只设置 width, height 的话 Android Studio 会提示换为 SideBox
- Swiper: 轮播图,使用时父组件要设置 width 与 height
- Expanded: 展开 Row、Column 或 Flex 的子级的小部件 这样孩子就能填满可用的空间(作为 Row/Column/Flex 的 children 使用)
- GestureDetector/InkWell: 为 child 添加点击事件的容器,InkWell 会在点击时出现水波纹
设置宽高和 margin/padding
Container(
margin: EdgeInsets.only(left: 10.h, right: 10.h, top: 5.w, bottom: 5.w),
padding: EdgeInsets.only(left: 10.h, right: 10.h, top: 10.w, bottom: 10.w),
decoration: BoxDecoration(
border: Border.all(color: Colors.black12, width: 2.r),
borderRadius: BorderRadius.all(Radius.circular(5.r)),
),
child: ,//
)使用 Swiper
SizedBox(
width: double.infinity,
height: 150.h,
child: Swiper(
indicatorLayout: PageIndicatorLayout.NONE,
autoplay: true,
pagination: const SwiperPagination(),
control: const SwiperControl(),
itemCount: 3,
itemBuilder: (context, index) {
return Container(
width: double.infinity,
margin: EdgeInsets.all(15.r),
height: 150.h,
color: Colors.lightBlue,
);
},
),
)ListView: 创建列表视图
ListView.builder(
itemBuilder: (context, index) {
return _listItemView(context, index);
},
itemCount: 10,
)点击时路由跳转
封装路由管理后,要用navigatorKey: RouteUtils.navigatorKey,onGenerateRoute: Routes.generateRoute,initialRoute: RoutePath.home,应用
GestureDetector(
onTap: () {
Navigator.pushNamed(context, RoutePath.webViewPage);
// Navigator.push(context, MaterialPageRoute(builder: (context) {
// return WebViewPage(title: "首页跳转来的");
// }));
},
child: _listItemView(context, index),
)封装路由管理
class Routes {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case RoutePath.home:
return pageRoute(HomePage());
case RoutePath.webViewPage:
return pageRoute(WebViewPage(title: "跳转"));
default:
return pageRoute(
Scaffold(
body: SafeArea(
child: Center(child: Text("路由 ${settings.name} 不存在")),
),
),
);
}
}
static MaterialPageRoute pageRoute(
Widget page, {
RouteSettings? settings,
bool? fullscreenDialog,
bool? maintainState,
bool? allowSnapshotting,
}) {
return MaterialPageRoute(
builder: (context) {
return page;
},
settings: settings,
fullscreenDialog: fullscreenDialog ?? false,
maintainState: maintainState ?? true,
allowSnapshotting: allowSnapshotting ?? true,
);
}
}
// 路由地址
class RoutePath {
// 首页
static const String home = "/";
// 网页页面
static const String webViewPage = "/web_view_page";
}封装路由工具类
import 'package:flutter/material.dart';
class RouteUtils {
RouteUtils._();
static final navigatorKey = GlobalKey<NavigatorState>();
// App 根节点 Context
static BuildContext get context => navigatorKey.currentContext!;
static NavigatorState get navigator => navigatorKey.currentState!;
// 普通动态跳转 --> page
static Future push(
BuildContext context,
Widget page, {
bool fullscreenDialog = false,
RouteSettings? settings,
bool maintainState = true,
}) {
return Navigator.push(
context,
MaterialPageRoute(
builder: (_) => page,
fullscreenDialog: fullscreenDialog,
settings: settings,
maintainState: maintainState,
),
);
}
// 根据路由路径跳转
static Future pushForNamed(
BuildContext context,
String name, {
Object? arguments,
}) {
return Navigator.pushNamed(context, name, arguments: arguments);
}
// 自定义 route 动态跳转
static Future pushForPageRoute(BuildContext context, Route route) {
return Navigator.push(context, route);
}
// 清空栈,只留目标页面
static Future pushNamedAndRemoveUntil(
BuildContext context,
String name, {
Object? arguments,
}) {
return Navigator.pushNamedAndRemoveUntil(
context,
name,
(route) => false,
arguments: arguments,
);
}
static Future pushAndRemoveUntil(
BuildContext context,
Widget page, {
bool fullscreenDialog = false,
RouteSettings? settings,
bool maintainState = true,
}) {
return Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => page,
fullscreenDialog: fullscreenDialog,
settings: settings,
maintainState: maintainState,
),
(route) => false,
);
}
// 用新的路由替换当前路由
static Future pushReplacement(
BuildContext context,
Route route, {
Object? result,
}) {
return Navigator.pushReplacement(context, route, result: result);
}
// 用新的路由替换当前路由
static Future pushReplacementNamed(
BuildContext context,
String name, {
Object? result,
Object? arguments,
}) {
return Navigator.pushReplacementNamed(
context,
name,
arguments: arguments,
result: result,
);
}
// 关闭当前页面
static void pop(BuildContext context) {
Navigator.pop(context);
}
// 关闭当前页面:包含返回值
static void popOfData<T extends Object?>(BuildContext context, {T? data}) {
Navigator.of(context).pop(data);
}
}ListView 的 shrinkWrap 与 physics
在 Flutter 中,ListView 的 shrinkWrap 属性非常有用,尤其是在嵌套列表视图或者在滚动视图不固定高度的情况下。
shrinkWrap 属性的类型为 bool,默认值为 false。以下是关于 shrinkWrap 的详细解释:
- 当
shrinkWrap为false时:ListView会尽可能多的占用垂直空间,这意味着ListView会尝试填充其父级提供的所有可用空间。这种情况下,ListView必须放在一个有固定高度的容器内,否则会报错,因为ListView需要知道其高度才能正确地进行布局。 - 当
shrinkWrap为true时:ListView会根据其子项的总高度来调整自己的高度,而不是尽可能多的占用可用空间。使用shrinkWrap为true可以让ListView根据其内容的实际高度来决定其高度,这样就不需要固定高度的外部容器了。 以下是一些使用shrinkWrap: true的场景:
- 嵌套
ListView:如果你在一个ListView中嵌套了另一个ListView,内层的ListView必须设置shrinkWrap: true,否则会报错,因为外层的ListView无法确定其高度。 - 动态列表高度:当你的
ListView中的项目数量或内容会动态变化时,设置shrinkWrap: true可以确保ListView的高度会根据内容的变化而调整。
需要注意的是,设置 shrinkWrap: true 可能会降低 ListView 的性能,因为它需要遍历所有子项来确定其高度,而不是仅仅使用一个滚动容器。因此,只有在你确实需要的时候才应该使用 shrinkWrap: true。
在 Flutter 中,ListView 的 physics 属性用于指定列表视图应如何响应用户的滚动行为。physics 属性的类型是 ScrollPhysics,它定义了滚动视图的滚动行为,例如滚动时的动量和弹跳效果。
以下是 physics 属性的一些常见用途和默认值:
- 默认情况下,
ListView的physics属性被设置为AlwaysScrollableScrollPhysics,这意味着无论列表的内容是否足以滚动,用户都可以尝试滚动列表视图。 - 如果你想要禁用滚动,可以将
physics属性设置为NeverScrollableScrollPhysics。这将阻止用户滚动列表视图,即使内容超出了视图的范围。 - 另一个常用的
ScrollPhysics是ClampingScrollPhysics,它是AlwaysScrollableScrollPhysics的一个子类,用于确保滚动不会超出内容的边界。 BouncingScrollPhysics允许滚动超出内容边界,并在用户释放时产生弹跳效果,这在 iOS 设备上比较常见。 以下是一些physics属性的示例:
// 允许滚动,并且在滚动超出边界时有弹跳效果
ListView(
physics: BouncingScrollPhysics(),
children: [...],
)
// 允许滚动,但是没有弹跳效果,滚动会被限制在内容边界内
ListView(
physics: ClampingScrollPhysics(),
children: [...],
)
// 禁用滚动
ListView(
physics: NeverScrollableScrollPhysics(),
children: [...],
)自定义 ScrollPhysics 可以让你更细致地控制滚动行为,例如改变滚动速度、动量、阻尼等。
总之,physics 属性是用于定制 ListView 的滚动体验的,它决定了列表视图如何响应用户的滚动输入。
让 banner 和 ListView 一起滑动
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView( // 让它们共同包裹在 SingleChildScrollView,相当于 Android 中的 ScrollView,但是需要子组件计算高度啥的
child: Column(
children: [
_banner(),
// 取消 Expanded 布局
ListView.builder(
itemBuilder: (context, index) {
return InkWell(
onTap: () {
RouteUtils.pushForNamed(
context,
RoutePath.webViewPage,
arguments: {"name": "使用路由传值"},
);
},
child: _listItemView(context, index),
);
},
itemCount: 10,
shrinkWrap: true, // 这个属性可以让 ListView 内部计算所有子组件的高度
physics: NeverScrollableScrollPhysics(), // 禁用 ListView 本身的滑动事件
),
],
),
),
),
);
}
}路由传参接收
传参
RouteUtils.pushForNamed(
context,
RoutePath.webViewPage,
arguments: {"name": "使用路由传值"},
);接收
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class WebViewPage extends StatefulWidget {
const WebViewPage({super.key});
@override
State<StatefulWidget> createState() {
return _WebViewPageState();
}
}
class _WebViewPageState extends State<WebViewPage> {
String? name;
@override
void initState() {
super.initState();
// 组件初始化完成后获取路由参数
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
var map = ModalRoute.of(context)?.settings.arguments;
if (map is Map) {
name = map["name"];
}
setState(() {}); // setState 方法会刷新整个组件,后面用状态管理代替
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// var map = ModalRoute.of(context)?.settings.arguments;
// if (map is Map) {
// name = map["name"];
// }
// setState(() {}); // setState 方法会刷新整个组件,后面用状态管理代替
}
@override
Widget build(BuildContext context) {
// var map = ModalRoute.of(context)?.settings.arguments;
// var name = "";
// if (map is Map) {
// name = map["name"];
// }
return Scaffold(
appBar: AppBar(title: Text("WebView ${name}")),
body: SafeArea(
child: Container(
child: InkWell(
onTap: () {
Navigator.pop(context);
},
child: SizedBox(width: 200.w, height: 200.h, child: Text("返回")),
),
),
),
);
}
}Dio 网络请求 && Provider 状态管理刷新组件
使用 JsonToDart(JSON To Dart) 插件做 json 数据转 dart 的类文件
home_vm.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:my_bilibili/datas/home_banner_data.dart';
import 'package:my_bilibili/datas/home_list_data.dart';
class HomeViewModel with ChangeNotifier {
static Duration timeout = Duration(seconds: 30);
Dio dio = Dio();
List<BannerItemData>? bannerList;
List<HomeListItemData>? listData;
HomeViewModel() {
dio.options = BaseOptions(
method: "GET",
baseUrl: "https://www.wanandroid.com",
connectTimeout: timeout,
receiveTimeout: timeout,
sendTimeout: timeout,
);
}
// 获取首页 banner 数据
Future getBanner() async {
Response resp = await dio.get("/banner/json");
HomeBannerData bannerData = HomeBannerData.fromJson(resp.data);
bannerList = bannerData.data ?? [];
notifyListeners();
}
// 获取首页文章列表
Future getHomeList() async {
Response resp = await dio.get("/article/list/1/json");
HomeData homeData = HomeData.fromJson(resp.data);
listData = homeData.data?.datas ?? [];
notifyListeners();
}
}home_page.dart
Widget build(BuildContext context) {
return ChangeNotifierProvider<HomeViewModel>(
create: (context) {
return viewModel;
},
child: Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Consumer<HomeViewModel>(
builder: (context, vm, child) {
return _banner(vm);
},
),
Consumer<HomeViewModel>(
builder: (context, vm, child) {
return _listView(vm);
},
),
],
),
),
),
),
);
}封装 Dio 网络请求工具
void main() {
// 开始运行 App 的时候调用初始化
DioInstance.instance().initDio(baseUrl: "https://www.wanandroid.com");
runApp(const MyApp());
}import 'package:dio/dio.dart';
import 'http_method.dart';
class DioInstance {
static DioInstance? _instance;
static const _defaultTimeout = Duration(seconds: 30);
final Dio _dio = Dio();
DioInstance._();
static DioInstance instance() {
return _instance ??= DioInstance._();
}
void initDio({
required String baseUrl,
String? method = HttpMethod.GET,
Duration connectTimeout = _defaultTimeout,
Duration receiveTimeout = _defaultTimeout,
Duration sendTimeout = _defaultTimeout,
ResponseType? responseType = ResponseType.json,
String? contentType,
}) {
_dio.options = BaseOptions(
method: method,
baseUrl: baseUrl,
connectTimeout: _defaultTimeout,
receiveTimeout: _defaultTimeout,
sendTimeout: _defaultTimeout,
responseType: responseType,
contentType: contentType,
);
}
// get 请求
Future<Response> get({
required String path,
Map<String, dynamic>? param,
Options? options,
CancelToken? cancelToken,
}) {
return _dio.get(
path,
queryParameters: param,
options:
options ??
Options(
method: HttpMethod.GET,
receiveTimeout: _defaultTimeout,
sendTimeout: _defaultTimeout,
),
cancelToken: cancelToken,
);
}
// post 请求
Future<Response> post({
required String path,
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) {
return _dio.post(
path,
data: data,
queryParameters: queryParameters,
options:
options ??
Options(
method: HttpMethod.GET,
receiveTimeout: _defaultTimeout,
sendTimeout: _defaultTimeout,
),
cancelToken: cancelToken,
);
}
}Dio 拦截器
使用拦截器
import 'package:dio/dio.dart';
import 'package:my_bilibili/http/print_log_interceptor.dart';
import 'package:my_bilibili/http/resp_interceptor.dart';
import 'http_method.dart';
class DioInstance {
static DioInstance? _instance;
static const _defaultTimeout = Duration(seconds: 30);
final Dio _dio = Dio();
DioInstance._();
static DioInstance instance() {
return _instance ??= DioInstance._();
}
void initDio({
required String baseUrl,
String? method = HttpMethod.GET,
Duration connectTimeout = _defaultTimeout,
Duration receiveTimeout = _defaultTimeout,
Duration sendTimeout = _defaultTimeout,
ResponseType? responseType = ResponseType.json,
String? contentType,
}) {
_dio.options = BaseOptions(
method: method,
baseUrl: baseUrl,
connectTimeout: _defaultTimeout,
receiveTimeout: _defaultTimeout,
sendTimeout: _defaultTimeout,
responseType: responseType,
contentType: contentType,
);
// 添加打印请求拦截器
_dio.interceptors.add(PrintLogInterceptor());
// 添加统一返回值处理拦截器
_dio.interceptors.add(ResponseInterceptor());
}
// get 请求
Future<Response> get({
required String path,
Map<String, dynamic>? param,
Options? options,
CancelToken? cancelToken,
}) {
return _dio.get(
path,
queryParameters: param,
options:
options ??
Options(
method: HttpMethod.GET,
receiveTimeout: _defaultTimeout,
sendTimeout: _defaultTimeout,
),
cancelToken: cancelToken,
);
}
// post 请求
Future<Response> post({
required String path,
Object? data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) {
return _dio.post(
path,
data: data,
queryParameters: queryParameters,
options:
options ??
Options(
method: HttpMethod.GET,
receiveTimeout: _defaultTimeout,
sendTimeout: _defaultTimeout,
),
cancelToken: cancelToken,
);
}
}继承 InterceptorsWrapper 的拦截器
super 里面实际上调用了 handler.next
import 'dart:developer';
import 'package:dio/dio.dart';
class PrintLogInterceptor extends InterceptorsWrapper {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
log("\nrequest----------------------->");
options.headers.forEach((key, value) {
log("请求头信息: key = $key value = ${value.toString()}");
});
log("path: ${options.uri}");
log("method: ${options.method}");
log("data: ${options.data}");
log("queryParameters: ${options.queryParameters.toString()}");
log("<-----------------------request\n");
super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
log("\nresponse----------------------->");
log("path: ${response.realUri}");
log("headers: ${response.headers.toString()}");
log("statusMessage: ${response.statusMessage}");
log("statusCode: ${response.statusCode}");
log("extra: ${response.extra.toString()}");
log("data: ${response.data}");
log("<-----------------------response\n");
super.onResponse(response, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
log("\nonError----------------------->");
log("error: ${err.toString()}");
log("<-----------------------onError\n");
super.onError(err, handler);
}
}继承 Interceptor 的拦截器
使用 handler.next 衔接下一个
import 'package:dio/dio.dart';
import 'package:my_bilibili/http/base_model.dart';
import 'package:oktoast/oktoast.dart';
class ResponseInterceptor extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.statusCode == 200) {
try {
// errorCode = 0 代表执行成功,不建议依赖任何非0的 errorCode.
// errorCode = -1001 代表登录失效,需要重新登录。
var resp = BaseModel.fromJson(response.data);
if (resp.errorCode == 0) {
if (resp.data == null) {
handler.next(
Response(requestOptions: response.requestOptions, data: true),
);
} else {
handler.next(
Response(
requestOptions: response.requestOptions,
data: resp.data,
),
);
}
} else if (resp.errorCode == -1001) {
// 需要登录
handler.reject(
DioException(
requestOptions: response.requestOptions,
message: "未登录",
),
);
showToast("请先登录");
}
} catch (e) {
handler.reject(DioException(requestOptions: response.requestOptions, message: "$e"));
}
} else {
handler.reject(DioException(requestOptions: response.requestOptions));
}
}
}下拉刷新上拉加载组件
dependencies:
# 下拉刷新 上拉加载
pull_to_refresh: ^2.0.0SmartRefresher(
controller: refreshController,
enablePullUp: true,
enablePullDown: true,
header: ClassicHeader(),
footer: ClassicFooter(),
onLoading: () {
// 上拉加载回调
viewModel.LoadMoreListData(valueChanged: (_) {
refreshController.loadComplete();
});
},
onRefresh: () {
// 下拉刷新回调
Future.wait([viewModel.getBanner(), viewModel.initListData()]).then((value) {
refreshController.refreshCompleted();
});
},
child: SingleChildScrollView(
child: Column(
children: [
Consumer<HomeViewModel>(
builder: (context, vm, child) {
return _banner(vm);
},
),
Consumer<HomeViewModel>(
builder: (context, vm, child) {
return _listView(vm);
},
),
],
),
),
)EasyRefresh(
child: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
buildHeader(),
SliverToBoxAdapter(
child: Padding(
padding:
const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx(
() => Text(
'共${_controller.favListInfo.value.data?.info?.mediaCount}条视频',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Theme.of(context).colorScheme.outline,
letterSpacing: 1),
),
),
),
),
buildVideoList()
],
),
onLoad: () async {
_page++;
await _controller.loadFavList(
mediaId: widget.oid, pn: _page, ps: _pageSize);
}
)BottomNavigationBar+IndexedStack 实现底部导航栏((IndexedStack 的话 build 所有,切换不 build))
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:my_bilibili/pages/home/home_page.dart';
import 'package:my_bilibili/pages/hot_key/hot_key_page.dart';
import 'package:my_bilibili/pages/knowledge/knowledge_page.dart';
import 'package:my_bilibili/pages/personal/personal_page.dart';
class TabPage extends StatefulWidget {
const TabPage({super.key});
@override
State<StatefulWidget> createState() {
return _TabPageState();
}
}
class _TabPageState extends State<TabPage> {
int currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: IndexedStack(
index: currentIndex,
children: [HomePage(), HotKeyPage(), KnowledgePage(), PersonalPage()],
),
),
bottomNavigationBar: Theme( // 移除水波纹效果
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: BottomNavigationBar(
items: _barItemList(),
currentIndex: currentIndex,
type: BottomNavigationBarType.fixed,
selectedLabelStyle: TextStyle(fontSize: 14.sp, color: Colors.black),
unselectedLabelStyle: TextStyle(fontSize: 12.sp, color: Colors.blueGrey),
onTap: (index) {
// 点击切换页面
setState(() {
currentIndex = index;
});
},
),
),
);
}
List<BottomNavigationBarItem> _barItemList() {
return [
BottomNavigationBarItem(
// activeIcon: Icon(Icons.home_filled),
icon: Icon(Icons.home_filled),
label: "首页",
),
BottomNavigationBarItem(icon: Icon(Icons.play_circle_fill), label: "热点"),
BottomNavigationBarItem(icon: Icon(Icons.line_axis_rounded), label: "体系"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "我的"),
];
}
}BottomNavigationBar+PageView 实现底部导航栏(PageView 的话每次切换都会重新 build)
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class NavInfo {
final String title;
final IconData icon;
final IconData selectedIcon;
NavInfo({
required this.title,
required this.icon,
required this.selectedIcon,
});
}
class _MyHomePageState extends State<MyHomePage> {
// 控制器
late PageController _pageController;
late List<NavInfo> _navList;
late List<NavigationDestination> navigationItem;
late List<NavigationRailDestination> navRailItem;
int _currentPage = 0;
@override
void initState() {
super.initState();
_navList = [
NavInfo(
title: '音频',
icon: Icons.video_collection_outlined,
selectedIcon: Icons.video_collection,
),
NavInfo(
title: '音乐',
icon: Icons.library_music_outlined,
selectedIcon: Icons.library_music,
),
NavInfo(
title: '我的',
icon: Icons.account_box_outlined,
selectedIcon: Icons.account_box,
),
];
navigationItem = [
for (var itemData in _navList)
NavigationDestination(
icon: Icon(itemData.icon),
selectedIcon: Icon(itemData.selectedIcon),
label: itemData.title,
tooltip: itemData.title,
),
];
navRailItem = [
for (var itemData in _navList)
NavigationRailDestination(
icon: Icon(itemData.icon),
selectedIcon: Icon(itemData.selectedIcon),
label: Text(itemData.title),
),
];
_pageController = PageController();
}
@override
Widget build(BuildContext context) {
double wth = ScreenSize.getWindowsWidth(context);
print('windowsWidth ================> $wth');
return Scaffold(
body: PageView(
controller: _pageController,
onPageChanged: (int index) {
setState(() {
_currentPage = index;
});
},
scrollDirection:
wth > ScreenSize.normal ? Axis.vertical : Axis.horizontal,
children: const [VideoMusicPage(), BiliMusicPage(), UserInfoPage()],
),
bottomNavigationBar: NavigationBar(
destinations: navigationItem,
selectedIndex: _currentPage,
onDestinationSelected: (index) {
setState(() {
_currentPage = index;
_pageController.jumpToPage(index);
});
},
),
);
}
}封装底部导航栏
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:my_bilibili/common_ui/navigation/navigation_bar_item.dart';
class NavigationBarWidget extends StatefulWidget {
NavigationBarWidget({
super.key,
required this.pages,
required this.labels,
required this.icons,
required this.activeIcons,
required this.onTabChange,
this.currentIndex,
}) {
if (pages.length != labels.length ||
pages.length != icons.length ||
pages.length != activeIcons.length) {
throw Exception("数组长度必须一致!");
}
}
// 页面数组
final List<Widget> pages;
// 底部标题
final List<String> labels;
// 导航栏的 icon 数组,切换前
final List<Icon> icons;
// 导航栏的 icon 数组,切换后
List<Icon> activeIcons;
final ValueChanged<int> onTabChange;
int? currentIndex;
@override
State<StatefulWidget> createState() {
return _NavigationBarWidgetState();
}
}
class _NavigationBarWidgetState extends State<NavigationBarWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: IndexedStack(
index: widget.currentIndex ?? 0,
children: widget.pages,
),
),
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: BottomNavigationBar(
items: _barItemList(),
currentIndex: widget.currentIndex ?? 0,
type: BottomNavigationBarType.fixed,
selectedLabelStyle: TextStyle(fontSize: 14.sp, color: Colors.black),
unselectedLabelStyle: TextStyle(
fontSize: 12.sp,
color: Colors.blueGrey,
),
onTap: (index) {
widget.onTabChange.call(index);
// 点击切换页面
setState(() {
widget.currentIndex = index;
});
},
),
),
);
}
List<BottomNavigationBarItem> _barItemList() {
List<BottomNavigationBarItem> list = [];
for (int i = 0; i < widget.pages.length; i++) {
list.add(
BottomNavigationBarItem(
activeIcon: NavigationBarItem(
builder: (context) {
return widget.activeIcons[i];
},
),
icon: widget.icons[i],
label: widget.labels[i],
),
);
}
return list;
}
}NavigationBarItem 实现图标放大
import 'package:flutter/material.dart';
// 用以实现图标放大的效果
class NavigationBarItem extends StatefulWidget {
const NavigationBarItem({super.key, required this.builder});
final WidgetBuilder builder;
@override
State<StatefulWidget> createState() => _NavigationBarItemState();
}
class _NavigationBarItemState extends State<NavigationBarItem> with TickerProviderStateMixin {
late AnimationController controller;
late Animation<double> animation;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(milliseconds: 300), vsync: this);
controller.forward();
animation = Tween<double>(begin: 0.7, end: 1).animate(controller);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(scale: animation, child: widget.builder(context),);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}使用封装的组件
Widget build(BuildContext context) {
return NavigationBarWidget(
pages: pages,
labels: labels,
icons: icons,
activeIcons: activeIcons,
currentIndex: 0,
onTabChange: (int index) {
print(index);
},
);
}使用 GridView
Widget _gridView({
required NullableIndexedWidgetBuilder builder,
required int itemCount,
}) {
return Container(
margin: EdgeInsets.only(top: 20.h),
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: GridView.builder(
// 把滑动事件交给 SingleChildScrollView
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
// 最大横轴范围
maxCrossAxisExtent: 120.w,
// 主轴的间隔
mainAxisSpacing: 8.r,
// 宽高比
childAspectRatio: 3,
// 横轴间隔
crossAxisSpacing: 8.r,
),
itemBuilder: builder,
itemCount: itemCount,
),
);
}使用 SharedPreferences 本地化存储
# 本地化存储
shared_preferences: ^2.0.15本地持久化存储工具类
import 'package:shared_preferences/shared_preferences.dart';
// 本地持久化存储工具类
class SpUtils {
SpUtils._();
static Future<bool> saveStringList(String key, List<String> values) async {
final sp = await SharedPreferences.getInstance();
return sp.setStringList(key, values);
}
static Future<List<String>?> getStringList(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.getStringList(key);
}
static Future<bool> saveBool(String key, bool value) async {
final sp = await SharedPreferences.getInstance();
return sp.setBool(key, value);
}
static Future<bool?> getBool(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.getBool(key);
}
static Future<bool> saveInt(String key, int value) async {
final sp = await SharedPreferences.getInstance();
return sp.setInt(key, value);
}
static Future<int?> getInt(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.getInt(key);
}
static Future<bool> saveString(String key, String value) async {
final sp = await SharedPreferences.getInstance();
return sp.setString(key, value);
}
static Future<String?> getString(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.getString(key);
}
static Future<bool> saveDouble(String key, double value) async {
final sp = await SharedPreferences.getInstance();
return sp.setDouble(key, value);
}
static Future<double?> getDouble(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.getDouble(key);
}
static Future<bool> remove(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.remove(key);
}
static Future<bool> removeAll(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.clear();
}
static Future<dynamic> getDynamic(String key) async {
final sp = await SharedPreferences.getInstance();
return sp.get(key);
}
}在 Interceptor 中 保存 cookie 设置 cookie
更好的实现
Future<void> initCookieJar() async {
final Directory appDocumentsDir = await getApplicationDocumentsDirectory();
final mCookieJar = PersistCookieJar(
storage: FileStorage("${appDocumentsDir.path}/user/cookies"),
ignoreExpires: true,
);
// WEB需要移除,Web交由浏览器自行管理Cookie
if(!kIsWeb){
dioClient.interceptors.add(CookieManager(mCookieJar));
_cookieJar = mCookieJar;
}
}Details
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:my_bilibili/contants.dart';
import 'package:my_bilibili/utils/sp_utils.dart';
class CookieInterceptor extends Interceptor {
bool isLogin(RequestOptions options) => options.path.contains("user/login");
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (!isLogin(options)) {
SpUtils.getStringList(Constants.spCookieList).then((cookieList) {
options.headers[HttpHeaders.cookieHeader] = cookieList;
handler.next(options);
});
} else {
handler.next(options);
}
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (isLogin(response.requestOptions)) {
// 取出 cookie 信息
dynamic list = response.headers[HttpHeaders.setCookieHeader];
List<String> cookieList = [];
if (list is List) {
for (String? cookie in list) {
log("CookieInterceptor cookie= ${cookie.toString()}");
cookieList.add(cookie ?? "");
}
SpUtils.saveStringList(Constants.spCookieList, cookieList);
}
}
super.onResponse(response, handler);
}
}TabBar 和 TabBarView 示例
import 'package:flutter/material.dart';
import 'package:my_bilibili/pages/knowledge/detail/knowledge_detail_child_tab.dart';
import 'package:my_bilibili/pages/knowledge/detail/knowledge_detail_vm.dart';
import 'package:my_bilibili/repository/datas/knowledge_list_data.dart';
import 'package:provider/provider.dart';
class KnowledgeDetailTabPage extends StatefulWidget {
const KnowledgeDetailTabPage({super.key, this.tabList});
final List<KnowledgeChildren>? tabList;
@override
State<StatefulWidget> createState() {
return _KnowledgeDetailTabPageState();
}
}
class _KnowledgeDetailTabPageState extends State<KnowledgeDetailTabPage>
with SingleTickerProviderStateMixin {
KnowledgeDetailViewModel viewModel = KnowledgeDetailViewModel();
late TabController tabController;
@override
void initState() {
super.initState();
viewModel.initTabs(widget.tabList);
tabController = TabController(
length: widget.tabList?.length ?? 0,
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) {
return viewModel;
},
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: TabBar(
tabs: viewModel.tabs,
controller: tabController,
labelColor: Colors.blue,
indicatorColor: Colors.blue,
isScrollable: true,
),
),
body: TabBarView(controller: tabController, children: _children()),
),
),
);
}
List<Widget> _children() {
return widget.tabList?.map((e) {
return KnowledgeDetailChildTab(cid: "${e.id}");
}).toList() ?? [];
}
}