How to setState() and not rebuild tabs in a TabBarView - flutter

I have this problem, i have a home page where it has tabs. I like when i switch tabs to make the TabBar show black the tab that is selected and also i want to change the color of the whole Scaffold. So i made also a custom controller and used it like this:
TabController _controller;
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 5);
_controller.index = 1;
_controller.addListener(() {
if (!_controller.indexIsChanging) {
setState(() {
scaffoldColor = colors[_controller.index];
});
}
});
}
The thing is that in this way all of my tabs are going to be rebuild and this is very bad because i have heavy tasks in few of them.
I also have used AutomaticKeepAliveClientMixin in all of the tabs but it didn't fix the problem. By the way i used it like this:
class Tab1 extends StatefulWidget {
#override
_Tab1State createState() => _Tab1State();
}
class _Tab1State extends State<Tab1> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
print("Tab 1 Has been built");
return Text("TAB 1");
}
#override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
}

If for heavy tasks you mean a Future, you should place it inside initState.
See this answer: How to load async Stream only one time in Flutter?.

Related

dispose() is called when using AutomaticKeepAliveClientMixin

I am under the impression that using AutomaticKeepAliveClientMixin would prevent the states dispose() callback from being called when the Widget isn't visible anymore.
However, I have a situation where dispose() and initState() get called every time I hide/show a Widget, even though I implemented AutomaticKeepAliveClientMixin correctly.
class IdleScreenState extends State<IdleScreen> with AutomaticKeepAliveClientMixin {
#override
void initState() {
super.initState();
print('IdleScreen initState');
}
#override
void dispose() {
print('IdleScreen dispose');
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
// ...build the page...
}
#override
bool get wantKeepAlive => true;
}
This is how I hide/show this Widget
class MainScreen extends State<MainScreen> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return somecondition ? IdleScreen() : OtherScreen();
}
#override
bool get wantKeepAlive => true;
}
Every time this Widget (screen) is shown, initState()gets called, and every time I hide it, dispose() gets called. It's as if the AutomaticKeepAliveClientMixin has no effect. All other similar issues I could find seem to be due to either missing the wantKeepAlive => true or the super.build(context), but they are 100% there in the code.
I tried supplying a GlobalKey for IdleScreen as well, but that didn't have any effect.
However, if I use an IndexedStack or Offstage to hide/show the widget, it works as expected (initState() and dispose() don't get called when hiding/showing the widget).
IndexedStack(
index: somecondition ? 0 : 1,
children: [
IdleScreen(),
OtherScreen()
],
),
Maybe I'm mistaken, but isn't the whole purpose of AutomaticKeepAliveClientMixin to not have to manually keep the widget around using this technique?
This is in a web project, if that matters.
The type argument T is the type of the StatefulWidget subclass of the State into which this class is being mixed.
you have to pass the widget class name like this..
class IdleScreenState extends State<IdleScreen>
with AutomaticKeepAliveClientMixin <IdleScreen> {...

Flutter control tab programmatically via Provider

I am unable to figure out how I can listen to changes in a class that uses the ChangeNotifier class. More specifically, outside the widget build method. So far I've tried simply attaching listeners to the class like so:
final providerObj = SomeProvider();
void initState() {
providerObj.addListener(() {
print("Something happens");
});
}
But this does nothing. I don't know why. I got it working once, but afterwards it just died on me. Nothing gets triggered, when there is a change in the provider class' variable values.
Here's a snippet of the SomeProvider
class SomeProvider with ChangeNotifier {
int _tabIndex = 0;
List<Tab> _tabs = [];
Tab _searchTab = new Tab(id: 0, name: 'Search');
void searchItems(String searchWord) {
_searchTab.items.addAll(_items
.where((item) => product.item.contains(searchWord))
.toList());
setTabIndex(0);
}
void setTabIndex(int index) {
_tabIndex = index;
notifyListeners();
}
/// Get all products
List<Category> get tabs {
return [..._tabs];
}
int get tabIndex {
return _tabIndex;
}
What I'm trying to accomplish is simple: Control the selected tab, when the selected index is in the SomeProvider class. Every time a tab is pressed, the index value gets saved in the provider. When the value changes, it should trigger an animateTo() call for a TabController instance.
The reason for this is because I have a separate search widget, which needs to change the tab to a Search tab. I just have no idea how I can listen to changes in the provider class outside the build() method and change the TabController index there.
So, I have a basic TabBarView to which I give a TabController instance.
class _ProductsState extends State<Products> with TickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(
vsync: this,
length: 0,
initialIndex: 0,
);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _fetchTabs(context) async {
await Provider.of<SomeProvider>(context, listen: false)
.fetchTabs();
_tabController = TabController(
vsync: this,
length: Provider.of<SomeProvider>(context, listen: false)
.tabs
.length,
);
}
Widget build(BuildContext context) {
// There's a FutureBuilder here etc.
...
child: TabBarView(
controller: _tabController,
...
What I'm trying to do is something like:
void triggeredWhenStateChanges() {
_tabController.animateTo(Provider.of<SomeProvider>(context, listen: false).tabIndex);
}
Little bit off-topic: I've used React before (not React-Native) and with it you can use something called a hook which can perform side effects when a value change is detected. With the useEffect hook I can normally execute some actions when a value in the state changes. I don't know how I can do this with Flutter, if it can be done at all.

initState() taking too long to load

I have home screen which two tabs. now when I get to home tab and got to search instantly. I get red screen for slight second and then all widgets get loaded.
Now where problem is,
in initState() I'm assigning store.filteredPOI to widget.floorPlan.pois.
I'm getting store.filteredPOI from a network call which will take some time. so in that fraction of time widget.floorPlan is null so how can I show a loader to prevent the red error screen,
code
class SearchTab extends StatefulWidget {
final FloorPlan floorPlan;
final bool isIndoorMapVisible;
final NetworkStatus networkStatus;
SearchTab({this.floorPlan, this.isIndoorMapVisible,this.networkStatus});
#override
_SearchTabState createState() => _SearchTabState();
}
class _SearchTabState extends State<SearchTab> {
final TextEditingController textController = TextEditingController();
SearchStore store;
#override
void initState() {
store = SearchStore();
store.filteredPOI = widget.floorPlan.pois; //<<<<<<<<
super.initState();
}
#override
Widget build(BuildContext context) {
return (...)
what I ended up doing was add the operation inside built method
#override
Widget build(BuildContext context) {
store.filteredPOI = widget.floorPlan.pois;
return (...)

Is there a better way to constantly rebuild a widget?

I have widget with data that changes regularly and I'm using a Timer.periodic to rebuild the widget. This starts out working smoothly but becomes choppy pretty quickly is there a better way to do this?
class _MainScreenState extends State<MainScreen> {
static const Duration duration = Duration(milliseconds: 16);
update(){
system.updatePos(duration.inMilliseconds/1000);
setState(() {});
}
#override
Widget build(BuildContext context) {
Timer.periodic(duration, (timer){
update();
});
return PositionField(
layoutSize: widget.square,
children: system.map
);
}
}
You are making a big mistake:
The build method must never have any side effects, because it is called again whenever setState is called (or when some higher up widget changes, or when the user rotates the screen...).
Instead, you want to create your Timer in initState, and cancel it on dispose:
class TimerTest extends StatefulWidget {
#override
_TimerTestState createState() => _TimerTestState();
}
class _TimerTestState extends State<TimerTest> {
Timer _timer;
int _foo = 0;
// this is only called once when the widget is attached
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (timer) => _update());
}
// stop the timer when the widget is detached and destroyed
#override
void dispose() {
_timer.cancel();
super.dispose();
}
void _update() {
setState(() {
_foo++;
});
}
#override
Widget build(BuildContext context) {
return Text('Foo: ${_foo}');
}
}

Share bloc across different routes

I'm trying to share same bloc across two routes.
But when I come back from second route the bloc get automatically disposed so in the first route I find myself with all the stream closed.
For example this is the first route (HomePage) where I instantiate the bloc, download a list from api and show it in the build method.
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
GroupsBloc _groupBloc;
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
_groupBloc = GroupsBloc();
_groupBloc.getAll();
}
#override
void dispose(){
_groupBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
}
Then I navigate to a second screen where I can add an item to the list.
_onAddGroupPress(){
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) => BlocProvider<GroupsBloc>(bloc: _groupBloc, child: GroupPage()),
fullscreenDialog: true
),
);
}
In the second screen I retrieve the bloc and I use it to add an item, then I go back to Home Page.
class GroupPage extends StatefulWidget {
#override
_GroupPageState createState() => _GroupPageState();
}
class _GroupPageState extends State<GroupPage> {
FormBloc _formBloc; //another bloc
GroupsBloc _groupBloc;
#override
void initState(){
super.initState();
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
_formBloc = FormBloc();
_groupBloc = BlocProvider.of<GroupsBloc>(context); //retrieve of the bloc
}
#override
void dispose() {
_formBloc?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
In the dispose method of the second screen I dispose only _formBloc bloc but _groupBloc gets disposed too, so when I come back in the first page I found myself with _groupBloc disposed and all it's stream closes.
I tought of passing the bloc to the second screen as a props but I don't know if it's the right way to do this.
It obviously depends on the scope of your Bloc, but there is nothing preventing you from sharing the same instance throughout your whole app.
ie. simply wrap your whole MaterialApp inside BlocProvider<GroupsBloc>(bloc: _groupBloc, child: MaterialApp( ... ))
if "groups" are not global to your app, you should probably just pass the bloc along to the second widget.