It is possible to preserve state of PopUpMenuButton? - flutter

Currently i am working on music app and according to my ui i have to display download, downloading progress and downloaded status shown inside popup menu item.But according to Popup menu button widget behaviour, it is dispose and unmounted.So when i closed popup menu item and again open the last status always display download instead of downloading.So it is possible to prevent popup menu button after close.
I tried callback functions, provider, getx, auto keep alive and also stateful builder but it is not working.

I am using ValueNotifier to preserve the download progress. To preserve the state you can follow this structure and use state-management property like riverpod/bloc
class DTest extends StatefulWidget {
const DTest({super.key});
#override
State<DTest> createState() => _DTestState();
}
class _DTestState extends State<DTest> {
/// some state-management , also can be add a listener
ValueNotifier<double?> downloadProgress = ValueNotifier(null);
Timer? timer;
_startDownload() {
timer ??= Timer.periodic(
Duration(milliseconds: 10),
(timer) {
downloadProgress.value = (downloadProgress.value ?? 0) + .01;
if (downloadProgress.value! > 1) timer.cancel();
},
);
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
child: ValueListenableBuilder(
valueListenable: downloadProgress,
builder: (context, value, child) => InkWell(
onTap: value == null ? _startDownload : null,
child: Text("${value ?? "Download"}")),
),
)
];
},
)
],
),
);
}
}

Related

ModalBarrier scrim does not cover AppBar

I've got a MaterialApp which uses a builder with a scaffold in it. When I navigate from page to page the scaffold and app bar does not rebuild, but the body of the scaffold does:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:raven_front/pages/pages.dart';
Future<void> main() async {
runApp(RavenMobileApp());
}
class RavenMobileApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/splash',
routes: pages.routes(context),
builder: (context, child) {
return SafeArea(
child: Scaffold(
extendBodyBehindAppBar: false,
appBar: BackdropAppBar(), // pretty much regular app bar
body: child!, // pages of app
));
},
);
}
}
but when I'm on a page where I need to show a bottom modal sheet, or alert box, or anything with a scrim, it doesn't apply to the app bar:
for example, I might make the ModalBottomSheet this way
await showModalBottomSheet<void>(
context: context,
elevation: 1,
barrierColor: AppColors.black38, // not applied to app bar
shape: components.shape.topRounded,
builder: (BuildContext context) {
...
});
With my setup of having a builder in the MaterialApp, how can I get the scrim to cover everything?
I tried saving the context used in the MaterialApp (highest level) and using that in the modal sheet, but that errored saying that context doesn't have a Navigator. I'm hoping I can keep the current design but extend the scrim over the app bar somehow.
can you believe it, I had to roll my own. the other option was to abandon the builder material app design mentioned in the question. This is what I had to do:
app bar:
Stack(
children: [
appBar,
AppBarScrim(),
])
app bar scrim
class AppBarScrim extends StatefulWidget {
const AppBarScrim({Key? key}) : super(key: key);
#override
State<AppBarScrim> createState() => _AppBarScrimState();
}
class _AppBarScrimState extends State<AppBarScrim> {
late List listeners = [];
final Duration waitForSheetDrop = Duration(milliseconds: 50);
bool applyScrim = false;
#override
void initState() {
super.initState();
listeners.add(streams.app.scrim.listen((bool value) async {
if (applyScrim && !value) {
await Future.delayed(waitForSheetDrop);
setState(() {
applyScrim = value;
});
}
if (!applyScrim && value) {
setState(() {
applyScrim = value;
});
}
}));
}
#override
void dispose() {
for (var listener in listeners) {
listener.cancel();
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
Navigator.of(components.navigator.routeContext!).pop();
streams.app.scrim.add(false);
},
child: AnimatedContainer(
duration: waitForSheetDrop,
color: applyScrim ? Colors.black38 : Colors.transparent,
height: applyScrim ? 56 : 0,
));
}
}
showDialog( // and show bottom modal sheet...
..
builder: (BuildContext context) {
streams.app.scrim.add(true); // trigger
return AlertDialog(...);
}).then((value) => streams.app.scrim.add(false)); // remove trigger

