Browse Source
feat(discover): 实现相亲与聚会脱单直播页面
feat(discover): 实现相亲与聚会脱单直播页面
- 新增相亲页面(DatingPage)和聚会脱单页面(PartyPage) - 实现RTC频道分页列表接口(getRtcChannelPage) - 创建发现页控制器(DiscoverController)管理频道数据 - 添加直播项组件(LiveItemWidget)用于展示频道列表 - 更新主页发现页结构,使用PageView切换相亲与聚会页面 - 修改直播间主播展示逻辑,优化RTC用户信息处理 - 完善RTC Manager中的远程用户管理逻辑 - 重构房间控制器中的角色管理和用户信息同步逻辑ios
13 changed files with 750 additions and 306 deletions
Unified View
Diff Options
-
19lib/components/home_appbar.dart
-
68lib/controller/discover/discover_controller.dart
-
124lib/controller/discover/room_controller.dart
-
34lib/model/discover/rtc_channel_model.dart
-
2lib/network/api_urls.dart
-
5lib/network/rtc_api.dart
-
40lib/network/rtc_api.g.dart
-
142lib/pages/discover/dating_page.dart
-
260lib/pages/discover/discover_page.dart
-
219lib/pages/discover/live_item_widget.dart
-
130lib/pages/discover/party_page.dart
-
1lib/rtc/rtc_manager.dart
-
12lib/widget/live/live_room_anchor_showcase.dart
@ -0,0 +1,68 @@ |
|||||
|
import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart'; |
||||
|
import 'package:dating_touchme_app/network/network_service.dart'; |
||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
/// 发现页相关控制器 |
||||
|
class DiscoverController extends GetxController { |
||||
|
DiscoverController({NetworkService? networkService}) |
||||
|
: _networkService = networkService ?? Get.find<NetworkService>(); |
||||
|
|
||||
|
final NetworkService _networkService; |
||||
|
|
||||
|
/// RTC 频道列表 |
||||
|
final rtcChannelList = <RtcChannelModel>[].obs; |
||||
|
|
||||
|
/// 加载状态 |
||||
|
final isLoading = false.obs; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
// 初始化时加载数据 |
||||
|
loadRtcChannelPage(); |
||||
|
} |
||||
|
|
||||
|
/// 获取 RTC 频道分页列表 |
||||
|
Future<void> loadRtcChannelPage() async { |
||||
|
try { |
||||
|
isLoading.value = true; |
||||
|
final response = await _networkService.rtcApi.getRtcChannelPage(); |
||||
|
final base = response.data; |
||||
|
print('API 响应: isSuccess=${base.isSuccess}, data=${base.data}'); |
||||
|
if (base.isSuccess) { |
||||
|
if (base.data != null) { |
||||
|
// base.data 是 PaginatedResponse<RtcChannelModel> |
||||
|
final paginatedData = base.data!; |
||||
|
print( |
||||
|
'分页数据: total=${paginatedData.total}, records长度=${paginatedData.records.length}', |
||||
|
); |
||||
|
|
||||
|
// 从 PaginatedResponse 的 records 中提取数据 |
||||
|
rtcChannelList.assignAll(paginatedData.records); |
||||
|
print('更新后的列表长度: ${rtcChannelList.length}'); |
||||
|
} else { |
||||
|
print('base.data 为 null'); |
||||
|
rtcChannelList.clear(); |
||||
|
} |
||||
|
} else { |
||||
|
final message = base.message.isNotEmpty ? base.message : '获取频道列表失败'; |
||||
|
print('API 请求失败: $message'); |
||||
|
SmartDialog.showToast(message); |
||||
|
rtcChannelList.clear(); |
||||
|
} |
||||
|
} catch (e, stackTrace) { |
||||
|
print('获取频道列表异常: $e'); |
||||
|
print('堆栈: $stackTrace'); |
||||
|
SmartDialog.showToast('获取频道列表异常:$e'); |
||||
|
rtcChannelList.clear(); |
||||
|
} finally { |
||||
|
isLoading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 刷新 RTC 频道列表 |
||||
|
Future<void> refreshRtcChannelPage() async { |
||||
|
await loadRtcChannelPage(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/// RTC 频道列表项实体类 |
||||
|
class RtcChannelModel { |
||||
|
final String channelId; |
||||
|
final String channelPic; |
||||
|
final String channelName; |
||||
|
|
||||
|
RtcChannelModel({ |
||||
|
required this.channelId, |
||||
|
required this.channelPic, |
||||
|
required this.channelName, |
||||
|
}); |
||||
|
|
||||
|
factory RtcChannelModel.fromJson(Map<String, dynamic> json) { |
||||
|
return RtcChannelModel( |
||||
|
channelId: json['channelId']?.toString() ?? '', |
||||
|
channelPic: json['channelPic']?.toString() ?? '', |
||||
|
channelName: json['channelName']?.toString() ?? '', |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Map<String, dynamic> toJson() { |
||||
|
return { |
||||
|
'channelId': channelId, |
||||
|
'channelPic': channelPic, |
||||
|
'channelName': channelName, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
String toString() { |
||||
|
return 'RtcChannelModel(channelId: $channelId, channelName: $channelName)'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,142 @@ |
|||||
|
import 'package:dating_touchme_app/controller/discover/discover_controller.dart'; |
||||
|
import 'package:dating_touchme_app/extension/ex_widget.dart'; |
||||
|
import 'package:dating_touchme_app/pages/discover/live_item_widget.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:tdesign_flutter/tdesign_flutter.dart'; |
||||
|
|
||||
|
import '../../controller/discover/room_controller.dart'; |
||||
|
|
||||
|
/// 相亲页面 |
||||
|
class DatingPage extends StatefulWidget { |
||||
|
const DatingPage({super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<DatingPage> createState() => _DatingPageState(); |
||||
|
} |
||||
|
|
||||
|
class _DatingPageState extends State<DatingPage> |
||||
|
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { |
||||
|
late final DiscoverController discoverController; |
||||
|
late final TabController _tabController; |
||||
|
late final RoomController roomController; |
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
if (Get.isRegistered<DiscoverController>()) { |
||||
|
discoverController = Get.find<DiscoverController>(); |
||||
|
} else { |
||||
|
discoverController = Get.put(DiscoverController()); |
||||
|
} |
||||
|
if (Get.isRegistered<RoomController>()) { |
||||
|
roomController = Get.find<RoomController>(); |
||||
|
} else { |
||||
|
roomController = Get.put(RoomController()); |
||||
|
} |
||||
|
_tabController = TabController(length: 4, vsync: this); |
||||
|
discoverController.loadRtcChannelPage(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
_tabController.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
super.build(context); |
||||
|
return Column( |
||||
|
children: [ |
||||
|
TDTabBar( |
||||
|
tabs: [ |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 16, left: 16), |
||||
|
child: Text('全部'), |
||||
|
), |
||||
|
), |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 16, left: 16), |
||||
|
child: Text('同城'), |
||||
|
), |
||||
|
), |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 12, left: 12), |
||||
|
child: Text('相亲视频'), |
||||
|
), |
||||
|
), |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 12, left: 12), |
||||
|
child: Text('相亲语音'), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
backgroundColor: Colors.transparent, |
||||
|
labelPadding: const EdgeInsets.only(right: 4, top: 10, bottom: 10, left: 4), |
||||
|
selectedBgColor: const Color.fromRGBO(108, 105, 244, 1), |
||||
|
unSelectedBgColor: Colors.transparent, |
||||
|
labelColor: Colors.white, |
||||
|
dividerHeight: 0, |
||||
|
tabAlignment: TabAlignment.start, |
||||
|
outlineType: TDTabBarOutlineType.capsule, |
||||
|
controller: _tabController, |
||||
|
showIndicator: false, |
||||
|
isScrollable: true, |
||||
|
onTap: (index) { |
||||
|
print('相亲页面 Tab: $index'); |
||||
|
}, |
||||
|
), |
||||
|
Expanded( |
||||
|
child: Obx(() { |
||||
|
print('DatingPage Obx 触发,列表长度: ${discoverController.rtcChannelList.length}'); |
||||
|
print('isLoading: ${discoverController.isLoading.value}'); |
||||
|
if (discoverController.isLoading.value && |
||||
|
discoverController.rtcChannelList.isEmpty) { |
||||
|
return Center( |
||||
|
child: CircularProgressIndicator( |
||||
|
color: const Color.fromRGBO(108, 105, 244, 1), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
if (discoverController.rtcChannelList.isEmpty) { |
||||
|
return Center( |
||||
|
child: Text( |
||||
|
'暂无直播频道', |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.w, |
||||
|
color: Colors.white.withOpacity(0.7), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
return SingleChildScrollView( |
||||
|
padding: EdgeInsets.symmetric(vertical: 5.w), |
||||
|
child: Wrap( |
||||
|
spacing: 7.w, |
||||
|
runSpacing: 7.w, |
||||
|
children: discoverController.rtcChannelList.map((channel) { |
||||
|
print('渲染频道: ${channel.channelId}, ${channel.channelName}'); |
||||
|
return LiveItemWidget( |
||||
|
channel: channel, |
||||
|
channelId: channel.channelId, |
||||
|
).onTap(() async{ |
||||
|
await roomController.joinChannel(channel.channelId); |
||||
|
}); |
||||
|
}).toList(), |
||||
|
), |
||||
|
); |
||||
|
}), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
bool get wantKeepAlive => true; |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,219 @@ |
|||||
|
import 'package:dating_touchme_app/controller/discover/room_controller.dart'; |
||||
|
import 'package:dating_touchme_app/generated/assets.dart'; |
||||
|
import 'package:dating_touchme_app/model/discover/rtc_channel_model.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
/// 直播项组件 |
||||
|
class LiveItemWidget extends StatefulWidget { |
||||
|
final dynamic item; |
||||
|
final String? channelId; |
||||
|
final RtcChannelModel? channel; |
||||
|
const LiveItemWidget({ |
||||
|
super.key, |
||||
|
this.item, |
||||
|
this.channelId, |
||||
|
this.channel, |
||||
|
}); |
||||
|
|
||||
|
@override |
||||
|
State<LiveItemWidget> createState() => _LiveItemWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _LiveItemWidgetState extends State<LiveItemWidget> { |
||||
|
late final RoomController roomController; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
if (Get.isRegistered<RoomController>()) { |
||||
|
roomController = Get.find<RoomController>(); |
||||
|
} else { |
||||
|
roomController = Get.put(RoomController()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return InkWell( |
||||
|
onTap: () async { |
||||
|
if (widget.channelId != null && widget.channelId!.isNotEmpty) { |
||||
|
await roomController.joinChannel(widget.channelId!); |
||||
|
} else if (widget.item is Map && widget.item['channelId'] != null) { |
||||
|
await roomController.joinChannel(widget.item['channelId'].toString()); |
||||
|
} |
||||
|
}, |
||||
|
child: ClipRRect( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(10.w)), |
||||
|
child: Stack( |
||||
|
children: [ |
||||
|
Container( |
||||
|
width: 171.w, |
||||
|
height: 171.w, |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(10.w)), |
||||
|
), |
||||
|
child: widget.channel != null && |
||||
|
widget.channel!.channelPic.isNotEmpty |
||||
|
? Image.network( |
||||
|
widget.channel!.channelPic, |
||||
|
width: 171.w, |
||||
|
height: 171.w, |
||||
|
fit: BoxFit.cover, |
||||
|
errorBuilder: (context, error, stackTrace) { |
||||
|
print('图片加载失败: ${widget.channel!.channelPic}'); |
||||
|
return Image.network( |
||||
|
"https://picsum.photos/400", |
||||
|
width: 171.w, |
||||
|
height: 171.w, |
||||
|
fit: BoxFit.cover, |
||||
|
); |
||||
|
}, |
||||
|
) |
||||
|
: Image.network( |
||||
|
"https://picsum.photos/400", |
||||
|
width: 171.w, |
||||
|
height: 171.w, |
||||
|
fit: BoxFit.cover, |
||||
|
), |
||||
|
), |
||||
|
Positioned( |
||||
|
top: 0, |
||||
|
left: 0, |
||||
|
child: Stack( |
||||
|
children: [ |
||||
|
Image.asset( |
||||
|
Assets.imagesSubscript, |
||||
|
width: 56.w, |
||||
|
height: 16.w, |
||||
|
), |
||||
|
SizedBox( |
||||
|
height: 16.w, |
||||
|
child: Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, |
||||
|
children: [ |
||||
|
SizedBox(width: 5.w), |
||||
|
Image.asset( |
||||
|
Assets.imagesLocationIcon, |
||||
|
width: 6.w, |
||||
|
height: 7.w, |
||||
|
), |
||||
|
SizedBox(width: 3.w), |
||||
|
Text( |
||||
|
"49.9km", |
||||
|
style: TextStyle( |
||||
|
fontSize: 8.w, |
||||
|
color: Colors.white, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
if (widget.item != null && widget.item is Map && widget.item["isNew"] == true) |
||||
|
Positioned( |
||||
|
top: 9.w, |
||||
|
right: 8.w, |
||||
|
child: Container( |
||||
|
width: 39.w, |
||||
|
height: 13.w, |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(13.w)), |
||||
|
color: const Color.fromRGBO(0, 0, 0, .3), |
||||
|
), |
||||
|
child: Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, |
||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||
|
children: [ |
||||
|
Container( |
||||
|
width: 5.w, |
||||
|
height: 5.w, |
||||
|
margin: EdgeInsets.only(right: 3.w), |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(5.w)), |
||||
|
color: const Color.fromRGBO(255, 209, 43, 1), |
||||
|
), |
||||
|
), |
||||
|
Text( |
||||
|
"等待", |
||||
|
style: TextStyle( |
||||
|
fontSize: 8.w, |
||||
|
color: Colors.white, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
Positioned( |
||||
|
left: 9.w, |
||||
|
bottom: 6.w, |
||||
|
child: Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
SizedBox( |
||||
|
width: 64.w, |
||||
|
child: Text( |
||||
|
widget.channel != null && widget.channel!.channelName.isNotEmpty |
||||
|
? widget.channel!.channelName |
||||
|
: "一直一直在等你一直一直在等你......", |
||||
|
maxLines: 1, |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
style: TextStyle( |
||||
|
fontSize: 8.w, |
||||
|
color: Colors.white, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 2.w), |
||||
|
Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, |
||||
|
children: [ |
||||
|
Text( |
||||
|
"福州 | 28岁", |
||||
|
style: TextStyle( |
||||
|
fontSize: 11.w, |
||||
|
color: Colors.white, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 5.w), |
||||
|
if (widget.item != null && widget.item is Map && widget.item["isNew"] == true) |
||||
|
Container( |
||||
|
width: 32.w, |
||||
|
height: 10.w, |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.all( |
||||
|
Radius.circular(10.w), |
||||
|
), |
||||
|
color: const Color.fromRGBO(255, 206, 28, .8), |
||||
|
), |
||||
|
child: Center( |
||||
|
child: Text( |
||||
|
"新人", |
||||
|
style: TextStyle( |
||||
|
fontSize: 8.w, |
||||
|
color: Colors.white, |
||||
|
fontWeight: FontWeight.w500, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,130 @@ |
|||||
|
import 'package:dating_touchme_app/controller/discover/discover_controller.dart'; |
||||
|
import 'package:dating_touchme_app/pages/discover/live_item_widget.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:tdesign_flutter/tdesign_flutter.dart'; |
||||
|
|
||||
|
/// 聚会脱单页面 |
||||
|
class PartyPage extends StatefulWidget { |
||||
|
const PartyPage({super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<PartyPage> createState() => _PartyPageState(); |
||||
|
} |
||||
|
|
||||
|
class _PartyPageState extends State<PartyPage> |
||||
|
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { |
||||
|
late final DiscoverController discoverController; |
||||
|
late final TabController _tabController; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
if (Get.isRegistered<DiscoverController>()) { |
||||
|
discoverController = Get.find<DiscoverController>(); |
||||
|
} else { |
||||
|
discoverController = Get.put(DiscoverController()); |
||||
|
} |
||||
|
_tabController = TabController(length: 4, vsync: this); |
||||
|
discoverController.loadRtcChannelPage(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
_tabController.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
super.build(context); |
||||
|
return Column( |
||||
|
children: [ |
||||
|
TDTabBar( |
||||
|
tabs: [ |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 16, left: 16), |
||||
|
child: Text('全部'), |
||||
|
), |
||||
|
), |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 16, left: 16), |
||||
|
child: Text('同城'), |
||||
|
), |
||||
|
), |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 12, left: 12), |
||||
|
child: Text('相亲视频'), |
||||
|
), |
||||
|
), |
||||
|
TDTab( |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.only(right: 12, left: 12), |
||||
|
child: Text('相亲语音'), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
backgroundColor: Colors.transparent, |
||||
|
labelPadding: const EdgeInsets.only(right: 4, top: 10, bottom: 10, left: 4), |
||||
|
selectedBgColor: const Color.fromRGBO(108, 105, 244, 1), |
||||
|
unSelectedBgColor: Colors.transparent, |
||||
|
labelColor: Colors.white, |
||||
|
dividerHeight: 0, |
||||
|
tabAlignment: TabAlignment.start, |
||||
|
outlineType: TDTabBarOutlineType.capsule, |
||||
|
controller: _tabController, |
||||
|
showIndicator: false, |
||||
|
isScrollable: true, |
||||
|
onTap: (index) { |
||||
|
print('聚会脱单页面 Tab: $index'); |
||||
|
}, |
||||
|
), |
||||
|
Expanded( |
||||
|
child: Obx(() { |
||||
|
if (discoverController.isLoading.value && |
||||
|
discoverController.rtcChannelList.isEmpty) { |
||||
|
return Center( |
||||
|
child: CircularProgressIndicator( |
||||
|
color: const Color.fromRGBO(108, 105, 244, 1), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
if (discoverController.rtcChannelList.isEmpty) { |
||||
|
return Center( |
||||
|
child: Text( |
||||
|
'暂无直播频道', |
||||
|
style: TextStyle( |
||||
|
fontSize: 14.w, |
||||
|
color: Colors.white.withOpacity(0.7), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
return SingleChildScrollView( |
||||
|
child: Wrap( |
||||
|
spacing: 7.w, |
||||
|
runSpacing: 7.w, |
||||
|
children: [ |
||||
|
...discoverController.rtcChannelList.map((channel) { |
||||
|
return LiveItemWidget( |
||||
|
channel: channel, |
||||
|
channelId: channel.channelId, |
||||
|
); |
||||
|
}), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
}), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
bool get wantKeepAlive => true; |
||||
|
} |
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save