Why state change error occurs on flutter_riverpod during initialization - flutter

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dataProvider = StateNotifierProvider<DataNotifier, List<int>>((ref) {
return DataNotifier();
});
class DataNotifier extends StateNotifier<List<int>> {
DataNotifier() : super([]);
Future<void> getData() async {
state = [];
await Future.delayed(const Duration(seconds: 2));
state = [1, 2];
}
}
void main() => runApp(ProviderScope(child: App()));
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SecondPage()),
);
},
child: const Text('Next page'),
),
),
),
);
}
}
class SecondPage extends ConsumerStatefulWidget {
const SecondPage({Key? key}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends ConsumerState<SecondPage> {
#override
void initState() {
super.initState();
ref.read(dataProvider.notifier).getData();
}
#override
Widget build(BuildContext context) {
final numbers = ref.watch(dataProvider);
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (_, index) {
return Text('data: $index');
},
itemCount: numbers.length,
),
);
}
}
I am new to riverpod and I noticed this error while changing state.
In the above code when I tap the "next page" button at the fresh start for the first time it works as expected but when I go back and again tap the "next page" button, an error shown below is thrown:
StateNotifierListenerError (At least listener of the StateNotifier Instance of 'DataNotifier' threw an exception
when the notifier tried to update its state.
Does anyone know why this occurs and how can I prevent it.

You can solve the issue using autoDispose
final dataProvider = StateNotifierProvider.autoDispose<DataNotifier, List<int>>(
(ref) => DataNotifier(),
);
For Future I prefer using FutureProvider.
More about riverpod

Related

How to set splash screen time out on flutter

I am new to flutter and am kinda lost on how to set up a time to my splash screen so after this time it goes to the main screen. am using rive for the splash screen
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
void main() {
runApp(const MaterialApp(home: SimpleAnimation()));
}
class SimpleAnimation extends StatelessWidget {
const SimpleAnimation({Key? key, this.loading}) : super(key: key);
final bool? loading;
#override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: RiveAnimation.asset('assets/splash/splash.riv',
fit: BoxFit.cover)
),
);
}
}
You can set 3 second time in initstate after navigate to which screen you want
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
// TODO: implement initState
super.initState();
// after 3 second it will navigate
Future.delayed(const Duration(seconds: 3)).then((val) {
// Navigation Here
});
}
#override
Widget build(BuildContext context) {
return const Scaffold(
// your code
);
}
}
#override
void initState() {
//set timer for splash screen
Timer(const Duration(seconds: 4), () async {
//add your logic here
Navigator.pushNamedAndRemoveUntil(
context, ScreenRoute.mainScreen, (route) => false);
super.initState();
}
This SimpleAnimation widget shows after the splash screen. While this is StatelessWidget widget, you can define method inside build method. Change Duration(seconds: 2) based on your need.
class SimpleAnimation extends StatelessWidget {
const SimpleAnimation({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
Future.delayed(const Duration(seconds: 2)).then((value) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NextScreen(),
));
});
return const Scaffold(
body: Center(
As folks already mentioned the straighforward way would be to add a delay and do navigation after it:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const MainScreen(),
),
);
}
});
}
#override
Widget build(BuildContext context) {
return const ColoredBox(color: Colors.green);
}
}
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const ColoredBox(color: Colors.yellow);
}
}
Though, with this implementation, you'll have to depend on the animation length. So when you'll update animation - you'll have not to forget to update it inside the splash screen. A more reliable (and complex) solution would be to listen to the animation status and do the navigation after the animation finishes. Like this (warning, change ):
class PlayOneShotAnimation extends StatefulWidget {
const PlayOneShotAnimation({Key? key}) : super(key: key);
#override
_PlayOneShotAnimationState createState() => _PlayOneShotAnimationState();
}
class _PlayOneShotAnimationState extends State<PlayOneShotAnimation> {
late RiveAnimationController _controller;
#override
void initState() {
super.initState();
_controller = OneShotAnimation(
'bounce',
autoplay: true,
onStop: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const MainScreen(),
),
);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RiveAnimation.network(
'https://cdn.rive.app/animations/vehicles.riv',
animations: const ['idle', 'curves'],
controllers: [_controller],
),
),
);
}
}
class MainScreen extends StatelessWidget {
const MainScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const ColoredBox(color: Colors.yellow);
}
}
This is my approach for splash screen, the advantage of this approach is to make sure that the splash screen launch only once when the app starting.
First define a static bool in app home class to indicate the app launch.
static bool launch = true;
Then at the home attribute in your MaterialApp widget at app home class, check if (launch) is true use a FutureBuilder to launch the splash screen, if (launch) is false set home to your second screen. With FutureBuilder you can set a timer for your splash screen, when it done your second screen will start (credit to https://stackoverflow.com/a/68699447/11619215).
home: launch? FutureBuilder(
future: Future.delayed(const Duration(seconds: 3)),
builder: (ctx, timer) =>
timer.connectionState == ConnectionState.done
? const SecondScreen(title: 'Flutter Demo Home Page')
: appSplashScreen(),
): const SecondScreen(title: 'Flutter Demo Home Page'),
In the Second screen, check if (launch) is true then set it to false. This will make sure that the splash screen will only launch once each time your application start.
if(AppHome.launch) {
AppHome.launch = false;
}
Below is the full code with appSplashScreen widget at the bottom:
import 'package:flutter/material.dart';
void main() {
runApp(const AppHome());
}
class AppHome extends StatelessWidget {
const AppHome({Key? key}) : super(key: key);
//static bool to indicate the launching of the app
static bool launch = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: launch? FutureBuilder(
future: Future.delayed(const Duration(seconds: 3)),
builder: (ctx, timer) =>
timer.connectionState == ConnectionState.done
? const SecondScreen(title: 'Flutter Demo Home Page')
: appSplashScreen(),
): const SecondScreen(title: 'Flutter Demo Home Page'),
);
}
}
class SecondScreen extends StatefulWidget {
const SecondScreen({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<SecondScreen> createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
#override
Widget build(BuildContext context) {
//mack sure your splash screen only launch once at your app starting
if(AppHome.launch) {
AppHome.launch = false;
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const Center(
child: Text(
'My Second screen',
),
),
);
}
}
Widget appSplashScreen() {
return Container(
decoration: const BoxDecoration(
////you can add background image/color to your splash screen
// image: DecorationImage(
// image: AssetImage('assets/background.png'),
// fit: BoxFit.cover,
// ),
color: Colors.white,
),
child: Center(
child: SizedBox(
//get MediaQuery from instance of window to get height and width (no need of context)
height: MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.height*0.5,
width: MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width*0.5,
child: Column(
children: const [
////you can add image to your splash screen
// Image(
// image: AssetImage('assets/splashscreen_image.png'),
// ),
FittedBox(
child: Text(
'Loading',
textAlign: TextAlign.center,
style: TextStyle(
decoration: TextDecoration.none,
),
)
),
CircularProgressIndicator(),
],
),
),
),
);
}

Flutter NullSafety cannot push Page after showing and change modalProgressHUD

I'm using Provider package to expose a simple boolean variabile that allow to change the status of variable "inAsyncCall" of the ModalProgressHUD widget.
When i try to do somethings before navigate to another page, and i want to display the circlular progress indicator during that computation, when the Future terminated, the current widget has been disposed and i cannot use Navigator.push():
Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
this is my Provider with ChangeNotifier class:
class CartProvider with ChangeNotifier {
bool _inAsync = false;
bool get inAsync => _inAsync;
void setInAsync(bool flag) {
this._inAsync = flag;
notifyListeners();
}
}
I inject the provider before the MaterialApp widget like this:
void main() async {
runApp(App());
}
class App extends StatefulWidget {
#override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => CartProvider(),
)
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
And this is the simple home page where i access via context the provider injected:
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
final cartProvider = context.watch<CartProvider>();
return ModalProgressHUD(
inAsyncCall: cartProvider.inAsync,
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('HOME'),
FirstStatefulWidget(),
],
),
),
bottomNavigationBar: SecondStatefulWidget(),
),
);
}
}
class FirstStatefulWidget extends StatefulWidget {
FirstStatefulWidget({Key? key}) : super(key: key);
#override
_FirstStatefulWidgetState createState() => _FirstStatefulWidgetState();
}
class _FirstStatefulWidgetState extends State<FirstStatefulWidget> {
late CartProvider cartProvider = context.read<CartProvider>();
Future doSomething() async {
cartProvider.setInAsync(true);
await Future.delayed(
Duration(seconds: 2),
() => {
cartProvider.setInAsync(false),
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(),
),
)
},
);
}
#override
Widget build(BuildContext context) {
return Container(
child: ElevatedButton(
child: Text('do call'),
onPressed: doSomething,
),
);
}
}
class SecondStatefulWidget extends StatefulWidget {
SecondStatefulWidget({Key? key}) : super(key: key);
#override
_SecondStatefulWidgetState createState() => _SecondStatefulWidgetState();
}
class _SecondStatefulWidgetState extends State<SecondStatefulWidget> {
late CartProvider cartProvider = context.read<CartProvider>();
void goToAnotherPageAfterCall() async {
try {
cartProvider.setInAsync(true);
Future.delayed(
Duration(seconds: 2),
() => {
cartProvider.setInAsync(false),
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => SecondPage(),
),
)
},
);
} on Exception catch (e) {
cartProvider.setInAsync(false);
}
}
#override
Widget build(BuildContext context) {
return Container(
child: ElevatedButton(
child: Text('goToAnotherPage'),
onPressed: goToAnotherPageAfterCall,
),
);
}
}