How to automatically show an alert dialog without pressing a button in Flutter?

I implemented the alert dialog in the initstate() method but Init state is only called once. In my case I want the alert to appear automatically every time a variable value changes for exemple. ( I need it to suddenly pop up during using the app)
You could use a ValueNotifier and a ValueListenableBuilder so that every time the value in the ValueNotifier changes, the ValueListenableBuilder rebuilds and shows a dialog, like so:
class MyWidget extends StatelessWidget {
ValueNotifier<int> dialogTrigger = ValueNotifier(0);
#override
Widget build(BuildContext context) {
return Column(
children: [
TextButton(
onPressed: () {
var random = Random();
dialogTrigger.value = random.nextInt(100);
},
child: const Text('Click me and change a value')
),
Expanded(
child: ValueListenableBuilder(
valueListenable: dialogTrigger,
builder: (ctx, value, child) {
Future.delayed(const Duration(seconds: 0), () {
showDialog(
context: ctx,
builder: (ctx) {
return AlertDialog(
title: const Text('Dialog'),
content: Text('Hey! I got a $value'),
);
});
});
return const SizedBox();
})
)
]
);
}
}
(Again, I'm only adding a button to change the value, not to launch the dialog. That way you can programmatically change the value, which eventually launches the dialog). See if that works for your purposes.

How to update content of a ModalBottomSheet from parent while active

I have a custom button which takes in a list of items. When it is pressed, it opens a modal bottom sheet and passes that list to the bottom sheet.
However, When the buttons items change it doesn't update the items in the bottom sheet.
How can I achieve this effect.
Simple example
ButtonsPage
|
Button(items: initialItems)
|
BottomSheet(items: initialItems)
** After a delay, setState is called in ButtonsPage with newItems, thereby sending newItems to the button
ButtonsPage
|
Button(items: newItems)
| ## Here, the bottom sheet is open. I want it to update initialItems to be newItems in the bottom sheet
BottomSheet(items: initialItems -- should be newItems)
Code Sample
This is my select field which, as shown, receives a list items and when it is pressed it opens a bottom sheet and sends the items received to the bottom sheet.
class PaperSelect extends StatefulWidget {
final List<dynamic> items;
PaperSelect({
this.items,
}) : super(key: key);
#override
_PaperSelectState createState() => _PaperSelectState();
}
class _PaperSelectState extends State<PaperSelect> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.disabled ? null : () => _showBottomSheet(context),
child: Container(
),
);
}
void _showBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (BuildContext context) => BottomSheet(
items: widget.items,
),
)
);
}
}
After some time (a network call), items is updated in the parent Widget of PaperSelect. PaperSelect then updates and receives the new items.
class BottomSheet extends StatefulWidget {
final dynamic items;
BottomSheet({
this.items,
}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _BottomSheetState();
}
}
class _BottomSheetState extends State<BottomSheet> {
dynamic items;
#override
void initState() {
print(widget.items);
items = widget.items;
super.initState();
}
#override
Widget build(BuildContext context) {
if(items==null) return Center(
child: SizedBox(
width: 140.0,
height: 140.0,
child: PaperLoader()
),
);
if(items==-1) return Text("Network Error");
return Column(
children: <Widget>
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (BuildContext context, int i) => ListTile(
onTap: () => Navigator.pop(context),
title: Text('')
)
),
),
],
);
}
}
Now, I want to send the updated data to the bottom sheet. However, it doesn't work because the ModalBottomSheet is already open.
How can I get around this?
I assume here that items is a List<String>, since you did not specify that at all. (You should generally not use dynamic in most cases, because it does not do any type checking at all). Anyway,
One thing you could do (beside countless others) is pass in a ValueNotifier<List<String>> instead of a List<String> and then user that with a ValueListenableBuilder in your bottom sheet. like:
// in the caller
final itemsNotifier = ValueNotifier(items);
// in your bottom sheet
ValueListenableBuilder<List<String>>(
valueListenable: itemsNotifier,
builder: (context, snapshot, child) {
final items = snapshot.data;
// build your list
},
)
then every time you would change itemsNotifier.value = [...] your items would rebuild.
Note when updating itemsNotifier.value: It must not be done inside build/didUpdateWidget/etc. so if this is the case you might use WidgetsBinding.instance.addPostFrameCallback(() => itemsNotifier.value = ... ) to postpone updating the value until after the current build phase.

