Dynamic AppBar of Flutter - dart

I have to change AppBar appearance and items dynamically (by tapping on another UI elements).
What is the best approach?
I tested several methods. For example,
return Scaffold(
appBar: StreamBuilder(
stream: bloc.tasks,
builder: (context, AsyncSnapshot<List<UserTask>> tasks) {
return new AppBar();/// my setup is here
}),
but this is obviously doesn't compile.

appBar requires a widget that implements PreferredSizeWidget, and StreamBuilder isn't one.
You can wrap that tree into a PreferredSize:
Scaffold(
appBar: PreferredSize(
preferredSize: const Size(double.infinity, kToolbarHeight),
child: // StreamBuilder
),
)

Related

Proper way of changing scaffold content depending on orientation

I have a screen with GoogleMap widget and couple Text widgets. What I'm trying to do is to let map utilize whole screen in landscape orientation without AppBar, while being restricted to Container size in portrait orientaion.
Now I just have 2 scaffold widgets which beign redrawn on each orientaion change, and after couple rotations whole device freezes and I have to reboot it.
Widget build(BuildContext context) {
final mediaQueryData = MediaQuery.of(context);
if (mediaQueryData.orientation == Orientation.landscape) {
return Scaffold(
body:GoogleMap(
...
),
);
}
else{
return Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: ListView(
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
children: <Widget>[
Text('Text'),
Text('Text'),
Container(
height: MediaQuery.of(context).size.height/3,
child: GoogleMap(
...
),
),
],
),
);
}
So I would really appreciate if someone could guide me to more efficient way of doing something like this.
Also if there's not, I would also like to know if it's possible to have working gestures (scroll, pan, etc.) in GoogleMap widget nested in scrollable ListView.
If anyone intrested someone suggested this method, but comment got deleted:
Widget build(BuildContext context) {
final isLandScape = MediaQuery.of(context).orientation == Orientation.landscape;
return Scaffold(
appBar: isLandScape ? null : AppBar(
title: Text('Title'),
),
I used same method also for Visibility widget that includes other widgets besides map:
visible: isLandScape ? false : true,
And same for Container with map:
Container(
height: isLandScape ? MediaQuery.of(context).size.height : MediaQuery.of(context).size.height/3,
child: GoogleMap(
///
),
),
Seems to be working stable now.

list length from StreamBuilder?

In the header of my flutter app, I want to display a Chip with the number of items in the list displayed in the body. The build method of the body is using a StreamBuilder to create the list.
The problem is that the length of the list isn't known until after the AppBar is built and the StreamBuilder finishes building the list.
Since I can't call setState() from within the build function, how can I get the value in the AppBar to update after the StreamBuilder is finished building the list?
I'm working through this example. Here's the code snippet:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Streams'),
elevation: 1.0,
),
body: Container(
child: _buildContent(),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _createCounter,
),
);
}
Widget _buildContent() {
return StreamBuilder<List<Counter>>(
stream: stream,
builder: (context, snapshot) {
return ListItemsBuilder<Counter>(
items: snapshot.hasData ? snapshot.data : null,
itemBuilder: (context, counter) {
return CounterListTile(
key: Key('counter-${counter.id}'),
counter: counter,
onDecrement: _decrement,
onIncrement: _increment,
onDismissed: _delete,
);
},
);
},
);
}
I've found a way around it by creating a second StreamBuilder just to update the Chip in the AppBar. Is it a good practice to have multiple StreamBuilders watching the same stream?

Flutter: How to display a snackbar from an appbar action

I'm trying to display a SnackBar after performing an action from the AppBar.
The AppBar cannot be built from a builder so it can't access is Scaffold ancestor.
I know we can use a GlobalKey object to access the context whenever we want, but I would like to know if there is a solution without using the GlobalKey.
I found some github issues and pull-request, but I can't find a solution from them
=> https://github.com/flutter/flutter/issues/4581 and https://github.com/flutter/flutter/pull/9380
Some more context:
I have an Appbar with a PopupMenuButton, which have one item. When the user click on this item I display a dialog which the showDialog method and if the user clicks on "ok" I want to display a SnackBar
You can use the Builder widget
Example:
Scaffold(
appBar: AppBar(
actions: <Widget>[
Builder(
builder: (BuildContext context) {
return IconButton(
icon: const Icon(Icons.message),
onPressed: () {
final snackBar = SnackBar(content: Text('Yay! A SnackBar!'));
Scaffold.of(context).showSnackBar(snackBar);
},
);
},
),
],
)
);
The Scaffold.appBar parameter requires a PreferredSizeWidget, so you can have a Builder there like this:
appBar: PreferredSize(
preferredSize: Size.fromHeight(56),
child: Builder(
builder: (context) => AppBar(...),
),
),
An option is to use two contexts in the dialog and use the context passed to the dialog to search for the Scaffold.
When you show a dialog, you are displaying a completely different page/route which is outside the scope of the calling page. So no scaffold is available.
Below you have a working example where you use the scope of the first page.
The problem, though, is that the SnackBar is not removed.
If instead you use a GlobalKey to get the Scaffold the problem is the same.
I would consider not using a Snackbar in this case, because it is associated to the page below. It is even greyed out by the dialog shadow.
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 StatelessWidget {
_showDialog(BuildContext context1) {
return showDialog(
context: context1,
builder: (BuildContext context) {
return AlertDialog(
content: Text("Dialog"),
actions: <Widget>[
new FlatButton(
child: new Text("OK"),
onPressed: () => Scaffold.of(context1).showSnackBar(SnackBar(
content: Text("Pressed"),
)),
),
],
);
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Test"),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry>[
PopupMenuItem(
child: ListTile(
title: Text('Show dialog'),
onTap: () => _showDialog(context),
),
),
];
},
)
],
),
);
}
}

