Merge pull request #83 from SyncrowIOT/SP-1397-FE-Create-Routine-SceneRoomsTabBarDevicesView-throws-TabBarController-initialization-error

Sp 1397 fe create routine scene rooms tab bar devices view throws tab bar controller initialization error
This commit is contained in:
Faris Armoush
2025-04-17 09:47:46 +03:00
committed by GitHub
8 changed files with 249 additions and 154 deletions

View File

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart';
import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_event.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_event.dart';
@ -7,19 +5,31 @@ import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_event.dart
import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_state.dart'; import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_state.dart';
class TabBarBloc extends Bloc<TabBarEvent, TabBarState> { class TabBarBloc extends Bloc<TabBarEvent, TabBarState> {
final DeviceManagerBloc deviceManagerBloc; TabBarBloc(this.deviceManagerBloc) : super(const TabBarInitialState()) {
TabBarBloc(this.deviceManagerBloc) : super(const Initial()) { on<TabBarTabChangedEvent>(_onTabBarTabChangedEvent);
on<TabChanged>(_handleTabChanged);
} }
FutureOr<void> _handleTabChanged( final DeviceManagerBloc deviceManagerBloc;
TabChanged event, Emitter<TabBarState> emit) {
void _onTabBarTabChangedEvent(
TabBarTabChangedEvent event,
Emitter<TabBarState> emit,
) {
_getDevices(event);
emit(
TabBarTabSelectedState(
roomId: event.roomId,
selectedTabIndex: event.selectedIndex,
),
);
}
void _getDevices(TabBarTabChangedEvent event) {
if (event.roomId == "-1") { if (event.roomId == "-1") {
deviceManagerBloc.add(FetchAllDevices()); deviceManagerBloc.add(FetchAllDevices());
} else { } else {
deviceManagerBloc.add(FetchDevicesByRoomId(event.roomId,event.unit)); deviceManagerBloc.add(FetchDevicesByRoomId(event.roomId, event.unit));
} }
emit(TabSelected(
roomId: event.roomId, selectedTabIndex: event.selectedIndex));
} }
} }

View File

@ -4,10 +4,14 @@ abstract class TabBarEvent {
const TabBarEvent(); const TabBarEvent();
} }
class TabChanged extends TabBarEvent { class TabBarTabChangedEvent extends TabBarEvent {
const TabBarTabChangedEvent({
required this.selectedIndex,
required this.roomId,
required this.unit,
});
final int selectedIndex; final int selectedIndex;
final String roomId; final String roomId;
final SpaceModel unit; final SpaceModel unit;
const TabChanged(
{required this.selectedIndex, required this.roomId, required this.unit});
} }

View File

@ -2,12 +2,16 @@ abstract class TabBarState {
const TabBarState(); const TabBarState();
} }
class Initial extends TabBarState { class TabBarInitialState extends TabBarState {
const Initial(); const TabBarInitialState();
} }
class TabSelected extends TabBarState { class TabBarTabSelectedState extends TabBarState {
const TabBarTabSelectedState({
required this.roomId,
required this.selectedTabIndex,
});
final int selectedTabIndex; final int selectedTabIndex;
final String roomId; final String roomId;
const TabSelected({required this.roomId, required this.selectedTabIndex});
} }

View File

