Why is Flutter dialog not rebuilding on change notifier? - flutter

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(),
),
);
}),
);
}
}

Related

Update child from parent in Flutter

Currently, I have two sample files Parent.dart and Child.dart.
In Parent.dart file this is what the code is like:
Parent.dart file:
children:
[
isDisabled
? Icon(Icons.public, color: Colors.grey)
: Icon(Icons.public, color:Colors.white),
InkWell(
onTap:()=> Navigator.push(context,MaterialPageRoute(builder: (context)=> Child(
isDisabled: isDisabled, function: ()=> function())),
]
function()
{
setState(()=> isDisabled = !isDisabled);
}
and in Child.dart the code is something like this:
children:
[
widget.isDisabled
? Icon(Icons.public, color: Colors.grey)
: Icon(Icons.public, color:Colors.white),
InkWell(
onTap:()=> widget.function(),
]
I have some data being fetched from a server that is used to populate a list of cards inside listview.builder.
What I'm trying to do is inherent variables from the parent and use their value to update the child. Currently, if I run this parent does change, but the child doesn't until you navigate back from parent to child.
For a better context: Imagine a list of cards. Each has an add-to-list button. Now if you click on the card it goes to another screen "child.dart" where it gives you more details about the item on the card you clicked. Now if you click the add-to-list button on the child screen it should also update the parent.
I tried different ways of achieving this "UI synchrony" for a better user experience. But I didn't find a proper way to implement it.
Things I tried: Provider (but it updates all the items on the list instead of each instance.),
a "hacky" method of editing the data in the list on the client side and updating the widget based on that. (This technique does work, but ewwwww)
I'm not really sure to understand your question.
If your question is how trigger a function in parent from child screen, here is your answer.
I made a working example. I think you were really close.
Another option for state management is riverpod 2.0
Or you can pass value in Navigator.pop and trigger the function in parent.
Parent model
class Parent {
String title;
bool isDisabled = false;
Parent({required this.title});
}
Main.dart
import 'package:flutter/material.dart';
import 'parent.dart';
import 'ParentCard.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp({super.key});
List<Parent> parentList = [Parent(title: 'Item 1'), Parent(title: 'Item 2')];
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: ListView.builder(
itemCount: parentList.length,
itemBuilder: (BuildContext context, int position) {
return ParentCard(title: parentList[position].title);
},
),
),
);
}
}
ParentCard.dart
import 'package:flutter/material.dart';
import 'child.dart';
class ParentCard extends StatefulWidget {
String title;
ParentCard({super.key, required this.title});
#override
State<ParentCard> createState() => _ParentCardState();
}
class _ParentCardState extends State<ParentCard> {
bool isDisabled = false;
#override
Widget build(BuildContext context) {
return Row(
children: [
Text(widget.title),
isDisabled
? Icon(Icons.public, color: Colors.green)
: Icon(Icons.public, color: Colors.black),
IconButton(
icon: Icon(Icons.plus_one),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ChildCard(isDisabled: isDisabled, handler: handler)),
),
)
],
);
}
handler() {
setState(() => isDisabled = !isDisabled);
}
}
** child.dart**
import 'package:flutter/material.dart';
class ChildCard extends StatefulWidget {
VoidCallback handler;
bool isDisabled;
ChildCard({super.key, required this.isDisabled, required this.handler});
#override
State<ChildCard> createState() => _ChildCardState();
}
class _ChildCardState extends State<ChildCard> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
const Text('child !'),
widget.isDisabled
? const Icon(Icons.public, color: Colors.green)
: const Icon(Icons.public, color: Colors.black),
ElevatedButton(
onPressed: () {
setState(() {
widget.isDisabled = !widget.isDisabled;
});
widget.handler();
},
child: const Text('click to trigger'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('pop it'),
)
]),
);
}
}

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),
),
),
],
),
),
);
}
}

How to save a boolean state in main.dart so it is retained throughout app

