Open nateshmbhat opened 4 months ago
Can u please check on this? This looks like a critical issue in useState...
I'm in holiday. But try:
useEffect(() {
Future(() => onTabChanged(activeIndex.value));
}, [activeIndex.value]);
I'm pretty sure you have an exception somewhere in your code because of this useEffect.
It's violating some widget rules, which would lead to Flutter not updating the UI.
Can someone pls tell me which widget rules is violated? This is the full code I pasted which can be run in any project.
I hardcoded missing data on your example because it's not reproducible as it is.
When I make it run and press on the tab buttons this error appears:
Exception has occurred.
FlutterError (setState() or markNeedsBuild() called during build.
This PracticeDetailScreen widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
PracticeDetailScreen
The widget which was currently being built when the offending call was made was:
TabBarRow)
So, when PracticeDetailScreen is still building there is another child widget triggering a rebuild in PracticeDetailScreen again.
This code "just works" meaning that I'm not 100% sure if these hooks are meant to be used like that, sorry I come from react and I din't find any documentation about this but the reasoning is this:
The state must be in one parent widget (PracticeDetailScreen) and be passed down to your child widgets as props. Instead you are making a new state inside
TabBarRow
and somehow (I didn't understand that part of the implementation) trying to sinchronize it with the parent state via a useEffect and a funtion.
and the code is this:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class PracticeDetailScreen extends HookConsumerWidget {
// I don't know what `YogaPracticeId` is so I leave it hardcoded.
final int practiceId;
// I put a default value, with 0 you trigger a empty response.
const PracticeDetailScreen({super.key, this.practiceId = 1});
@override
Widget build(BuildContext context, WidgetRef ref) {
// The data it's not important in this case so we can leave it hardcoded
final practiceData = practiceId != 0 ? null : 'some data';
final activeIndex = useState<int>(0);
if (practiceData == null) {
return const Scaffold(
body: Center(
child: Text('Practice not found'),
),
);
}
// Removed unncessesary dependencies
return Scaffold(
body: SafeArea(
child: Column(
children: [
TabBarRow(activeIndex: activeIndex),
activeIndex.value == 0
? const IntroTabView()
: const MeditateTabView()
],
),
),
);
}
}
class TabBarRow extends StatelessWidget {
// Here we receive the state `activeIndex` from the parent that is of type ValueNotifier<int>
final ValueNotifier<int> activeIndex;
const TabBarRow({super.key, required this.activeIndex});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 5),
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
side: const BorderSide(width: 1, color: Color(0xFFE8EAEB)),
borderRadius: BorderRadius.circular(6),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TabBarButton(
onPressed: () {
activeIndex.value = 0;
},
isActive: activeIndex.value == 0,
title: 'Intro',
),
),
Expanded(
child: TabBarButton(
onPressed: () {
activeIndex.value = 1;
},
isActive: activeIndex.value == 1,
title: 'Meditate'),
),
],
),
);
}
}
class TabBarButton extends StatelessWidget {
const TabBarButton(
{super.key,
required this.onPressed,
required this.isActive,
this.title = 'Intro'});
final bool isActive;
final VoidCallback onPressed;
final String title;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
onPressed();
},
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: ShapeDecoration(
color: isActive ? const Color(0xFF1FA1AA) : null,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
title,
style: TextStyle(
color: isActive ? Colors.white : Colors.grey,
fontSize: 14,
fontFamily: 'Open Sans',
fontWeight: FontWeight.w600,
height: 0,
),
),
],
),
),
);
}
}
class IntroTabView extends HookConsumerWidget {
const IntroTabView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container(
height: 100,
width: 50,
color: Colors.blue,
child: const Text('intro'),
);
}
}
class MeditateTabView extends HookConsumerWidget {
const MeditateTabView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return const Text('meditate');
}
}
I would investigate a bit more if I were you, maybe some experienced flutter dev can help, but in the meantime it works. If this approach have unknown pitfalls then I think the other option is to use a provider.
Describe the bug Calling activeIndex.value = newIndex is not causing a re-build of the widget.
To Reproduce Full Reproducible code :
Package versions : hooks_riverpod: ^2.5.1 flutter_hooks: ^0.20.5 riverpod_annotation: ^2.3.5
Expected behavior Clicking on the "Intro" button or "Meditate" button should show the corresponding IntroTabView and MeditateTabView .