I want to use MediaQuery to build widgets based on screen height and width. The problem, also referenced in #26004, is that I only want to query the size data once, for example in initState. MediaQuery documentation states
Querying the current media using MediaQuery.of will cause your widget to rebuild automatically whenever the MediaQueryData changes (e.g., if the user rotates their device).
, but that causes unnecessary rebuilds in my application. Specifically, it causes rebuild of widgets if there are changes to insets or padding (such as when keyboard is displayed).
Is there an alternative to MediaQuery which wouldn't cause rebuilds when MediaQueryData changes?
I had this issue as well and initially thought that the MediaQuery is causing unnecessary rebuilds, but if you think about it you do want the widgets to rebuild (in cases of device rotation, keyboard popup) for the app to have a responsive design.
You could do something like this:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Builder(builder: (context) {
ResponsiveApp.setMq(context);
return MyHomePage(title: 'Flutter Demo Home Page');
}),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Flex(
direction:
ResponsiveApp().mq.size.width > ResponsiveApp().mq.size.height
? Axis.horizontal
: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class ResponsiveApp {
static MediaQueryData _mediaQueryData;
MediaQueryData get mq => _mediaQueryData;
static void setMq(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
}
}
I set the mediaQueryData at the beginning with ResponsiveApp.setMq(context) and I used the Builder because you can only use the MediaQuery one context below the MaterialApp widget. After the _mediaQueryData is set you can get it whenever you want to build widgets based on the screen size.
In this code I just change the Axis direction when the device is rotated and the widget needs to rebuild to show the changed direction.
You could also have something like :
if (_mediaQueryData.size.shortestSide < 400)
//phone layout
else if(_mediaQueryData.size.shortestSide >= 400 && _mediaQueryData.size.shortestSide < 600)
//tablet layout
else
//web layout
and resizing the window in web will cause the widgets to rebuild multiple times and display the desired layout.
But if you don't want to use MediaQuery at all, you can check the Window class from dart:ui.
LayoutBuilder seems preferable over every use of MediaQuery for sizing a viewport (either the whole screen, or the space left in a column or other layout).
LayoutBuilder also works hard to avoid rebuilding its child if the size doesn't change and the parents haven't had to re-layout.
The builder function is called in the following situations:
The first time the widget is laid out.
When the parent widget passes different layout constraints.
When the parent widget updates this widget.
When the dependencies that the builder function subscribes to change.
The builder function is not called during layout if the parent passes
the same constraints repeatedly.
And you don't have to think about "the height of the appbar" ever again, because you're getting the space left, not the total space on the screen.
Check it out: https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html
In my case, the problem was happening because I was controlling the focus manually using:
onEditingComplete: () {
FocusScope.of(context).nextFocus();
}
The context used was the Parent's context, and it was causing the rebuilding. Not sure why it happened, but it stopped once I've wrap the TextFormField with a Builder and started using its context instead.
Note: I'm also using MediaQuery.of(context).size.height normally (without the rebuild side effect) to set the Widget's Parent height 🤔
Related
I have a List view. Builder with an image. The image has an opacity depending on a provider element (provider element is true, the opacity is 1, if it false opacity is 0). Then I have another class, from that class I can update the provider of the list view image, however in order to change the effect of the change, I have to set state the list view widget.
Even if it is good and is working properly, I don't want to do that because when I set state the list view widget, the list view position restart and I need to scroll again at the element of the list view, I know that I can save the scroll position (with a controller) but because my list view is too large, it needs a lot of time to get at the position, so I don't like it.
Any idea ?
If you want to rebuild the UI without calling setState, you can use the help of ValueNotifier and the ValueListenableBuilder widget.
Here is a simplified counter app, if you take a look at the console, you'll see that print('build'); is only called initially running the build method and not called again when calling _increment():
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: PageLoadApp()));
final counter = ValueNotifier<int>(0);
class PageLoadApp extends StatefulWidget {
const PageLoadApp({Key? key}) : super(key: key);
#override
State<PageLoadApp> createState() => _PageLoadAppState();
}
class _PageLoadAppState extends State<PageLoadApp> {
void _increment() {
counter.value++;
}
#override
Widget build(BuildContext context) {
print('build');
return Scaffold(
body: Scaffold(
body: ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, _) {
return Center(
child: Text('$value', style: const TextStyle(fontSize: 30)),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
)));
}
}
Additionally, this widget can be a StatelessWidget.
Since you haven't provided a code example, you'll have to adapt this provided example for your case.
Here is a YouTube video by the Google team explaining ValueListenableBuilder
I've recently developed comment view below the detail posts.
Like the image I attached, I'd like to show images for each comment but the image Container should be transparent to see the last comment.
But I think Scaffold doesn't allow bottomSheet to have transparent children.
Are there anyone having an idea to solve this problem?
class PostDetail extends StatelessWidget {
final int maxRenderImgCnt = 4;
final Post post;
PostDetail(this.post);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar( ... ),
body: SingleChildScrollView( ... ),
bottomSheet: CommonTextField(onTap: null, editTarget: null),
You can wrap the widget with an opacity widget but there is also another way which is more efficient even for changing it later, that is the ThemeData widget:
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: Colors.black.withOpacity(0),
),
I am trying to perform basic widget testing in Flutter. Basically I would like to have a list with list of data, and display each of the items in a custom widget (BasicListItem) which also has a ListTile widget in it.
Root widget:
class MyApp extends StatelessWidget {
final List taskList = ['List-1', 'List-2', 'List-3'];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: ListView.builder(
itemCount: taskList.length, itemBuilder: _itemBuilder),
),
);
}
Widget _itemBuilder(BuildContext context, int index) {
final String item = taskList[index];
return BasicListItem(key: Key(item), title: item);
}
}
The list item widget (BasicListItem) takes a title, and use it inside the ListTile widget.
class BasicListItem extends StatelessWidget {
final String title;
const BasicListItem({required Key key, required this.title})
: super(key: key);
#override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(Icons.map),
title: Text(title),
);
}
}
This is the test for it:
testWidgets('has title and Icons', (WidgetTester tester) async {
const testKey = Key('my-key-1');
const testTitle = 'Demo title';
await tester.pumpWidget(BasicListItem(key: testKey, title: testTitle));
expect(find.text(testTitle), findsOneWidget);
});
But the test throws an error:
No Material widget found. ListTile widgets require a Material widget
ancestor.
...
...
The following TestFailure object was thrown running a test:
Expected: exactly one matching node in the widget tree Actual:
_TextFinder:<zero widgets with text "Demo title" (ignoring offstage widgets)>
However, the test does pass if I wrap ListTile around a MaterialApp, inside the BasicListItem build method. Like so:
#override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
home: Scaffold(
body: ListTile(
leading: Icon(Icons.map),
title: Text(title),
),
)
);
}
But doing this I cannot use it inside the ListView widget. And also I would like to have modular/separate custom widgets so that I can use it on different places as well. I am new and maybe I am missing something. How can I build custom widget and test it out? Could you help me out please.
I didn't understand Darshan's answer at first, because I think the code he provided made me implement the MaterialApp and Material widget into BasicListItem widget class build method directly, instead of implementing it on just the test suit. But that gave me the clue to implement it.
So, this is the final test case. I did wrapped MaterialApp and Material widget with BasicListItem, but not in the build method, instead I wrapped them just on the test case:
testWidgets('has title and Icons', (WidgetTester tester) async {
const testKey = Key('my-key-1');
const testTitle = 'Demo title';
await await tester.pumpWidget(MaterialApp(
home: Material(
child: BasicListItem(key: testKey, title: testTitle),
),
));;
expect(find.text(testTitle), findsOneWidget);
});
I hope this will help others like me as well.
The ListTile component comes from the Material part of Flutter UI components & is not an independent widget, therefore it needs a MaterialApp as parent.
You can check that the ListTile is under material library here: https://api.flutter.dev/flutter/material/ListTile-class.html
Also, you can create as many custom Widgets to use in separate modules,
the only requirement would be to use MaterialApp at the very beginning of the app initialisation.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
/// Only this needs to be a MaterialApp
return MaterialApp(
title: 'Welcome to Flutter',
/// this point to different screen widget also, like MainScreen()
/// Or you can start using Scaffold from here as well.
home: Scaffold(
appBar: AppBar(
title: Text('Welcome to Flutter'),
),
body: Center(
child: Text('Hello World'),
),
),
);
}
}
It is not necessary to use MaterialApp as a parent on every custom widget you build. Just the root can be fine too.
But if you are using a single widget to simply test out, & it requires a Material ancestor, you can simply wrap the widget in a Material widget as well.
Okay, this is not specifically in Flutter Docs but is hinted about all over the place. On flutter test side we are pumping a root widget to render a frame as our palette used to test widgets.
Translates to you need to create a Root App Widget to wrap the widget under test. eBay's Golden Toolkit supplies the hooks to make this possible via pumpWidgetBuilder which is an extension of Widget Tester.
For more see my blog, https://fredgrott.medium.com
Let's say I have a base page in a Material App.
The basepage only has one widget, a scaffold.
The scaffold contains an appbar, that is to remain constant through the app, in every page.
The scaffold also contains a body, which should be overriden by the pages that extend the base to display their contents.
How can I go about to do this?
Thanks for the help!
You could create a globally accessible page like this:
base_page.dart
import 'package:flutter/material.dart';
class BasePage extends StatelessWidget {
/// Body of [BasePage]
final Widget body;
const BasePage({#required this.body, Key key})
: assert(body != null),
super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Your appbar content here
),
body: body,
);
}
}
And when you want to use it, just provide the body to the new class like this:
main.dart
import 'package:flutter/material.dart';
import 'base_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BasePage(
// This is where you give you custom widget it's data.
body: Center(child: Text('Hello, World')),
);
}
}
One way to do this is to create a variable holding the current route in your StatefulWidget.
Widget currentBody = your initial body ;
and then change that variable whenever you want to switch the body using setState:
SetState(() { currentBody = your new body widget }) ;
and in your scaffold after the appbar you put !
body : currentBody ;
You have many ways to do this, one is to use the Bloc package, but another way is to use a Bottom Navigation Bar](https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html)
The bottom navigation bar consists of multiple items in the form of text labels, icons, or both, laid out on top of a piece of material. It provides quick navigation between the top-level views of an app. For larger screens, side navigation may be a better fit.
A bottom navigation bar is usually used in conjunction with a Scaffold, where it is provided as the Scaffold.bottomNavigationBar argument.
I provided an example in my answer for transparent appbar, of course you do not need your appbar to be transparent.
class HomePageState extends State<Homepage> {
List<Widget> widgets = [Text("haha"), Placeholder(), Text("hoho")]; // as many widgets as you have buttons.
Widget currentWidget = widgets[0];
void _onItemTapped(int index) {
setState(() {
NavigationBar._selectedIndex = index;
currentWidget = widgets[index];
});
}
#override
Widget build(BuildContext context) {
return return MaterialApp(
home: Scaffold(
extendBody: true, // very important as noted
bottomNavigationBar: BottomNavigationBar(
currentIndex: NavigationBar._selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
backgroundColor: Color(0x00ffffff), // transparent
type: BottomNavigationBarType.fixed,
unselectedItemColor: Colors.blue,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.grade),
title: Text('Level'),
),
[...] // remaining in the link
),
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: ExactAssetImage("assets/background.png"), // because if you want a transparent navigation bar I assume that you have either a background image or a background color.
fit: BoxFit.fill
),
),
child: currentWidget
),
),
);
}
}
...
The Bloc architecture is harder to understand, you will need to read documentation and try tutorials, but it is also very interesting to implement.
I want to be able to display a zoomable image that initialy fill the screen width and keep aspect ratio,
I also need the screen to be scrollable (SingleChildScrollView) because it will contain other element except the image
The attached full App is generated by
flutter create --androidx view_photo
add getter
code:
get myImage {
var photoView = PhotoView(
initialScale: PhotoViewComputedScale.covered,
imageProvider: AssetImage("assets/1.jpg"),
);
return Container(
height: 100, // set auto according to image height
child: photoView);
}
add myImage to Column
The problem: I want the height of the image to be according to the image size, so that when it display the container will have width/height set to max according to image aspect ratio.
If I remove the height, the app will throw error:
_RenderLayoutBuilder object was given an infinite size during layout.
I guess I can calculate the height according to the image aspect ratio and the screen width (how?) but I prefer a simpler way to achieve this.
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
get myImage {
var photoView = PhotoView(
initialScale: PhotoViewComputedScale.covered,
imageProvider: AssetImage("assets/1.jpg"),
);
return Container(height: 100, child: photoView);
}
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
myImage,
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
NOTE: if you are struggling with this, then there is a simple by pass: open the image in a new screen
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:photo_view/photo_view.dart';
class ReceiptImgZoomable extends StatefulWidget {
String renderUrl;
ReceiptImgZoomable(this.renderUrl) : super();
#override
_ReceiptImgZoomableState createState() => _ReceiptImgZoomableState();
}
class _ReceiptImgZoomableState extends State<ReceiptImgZoomable> {
#override
Widget build(BuildContext context) {
// var image = Image.file(File(widget.renderUrl));
var image = PhotoView(
imageProvider: FileImage(File(widget.renderUrl)),
);
return Container(
child: image,
);
}
}
and:
#override
Widget build(BuildContext context) {
// InkWell is a special Material widget that makes its children tappable
// and adds Material Design ink ripple when tapped.
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
// builder methods always take context!
builder: (context) {
return ReceiptImgZoomable(widget.renderUrl);
},
),
);
},
child: receiptImage);
}
To simply handle zooming-in and out of a displayed image, you can use Flutter's InteractiveViewer.