I have a challenge in retaining a boolean value state in my dashboard screen after I close or reload app.
On the dashboard screen, there is a ListTile where I can mark a card as verified by tapping on that card. Once the card is tapped, I set the bool verified state from false to true which works fine as long as I haven't closed or reloaded the app. Once the app is closed or reloaded, the boolean state is set back to false.
How can I initialize the boolean state in main.dart so that the verified status is always retained once it is set from the dashboard screen and can be used anywhere (more screens) within the app
here is the code:
Dashboard screen
class Dashboard extends StatefulWidget {
Dashboard({Key? key}) : super(key: key);
#override
_DashboardState createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
bool _verified = false;
//Retrieving card info from database
bool isFetching = false;
late String cardInfo = retrieveData; //url to php script for retrieving from database
List cardData = [];
getCardData() async {
setState(() => isFetching = true);
var response = await http.get(Uri.parse(cardInfo));
if (response.statusCode == 200) {
setState(() {
cardData = json.decode(response.body);
});
}
setState(() => isFetching = false);
return cardData;
}
#override
void initState() {
super.initState();
getCardData();
_verified;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Approve Card'),
centerTitle: true,
),
body: Container(
child: Card(
child: ListView.builder(
physics: const ClampingScrollPhysics(),
shrinkWrap: true,
primary: false,
itemCount: cardData.length, //coming from mysql database
itemBuilder: (context, index) {
return ListTile(
leading: Container(
padding: const EdgeInsets.only(left: 15.0),
alignment: Alignment.center,
height: 50,
width: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
image: DecorationImage(
image: NetworkImage(
'http://url/uploads/${cardData[index]['logo']}'),
fit: BoxFit.cover,
),
),
),
title: Text(
cardData[index]['name'],
style: TextStyle(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
cardData[index]['email'],
),
trailing: Bounce(
duration: const Duration(milliseconds: 100),
onPressed: () { //onPressed set verified state to true
//After app is reloaded, it is set back to false
setState(() {
col = iconTip;
_verified = true;
});
var url = Uri.parse(verifiedCards), //http url to php script
response = http.post(url, body: {
"card": cardData[index]['card'],
});
getCardData();
},
child: Container(
padding: const EdgeInsets.all(15.0),
color: col,
child: Icon(Icons.check_sharp),
),
),
);
}),
),
),
);
}
}
}
Main.dart screen
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: My Flutter App,
theme: ThemeData(
colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.green,
backgroundColor: white,
),
),
initialRoute: '/',
routes: {
'/': (context) => const SplashScreen(),
'/dashboard': (context) => const Dashboard(),
},
);
}
}
Use the shared_preferences package to save data that needs to persist between app reloads.
https://pub.dev/packages/shared_preferences
// Save value
await prefs.setBool('verified', true);
// Read value
final bool? verified= prefs.getBool('verified');
When you want to change your application state entire application then you can use Provider Package.
First create Model Class
class MyChangeNotifier extends ChangeNotifier{
bool _verified;
void setVarified(bool verified){
_verified = verified;
notifyListeners();
}
bool get verified => _verified;
}
Attach Notifier with main.dart file.
runApp(MultiProvider(
providers: [
Provider<MyChangeNotifier>(create: (_) => MyChangeNotifier()),
],
child: MyApp(),
));
Use State in your application
// For change State from widgets
ElevatedButton(onPressed: (){
context.read<MyChangeNotifier>().setVarified([true / false]);
}, child: Text('Event Setter'));
// Access State for widget
bool check = context.read<MyChangeNotifier>().verified;
Also, you can check Flutter State Management in Hindi video tutorial for more help.
A complete example with Shared Preference :
Writing boolean value:
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool('isfirstRun', false);
Reading boolean value:
SharedPreferences prefs = await SharedPreferences.getInstance();
bool myboolean = prefs.getBool(key) ?? true; // Or false depending of what you want for default value. This is required for Null safety
And don't forget to import share preference package:
import 'package:shared_preferences/shared_preferences.dart';
Thanks guys for your answers, I appreciate. I figured it out with the shared preferences I onced used. I was thinking there was a way to do so without using shared preferences but since it does the work and it's not too complicated, I decided to use it

How to control gif animation in Flutter?

