PageView是Flutter中的一个可滑动的页面切换组件,可以让用户通过左右或者上下滑动来切换不同页面的页面。这里我们主要介绍一个案例,由PageView.builder实现,类似抖音的上下滑动视频的效果。
效果图

核心滑动组件
Dart
PageView.builder(
controller: _pageController,
scrollDirection: Axis.vertical, // 垂直滑动
itemCount: RoleList.length,//总页数
onPageChanged: (int index) { //页面切换回调
setState(() {
_currentIndex = index; //更新当前页索引
});
},
itemBuilder: (context, index) { //懒加载创建页面
return VideoPlayerItem(
roleModel: RoleList[index],
isActive: _currentIndex == index, // 只有当前视频才播放
);
},
),
核心数据模型
Dart
class RoleModel {
final String id; // 唯一标识(重要:用于区分不同角色)
final String name; // 角色名称(显示用)
final String description; // 角色描述(副标题)
final String prompt; // 角色介绍(显示在气泡中)
String imageUrl; // 背景图片URL
DateTime createdAt; // 创建时间(元数据)
DateTime updatedAt; // 更新时间(元数据)
}
PageView的工作原理
Dart
┌─────────────────────────────────────────────┐
│ 可见区域 (Viewport) │
│ ┌─────────────────────────────────────┐ │
│ │ 当前显示的页面 │ │
│ │ (index = 2) │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
↑ ↓
┌─────────┐ ┌─────────┐
│ 页面 1 │ │ 页面 3 │
│(预加载) │ │(预加载) │
└─────────┘ └─────────┘
↑ ↑
┌─────────────────────────────────┐
│ PageView 缓存机制 │
│ - 默认缓存前后各1页 │
│ - 滑动时动态创建/销毁 │
└─────────────────────────────────┘
数据流向图
Dart
用户滑动屏幕
↓
PageView 检测手势
↓
触发 onPageChanged 回调
↓
setState(() { _currentIndex = index })
↓
重建 PageView.builder
↓
itemBuilder 根据新 index 创建页面
↓
传递 isActive 参数(_currentIndex == index)
↓
子页面根据 isActive 决定是否执行资源消耗操作
实现步骤
1.创建数据模型
Dart
///角色类
class RoleModel {
final String id;
final String description;
String imageUrl;
String name;
String prompt;
DateTime createdAt;
DateTime updatedAt;
RoleModel({
required this.id,
required this.description,
required this.imageUrl,
required this.name,
required this.prompt,
required this.createdAt,
required this.updatedAt,
});
// 从 JSON 创建对象
factory RoleModel.fromJson(Map<String, dynamic> json) {
return RoleModel(
id: json["id"] ?? "",
description: json["description"] ?? "",
imageUrl:json["image_url"] ?? "",
name: json["name"] ?? "",
prompt: json["prompt"] ?? "",
createdAt: DateTime.tryParse(json["created_at"] ?? "") ?? DateTime.now(),
updatedAt: DateTime.tryParse(json["updated_at"] ?? "") ?? DateTime.now(),
);
}
// 转换为 JSON
Map<String, dynamic> toJson() {
return {
"id": id,
"description": description,
"image_url":imageUrl,
"name":name,
"prompt":prompt,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
};
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is RoleModel && other.id == id;
}
@override
int get hashCode => id.hashCode;
}
2.创建主页的主要框架
Dart
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
// 1. 声明数据变量
late List<RoleModel> roleList = []; // 角色列表数据
late PageController _pageController; // 页面控制器
int _currentIndex = 0; // 当前页面索引
// 2. 初始化方法
@override
void initState() {
super.initState();
_pageController = PageController(initialPage: _currentIndex);
_loadRoleData(); // 加载数据
}
// 3. 释放资源
@override
void dispose() {
_pageController.dispose(); // ⚠️ 必须释放
super.dispose();
}
// 4. 加载数据方法(待实现)
Future<void> _loadRoleData() async {
// 将在第三步实现
}
// 5. 构建UI(待实现)
@override
Widget build(BuildContext context) {
return Scaffold(
// 将在第四步实现
);
}
}
3.准备数据
Dart
List<RoleModel> _getMockData() {
return [
RoleModel(
id: "1",
name: "温柔女友",
description: "善解人意的知心伴侣",
prompt: "你好呀!今天过得怎么样?",
imageUrl: "https://picsum.photos/id/20/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
RoleModel(
id: "2",
name: "知心姐姐",
description: "人生导师,为你答疑解惑",
prompt: "生活中有遇到什么困难吗?",
imageUrl: "https://picsum.photos/id/24/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
// 添加更多角色...
];
}
4.实现核心滑动功能
Dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
leading: IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: Icon(Icons.arrow_back_ios)
),
title: Column(
children: [
Text(practicePartner),
Text("滑动切换角色", style: TextStyle(fontSize: 10)),
],
),
centerTitle: true,
),
backgroundColor: Color(0xFFF3F3F3),
body: PageView.builder(
controller: _pageController,
scrollDirection: Axis.vertical, // 垂直滑动
itemCount: RoleList.length,//总页数
onPageChanged: (int index) { //页面切换回调
setState(() {
_currentIndex = index; //更新当前页索引
});
},
itemBuilder: (context, index) { //懒加载创建页面
return VideoPlayerItem(
roleModel: RoleList[index],
isActive: _currentIndex == index, // 只有当前视频才播放
);
},
),
);
}
5.创建子页面组件
Dart
class VideoPlayerItem extends StatefulWidget {
final RoleModel roleModel;
final bool isActive;
const VideoPlayerItem({
Key? key,
required this.roleModel,
required this.isActive,
}) : super(key: key);
@override
State<VideoPlayerItem> createState() => _VideoPlayerItemState();
}
class _VideoPlayerItemState extends State<VideoPlayerItem> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(widget.roleModel.imageUrl,), //背景图 图片未完善,先不使用图片
fit: BoxFit.cover
),
),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
//聊天列表 - 只显示最新消息
Expanded(child: Container()),
Container(//角色介绍
padding: const EdgeInsets.symmetric(horizontal: 10,vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF2187EF).withOpacity(0.8),borderRadius: BorderRadius.circular(15)
),
child: RichText(
text: TextSpan(
children: [
WidgetSpan(child: Image.asset("assets/images/banana.png",scale: 8.9,)),
const TextSpan(text: ' '), //空格
TextSpan(
text: widget.roleModel.prompt,
style: TextStyle(color: Colors.white)
)
]
),
)
),
const SizedBox(height: 5,),
Row(
children: [
//名称和介绍
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("@${widget.roleModel.name}",style: TextStyle(color: Colors.white,fontSize: 15,fontWeight: FontWeight.bold),),
Text("${widget.roleModel.description}",style: TextStyle(color: Colors.black.withOpacity(0.6),fontSize: 9)),
],
),
const Spacer(),
],
),
const SizedBox(height: 10,),
],
),
)
)
);
}
}
代码实例
Dart
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:async';
import 'package:my_flutter/role_model.dart';
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage>{
String practicePartner = "AI 语音助手";
late PageController _pageController;
int _currentIndex = 0;
// 角色列表数据
late List<RoleModel> RoleList = [];
@override
void initState() {
super.initState();
_pageController = PageController(initialPage: _currentIndex);
initRoleList();
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
Future<void> initRoleList() async{
// 模拟从服务器获取数据
await Future.delayed(Duration(milliseconds: 500));
setState(() {
RoleList = getMockRoleList();
});
}
// 提供模拟数据
List<RoleModel> getMockRoleList() {
return [
RoleModel(
id: "1",
name: "温柔女友",
description: "善解人意的知心伴侣,随时倾听你的心声",
prompt: "你好呀!我是你的温柔女友,今天过得怎么样?有什么想和我聊聊的吗?我会一直陪在你身边的~",
imageUrl: "https://picsum.photos/id/20/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
RoleModel(
id: "2",
name: "知心姐姐",
description: "人生导师,为你答疑解惑,指引方向",
prompt: "妹妹/弟弟,生活中有遇到什么困难吗?姐姐可以给你一些建议哦。记得要保持积极乐观的心态!",
imageUrl: "https://picsum.photos/id/24/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
RoleModel(
id: "3",
name: "专业导师",
description: "职场专家,助你提升技能,实现梦想",
prompt: "欢迎来到职场课堂。今天我们来聊聊如何提升专业技能,你目前在职业发展上有什么困惑吗?",
imageUrl: "https://picsum.photos/id/26/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
RoleModel(
id: "4",
name: "开心果",
description: "幽默风趣,为你带来欢乐和正能量",
prompt: "嘿嘿,今天又有什么开心事啊?让我给你讲个笑话:为什么程序员总是分不清万圣节和圣诞节?因为 Oct 31 = Dec 25!哈哈哈!",
imageUrl: "https://picsum.photos/id/29/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
RoleModel(
id: "5",
name: "健身教练",
description: "专业私教,带你科学运动,塑造完美身材",
prompt: "坚持就是胜利!今天的目标是100个俯卧撑,你准备好了吗?健身不仅能改变体型,更能磨练意志!",
imageUrl: "https://picsum.photos/id/28/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
RoleModel(
id: "6",
name: "心理顾问",
description: "心理健康专家,帮你缓解压力,找回平静",
prompt: "深呼吸,放松身心。你最近的睡眠质量如何?让我教你一个缓解压力的冥想方法。",
imageUrl: "https://picsum.photos/id/30/400/600",
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
leading: IconButton(
onPressed: (){
Navigator.pop(context);
},
icon: Icon(Icons.arrow_back_ios)
),
title: Column(
children: [
Text(practicePartner),
Text("滑动切换角色", style: TextStyle(fontSize: 10)),
],
),
centerTitle: true,
),
backgroundColor: Color(0xFFF3F3F3),
body: PageView.builder(
controller: _pageController,
scrollDirection: Axis.vertical, // 垂直滑动
itemCount: RoleList.length,//总页数
onPageChanged: (int index) { //页面切换回调
setState(() {
_currentIndex = index; //更新当前页索引
});
},
itemBuilder: (context, index) { //懒加载创建页面
return VideoPlayerItem(
roleModel: RoleList[index],
isActive: _currentIndex == index, // 只有当前视频才播放
);
},
),
);
}
}
//===========================子页面详情===================================
class VideoPlayerItem extends StatefulWidget {
final RoleModel roleModel;
final bool isActive;
const VideoPlayerItem({
Key? key,
required this.roleModel,
required this.isActive,
}) : super(key: key);
@override
State<VideoPlayerItem> createState() => _VideoPlayerItemState();
}
class _VideoPlayerItemState extends State<VideoPlayerItem> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(widget.roleModel.imageUrl,), //背景图 图片未完善,先不使用图片
fit: BoxFit.cover
),
),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
//聊天列表 - 只显示最新消息
Expanded(child: Container()),
Container(//角色介绍
padding: const EdgeInsets.symmetric(horizontal: 10,vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF2187EF).withOpacity(0.8),borderRadius: BorderRadius.circular(15)
),
child: RichText(
text: TextSpan(
children: [
WidgetSpan(child: Image.asset("assets/images/banana.png",scale: 8.9,)),
const TextSpan(text: ' '), //空格
TextSpan(
text: widget.roleModel.prompt,
style: TextStyle(color: Colors.white)
)
]
),
)
),
const SizedBox(height: 5,),
Row(
children: [
//名称和介绍
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("@${widget.roleModel.name}",style: TextStyle(color: Colors.white,fontSize: 15,fontWeight: FontWeight.bold),),
Text("${widget.roleModel.description}",style: TextStyle(color: Colors.black.withOpacity(0.6),fontSize: 9)),
],
),
const Spacer(),
],
),
const SizedBox(height: 10,),
],
),
)
)
);
}
}