Flutter CircularprogressIndicator with Navigation

How to implement flutter code so that as soon as my application is launched, it will show circularprogressindicator and then load another class through Navigation.push
As I know navigation.push requires a user action like ontap or onpressed
Please assist me with this
The requirement you need is of Splash Screen, which stays for a while, and then another page comes up. There are certain things you can do, that is
Use Future.delayed constructor, which can delay a process by the Duration time you provide, and then implement your OP, in this case, you Navigator.push()
Future.delayed(Duration(seconds: your_input_seconds), (){
// here you method will be implemented after the seconds you have provided
Navigator.push();
});
The above should be called in the initState(), so that when your page comes up, the above process happens and you are good do go
You can use your CircularProgressIndicator normally in the FirsScreen
Assumptions:
Our page will be called FirstPage and SecondPage respectively.
We will be going from FirstPage -> SecondPage directly after N seconds
Also, if you are working on a page like this, you don't want to go back to that page, so rather than using Navigator.push(), use this pushAndRemoveUntil
Let us jump to the code now
FirstPage.dart
// FIRST PAGE
class FirstPage extends StatefulWidget {
FirstPage({Key key, this.title}) : super(key: key);
final String title;
#override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
//here is the magic begins
#override
void initState(){
super.initState();
//setting the seconds to 2, you can set as per your
// convenience
Future.delayed(Duration(seconds: 2), (){
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(
builder: (context) => SecondPage()
), (_) => false);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
height: double.infinity,
width: double.infinity,
child: Center(
child: CircularProgressIndicator()
)
)
);
}
}
SecondPage.dart
// SECOND PAGE
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Page"),
),
body: Container(
height: double.infinity,
width: double.infinity,
child: Center(
child: Text('Welcome to Second Page')
)
)
);
}
}
Result
Look at how the page works, with out having any buttons, stays for 2 seconds and then go to second page. But also, no back button, since going back is not the right choice. You must remove all the items from the stack if you are making a page like this
EDITS AS PER THE ERROR
Since I can see that you're currently getting an error because, the Widget is not ready, to even call the Future.delayed(). To do that what you need to do is, make changes in your FirstPage.dart, initState() method. Rest can left as is
#override()
void initState(){
super.initState();
// Ensures that your widget is first built and then
// perform operation
WidgetsBinding.instance.addPostFrameCallback((_){
//setting the seconds to 2, you can set as per your
// convenience
Future.delayed(Duration(seconds: 2), (){
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(
builder: (context) => SecondPage()
), (_) => false);
});
});
}
OR
If WidgetsBinding.instance.addPostFrameCallback((_){}, this doesn't comes handy, use this in place of the mentioned function
// This needs to be imported for this particular only
// i.e., ScheduleBider not WidgetBinding
import 'package:flutter/scheduler.dart';
#override
void initState(){
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_){
//setting the seconds to 2, you can set as per your
// convenience
Future.delayed(Duration(seconds: 2), (){
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(
builder: (context) => SecondPage()
), (_) => false);
});
});
}

Update SnackBar content in Flutter

