Provider: trigger setState inside another widget - flutter

Hello i need help with my Provider that should trigger changes.
I am using Provider for my State management.
Ill inserted the Provider as followed:
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => ThemeProvider()..initialize(),
),
ChangeNotifierProvider(
create: (_) => HeadingProvider(),
),
],
child: const Master(),
);
}
}
My Provider looks like this:
class HeadingProvider extends ChangeNotifier {
List<Widget> myWidgetList = [
const Text("hello"),
];
addHeading() {
myWidgetList.add(const Text("hello 2"));
notifyListeners();
}
}
I now have two different Widgets, one that triggers the function addHeading() and one that shows the list of the Provider. When I now call the function addHeading() the element Text("hello 2") gets created but does not trigger show in the UI. I have to manually hotreload to see changes.
Provider is referred like this in the two widgets
final headingController = Provider.of<HeadingProvider>(context);
Widget that triggers the function.
DragItemWidget(
onTap: () {
headingController.addHeading();
},
Widget that displayes the list.
Container(
width: 390,
height: 300,
child: ListView(
children: headingController.myWidgetList
),
),
Can you help me how I can see the added element in the UI immediately? (Maybe I have to trigger setState ?)

Wrap you ListView into a Consumer so that it will be rebuilt when you call notifyListeners():
Container(
width: 390,
height: 300,
child: Consumer<HeadingProvider>(builder: (context, headingProvider, _) {
return ListView(
children: headingProvider.myWidgetList
);
}),
),

Related

Animating LinearProgressIndicator within bloc pattern

I have a bloc which emits states that contain some progress value (0-100%). This value is to be displayed with LinearProgressIndicator. Updates may occur in chunks, meaning progress can jump from, say, 0% to 30% in a single step.
Below is a simple snippet to reproduce this behavior:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class StartProgressEvent {}
class ProgressState {
final double progress;
ProgressState(this.progress);
}
class ProgressBloc extends Bloc<StartProgressEvent, ProgressState> {
ProgressBloc() : super(ProgressState(0)) {
on<StartProgressEvent>(_startProgress);
}
void _startProgress(
StartProgressEvent event,
Emitter<ProgressState> emit,
) async {
emit(ProgressState(0.1));
await Future.delayed(const Duration(seconds: 3));
emit(ProgressState(0.4));
await Future.delayed(const Duration(seconds: 3));
emit(ProgressState(0.7));
await Future.delayed(const Duration(seconds: 3));
emit(ProgressState(1.0));
}
}
void main() {
runApp(const DemoApp());
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: BlocProvider<ProgressBloc>(
create: (context) => ProgressBloc()..add(StartProgressEvent()),
child: BlocBuilder<ProgressBloc, ProgressState>(
builder: (context, state) => LinearProgressIndicator(value: state.progress),
),
),
),
),
);
}
}
This snippet shows the following indicator:
Instead of updating instantly, I want my indicator to animate ongoing changes, i.e. to update smoothly from its previous state to the current one.
I found this answer suggesting that we use TweenAnimationBuilder to animate LinearProgressIndicator but it implies that we know its current value which we don't.
In a broader sense this question is not limited to progress indicator. I believe it can be framed this way: how can we animate between two consecutive "states" of a widget (either stateless or stateful) within bloc architecture?
You can try AnimatedFractionallySizedBox with your duration instead of LinearProgressIndicator
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocProvider<ProgressBloc>(
create: (context) => ProgressBloc()..add(StartProgressEvent()),
child: BlocBuilder<ProgressBloc, ProgressState>(
builder: (context, state) => AnimatedFractionallySizedBox(
duration: Duration(seconds: 3),
alignment: Alignment.centerLeft,
widthFactor: state.progress,
child: Container(
alignment: Alignment.centerLeft,
width: double.infinity,
height: 10,
color: Colors.blue,
),
)
// LinearProgressIndicator(value: state.progress),
),
),
],
),
),
);
}
}

Error: Could not find the correct Provider<Networkprovider> above this Widget

