1、先说StatelessWidget和StatefulWidget之争
我记得最初自己生啃flutter的时候,只是那些晦涩难懂的“尽量使用StatelessWidget”“StatefulWidget适合有状态的组件,StatelessWidget适合无状态的组件”——到底啥是所谓的“有状态”“无状态”?
当然我现在也理解不完全啊,只是知道类似于Text之类的组件是StatelessWidget,如果需要改变一些东西就用外部状态(例如provider的notifyListeners())刷新;如果你对这个组件有什么刚开始就要干什么事情的需求,那就要用StatefulWidget。
大哥们如果认为这是错的,我错了!轻喷
2、先从main.dart跳转到那个新的页面
上代码!
main.dart
import 'package:day06/detail_page.dart';
import 'package:day06/main_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_styled_toast/flutter_styled_toast.dart';
import 'package:provider/provider.dart';
import 'detail_provider.dart';
void main() {
runApp(
MultiProvider(
providers: [ChangeNotifierProvider(create: (context) => MainProvider()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
// home: const DetailPage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
final provider = Provider.of<MainProvider>(context);
return Scaffold(
appBar: AppBar(
backgroundColor: Theme
.of(context)
.colorScheme
.inversePrimary,
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(provider.text),
ElevatedButton(
onPressed: provider.getList,
child: const Text('请求数据'),
),
ElevatedButton(
onPressed: () => provider.addContact(),
child: const Text('添加一个联系人'),
),
Expanded(
child: RefreshIndicator(
onRefresh: () async {
// 刷新时重置页码并重新加载数据
provider.resetPage();
await provider.getListPage(provider.pageNum);
},
child: NotificationListener<ScrollNotification>(
onNotification: (scrollInfo){
if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
// 滚动到底部,加载下一页
if(!provider.isLoading && !provider.finalPage){
provider.addPage();
provider.getListPage(provider.pageNum);
}
}
return false;
},
child: ListView.builder(
itemCount:provider.dataList.isEmpty? 0 : provider.dataList.length + 1,
itemBuilder: (context, index) {
if (index < provider.dataList.length) {
return ListTile(
title: Text('${provider.dataList[index].lastName} ${provider.dataList[index].firstName}'),
subtitle: Text('${provider.dataList[index].phoneNumber}'),
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DetailPage(id: provider.dataList[index].id),
),
);
},
);
} else {
// 显示加载提示
return Center(child: provider.finalPage
? Text('没有更多数据了')
: CircularProgressIndicator()
);
}
},
),
),
)
)
],
),
),
);
}
Future<dynamic> showADialog(context,title,content){
return showDialog(
context: context,
builder: (context)=>AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(3),
child: Text('某种中立按钮?'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(2),
child: Text('取消'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(1),
child: Text('确定'),
),
],
)
);
}
}
重点在这里:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DetailPage(id: provider.dataList[index].id),
),
);
这一截意思就是说用Navigator.of(context).push()跳转,MaterialPageRoute()控制一些跳转时的参数(具体控制什么自行问搜索引擎),然后在builder属性里new出新的DetailPage页面,并且要传入一个id参数(也就是联系人的id)。如果不想考虑这么多,直接抄就对了!
那么新的DetailPage现在长啥样呢?
detail_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'detail_provider.dart';
class DetailPage extends StatefulWidget {
final int? id;
const DetailPage({super.key, this.id});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('详情'),
),
body: Column(
children: [
],
),
);
}
@override
void dispose() {
super.dispose();
}
}
这里用StatefulWidget是因为一会要在进来页面的时候要加载一些东西——也就是需要这个页面的生命周期啦!
3、然后再往里面填充一点文本框?整个返回按钮?
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'detail_provider.dart';
class DetailPage extends StatefulWidget {
final int? id;
const DetailPage({super.key, this.id});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('详情'),
leading: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: const Icon(Icons.arrow_back_ios),
),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '姓',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '名',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '年龄',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '性别',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '联系电话',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '地址',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}
这样页面也有了,返回按钮也有了,点下那个返回按钮就会执行Navigator.of(context).pop()返回上一级页面。
4、现在页面好了,数据呢?不得准备一下?
detail_provider.dart
import 'package:day06/bean/contact_bean.dart';
import 'package:flutter/material.dart';
import 'bean/base_bean.dart';
import 'dio_util.dart';
class DetailProvider with ChangeNotifier {
final TextEditingController firstName = TextEditingController();
final TextEditingController lastName = TextEditingController();
final TextEditingController age = TextEditingController();
final TextEditingController sex = TextEditingController();
final TextEditingController phoneNumber = TextEditingController();
final TextEditingController address = TextEditingController();
Future<void> getDetail(int id) async {
await DioUtil.getInstance().get(
"http://192.168.5.116:20080/contact/detail",
{'id': id},
() {
},
(data) {
BaseBean bean = BaseBean.fromJson(data);
var contact=ContactBean.fromJson(bean.data);
firstName.text=contact.firstName??'';
lastName.text=contact.lastName??'';
age.text=contact.age.toString();
sex.text=contact.sex==1?'男':'女';
phoneNumber.text=contact.phoneNumber??'';
address.text=contact.address??'';
notifyListeners();
},
(reason) {
notifyListeners();
},
);
}
@override
void dispose() {
firstName.dispose();
lastName.dispose();
age.dispose();
sex.dispose();
phoneNumber.dispose();
address.dispose();
// TODO: implement dispose
super.dispose();
}
}
这样数据不就来了嘛~
首先建了一堆TextEditingController,用于绑定前面页面的TextFormField——这样就能获取到UI那边输入的字和给UI赋值了。然后用getDetail传一个id参数请求接口返回来给TextEditingController赋值从而让前端显示。
然后TextEditingController需要销毁,所以需要重写dispose()方法,在super.dispose()前手动执行一下各个TextEditingController的dispose()方法。
5、让数据回到页面!
接下来就是该绑定了,但是接口在哪调用呢?它讲道理是打开页面后就调用啊~总不可能点进详情还要再按个按钮吧。
还有这是个详情页面,我不想让这些输入框可以编辑。
那就改一下吧
detail_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'detail_provider.dart';
class DetailPage extends StatefulWidget {
final int? id;
const DetailPage({super.key, this.id});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
@override
void initState() {
super.initState();
final detailProvider = Provider.of<DetailProvider>(context, listen: false);
if (widget.id != null) {
detailProvider.getDetail(widget.id!);
}
}
@override
Widget build(BuildContext context) {
final detailProvider = Provider.of<DetailProvider>(context);
return Scaffold(
appBar: AppBar(
title: const Text('详情'),
leading: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: const Icon(Icons.arrow_back_ios),
),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.lastName,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '姓',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.firstName,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '名',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.age,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '年龄',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.sex,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '性别',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.phoneNumber,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '联系电话',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.address,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '地址',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
],
),
);
}
@override
void dispose() {
super.dispose();
}
}
至于输入框改为不可编辑的问题,其实这里有两种办法:enabled和readOnly,前者为false的话真就只能看,颜色置灰;但后者虽然不能编辑但颜色是正常的,甚至可以复制。所以再这边用的是readOnly。
代码里所有的TextFormField都绑定了来自于Provider的属于各自的controller,这样就可以正常显示各自的文字了。
至于如何在打开页面的时候就调用接口拿数据,那就得重写_DetailPageState的initState()方法。
@override
void initState() {
super.initState();
final detailProvider = Provider.of<DetailProvider>(context, listen: false);
if (widget.id != null) {
detailProvider.getDetail(widget.id!);
}
}
那就该有人问了:为啥又在这里引用了provider?做全局变量不好吗?
看见那个listen: false了没?写了这个就可以不用监听状态变化,我在这初始化数据为啥要监听状态变化呢对吧。省的没事全局触发build()导致额外无用的刷新。
整到现在应该是能看没啥问题了
6、新的问题来了:怎么看详情还能看出残影?
现在点进去第一个,加载正常,返回来,再点进去第二个,怎么会出现第一个的数据???
那就得有个清理的方法插在provider里了:
void clearData() {
lastName.clear();
firstName.clear();
age.clear();
sex.clear();
phoneNumber.clear();
address.clear();
notifyListeners(); // 通知监听器状态改变
}
插进去方法就在返回的时候清理掉:
第一个注意的地方就是按下页面左上角返回按钮的时候
第二个就是Android的返回键(谁让我是个安卓开发)
所以UI代码现在就变成这样:
detail_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'detail_provider.dart';
class DetailPage extends StatefulWidget {
final int? id; // 示例参数,可按需修改
const DetailPage({super.key, this.id});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
@override
void initState() {
super.initState();
final detailProvider = Provider.of<DetailProvider>(context, listen: false);
if (widget.id != null) {
detailProvider.getDetail(widget.id!);
}
}
@override
Widget build(BuildContext context) {
final detailProvider = Provider.of<DetailProvider>(context);
return PopScope(
onPopInvokedWithResult: (didPop, result) {
detailProvider.clearData();
},
child: Scaffold(
appBar: AppBar(
title: const Text('详情'),
leading: GestureDetector(
onTap: () {
detailProvider.clearData();
Navigator.of(context).pop();
},
child: const Icon(Icons.arrow_back_ios),
),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 已有输入框
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.lastName,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '姓',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.firstName,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '名',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.age,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '年龄',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.sex,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '性别',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.phoneNumber,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '联系电话',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
child: TextFormField(
readOnly: true,
controller: detailProvider.address,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '地址',
floatingLabelBehavior: FloatingLabelBehavior
.auto, // 确保提示文字不消失
),
),
),
],
),
)
);
}
@override
void dispose() {
super.dispose();
}
}
第一个是在返回按钮(appBar的leading属性里)那手动调用一下clearData()方法,第二个监听安卓返回按钮的办法就是用PopScope包裹Scaffold,在PopScope的onPopInvokedWithResult函数里手动清理一下也就OK了。
7、到最后了,送个状态呗~给个加载中状态和加载失败了展示原因和刷新
不多啰嗦上代码!
detail_pge.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'detail_provider.dart';
class DetailPage extends StatefulWidget {
final int? id; // 示例参数,可按需修改
const DetailPage({super.key, this.id});
@override
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
@override
void initState() {
super.initState();
final detailProvider = Provider.of<DetailProvider>(context, listen: false);
// 调用网络请求,这里假设 DetailProvider 有 fetchData 方法
if (widget.id != null) {
detailProvider.getDetail(widget.id!);
}
}
@override
Widget build(BuildContext context) {
final detailProvider = Provider.of<DetailProvider>(context);
return PopScope(
onPopInvokedWithResult: (didPop, result) {
detailProvider.clearData();
},
child: Scaffold(
appBar: AppBar(
title: const Text('详情'),
leading: GestureDetector(
onTap: () {
detailProvider.clearData();
Navigator.of(context).pop();
},
child: const Icon(Icons.arrow_back_ios),
),
),
body: Center(
child: detailProvider.isLoading
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text('正在加载...'),
SizedBox(height: 16),
CircularProgressIndicator(),
],
)
: detailProvider.error != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(detailProvider.error!),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (widget.id != null) {
detailProvider.getDetail(widget.id!);
}
},
child: const Text('点击刷新'),
),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 已有输入框
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: TextFormField(
readOnly: true,
controller: detailProvider.lastName,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '姓',
floatingLabelBehavior:
FloatingLabelBehavior.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: TextFormField(
readOnly: true,
controller: detailProvider.firstName,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '名',
floatingLabelBehavior:
FloatingLabelBehavior.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: TextFormField(
readOnly: true,
controller: detailProvider.age,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '年龄',
floatingLabelBehavior:
FloatingLabelBehavior.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: TextFormField(
readOnly: true,
controller: detailProvider.sex,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '性别',
floatingLabelBehavior:
FloatingLabelBehavior.auto, // 确保提示文字不消失
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: TextFormField(
readOnly: true,
controller: detailProvider.phoneNumber,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '联系电话',
floatingLabelBehavior:
FloatingLabelBehavior.auto, // 确保提示文字不消失
),
),
),
// 新增输入框
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
),
child: TextFormField(
readOnly: true,
controller: detailProvider.address,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: '地址',
floatingLabelBehavior:
FloatingLabelBehavior.auto, // 确保提示文字不消失
),
),
),
],
),
),
),
);
}
@override
void dispose() {
super.dispose();
}
}
detail_provider.dart
import 'package:day06/bean/contact_bean.dart';
import 'package:flutter/material.dart';
import 'bean/base_bean.dart';
import 'dio_util.dart';
class DetailProvider with ChangeNotifier {
final TextEditingController firstName = TextEditingController();
final TextEditingController lastName = TextEditingController();
final TextEditingController age = TextEditingController();
final TextEditingController sex = TextEditingController();
final TextEditingController phoneNumber = TextEditingController();
final TextEditingController address = TextEditingController();
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _error;
String? get error => _error;
Future<void> getDetail(int id) async {
_error = null;
await DioUtil.getInstance().get(
"http://192.168.5.116:20080/contact/detail",
{'id': id},
() {
_isLoading = true;
notifyListeners();
},
(data) {
_isLoading = false;
BaseBean bean = BaseBean.fromJson(data);
var contact = ContactBean.fromJson(bean.data);
firstName.text = contact.firstName ?? '';
lastName.text = contact.lastName ?? '';
age.text = contact.age.toString();
sex.text = contact.sex == 1 ? '男' : '女';
phoneNumber.text = contact.phoneNumber ?? '';
address.text = contact.address ?? '';
notifyListeners();
},
(reason) {
_isLoading = false;
_error = reason;
notifyListeners();
},
);
}
void clearData() {
lastName.clear();
firstName.clear();
age.clear();
sex.clear();
phoneNumber.clear();
address.clear();
notifyListeners(); // 通知监听器状态改变
}
@override
void dispose() {
firstName.dispose();
lastName.dispose();
age.dispose();
sex.dispose();
phoneNumber.dispose();
address.dispose();
// TODO: implement dispose
super.dispose();
}
}
没啥可说的,就是加了个isLoading和error,当前端isLoading的时候转圈,转完了有数据展示数据,报错就展示错误原因呗~