I have a button that displays a SnackBar (or toast) before moving to the next page. I have a countdown and after 5 seconds I push Page2.
RaisedButton(
onPressed: () {
_startTimer();
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(
'Prepare yourself to start in ${widget._current.toString()}!'), // doesn't work here
duration: new Duration(seconds: widget._start),
action: SnackBarAction(
label: widget._current.toString(), // and neither does here
onPressed: () {
// Some code to undo the change.
},
),
);
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text(
"I'm ready",
style: TextStyle(fontSize: 20),
),
),
Nothing to see on the countdown but I'll paste it just in case:
void _startTimer() {
CountdownTimer countDownTimer = new CountdownTimer(
new Duration(seconds: widget._start),
new Duration(seconds: 1),
);
var sub = countDownTimer.listen(null);
sub.onData((duration) {
setState(() {
widget._current = widget._start - duration.elapsed.inSeconds;
});
});
sub.onDone(() {
print("Done");
sub.cancel();
});
}
So if I display the countdown somewhere else (inside a Text for example) it works but it seems that the SnackBar doesn't change its contain, it always get the max number of the countdown.
you need to implement a custom widget with countdown logic in side for the content field of snackbar, like this:
class TextWithCountdown extends StatefulWidget {
final String text;
final int countValue;
final VoidCallback? onCountDown;
const TextWithCountdown({
Key? key,
required this.text,
required this.countValue,
this.onCountDown,
}) : super(key: key);
#override
_TextWithCountdownState createState() => _TextWithCountdownState();
}
class _TextWithCountdownState extends State<TextWithCountdown> {
late int count = widget.countValue;
late Timer timer;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), _timerHandle);
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: Text("[$count] " + widget.text),
);
}
void _timerHandle(Timer timer) {
setState(() {
count -= 1;
});
if (count <= 0) {
timer.cancel();
widget.onCountDown?.call();
}
}
}
That is because the snack bar is built once, when the button is clicked. When the state updates, it rebuilds the widget tree according to the changes. The snack bar initially isn't in the widget tree, so it doesn't update.
Try to use stack and show a snack bar, and then you should be able to manipulate it however you need.
Hope it helps.
Update SnackBar Content
SnackBar content can be updated/rebuilt while it's visible by making its content: widget dynamic.
In the code sample below we're using a ValueNotifier and ValueListenableBuilder to dynamically rebuild the content of the SnackBar whenever ValueNotifier is given a new value.
(There are many ways to to maintain state values & rebuild widgets when it changes, such as RiverPod, GetX, StreamBuilder, etc. This example uses the Flutter native ValueListenableBuilder.)
When running this Flutter page, click the FAB to show the SnackBar, then click on the center text to update the SnackBar's content (multiple times if you like).
Example
Use SnackBarUpdateExample() widget in your MaterialApp home: argument to try this example in an emulator or device.
class SimpleCount {
int count = 0;
}
class SnackBarUpdateExample extends StatelessWidget {
static const _initText = 'Initial text here';
/// This can be "listened" for changes to trigger rebuilds
final ValueNotifier<String> snackMsg = ValueNotifier(_initText);
final SimpleCount sc = SimpleCount();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SnackBar Update'),
),
/// ↓ Nested Scaffold not necessary, just prevents FAB being pushed up by SnackBar
body: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Click FAB to show SnackBar'),
SizedBox(height: 20,),
Text('Then...'),
SizedBox(height: 20,),
InkWell(
child: Text('Click → here ← to update SnackBar'),
onTap: () {
sc.count++;
snackMsg.value = "Hey! It changed! ${sc.count}";
},
), /// When snackMsg.value changes, the ValueListenableBuilder
/// watching this value, will call its builder function again,
/// and update its SnackBar content widget
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () => _showSnackBar(context),
),
),
);
}
void _showSnackBar(BuildContext context) {
var _controller = ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: SnackContent(snackMsg))
);
/// This resets the snackBar content when it's dismissed
_controller.closed.whenComplete(() {
snackMsg.value = _initText;
sc.count = 0;
});
}
}
/// The ValueListenableBuilder rebuilds whenever [snackMsg] changes.
class SnackContent extends StatelessWidget {
final ValueNotifier<String> snackMsg;
SnackContent(this.snackMsg);
#override
Widget build(BuildContext context) {
/// ValueListenableBuilder rebuilds whenever snackMsg value changes.
/// i.e. this "listens" to changes of ValueNotifier "snackMsg".
/// "msg" in builder below is the value of "snackMsg" ValueNotifier.
/// We don't use the other builder args for this example so they are
/// set to _ & __ just for readability.
return ValueListenableBuilder<String>(
valueListenable: snackMsg,
builder: (_, msg, __) => Text(msg));
}
}