I am new to flutter and was trying out the implementation of Network Connectivity with the Flutter Provider. I got across this error and have tried every bit of code on the Internet from changing the context and changing the place where the Provider might lie so that the child widgets will get the context. When I am trying to get the value of res in welcome. dart I am getting the error.
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that Welcome is under your MultiProvider/Provider.
This usually happens when you are creating a provider and trying to read it immediately
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider(
create: (context) => Networkprovider().networkController.stream,
initialData: Networkprovider().initRes),
],
child: MaterialApp(
theme: ThemeData(),
initialRoute: Welcome.id,
routes: {
Welcome.id: (context) => Welcome(),
NavigatorPage.id: (context) => const NavigatorPage(),
},
),
);
}
}
NetworkProvider.dart
class Networkprovider extends ChangeNotifier {
late StreamSubscription<ConnectivityResult> _subscription;
late StreamController<ConnectivityResult> _networkController;
late ConnectivityResult initRes = ConnectivityResult.none;
StreamSubscription<ConnectivityResult> get subscription => _subscription;
StreamController<ConnectivityResult> get networkController =>
_networkController;
Networkprovider() {
startup();
}
void startup() {
_networkController = StreamController<ConnectivityResult>();
networkStatusChangeListener();
}
void networkStatusChangeListener() async {
_networkController.sink.add(await Connectivity().checkConnectivity());
_subscription = Connectivity().onConnectivityChanged.listen((event) {
_networkController.sink.add(event);
});
}
void disposeStreams() {
_subscription.cancel();
_networkController.close();
}
}
Welcome.dart
class Welcome extends StatelessWidget {
static String id = "welcome_screen";
const Welcome({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var res = Provider.of<Networkprovider>(context);
return SafeArea(
child: Scaffold(
backgroundColor: MAIN_BACKGROUND_COLOR,
body: SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: const <Widget>[
Image(image: AssetImage("assets/images/Magnet_logo.png"))
],
),
),
const Padding(
padding: EdgeInsets.all(20.0),
child:
Image(image: AssetImage("assets/images/Mannify_logo.png")),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Spinner(),
),
],
),
),
),
);
}
}
Declare ChangeNotifierProvider like this
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Networkprovider()),
],
child: <your widget>,
)
then access like this
final provider = Provider.of<Networkprovider>(context);

Why is Flutter dialog not rebuilding on change notifier?

Well the issue is kinda simple, but it needs to be done on a specific way. First I have a Class extending "ChangeNotifier" this class will perform some async tasks, so while it is doing so there's a variable that indicates if the class is currently bussy or not, so far it works flawlessly.
Using Riverpod as state managment I instanciate said class and provide it along my widget tree, but there's one Widget that needs to display a dialog and inside this dialog it can execute async tasks from the Class that I've been passing around. It all works except for the fact that I would like to display a CircularProgressIndicator inside this dialog, and it doesn't seems to be reacting propperly to the state changes.
Here's a sample code to recreate the scenario:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final dataProvider = ChangeNotifierProvider<Data>((_) => Data());
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'huh?',
theme: ThemeData(primarySwatch: Colors.blue),
home: FirstPage(),
);
}
}
class FirstPage extends HookWidget {
#override
Widget build(BuildContext context) {
final data = useProvider(dataProvider);
print('DATA STATE [source: FirstPage, data: ${data.loading}]');
return Scaffold(
body: Center(
child: Container(
width: 200,
height: 50,
child: ElevatedButton(
child: Text('show dialog'),
onPressed: () => showDialog(
context: context,
builder: (_) => Alert(data: data),
),
),
),
),
);
}
}
class Alert extends StatelessWidget {
const Alert({required this.data});
final Data data;
Widget build(BuildContext context) {
print('DATA STATE [source: Alert, data: ${data.loading}]');
return AlertDialog(
content: Container(
width: 500,
height: 500,
padding: EdgeInsets.symmetric(horizontal: 100, vertical: 200),
child: ElevatedButton(
child: data.loading ? CircularProgressIndicator(color: Colors.white) : Text('click here'),
onPressed: () async => await data.randomTask(),
),
),
);
}
}
class Data extends ChangeNotifier {
Data({
this.loading = false,
});
bool loading;
Future<void> randomTask() async {
print('Actually waiting 3 seconds..');
_update(loading: true);
await Future.delayed(Duration(seconds: 3));
print('Waiting done.');
_update(loading: false);
}
void _update({bool? loading}) {
this.loading = loading ?? this.loading;
notifyListeners();
}
}
Notice the prints I've placed, because of them if you run the app you'll see outputs on the console like:
DATA STATE [source: FirstPage, data: false]
DATA STATE [source: Alert, data: false]
Actually waiting 3 seconds..
DATA STATE [source: FirstPage, data: true]
Waiting done.
DATA STATE [source: FirstPage, data: false]
Which means that the state is actually changing, and everything is working fine, except for the dialog that seems to be static.
I already tried adding a "loading" bool as part of the "Alert" widget, and letting it manage its own state, and it works, but the code is not as clean as I would like to, because the Class "Data" is supposed to manage this kind of stuff.
Is there anything that can be done?
Thankyou in advance!
Adding StatefulBulider do the trick
class Alert extends StatelessWidget {
const Alert({required this.data});
final Data data;
Widget build(BuildContext context) {
print('DATA STATE [source: Alert, data: ${data.loading}]');
return AlertDialog(
content: StatefulBuilder(builder: (context, setState) {
return Container(
width: 500,
height: 500,
padding: EdgeInsets.symmetric(horizontal: 100, vertical: 200),
child: ElevatedButton(
child: data.loading
? CircularProgressIndicator(color: Colors.white)
: Text('click here'),
onPressed: () async => await data.randomTask(),
),
);
}),
);
}
}