SafeArea not working in persistent bottomsheet in Flutter

I am using MaterialApp and Scaffold + SafeArea for screens. All works well until I try to use persistent BottomSheet. BottomSheet content igores SafeArea and are shown below system controls, for example in iPhone X.
I tried to wrap BottomSheet contents in another SafeArea element, but it did not help.
Is there a way to get the same functionality as SafeArea to work in BottomSheet? If yes then how?
Just make the root Widget of the showModalBottomSheet be a SafeArea Widget
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[...
I have faced this issue too. When I changed code from showModalBottomSheet to _scaffoldKey.currentState.showBottomSheet my SafeArea stopped working.
You can solve it with these steps:
create key for your Scaffold GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
assign created key to your Scaffold key: _scaffoldKey,
set bottom padding to your bottom sheet
padding: EdgeInsets.only(bottom: MediaQuery.of(_scaffoldKey.currentState.context).viewPadding.bottom)
Here is a result, I also added 15 padding to top, left and right.
I solved the problem by adding container and padding inside with: MediaQueryData.fromWindow(WidgetsBinding.instance.window).padding.top
Container(
padding: EdgeInsets.only(
top: MediaQueryData.fromWindow(WidgetsBinding.instance.window).padding.top),
child: MyOriginalWidgetForBottomSheet(),
)
Don't get from the context (like MediaQuery.of(context).padding.top) as the padding always returns 0
In my case the top safe area was the problem and a practical workaround was to set padding to the height of the app bar with AppBar().preferredSize.height
Padding(
padding: EdgeInsets.fromLTRB(0,AppBar().preferredSize.height,0,0),
child: Text(
'Your Order',
style: headingMediumBlack,
),
),
I tried out this simple code and it works as intended in the iOS Simulator with an iPhone X:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new SafeArea(
child: new Scaffold(
appBar: new AppBar(
title: new Text('SafeArea demo'),
),
body: new Center(
child: new TapMe(),
),
),
));
}
}
class TapMe extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text('Tap Me'),
onPressed: () => Scaffold
.of(context)
.showBottomSheet((context) => new Text('I\'ve been tapped')),
);
}
}
What version of Flutter are you using?
Faced this issue too with top-notch.
Resolved using dart:ui.window.viewPadding.top / ui.window.devicePixelRatio
How it works:
The dart:ui.window.viewPadding is the number of physical pixels on each side of the display rectangle into which the view can render.
By dividing to ui.window.devicePixelRatio we should receive logic pixels.
So in summary code is
showModalBottomSheet<bool>(
context: context,
builder: (BuildContext context) => Container(
height: MediaQuery.of(context).size.height,
padding: EdgeInsets.only(
top: ui.window.viewPadding.top / ui.window.devicePixelRatio,
),
),
);
IMHO, The cleanest workaround is to set InitialChildSize to 0.9
DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.2,
maxChildSize: 0.9,
expand: false,
builder: (BuildContext context, ScrollController scrollController) {
}
)
This has been fixed -- just set useSafeArea: true in showModalBottomSheet. https://github.com/flutter/flutter/issues/39205

AppBar Title sticks to the left side of the screen

Why does the Title stick to the left instead of leaving some space ?
This is the Scaffold code
return new Scaffold(
appBar: new AppBar(title: const Text('Friendlychat')),);
This is a regression in current flutter alpha release which is fixed in master - see this issue
For now, any of the workarounds mentioned in the answer by aziza can be used, but you will have to revert it when the new alpha is released
There are few ways you can modify that:
You can wrap your title widget inside a Padding widget and use the padding property to indent the area on the left.
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Padding (child: new Text ("Friendly Chat"),
padding:const EdgeInsets.only(left: 20.0) ),
),
);
}
Or you can add an empty Container in the leading property of your AppBar.
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title:new Text ("Friendly Chat"),
leading: new Container(),
),
);
}
If you want the title to be centered, set the property centerTitle in your AppBar to true
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title:new Text ("Friendly Chat"),
centerTitle: true,
),
);
}
This is a regression in current flutter alpha release which is fixed in master as pointed out by #aptik.Thanks

Resources