How to setState widget by other widget Flutter ,simplecode below

right widget has gesterdetector that adds a String ("ZzZ") to List;
left widget shows all String there in String list by List view Buildder,
right widget adds "ZzZ" to list after pressing the button successfully but it dosent sets ui state...
in android studio after hot reload it shows all added "ZzZ"
import 'package:flutter/material.dart';
List<String> ListOfZzZ=[];
class homescreen extends StatefulWidget {
#override
_homescreenState createState() => _homescreenState();
}
class _homescreenState extends State<homescreen> {
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: Row(children: [
Expanded(child:RightSidewidget()),
Expanded(child:LeftSidewidget())
],
)),
);
}
}
class RightSidewidget extends StatefulWidget {
#override
_RightSidewidgetState createState() => _RightSidewidgetState();
}
class _RightSidewidgetState extends State<RightSidewidget> {
#override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(child:Text("add new ZzZ"),),
**onTap: (){
setState(() {
ListOfZzZ.add("ZzZ");
});},);**
}
}
class LeftSidewidget extends StatefulWidget {
#override
_LeftSidewidgetState createState() => _LeftSidewidgetState();
}
class _LeftSidewidgetState extends State<LeftSidewidget> {
#override
Widget build(BuildContext context) {
return Container(child:
ListView.builder(
itemCount: ListOfZzZ.length,
itemBuilder: (context,index)=>Text(ListOfZzZ[index])),);
}
}
check the Provider package it can help you achieve what you want, ere is a really good tutorial by the flutter devs showing how to use manage the state of your app and notify widgets of the changes other widgets have.
setState rebuild in very specyfic way. you can read about this in here:
https://api.flutter.dev/flutter/widgets/State/setState.html
in simple world setState call the nearest build (I think this is not full true, but this intuitions works for me)
In your code when you tap right widget and call setState only rightwidget will be rebuild.
So this is the easy solutions:
Make left and right widget statless.
In homescreen in row add gestureDetector(or textButton like in my example) and here call setState. When you do that, all homeSreen will be rebuild so left and right widget too. and your list will be actual. Here is example:
List<String> ListOfZzZ = [];
class homescreen extends StatefulWidget {
#override
_homescreenState createState() => _homescreenState();
}
class _homescreenState extends State<homescreen> {
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: Row(
children: [
Expanded(
child: TextButton(
onPressed: () => setState(() {
ListOfZzZ.add("ZzZ");
}),
child: RightSidewidget())),
Expanded(child: LeftSideWidget())
],
)),
);
}
}
class RightSidewidget extends StatelessWidget {
const RightSidewidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.amber[50],
child: Text("add new ZzZ"),
);
}
}
class LeftSideWidget extends StatelessWidget {
const LeftSideWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: ListView.builder(
itemCount: ListOfZzZ.length,
itemBuilder: (context, index) => Text(ListOfZzZ[index])),
);
}
}
The hard way, but more elegant and better is to use some state manager like bloc. Here is official site: https://bloclibrary.dev/#/gettingstarted
there is a lot of tutorials and explanations. But this is not solutions for 5 minutes.
Edit: I make some solution with BLoC. I hope this help. I use flutter_bloc and equatable packages in version 7.0.1
void main() {
EquatableConfig.stringify = kDebugMode;
Bloc.observer = SimpleBlocObserver();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('myList'),
),
body: BlocProvider(
create: (context) => MylistBloc()..add(AddToList('Start')),
child: Row(
children: [
Expanded(flex: 1, child: buttonsPanel()),
Expanded(flex: 1, child: ListOfZzZ()),
],
),
),
),
);
}
}
class ListOfZzZ extends StatefulWidget {
const ListOfZzZ({Key? key}) : super(key: key);
#override
_ListOfZzZState createState() => _ListOfZzZState();
}
class _ListOfZzZState extends State<ListOfZzZ> {
late MylistBloc _mylistBloc;
#override
Widget build(BuildContext context) {
return BlocBuilder<MylistBloc, MylistState>(
//builder: (context, state) {return ListView.builder(itemBuilder: (BuildContext context,int index){return ListTile(title: state.positions[index];)},);},
builder: (context, state) {
if (state.positions.isEmpty) {
return const Center(child: Text('no posts'));
} else {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(state.positions[index]));
},
itemCount: state.positions.length,
);
}
},
);
}
}
class buttonsPanel extends StatefulWidget {
const buttonsPanel({Key? key}) : super(key: key);
#override
_buttonsPanelState createState() => _buttonsPanelState();
}
class _buttonsPanelState extends State<buttonsPanel> {
late MylistBloc _mylistBloc;
#override
void initState() {
super.initState();
_mylistBloc = context.read<MylistBloc>();
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () => {_mylistBloc.add(AddToList('Spam'))},
child: Text('Spam')),
TextButton(
onPressed: () => {_mylistBloc.add(AddToList('Ham'))},
child: Text('Ham')),
],
);
}
class SimpleBlocObserver extends BlocObserver {
#override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
}
#override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print(error);
super.onError(bloc, error, stackTrace);
}
}
class MylistState extends Equatable {
final List<String> positions;
final int lenght;
const MylistState({this.positions = const <String>[], this.lenght = 0});
#override
List<Object> get props => [positions];
#override
String toString() => 'Lenght: {$lenght} Positions: {$positions}';
#override
MylistState copyWith(List<String>? positions) {
return MylistState(positions: positions ?? this.positions);
}
}
abstract class MylistEvent extends Equatable {
const MylistEvent();
#override
List<Object> get props => [];
}
class AddToList extends MylistEvent {
final String posToAdd;
#override
AddToList(this.posToAdd);
}
class MylistBloc extends Bloc<MylistEvent, MylistState> {
MylistBloc() : super(MylistState(positions: const <String>[]));
#override
Stream<MylistState> mapEventToState(
MylistEvent event,
) async* {
if (event is AddToList) {
yield await _mapListToState(state, event.posToAdd);
}
}
Future<MylistState> _mapListToState(
MylistState state, String posToAdd) async {
List<String> positions = [];
positions.addAll(state.positions);
positions.add(posToAdd);
return MylistState(positions: positions, lenght: positions.length);
}
}
}