Managing state in Flutter using Provider

I'm trying to implement Provider state management on counter application to understand Provider's functionality better. I have added two buttons with respect to two different text widget. So, now whenever I click any of the two widget both the Text widgets get update and give same value. I want both the widgets independent to each other.
I have used ScopedModel already and got the desire result but now I want to try with provider.
Image Link : https://i.stack.imgur.com/ma3tR.png
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("====Home Page Rebuilt====");
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
],
)),
);
}
}
class CustomWidget extends StatelessWidget {
final String number;
const CustomWidget({Key key, this.number}) : super(key: key);
#override
Widget build(BuildContext context) {
print("====Number Page Rebuilt====");
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return Text(
value.count.toString(),
style: Theme.of(context).textTheme.headline3,
);
},
),
FlatButton(
color: Colors.blue,
onPressed: () =>
Provider.of<CounterModel>(context, listen: false).increment(),
child: Text("Click"),
),
],
);
}
}
If you want them independent from each other, then you need to differentiate them somehow. I have a bit of a different style to implement the Provider and it hasn't failed me yet. Here is a complete example.
You should adapt your implementation to something like this:
Define your provider class that extends ChangeNotifier in a CounterProvider.dart file
import 'package:flutter/material.dart';
class CounterProvider extends ChangeNotifier {
/// You can either set an initial value here or use a UserProvider object
/// and call the setter to give it an initial value somewhere in your app, like in main.dart
int _counter = 0; // This will set the initial value of the counter to 0
int get counter => _counter;
set counter(int newValue) {
_counter = newValue;
/// MAKE SURE YOU NOTIFY LISTENERS IN YOUR SETTER
notifyListeners();
}
}
Wrap your app with a Provider Widget like so
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// don't forget to import it here too
import 'package:app/CounterProvider.dart';
void main() {
runApp(
MaterialApp(
initialRoute: '/root',
routes: {
'/root': (context) => MyApp(),
},
title: "Your App Title",
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// Makes data available to everything below it in the Widget tree
/// Basically the entire app.
ChangeNotifierProvider<CounterProvider>.value(value: CounterProvider()),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}
Access and update data anywhere in the app
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// MAKE SURE TO IMPORT THE CounterProvider.dart file
import 'package:app/CounterProvider.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
CounterProvider counterProvider;
#override
Widget build(BuildContext context) {
/// LISTEN TO THE CHANGES / UPDATES IN THE PROVIDER
counterProvider = Provider.of<CounterProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
_showCounterButton(1),
_showCounterButton(2),
],
),
),
);
}
Widget _showCounterButton(int i) {
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Text(
i == 1
? counterProvider.counter1.toString()
: counterProvider.counter2.toString(),
style: Theme.of(context).textTheme.headline3,
),
FlatButton(
color: Colors.blue,
onPressed: () {
/// UPDATE DATA IN THE PROVIDER. BECAUSE YOU're USING THE SETTER HERE,
/// THE LISTENERS WILL BE NOTIFIED AND UPDATE ACCORDINGLY
/// you can do this in any other file anywhere in the Widget tree, as long as
/// it it beneath the main.dart file where you defined the MultiProvider
i == 1
? counterProvider.counter1 += 1
: counterProvider.counter2 += 1;
setState(() {});
},
child: Text("Click"),
),
],
);
}
}
If you want, you can change the implementation a bit. If you have multiple counters, for multiple widgets, then just create more variables in the CounterProvider.dart file with separate setters and getters for each counter. Then, to display/update them properly, just use a switch case inside the _showCounterButton() method and inside the onPressed: (){ switch case here, before setState((){}); }.
Hope this helps and gives you a better understanding of how Provider works.

