Flutter - Accessing a Provider from a higher point in the Widget tree - flutter

I've been working with Flutter recently, and I saw that there was many ways to deal with state management.
Following the recommendations there, I've been using Provider to deal with the state of my app.
I can update a part of my state from one of the widgets in my UI. To do that, I can call a method of the provider that's above the current widget in the context. No problems with this.
But I want the update of my state to be made from an overlay.
The issue is: When I'm inserting an OverlayEntry with Overlay.of(context)?.insert(), it inserts the overlayEntry to the closest Overlay, which is in general the root of the app, which is above the ChangeProvider. As a result, I get an exception saying I can't find the Provider from the OverlayEntry.
Here is a replication code I've been writting:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider(
create: (context) => NumberModel(), // All widgets that will be lower in the widget tree will have access to NumberModel
child: NumberDisplayer()
),
);
}
}
// Simple ChangeNotifier. We have a number that we can increment.
class NumberModel extends ChangeNotifier {
int _number = 10;
int get number => _number;
void add_one() {
_number = number + 1;
notifyListeners();
}
}
// This class displays a number, and a button.
class NumberDisplayer extends StatelessWidget {
NumberDisplayer({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var overlayEntry = OverlayEntry(builder: (context) =>
Positioned(
top: 100,
left: 50,
child: FloatingActionButton(onPressed: (){
// Throws "Error: Could not find the correct Provider<NumberModel> above this _OverlayEntryWidget Widget"
Provider.of<NumberModel>(context, listen: false).add_one();
})));
return Consumer<NumberModel>(
builder: (context, numberModel, child) {
return Column(
children: [
Text('Number: ${numberModel.number}'),
FloatingActionButton(onPressed: () {
Overlay.of(context)?.insert(overlayEntry);
})
],
);
},
);
}
}
I would like to find a way to update the information in my provider from the overlay, but I'm not sure how to approach this problem.
Thanks for your help everyone !

Related

Flutter - How to safely change theme after awaiting theme data from an API in a Widget?

Using a normal setup for handling Theme with a ChangeNotifier that notifies the whole app / everything below it in the three - that something should be redrawn.
This approach seems general and there's multiple "guides" doing it this way. And this works works well when clicking a Button to change it. However, if the data for a Theme is coming from an API - where can we safely update the same value before rendering a Widget?
This is an example code where the ThemeData is somehow "downloaded" and supposed to be updated before rendering the view once the StreamBuilder is done. This, of course, causes the same Widget that's downloading something being redrawn while building so I'm getting a warning for that.
How can this be solved? The Theme can just be a single color that is downloaded and changed dynamically. And so far I haven't seen themes being changed inside one single widget while the "main one" is unchanged. Not sure what's the best approach to this (or similiar) issue - since it can't be uncommon in an mostly online based world.
Edit #1: Just to clarify - the Theme might change depending on the Widget / Page / Screen being loaded and it's not a "one time thing" where you initialize it at the beginning but with each screen being loaded - to customize that particular page based on online API data.
Example code:
void main() {
runApp(ChangeNotifierProvider(
create: (context) => ThemeConfig(),
child: MyApp()
));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child)
{
return MaterialApp(
theme: state.getTheme()
)
});
}
}
class _MyScreen extends State<MyScreen>
{
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Api.downloadTheme(),
builder: (context, snapshot)
{
// If OK render screen - But where to safely set the "Theme" from API?
return MyWidget(context.data)
});
)
}
}
class _MyWidget extends State<MyWidget>
{
#override
void initState() {
super.initState();
// This will cause the Widget tree to be redrawn while it's drawing and not work at all
// So when I've downloaded the data - where can this safely be changed?
Provider.of<ThemeConfig>(context).setTheme(widget.data.theme);
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I'm not sure if I understood you correctly, but if you wander how to update the theme by fetching if from an api, here is the example of simulating an api call which updates the theme:
void main() {
runApp(
ChangeNotifierProvider(
lazy: false, // triggering ThemeConfig constructor
create: (context) => ThemeConfig(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ThemeConfig>(builder: (context, state, child) {
return MaterialApp(
theme: state.theme,
home: MyScreen(),
);
});
}
}
class ThemeConfig with ChangeNotifier {
ThemeConfig() {
// trigger theme fetch
getTheme();
}
ThemeData theme = ThemeData(primarySwatch: Colors.blue); // initial theme
Future<void> getTheme() async {
// TODO: fetch your theme data here and then update it like below
await Future.delayed(Duration(seconds: 3)); // simulating waiting for response
theme = ThemeData(primarySwatch: Colors.red);
notifyListeners();
}
}
class MyScreen extends StatelessWidget {
const MyScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(), // notice how the colors change
body: Center(
child: Container(
height: 200,
width: 200,
color: Theme.of(context).primaryColor,
),
),
);
}
}

Confusion about Provider with multiple Consumer Widgets

I am confused about Provider. I think Provider is meant to encapsulate the state of a Widget so it can be accessed somewhere else throughout the program. The problem is: What if I want a certain stateless widget multiple times? I created an example for this:
Lets say we want to model a few pieces of paper. Each piece of paper has some unique writing on it. I could now make a provider for a single piece of paper like this:
class PaperSheetProvider extends ChangeNotifier {
String uniqueText = "";
void setUniqueText(String newText) {
uniqueText = newText;
notifyListeners();
}
}
and I make a simple paper widget to consume that provider like:
class PaperPieceWidget extends StatelessWidget {
const PaperPieceWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<PaperSheetProvider>(
builder: ((context, value, child) => Text(value.uniqueText)),
);
}
}
and at last, I make 2 paper widgets along with a button to change the text of the paper:
Column(
children: [
PaperPieceWidget(),
PaperPieceWidget(),
OutlinedButton(
onPressed: () {
Provider.of<PaperSheetProvider>(context, listen: false).setUniqueText('blablaablaa');
},
child: Text("change paper contents"))
],
),
(The ChangeNotifierProvider is near the root of the whole widget tree to simplify the code a bit)
Simple enough. But now If I click the button, I get:
Basically, the two paper pieces have the same writing. Which should not be the case, each piece of paper should have their own, unique writing. How do I do this correctly?
Full code in case anything is unclear:
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String title = 'Shortcuts and Actions Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Column(
children: [
PaperPieceWidget(),
PaperPieceWidget(),
OutlinedButton(
onPressed: () {
Provider.of<PaperSheetProvider>(context, listen: false)
.setUniqueText('blablaablaa');
},
child: Text("change paper contents"))
],
),
);
}
}
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider(create: ((context) => PaperSheetProvider()))
], child: const MyApp()));
}
class PaperPieceWidget extends StatelessWidget {
const PaperPieceWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<PaperSheetProvider>(
builder: ((context, value, child) => Text(value.uniqueText)),
);
}
}
//(provider is posted entirely above)
Wrap each widget with own provider. Something like this:
PaperSheetProvider provider1;
ChangeNotifierProvider.value(
value: provider1,
child: PaperPieceWidget(),
)