I'm trying to restart an animated gif on Flutter. The gif image loads from network without a problem and animates after loading. I need to restart the animation on tapping a button.
Tried so far:
- setState
- change Key to some other unique key and setState to rebuild.
Solution as #chemamolins 's suggestion:
int _robotReloadCount=0;
....
GestureDetector(
onTap: () {
onTapRobot();
},
child: Center(
child: Container(
margin: EdgeInsets.only(top: 55.0, bottom: 5.0),
height: 150.0,
width: 150.0,
child:
FadeInImage(
key: this._robotImageKey,
placeholder: AssetImage('assets/common/robot_placeholder.png'),
image: NetworkImage(snapshot.data['robot_image_path'] +"robot_level" +snapshot.data['robot_level'].toString() +".gif"+"?"+this._robotReloadCount.toString()))),
),
),
....
onTapRobot() async{
setState(() {
this._robotReloadCount++;
});
}
I have done a lot of tests and it is not easy. The image is cached by the 'ImageProvider' and whatever you change or no matter the times you invoke build() the image is loaded from what is available in the cache.
So, apparently, you only have two options.
Either you rebuild with a new url, for instance by appending #whatever to the image url.
Or you remove the image from the cache as shown in the code below.
In either case you need to fetch again the image from the network.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String url = "https://media.giphy.com/media/hIfDZ869b7EHu/giphy.gif";
void _evictImage() {
final NetworkImage provider = NetworkImage(url);
provider.evict().then<void>((bool success) {
if (success) debugPrint('removed image!');
});
setState(() {});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: Image.network(url),
),
floatingActionButton: new FloatingActionButton(
onPressed: _evictImage,
child: new Icon(Icons.remove),
),
);
}
}

How to implement a Slider within an AlertDialog in Flutter?

I am learning app development on Flutter and can't get my Slider to work within the AlertDialog. It won't change it's value.
I did search the problem and came across this post on StackOverFlow:
Flutter - Why slider doesn't update in AlertDialog?
I read it and have kind of understood it. The accepted answer says that:
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
However I am not able to understand how exactly does it have to be implemented as not enough background code is provided.
This is what my current implementation looks like:
double _fontSize = 1.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(qt.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_size),
onPressed: () {
getFontSize(context);
},
),
],
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15.0),
itemCount: 3,
itemBuilder: (context, index) {
if (index == 0) {
return _getListTile(qt.scripture, qt.reading);
} else if (index == 1) {
return _getListTile('Reflection:', qt.reflection);
} else {
return _getListTile('Prayer:', qt.prayer);
}
})
);
}
void getFontSize(BuildContext context) {
showDialog(context: context,builder: (context){
return AlertDialog(
title: Text("Font Size"),
content: Slider(
value: _fontSize,
min: 0,
max: 100,
divisions: 5,
onChanged: (value){
setState(() {
_fontSize = value;
});
},
),
actions: <Widget>[
RaisedButton(
child: Text("Done"),
onPressed: (){},
)
],
);
});
}
Widget parseLargeText(String text) {...}
Widget _getListTile(String title, String subtitle) {...}
I understand that I will need to make use of async and await and Future. But I am not able to understand how exactly. I've spent more than an hour on this problem and can't any more. Please forgive me if this question is stupid and noobish. But trust me, I tried my best.
Here is a minimal runnable example. Key points:
The dialog is a stateful widget that stores the current value in its State. This is important because dialogs are technically separate "pages" on your app, inserted higher up in the hierarchy
Navigator.pop(...) to close the dialog and return the result
Usage of async/await
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double _fontSize = 20.0;
void _showFontSizePickerDialog() async {
// <-- note the async keyword here
// this will contain the result from Navigator.pop(context, result)
final selectedFontSize = await showDialog<double>(
context: context,
builder: (context) => FontSizePickerDialog(initialFontSize: _fontSize),
);
// execution of this code continues when the dialog was closed (popped)
// note that the result can also be null, so check it
// (back button or pressed outside of the dialog)
if (selectedFontSize != null) {
setState(() {
_fontSize = selectedFontSize;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Font Size: ${_fontSize}'),
RaisedButton(
onPressed: _showFontSizePickerDialog,
child: Text('Select Font Size'),
)
],
),
),
);
}
}
// move the dialog into it's own stateful widget.
// It's completely independent from your page
// this is good practice
class FontSizePickerDialog extends StatefulWidget {
/// initial selection for the slider
final double initialFontSize;
const FontSizePickerDialog({Key key, this.initialFontSize}) : super(key: key);
#override
_FontSizePickerDialogState createState() => _FontSizePickerDialogState();
}
class _FontSizePickerDialogState extends State<FontSizePickerDialog> {
/// current selection of the slider
double _fontSize;
#override
void initState() {
super.initState();
_fontSize = widget.initialFontSize;
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Font Size'),
content: Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
divisions: 9,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
),
actions: <Widget>[
FlatButton(
onPressed: () {
// Use the second argument of Navigator.pop(...) to pass
// back a result to the page that opened the dialog
Navigator.pop(context, _fontSize);
},
child: Text('DONE'),
)
],
);
}
}
You just need to warp the AlertDialog() with a StatefulBuilder()