Flutter应用实现双标签Tab Bar并对接Firestore数据库求助
实现方案
基于你现有的代码完成双Tab页改造后的完整可运行代码如下:
import 'package:chat_app/group_chats/group_info.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import '../constants.dart'; import '../global_data.dart'; class FinancialRoom extends StatefulWidget { final String groupChatId, groupName; const FinancialRoom({required this.groupName, required this.groupChatId, Key? key}) : super(key: key); @override State<FinancialRoom> createState() => _FinancialRoomState(); } class _FinancialRoomState extends State<FinancialRoom> with SingleTickerProviderStateMixin { late TabController _tabController; final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseAuth _auth = FirebaseAuth.instance; final _formKey = GlobalKey<FormState>(); TextEditingController amountValue = TextEditingController(); @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); } @override void dispose() { _tabController.dispose(); amountValue.dispose(); super.dispose(); } void onSend() async { Map<String, dynamic> data = { "amount": amountValue.text, "sendBy": _auth.currentUser!.displayName, "type": "text", "time": FieldValue.serverTimestamp(), }; amountValue.clear(); await _firestore .collection('groups') .doc(widget.groupChatId) .collection('chats') .add(data); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.groupName), bottom: TabBar( controller: _tabController, tabs: const [ Tab(text: 'Need Help'), Tab(text: 'Help Requests'), ], ), actions: [ IconButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => GroupInfo( groupName: widget.groupName, groupId: widget.groupChatId, ), ), ), icon: const Icon(Icons.more_vert)), ], ), body: SafeArea( child: TabBarView( controller: _tabController, children: [ // 第一个Tab:数据上传表单页 Padding( padding: const EdgeInsets.all(20.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ const SizedBox(height: 10.0), TextFormField( controller: amountValue, decoration: InputDecoration( hintText: 'Enter the amount you want', labelText: 'Enter the amount you want', prefixIcon: const Icon(Icons.account_balance_wallet_outlined), enabledBorder: kEnabledBorder, focusedBorder: kFocusedBorder, errorBorder: kErrorBorder, focusedErrorBorder: kErrorBorder, ), validator: (value) { if (value!.isEmpty) { return 'Please enter the amount you want'; } return null; }, ), const SizedBox(height: kInputSpacing), SizedBox( width: double.infinity, child: TextButton( style: TextButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0)), backgroundColor: Colors.blue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16.0), ), onPressed: () { if (_formKey.currentState!.validate()) { onSend(); } }, child: Text( 'SEND', style: kButtonTextStyle, ), ), ), ], ), ), ), // 第二个Tab:数据拉取展示页 StreamBuilder<QuerySnapshot>( stream: _firestore .collection('groups') .doc(widget.groupChatId) .collection('chats') .orderBy('time') .snapshots(), builder: (context, snapshot) { if (snapshot.hasData) { final Size size = MediaQuery.of(context).size; return ListView.builder( itemCount: snapshot.data!.docs.length, itemBuilder: (context, index) { Map<String, dynamic> data = snapshot.data!.docs[index].data() as Map<String, dynamic>; return messageTile(size, data); }, ); } else { return const Center(child: CircularProgressIndicator()); } }, ) ], ), ), ); } Widget messageTile(Size size, Map<String, dynamic> data) { return Builder(builder: (_) { if (data['type'] == "text") { return Container( width: size.width, alignment: data['sendBy'] == _auth.currentUser!.displayName ? Alignment.centerRight : Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 14), margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: Colors.blue, ), child: Column( children: [ Text( data['sendBy'], style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white, ), ), SizedBox( height: size.height / 200, ), Text( data['amount'], style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, ), ), ], )), ); } else if (data['type'] == "img") { return Container( width: size.width, alignment: data['sendBy'] == _auth.currentUser!.displayName ? Alignment.centerRight : Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 8), height: size.height / 2, child: Image.network( data['amount'], ), ), ); } else if (data['type'] == "notify") { return Container( width: size.width, alignment: Alignment.center, child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), color: Colors.black38, ), child: Text( data['message'], style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ); } else { return const SizedBox(); } }); } }
核心修改说明
- 原无状态组件改为有状态组件,混入
SingleTickerProviderStateMixin实现TabController的初始化,适配TabBar的切换动画需求 - AppBar新增bottom属性配置TabBar,包含你需要的「Need Help」和「Help Requests」两个标签
- 原金额提交表单放在第一个Tab页面,Firestore数据拉取列表放在第二个Tab页面,完全复用你原有业务逻辑
- 修正了原代码中
_formKey绑定错误的问题:原代码把表单key绑定到Container上,现已改为绑定到Form组件上,保证表单验证逻辑正常生效 - 替换了Flutter 3.0+已废弃的
FlatButton为TextButton,兼容新版本运行 - 移除了原布局中嵌套ListView的结构,改用TabBarView承载两个页面,避免滑动冲突和布局溢出问题
内容的提问来源于stack exchange,提问作者Deep Shah
相关产品推荐
相关产品推荐