Repository provider in the flutter_bloc library doesn't provide repository with when pushing new route

I am using the flutter_bloc library to architect my app. In addition to the BlocProvider I am using the Repository Provider, since I will be using a specific repository extensively throughout my app. But I am having an issue with regards to context . Below is snippets of my code:
main.dart
void main() async {
.......
appRepository _appRepository = AppRepository();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
runApp(
BlocProvider(
builder: (context) =>
AuthenticationBloc(appRepository: _appRepository)..dispatch(AppStarted()),
child: App(appRepository: _appRepository,),
),
);
});
}
class App extends StatelessWidget {
............
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
builder: (BuildContext context, AuthenticationState state) {
.....
if (state is AuthenticationUnauthenticated) {
return SafeArea(
top: false,
bottom: false,
child: RepositoryProvider(
builder: (context) => _appRepository,
child: LoginPage(firebaseMessaging: _firebaseMessaging),
),
);
}
......
},
),
);
}
}
Register button found in login form:
register_button.dart
class RegisterButton extends StatelessWidget {
final FirebaseMessaging _firebaseMessaging;
RegisterButton({
Key key,
#required FirebaseMessaging firebaseMessaging,
}) : assert(firebaseMessaging != null),
_firebaseMessaging = firebaseMessaging,
super(key: key);
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Don't have an account?", style: TextStyle(color: Colors.black)),
SizedBox(width: 4.0),
GestureDetector(
child: Text("Register here!",
style: TextStyle(
color: Color(0xFF585B8D), fontWeight: FontWeight.w500)),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return RegisterPage(
firebaseMessaging: _firebaseMessaging,
);
}),
);
},
)
],
);
}
register_page.dart
class RegisterPage extends StatelessWidget {
final FirebaseMessaging _firebaseMessaging;
RegisterPage({
Key key,
#required FirebaseMessaging firebaseMessaging,
}) : assert(firebaseMessaging != null),
_firebaseMessaging = firebaseMessaging,
super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider(
builder: (context) => RegisterBloc(
appRepository: RepositoryProvider.of<AppRepository>(context),
firebaseMessaging: _firebaseMessaging,
),
child: RegisterForm(),
),
);
}
}
Question:
I'm getting an error when I click on the register button on my login form that says the following:
No ancestor could be found starting from the context that was passed to RepositoryProvider.of<AppRepository>().
This can happen if:
1. The context you used comes from a widget above the RepositoryProvider.
2. You used MultiRepositoryProvider and didn't explicity provide the RepositoryProvider types.
Good: RepositoryProvider<AppRepository>(builder: (context) => AppRepository())
Bad: RepositoryProvider(builder: (context) => AppRepository()).
The context used was: BlocProvider<RegisterBloc>(dirty, state: _DelegateWidgetState#a87b2(lifecycle state: created))
Why am I getting this error? This problem seems to be fixed if I put the repository provider as the child of the blocprovider and app as the child repository provider in the main function and then deleting the invidual repository providers in App(). I'm guessing the issue is from pushing the material page route from the button. I don't think I understand how context or provider exactly works in Flutter. I thought the provider would look up the widget tree for the repository/bloc, does pushing a route some how break this continuity?
When you use Navigator.of(context).push or Navigator.of(context).pushNamed the widget pushed is not a child of the widget that call Navigator.of(context).push or Navigator.of(context).pushNamed, this widget is a child of the closest instance of Navigator that encloses the given context, in your case the Navigator is created by the MaterialApp, so if you want to provide the Repository or Bloc to different routes, the Provider must be a parent of the Navigator, in your case must be a parent of MaterialApp.