@ -20,82 +20,69 @@ class SceneRoomsTabBarDevicesView extends StatefulWidget {
_SceneRoomsTabBarDevicesViewState(); _SceneRoomsTabBarDevicesViewState();
} }
class _SceneRoomsTabBarDevicesViewState class _SceneRoomsTabBarDevicesViewState extends State<SceneRoomsTabBarDevicesView>
extends State<SceneRoomsTabBarDevicesView>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final TabController _tabController; late final TabController _tabController;
List<SubSpaceModel>? rooms = [];
late final SpaceModel selectedSpace; late final SpaceModel selectedSpace;
var rooms = <SubSpaceModel>[];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { selectedSpace = HomeCubit.getInstance().selectedSpace!;
selectedSpace = HomeCubit.getInstance().selectedSpace!;
rooms = List.from(selectedSpace.subspaces ?? []); rooms = List.from(selectedSpace.subspaces);
final defaultSubSpaceModel = SubSpaceModel(
name: 'All Devices',
devices: context.read<DevicesCubit>().allDevices,
id: '-1',
);
if (rooms != null && rooms!.isNotEmpty) { if (rooms.isNotEmpty) {
if (rooms![0].id != '-1') { final isFirstRoomIdValid = rooms[0].id != '-1';
rooms?.insert( if (isFirstRoomIdValid) {
0, rooms.insert(0, defaultSubSpaceModel);
SubSpaceModel(
name: 'All Devices',
devices: context.read<DevicesCubit>().allDevices,
id: '-1',
),
);
}
} else {
rooms = [
SubSpaceModel(
name: 'All Devices',
devices: context.read<DevicesCubit>().allDevices,
id: '-1',
)
];
} }
} else {
_tabController = TabController(length: rooms!.length, vsync: this); rooms = [defaultSubSpaceModel];
_tabController.addListener(_handleTabSwitched);
setState(() {});
});
}
void _handleTabSwitched() {
if (_tabController.indexIsChanging) {
final value = _tabController.index;
/// select tab
context.read<TabBarBloc>().add(TabChanged(
selectedIndex: value,
roomId: rooms?[value].id ?? '',
unit: selectedSpace));
return;
} }
_tabController = TabController(length: rooms.length, vsync: this);
_tabController.addListener(_handleTabSwitched);
} }
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
_tabController.dispose();
_tabController.removeListener(() {}); _tabController.removeListener(() {});
_tabController.dispose();
}
void _handleTabSwitched() {
if (_tabController.indexIsChanging) {
final index = _tabController.index;
context.read<TabBarBloc>().add(
TabBarTabChangedEvent(
selectedIndex: index,
roomId: rooms[index].id ?? '',
unit: selectedSpace,
),
);
return;
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultScaffold( return DefaultScaffold(
title: StringsManager.createScene, padding: EdgeInsetsDirectional.zero,
padding: EdgeInsets.zero,
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () => navigateToRoute(context, Routes.sceneTasksRoute),
navigateToRoute(context, Routes.sceneTasksRoute); icon: const Icon(Icons.arrow_back_ios),
},
icon: const Icon(
Icons.arrow_back_ios,
),
), ),
title: StringsManager.createScene,
child: SceneDevicesBody(tabController: _tabController, rooms: rooms), child: SceneDevicesBody(tabController: _tabController, rooms: rooms),
); );
} }

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart';
import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_state.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_state.dart';
import 'package:syncrow_app/features/devices/model/subspace_model.dart'; import 'package:syncrow_app/features/devices/model/subspace_model.dart';
@ -8,60 +7,44 @@ import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_bloc.dart'
import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_state.dart'; import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_state.dart';
import 'package:syncrow_app/features/scene/enum/create_scene_enum.dart'; import 'package:syncrow_app/features/scene/enum/create_scene_enum.dart';
import 'package:syncrow_app/features/scene/model/scene_settings_route_arguments.dart'; import 'package:syncrow_app/features/scene/model/scene_settings_route_arguments.dart';
import 'package:syncrow_app/features/scene/widgets/scene_list_tile.dart'; import 'package:syncrow_app/features/scene/widgets/scene_devices/scene_devices_body_tab_bar.dart';
import 'package:syncrow_app/features/shared_widgets/default_container.dart'; import 'package:syncrow_app/features/scene/widgets/scene_devices/scene_devices_list.dart';
import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart'; import 'package:syncrow_app/features/shared_widgets/app_loading_indicator.dart';
import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart';
import 'package:syncrow_app/navigation/routing_constants.dart';
import 'package:syncrow_app/utils/context_extension.dart';
import 'package:syncrow_app/utils/resource_manager/color_manager.dart';
class SceneDevicesBody extends StatelessWidget { class SceneDevicesBody extends StatelessWidget {
const SceneDevicesBody({ const SceneDevicesBody({
super.key, required this.tabController,
required TabController tabController,
required this.rooms, required this.rooms,
}) : _tabController = tabController; super.key,
});
final TabController _tabController; final TabController tabController;
final List<SubSpaceModel>? rooms; final List<SubSpaceModel> rooms;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isAutomationDeviceStatus =
((ModalRoute.of(context)?.settings.arguments as SceneSettingsRouteArguments?)?.sceneType ==
CreateSceneEnum.deviceStatusChanges.name);
return BlocBuilder<TabBarBloc, TabBarState>( return BlocBuilder<TabBarBloc, TabBarState>(
builder: (context, tabState) { builder: (context, state) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TabBar( SceneDevicesBodyTabBar(
controller: _tabController, tabController: tabController,
dividerColor: Colors.transparent, rooms: rooms,
indicatorColor: Colors.transparent, selectedRoomId: state is TabBarTabSelectedState ? state.roomId : '-1',
tabs: [
...rooms!.map((e) => Tab(
child: BodyLarge(
text: e.name ?? '',
textAlign: TextAlign.start,
style: context.bodyLarge.copyWith(
color: (tabState is TabSelected) && tabState.roomId == e.id
? ColorsManager.textPrimaryColor
: ColorsManager.textPrimaryColor.withOpacity(0.2),
),
),
)),
],
isScrollable: true,
tabAlignment: TabAlignment.start,
), ),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: controller: tabController,
rooms!.map((e) => _buildRoomTab(e, context, isAutomationDeviceStatus)).toList(), children: rooms
.map(
(room) => _buildRoomTab(
room,
_isAutomationDeviceStatus(context),
),
)
.toList(),
), ),
), ),
], ],
@ -70,52 +53,46 @@ class SceneDevicesBody extends StatelessWidget {
); );
} }
Widget _buildRoomTab(SubSpaceModel room, BuildContext context, bool isAutomationDeviceStatus) { bool _isAutomationDeviceStatus(BuildContext context) {
final routeArguments =
ModalRoute.of(context)?.settings.arguments as SceneSettingsRouteArguments?;
final deviceStatusChangesScene = CreateSceneEnum.deviceStatusChanges.name;
final sceneType = routeArguments?.sceneType;
return sceneType == deviceStatusChangesScene;
}
Widget _buildRoomTab(
SubSpaceModel room,
bool isAutomationDeviceStatus,
) {
return BlocBuilder<DeviceManagerBloc, DeviceManagerState>( return BlocBuilder<DeviceManagerBloc, DeviceManagerState>(
builder: (context, state) { builder: (context, state) {
if (state.loading && state.devices == null) { final isLoading = state.loading && state.devices == null;
return const Center(child: CircularProgressIndicator()); final hasData =
} else if (state.devices != null && state.devices!.isNotEmpty) { state.devices != null && (state.devices?.isNotEmpty ?? false);
return ListView.builder( final hasError = state.error != null;
itemCount: state.devices!.length,
itemBuilder: (context, index) { final widgets = <bool, Widget>{
final device = state.devices![index]; isLoading: const AppLoadingIndicator(),
return DefaultContainer( hasError: Center(child: Text('${state.error}')),
child: SceneListTile( hasData: SceneDevicesList(
minLeadingWidth: 40, devices: state.devices ?? [],
leadingWidget: SvgPicture.asset(device.icon ?? ''), isAutomationDeviceStatus: isAutomationDeviceStatus,
titleWidget: BodyMedium( ),
text: device.name ?? '', };
style: context.titleSmall.copyWith(
color: ColorsManager.secondaryTextColor, final invalidWidgetEntry = MapEntry(
fontWeight: FontWeight.w400, true,
fontSize: 20, Center(child: Text('This subspace has no devices')),
), );
),
trailingWidget: const Icon( final validWidgetEntry = widgets.entries.firstWhere(
Icons.arrow_forward_ios_rounded, (entry) => entry.key == true,
color: ColorsManager.greyColor, orElse: () => invalidWidgetEntry,
size: 16, );
),
onPressed: () { return validWidgetEntry.value;
Navigator.pushNamed(
context,
Routes.deviceFunctionsRoute,
arguments: {
"device": device,
"isAutomationDeviceStatus": isAutomationDeviceStatus
},
);
},
),
);
},
);
} else if (state.error != null) {
return const Center(child: Text('Something went wrong'));
} else {
return const SizedBox();
}
}, },
); );
} }

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:syncrow_app/features/devices/model/subspace_model.dart';
import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart';
import 'package:syncrow_app/utils/context_extension.dart';
import 'package:syncrow_app/utils/resource_manager/color_manager.dart';
class SceneDevicesBodyTabBar extends StatelessWidget {
const SceneDevicesBodyTabBar({
required this.tabController,
required this.rooms,
required this.selectedRoomId,
super.key,
});
final String selectedRoomId;
final TabController tabController;
final List<SubSpaceModel> rooms;
@override
Widget build(BuildContext context) {
return TabBar(
controller: tabController,
dividerColor: Colors.transparent,
indicatorColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: rooms.map((e) {
final isSelected = selectedRoomId == e.id;
return Tab(
child: BodyLarge(
text: e.name ?? '',
textAlign: TextAlign.start,
style: context.bodyLarge.copyWith(
color: isSelected
? ColorsManager.textPrimaryColor
: ColorsManager.textPrimaryColor.withValues(
alpha: 0.2,
),
),
),
);
}).toList(),
);
}
}