Using the auto_route and flutter_bloc libraries to navigate page not working

I trying to using the auto_route and flutter_bloc libraries to navigate page, but BlocListener is not triggered.
I'm using print(SplashRoute == NavigationState.initial().routeType); to check the trigger condition with BlocListener, it's return true.
However, the BlocListener still not triggered.
How do I fix my code problem :(? This is the sample code of my app. Thanks.
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const AppWidget());
}
class AppWidget extends StatelessWidget {
const AppWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final rootRouter = RootRouter();
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => NavigationCubit()..nav(SplashRoute),
),
// ... Other blocProvider
],
child: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
theme: state.themeData,
routerDelegate: rootRouter.delegate(),
routeInformationParser: rootRouter.defaultRouteParser(),
);
},
));
}
}
class SplashPage extends StatelessWidget {
const SplashPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
print(SplashRoute == NavigationState.initial().routeType); // <------ return ture
return MultiBlocListener(
listeners: [
BlocListener<NavigationCubit, NavigationState>( // <------ Not working here
listenWhen: (p, c) => c.routeType is SplashRoute,
listener: (context, state) {
LoggerService.simple.i('NavigationCubit page listening!!');
context.read<NavigationCubit>().nav(HomeRoute);
context.pushRoute(const HomeRoute());
},
// ... Other blocListener
),
],
child: const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
navigation_state.dart
part of 'navigation_cubit.dart';
#freezed
abstract class NavigationState with _$NavigationState {
const factory NavigationState({
required Type routeType,
}) = _NavigationState;
factory NavigationState.initial() => const NavigationState(
routeType: SplashRoute,
);
}
navigation_cubit.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../presentation/routes/router.gr.dart';
part 'navigation_cubit.freezed.dart';
part 'navigation_state.dart';
class NavigationCubit extends Cubit<NavigationState> {
NavigationCubit() : super(NavigationState.initial());
void nav(Type routeType) {
emit(
state.copyWith(
routeType: routeType,
),
);
}
#override
Future<void> close() async {
return super.close();
}
}
There are two things I can interpret from your code. Either way it will not work as you had hoped.
My understanding from your question is that you are trying to navigate using the BlocListener on initial state, which doesn't work.
The reason is that BlocListener is not triggered on initial state as it is not a state change, but rather something that is defined by the bloc.
The second thing I see is that you call the nav method when providing the bloc, which is a good thing: NavigationCubit()..nav(SplashRoute). However, it will set the same value for the parameter routeType, which will not trigger a state change as it is the same value. Meaning that the BlocListener will not be triggered.
Set routeType to something else initially, perhaps set it to null, so that your bloc can identify a state change, then your BlocListener will be triggered.
EDIT:
Also, c.routeType is SplashRoute doesn't seem right. try changing to c.routeType == SplashRoute in your listenWhen property, otherwise your function in the listener property will not trigger

Consumer and context.watch in MultiProvider

I am trying Flutter for the first time, and I am a little confused by the MultiProvider class.
The question is straightforward, but I didn't find an explanation:
when should one use Consumer and when context.watch?
For instance, taking one of the examples apps I have found, I tried using two providers for two global states, the theme and the status of the app:
runApp(
MultiProvider(providers: [
ChangeNotifierProvider(create: (context) => AppTheme()),
ChangeNotifierProvider(create: (context) => AppStatus()),
],
child: const MyApp()
));
Then the app widget accesses the theme with Consumer:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<AppTheme>(
builder: (context, appTheme, child) {
// ...
As far as I understand, now all children widgets will inherit the provider. Is it right?
My home page, then, called by the MyApp class does not use Consumer, but context.watch:
#override
Widget build(BuildContext context) {
final appTheme = context.watch<AppTheme>();
final appStatus = context.watch<AppStatus>();
return NavigationView(
// ...
It works, don't get me wrong, but I just copied the row above my appStatus, so I don't really fully understand it. This is also due to another screen that I've concocted to access the AppStatus global state, but I use Consumer, as suggested by the Flutter documentation:
class _ViewerState extends State<Viewer> {
#override
Widget build(BuildContext context) {
return Consumer<AppStatus>(
builder: (context, appStatus, child) {
return ScaffoldPage.scrollable(
header: const PageHeader(title: Text('Test')),
children: [
FilledButton(child: Text("Try ${appStatus.count}"), onPressed: (){ appStatus.increment(); debugPrint('pressed ${appStatus.count}'); }),
FilledButton(child: Text("Reset"), onPressed: (){ appStatus.reset(); }),
]);
},
);
}
}
I have the feeling that I am misusing something here, and I do not really understand what's going on under the hood...
context.watch<T>() and Consumer<T> does the same thing. Most of the time context.watch<T>() is just more convenient. In some cases where context is not available Consumer<T> is useful.

StreamBuilder not re-rendering the widget inside?

I created this code, what i want to happen is when i press on the button i want the piechart to re-render with the new values (which should be old values but the food value increased by 1)
I am using a piechart from pie_chart: 0.8.0 package.
Deposit is nothing but a pojo (String category and int deposit)
the bloc.dart contains a global instance of the bloc, a getter for the stream and initialization of a stream of type
Here's my code:
import 'package:flutter/material.dart';
import 'package:pie_chart/pie_chart.dart';
import 'bloc.dart';
import 'Deposit.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'bloc Chart',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
Map<String, double> datamap = new Map();
#override
Widget build(BuildContext context) {
datamap.putIfAbsent("Food", () => 5);
datamap.putIfAbsent("transportation", () => 3);
return Scaffold(
appBar: AppBar(
title: Text("PieChart using blocs"),
),
body: Column(
children: <Widget>[
StreamBuilder<Deposit>(
stream: bloc.data, //A stream of Deposit data
builder: (context, snapshot) {
addDeposit(Deposit("Food", 1), datamap);
debugPrint("Value of food in map is: ${datamap["Food"]}");
return PieChart(dataMap: datamap);
}),
SizedBox.fromSize(
size: Size(20, 10),
),
RaisedButton(
onPressed: () {
bloc.add(Deposit("Food", 1)); //returns the stream.add
},
child: Icon(Icons.add),
),
],
),
);
}
void addDeposit(Deposit dep, Map<String, double> map) {
if (map.containsKey(dep.category)) {
map.update(dep.category, (value) => value + dep.price);
} else
map.putIfAbsent(dep.category, () => dep.price);
}
}
I think your problem is that the stream doesn't trigger new events. You don't have to close the stream to rebuild. I can't see anywhere in your code where you are triggering new events for the stream. Check below code to see a simple way how you can update a StatelessWidget using a StreamBuilder.
class CustomWidgetWithStream extends StatelessWidget {
final CustomBlock block = CustomBlock();
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
StreamBuilder(
stream: block.stream,
builder: (context, stream) {
return Text("${stream.data.toString()}");
}),
RaisedButton(
onPressed: () {
block.incrementNumber();
},
child: Text("Increment"),
)
],
);
}
}
class CustomBlock {
num counter = 10;
final StreamController<num> _controller = StreamController();
Stream<num> get stream => _controller.stream;
CustomBlock() {
_controller.onListen = () {
_controller.add(counter); // triggered when the first subscriber is added
};
}
void incrementNumber() {
counter += 1;
_controller.add(counter); // ADD NEW EVENT TO THE STREAM
}
dispose() {
_controller.close();
}
}
Although this is a working code snippet, I would strongly suggest to change your widget from StatelessWidget to StatefulWidget, for two reasons:
* if you go "by the book", if a widget changes the content by itself, then it's not a StatelessWidget, a stateless widget only displays data that is given to it. In your case, the widget is handling the tap and then decides what to do next and how to update itself.
* if you are using streams, in a stateful widget you can safely close the stream, as you can see in the above code, there's no safe way to close the stream. If you don't close the stream, there might be unwanted behaviour or even crashes.
This is my bloc file
import 'package:rxdart/rxdart.dart';
import 'package:testing/Deposit.dart';
class Bloc{
final _data = new BehaviorSubject<Deposit>();
Stream<Deposit> get data => _data.stream;
Function(Deposit) get add => _data.sink.add;
void dispose(){
_data.close();
}
}
Bloc bloc = new Bloc();