Skip a tab while switching among tabs in TabBarView

In my flutter app, I use a simple tab-bar. I used the code from the flutter website and updated to make sure that I can keep the state of each tab using AutomaticKeepAliveClientMixin.
I have 3 tabs and each tab is fetching a list of data (why I need to use AutomaticKeepAliveClientMixin) from my backend API.
The problem is that when I switch between first and 3rd tabs (Page1 and Page3), the middle tab keeps rebuilding over and over again until I switch to that tab (Page2) and only at that point it doesn't get rebuilt anymore.
Every rebuild results in fetching data from API and that's not desirable.
Below, i have included a simplified code to reproduce this issue.
You can see in the debug console once switching between 1st and 3rd tab (without switching to 2nd tab) that it keeps printing "p2" (in my real app, it keeps fetching data for the 2nd tab).
Is there a way to switch between tabs without other tabs in between being built/rebuilt?
This is my code.
import 'package:flutter/material.dart';
void main() {
runApp(TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Page1(),
Page2(),
Page3(),
],
),
),
),
);
}
}
class Page1 extends StatefulWidget {
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1>
with AutomaticKeepAliveClientMixin<Page1> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p1');
return Container(
child: Center(
child: Icon(Icons.directions_car),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2>
with AutomaticKeepAliveClientMixin<Page2> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p2');
return Container(
child: Center(
child: Icon(Icons.directions_transit),
),
);
}
}
class Page3 extends StatefulWidget {
#override
_Page3State createState() => _Page3State();
}
class _Page3State extends State<Page3>
with AutomaticKeepAliveClientMixin<Page3> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('p3');
return Container(
child: Center(
child: Icon(Icons.directions_bike),
),
);
}
}
I believe this isn't a bug with flutter, but ultimately comes down to your implementation.
Please take a look at the code I wrote for you.
import 'package:flutter/material.dart';
import 'dart:async';
class FakeApi {
Future<List<int>> call() async {
print('calling api');
await Future.delayed(const Duration(seconds: 3));
return <int>[for (var i = 0; i < 100; ++i) i];
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp() : super(key: const Key('MyApp'));
#override
Widget build(BuildContext context) => const MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatelessWidget {
const MyHomePage() : super(key: const Key('MyHomePage'));
static const _icons = [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
];
#override
Widget build(BuildContext context) => DefaultTabController(
length: _icons.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [for (final icon in _icons) Tab(icon: icon)],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Center(child: _icons[0]),
StaggeredWidget(_icons[1]),
Center(child: _icons[2]),
],
),
),
);
}
class StaggeredWidget extends StatefulWidget {
const StaggeredWidget(this.icon)
: super(key: const ValueKey('StaggeredWidget'));
final Icon icon;
#override
_StaggeredWidgetState createState() => _StaggeredWidgetState();
}
class _StaggeredWidgetState extends State<StaggeredWidget> {
Widget _child;
Timer _timer;
#override
void initState() {
super.initState();
_timer = Timer(const Duration(milliseconds: 150), () {
if (mounted) {
setState(() => _child = MyApiWidget(widget.icon));
}
});
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) => _child ?? widget.icon;
}
class MyApiWidget extends StatefulWidget {
const MyApiWidget(this.icon, [Key key]) : super(key: key);
final Icon icon;
#override
_MyApiWidgetState createState() => _MyApiWidgetState();
}
class _MyApiWidgetState extends State<MyApiWidget>
with AutomaticKeepAliveClientMixin {
final _api = FakeApi();
#override
Widget build(BuildContext context) {
print('building `MyApiWidget`');
super.build(context);
return FutureBuilder<List<int>>(
future: _api(),
builder: (context, snapshot) => !snapshot.hasData
? const Center(child: CircularProgressIndicator())
: snapshot.hasError
? const Center(child: Icon(Icons.error))
: ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text('item $index'),
),
),
);
}
#override
bool get wantKeepAlive => true;
}