View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_app/features/devices/model/device_model.dart';
import 'package:syncrow_app/features/scene/widgets/scene_list_tile.dart';
import 'package:syncrow_app/features/shared_widgets/default_container.dart';
import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart';
import 'package:syncrow_app/navigation/routing_constants.dart';
import 'package:syncrow_app/utils/context_extension.dart';
import 'package:syncrow_app/utils/resource_manager/color_manager.dart';
class SceneDevicesList extends StatelessWidget {
const SceneDevicesList({
required this.isAutomationDeviceStatus,
required this.devices,
super.key,
});
final List<DeviceModel> devices;
final bool isAutomationDeviceStatus;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: devices.length,
itemBuilder: (context, index) => _buildDeviceTile(context, devices[index]),
);
}
Widget _buildDeviceTile(
BuildContext context,
DeviceModel device,
) {
return DefaultContainer(
child: SceneListTile(
minLeadingWidth: 40,
leadingWidget: SvgPicture.asset(device.icon ?? ''),
titleWidget: BodyMedium(
text: device.name ?? '',
style: context.titleSmall.copyWith(
color: ColorsManager.secondaryTextColor,
fontWeight: FontWeight.w400,
fontSize: 20,
),
),
trailingWidget: const Icon(
Icons.arrow_forward_ios_rounded,
color: ColorsManager.greyColor,
size: 16,
),
onPressed: () => _navigateToDeviceFunctions(context, device),
),
);
}
void _navigateToDeviceFunctions(
BuildContext context,
DeviceModel device,
) {
Navigator.of(context).pushNamed(
Routes.deviceFunctionsRoute,
arguments: {
"device": device,
"isAutomationDeviceStatus": isAutomationDeviceStatus,
},
);
}
}