flutter: how are dependencies on inherited widgets discovered?

I'm currently reading the example code of the provider package:
// ignore_for_file: public_member_api_docs
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Counter()),
],
child: Consumer<Counter>(
builder: (context, counter, _) {
return MaterialApp(
supportedLocales: const [Locale('en')],
localizationsDelegates: [
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
_ExampleLocalizationsDelegate(counter.count),
],
home: const MyHomePage(),
);
},
),
);
}
}
class ExampleLocalizations {
static ExampleLocalizations of(BuildContext context) =>
Localizations.of<ExampleLocalizations>(context, ExampleLocalizations);
const ExampleLocalizations(this._count);
final int _count;
String get title => 'Tapped $_count times';
}
class _ExampleLocalizationsDelegate
extends LocalizationsDelegate<ExampleLocalizations> {
const _ExampleLocalizationsDelegate(this.count);
final int count;
#override
bool isSupported(Locale locale) => locale.languageCode == 'en';
#override
Future<ExampleLocalizations> load(Locale locale) =>
SynchronousFuture(ExampleLocalizations(count));
#override
bool shouldReload(_ExampleLocalizationsDelegate old) => old.count != count;
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Title()),
body: const Center(child: CounterLabel()),
floatingActionButton: const IncrementCounterButton(),
);
}
}
class IncrementCounterButton extends StatelessWidget {
const IncrementCounterButton({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: Provider.of<Counter>(context).increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}
}
class CounterLabel extends StatelessWidget {
const CounterLabel({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${counter.count}',
style: Theme.of(context).textTheme.display1,
),
],
);
}
}
class Title extends StatelessWidget {
const Title({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text(ExampleLocalizations.of(context).title);
}
}
When the user presses the FloatingRadioButton within IncrementCounterButton, build() is called on CounterLabel and IncrementCounterButton.
They both depend on an inherited widget, which is updated.
How does flutter discover this dependency?
I assume that the BuildContext is modified by the call to Provider.of<>().
Is this why we add the IncrementCounterButton, which has no functionality on its own?
Just to move the call to Provider.of<>() outside of its bigger parent widget, which would be more expensive to rebuild?
The binding widget an InheritedWidget and its consumers is created through BuildContext.
Consider the following InheritedWidget:
class Foo extends InheritedWidget {}
Then the descendants of Foo can subscribe to it by calling:
BuildContext context
context.inheritFromWidgetOfExactType(Foo);
It's worth noting that a widget can obtain the InheritedWidget without subscribing to it, by instead doing:
BuildContext context
context.ancestorInheritedElementForWidgetOfExactType(Foo);
This call is usually performed internally by the .of(context) pattern.
In the case of provider, that subscription is done by calling Provider.of<T>(context).
provider also exposes an optional argument to purposefully not subscribe to the inherited widget:
T value = Provider.of<T>(context, listen: false);