View File

@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_app/features/app_layout/model/community_model.dart'; import 'package:syncrow_app/features/app_layout/model/community_model.dart';
import 'package:syncrow_app/features/app_layout/model/space_model.dart'; import 'package:syncrow_app/features/app_layout/model/space_model.dart';
import 'package:syncrow_app/features/app_layout/view/app_layout.dart'; import 'package:syncrow_app/features/app_layout/view/app_layout.dart';
import 'package:syncrow_app/features/auth/view/otp_view.dart';
import 'package:syncrow_app/features/auth/view/login_view.dart'; import 'package:syncrow_app/features/auth/view/login_view.dart';
import 'package:syncrow_app/features/auth/view/otp_view.dart';
import 'package:syncrow_app/features/auth/view/sign_up_view.dart'; import 'package:syncrow_app/features/auth/view/sign_up_view.dart';
import 'package:syncrow_app/features/dashboard/view/dashboard_view.dart'; import 'package:syncrow_app/features/dashboard/view/dashboard_view.dart';
import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/device_manager_bloc/device_manager_bloc.dart';
@ -17,11 +17,12 @@ import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_bloc.dart'
import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_event.dart'; import 'package:syncrow_app/features/scene/bloc/tab_change/tab_change_event.dart';
import 'package:syncrow_app/features/scene/view/device_functions_view.dart'; import 'package:syncrow_app/features/scene/view/device_functions_view.dart';
import 'package:syncrow_app/features/scene/view/scene_auto_settings.dart'; import 'package:syncrow_app/features/scene/view/scene_auto_settings.dart';
import 'package:syncrow_app/features/scene/view/scene_tasks_view.dart';
import 'package:syncrow_app/features/scene/view/scene_rooms_tabbar.dart'; import 'package:syncrow_app/features/scene/view/scene_rooms_tabbar.dart';
import 'package:syncrow_app/features/scene/view/scene_tasks_view.dart';
import 'package:syncrow_app/features/scene/view/scene_view.dart'; import 'package:syncrow_app/features/scene/view/scene_view.dart';
import 'package:syncrow_app/features/scene/view/smart_automation_select_route.dart'; import 'package:syncrow_app/features/scene/view/smart_automation_select_route.dart';
import 'package:syncrow_app/features/splash/view/splash_view.dart'; import 'package:syncrow_app/features/splash/view/splash_view.dart';
import 'routing_constants.dart'; import 'routing_constants.dart';
class Router { class Router {
@ -88,7 +89,7 @@ class Router {
BlocProvider( BlocProvider(
create: (BuildContext context) => create: (BuildContext context) =>
TabBarBloc(context.read<DeviceManagerBloc>()) TabBarBloc(context.read<DeviceManagerBloc>())
..add(TabChanged( ..add(TabBarTabChangedEvent(
selectedIndex: 0, selectedIndex: 0,
roomId: '-1', roomId: '-1',
unit: SpaceModel( unit: SpaceModel(