I am trying to build a gridview nested in a listview with flutter, based on the attached minimal viable example, while trying to leverage the inner gridviews cacheExtent feature, which shall prevent items to be built all at once. In the full application i do kind of heavy stuff, so i want as few items to be loaded at the same time as possible, but unfortunately it looks like cacheExtent is ignored for the inner list, when nesting multiple list. For the surrounding list cacheExtent works as expected.
Does anyone have a solution for this and can explain me, while it won't work for the inner list? I am not only looking for a copy&paste solution, i am really trying to understand whats going on in flutter here, since i guess it is caused by any fundamental layouting policy which i don't know yet.
Environment:
[✓] Flutter (Channel stable, 2.10.0, on macOS 12.2.1 21D62 darwin-arm, locale en-DE)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
Example:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:developer';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
// works as expected, second instance of InnerList will not be loaded on startup
cacheExtent: 1,
shrinkWrap: true,
itemCount: 2,
itemBuilder: (context, index) {
return InnerList(index);
},
),
appBar: AppBar(
title: const Text('Home'),
),
drawer: const Drawer(
child: Text("Foobar"),
),
);
}
}
class ItemWidget extends StatelessWidget {
int index;
ItemWidget(this.index);
#override
Widget build(BuildContext context) {
// indicates all widgets are getting build at once, ignoring cacheExtent
log("building widget " + index.toString());
// TODO: implement build
return SizedBox(
height: 120,
width: 120,
child: Center(child: Text(index.toString())),
);
}
}
class InnerList extends StatelessWidget {
int index;
InnerList(this.index);
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Foobar " + index.toString()),
GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120.0,
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
),
itemCount: 100,
primary: false,
// this is ignored, all items of type ItemWidget will be loaded at once
cacheExtent: 1,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return ItemWidget(index);
})
],
);
}
}
// Update
The accepted answer pointed me into the right direction, leading my search to a video explaining why it won't work - the simple answer is: shrinkWrap forces lists to evaluate all childs, to determine their height. It is even shown on a very similar example, using shrinkWrap and physics properties of the list. The solution with Slivers now looks similar like follows, even if i am still kind of skeptic constructing the outer list used by CostumScrollView in a loop.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:developer';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Widget> innerLists = [];
#override
void initState() {
super.initState();
// construct lists
for(var i=0; i<10; i++) {
innerLists.add(SliverToBoxAdapter(
child: Text("Foobar " + i.toString())
));
innerLists.add(InnerList(i));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(slivers: innerLists),
appBar: AppBar(
title: const Text('Home'),
),
drawer: const Drawer(
child: Text("Foobar"),
)
);
}
}
class ItemWidget extends StatelessWidget {
int index;
ItemWidget(this.index);
#override
Widget build(BuildContext context) {
// TODO: implement build
return SizedBox(
height: 120,
width: 120,
child: Center(child: Text(index.toString())),
);
}
}
class InnerList extends StatelessWidget {
int index;
InnerList(this.index);
#override
Widget build(BuildContext context) {
return
SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120.0,
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ItemWidget(index);
},
childCount: 100
)
);
}
}
shrinkWrap makes GridView/ListView decides height itself.
Without shrinkWrap, ListView expands to main axis infinitely. So its height is constrained by parent box constraint, and fills available space. In that case render space is definite. ListView only build childs on the visible space.
With shrinkWrap, ListView try to shrink, as short as possible unless it can contain all childs, so its height is sum of child's height (roughly). In this case ListView doesn't see parent constraint so ListView doesn't know how many childs it should build, so it builds all elements.
Now let's see your example. With replacing log to print, we get the following.
building widget 0
building widget 1
...
building widget 99
building widget 0
building widget 1
...
building widget 99
Although I can't see the second InnerList, ListView built the second child. Now with commenting out ListView.builder's shrinkWrap
building widget 0
building widget 1
...
building widget 99
If you scroll down, log will be same with the first.
GridView is same. But you can't comment out GridView's shrinkWrap due to layout error. It is because ListView gives infinite height to its children (GridView). If GridView expands, its height will be infinity.
Of course using SizedBox will solve this partially, but maybe it is not what you want.
If you really want on demand load here, you may use sliver.
Add: cacheExtent doesn't mean item count. It is pixel count.
Related
I need to listen the overScroll Offset Values. When I scroll either upward or downward in a fast speed, I didn't get all offset values.
Basically I'm facing issue when I scroll Fast so I don't get all offset values from 0.0 to XYZ value. I get this kind of values in my console(as I printed them).
0.0,
20.3345,
90.355,
400.354,
As you can see there is a difference in these value.
I figured out that scrollController's addListener method is not listening all offset values.
You can try running my code as well.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late ScrollController scrollController;
#override
void initState() {
scrollController = ScrollController();
scrollController.addListener(() {
print(scrollController.position.pixels.toString() + " ----Pixels");
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body:CustomScrollView(
controller: scrollController,
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 22, vertical: 20),
child:Column(
children: List.generate(
50,
(index) => ListTile(
leading: Icon(Icons.ac_unit),
title: Text('demo'),
)),
)
)
)]));
}
`
This is my code. Please have a look at this issue, I want to thank you in advance.
I have a Flutter app which in one of the pages it presents a list of more than 200 items. How can I make it so that when the app is running in a desktop environment for example it displays more than one column dynamically and that when it runs on phones it displays a single columns or when it run on tablet in vertically orientation it displays one column and more than one column when the device is on horizontal orientation?
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'something here',
),
Text(
'something here',
),
Text(
'something here',
),
Text(
'something here',
),
],
),
),
);
}
}
This will produce a single column no matter the device or orientation.
Try using GridView instead of Column, in that way you can adjust number of columns with crossAxisCount property of GridView. I am coming to that in a while.
Lets talk about how to adjust the number of columns dynamically depending upon the screen size. For example, I want single column in mobile, 2 columns in tab and 4 columns in desktop.
I'll create an enum named LayoutSize that represents different layout sizes. Also create an extension that returns number of columns depending upon the LayoutSize.
enum LayoutSize { small, medium, large }
extension LayoutSizeX on LayoutSize {
int get noOfColumns {
switch (this) {
case LayoutSize.small:
return 1;
case LayoutSize.medium:
return 2;
case LayoutSize.large:
return 4;
}
}
}
Now it's time to think about Breakpoints that will represent maximum width for different LayoutSize. You can change the numbers according to your requirements.
abstract class Breakpoints {
static const double small = 760;
static const double medium = 1644;
static const double large = 1920;
}
Finally wrap up all these logic and create the UI. Here LayoutBuilder will serve the purpose of changing the number of columns whenever there is a change in constraints.
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final screenWidth = MediaQuery.of(context).size.width;
if (screenWidth <= Breakpoints.small) {
return _buildMyWidget(LayoutSize.small);
}
if (screenWidth <= Breakpoints.medium) {
return _buildMyWidget(LayoutSize.medium);
}
if (screenWidth <= Breakpoints.large) {
return _buildMyWidget(LayoutSize.large);
}
return _buildMyWidget(LayoutSize.small);
});
}
Widget _buildMyWidget(LayoutSize size) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: size.noOfColumns,
),
itemBuilder: (context, index) {
return Container();
},
);
}
}
I know this solution requires a little bit of extra coding, but believe me this is going to be very much efficient in long run, even in building the whole app responsive, not only a particular widget.
This is the minimal code you need to write in this particular case. But if you're interested in much generic way, here is a link to a gist.
You can add your list Items conditionally in two ways:
Using if() statement:
Column(
children: [
ListTile(
...
),
if (_condition)
Container(
...
),
],
),
Using condition like this:
Column (
children: [
x == 1 ? Widget1() : Widget2(),
],
)
What you need here is to figure out the OS and also the screen type, the Platform Class will give you some information about the OS:
import 'dart:io' show Platform;
if (Platform.isWindows) ...
and the MediaQuery class will give you useful information about the screen and sizes:
final isLandscape = (MediaQuery.of(context).orientation == Orientation.landscape);
There is no direct way to figure out that the Device is a Tablet or a Phone, but there are some tricks to get device type, here are some examples
You need a combination of these approaches to achive what you want
I'm a newbie at Flutter. I'm trying to create a Draggable but I don't want to use a DragTarget. I want it to drag and drop wherever I want. When I did that after the drop operation object disappeared. I don't want it to disappear. How can I do that? I checked the resources and YouTube, however, I cannot find anything useful. Thanks in advance.
Following up to my comment, here is a code example.
You have a Stack, with your Draggable element, you then rebuild it to the new position using a Positioned widget and an offset.
You could also recreate a DragTarget following the same exemple, just repositioning it if you need its features.
You might need to adapt a bit the offset in order to make it react to your target element size.
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _started = false;
Offset _position = Offset(20, 20);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: <Widget>[
Positioned(
top: _position.dy - 30, // Size of widget to reposition correctly
left: _position.dx,
child: Draggable(
child: Chip(label: Text('Element')),
feedback: Material(
child: Chip(
label: Text('Element'),
)),
onDragEnd: (details) {
print(details.offset);
setState(() {
if (!_started) _started = true;
_position = details.offset;
});
},
),
),
],
),
),
);
}
}
My app requires a list view of images retrieved from a remote source to be displayed in a list. The images found below originate from the site randomuser.me and is free to use (testing, etc)
Problem:
The images are retrieved correctly and are displaying (these images are 128x128, but naturally sizes can change), however there are extremely large gaps between the images (height/width dimensions have been used nor fit options - I have previously used this to attempt to address the issue with no success).
Suggestions are to use ListTile and various solutions around this by setting dense: true or removing ListView padding and a few others, however non of these have worked.
I played around with large images - these fill up the entire screen, and I can scale them down, BUT when scaling these images, there are still large empty spaces left inbetween (expected) - these same gaps are seen in my case with NO scaling done.
MVCE
import 'package:flutter/material.dart';
void main() async {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
List<String> imgSrc = [
"https://randomuser.me/api/portraits/women/84.jpg",
"https://randomuser.me/api/portraits/men/82.jpg",
"https://randomuser.me/api/portraits/women/11.jpg",
"https://randomuser.me/api/portraits/women/61.jpg",
"https://randomuser.me/api/portraits/men/1.jpg",
];
Widget listview() {
List<Widget> imgList = imgSrc.map((e) => Image.network(e)).map((e) => Image(image: e.image,)).toList();
return ListView(
children: imgList,
);
}
return Scaffold(
body: Container(
width: double.infinity,
child: listview()),
);
}
}
Want to create something like this:
I am not sure what is causing these spaces inbetween - advice would be appreciated!
Update
After playing around with it a little more, the only way I can get it to work as shown in the desired result it to add atleast 1 hardcoded dimension and use BoxFit.scaleDown which will scale based on the given dimension.
Example:
List<Widget> imgList = imgSrc
.map((e) => Image.network(e))
.map((e) => Image(
image: e.image,
fit: BoxFit.scaleDown,
height: 64,
))
.toList();
Yes, you need the fit parameter and try different options from (https://api.flutter.dev/flutter/painting/BoxFit-class.html) and see what would work for you. I've cleaned the code a bit while trying answer the questions and posting here.
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.purple,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
List a = [
"https://randomuser.me/api/portraits/women/84.jpg",
"https://randomuser.me/api/portraits/men/82.jpg",
"https://randomuser.me/api/portraits/women/11.jpg",
"https://randomuser.me/api/portraits/women/61.jpg",
"https://randomuser.me/api/portraits/men/1.jpg",
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Displaying Images"),
),
body: ListView.builder(
itemBuilder: (BuildContext ctx, int index) {
return Container(
child: Image(
image: NetworkImage(a[index]),
fit: BoxFit.fill,
),
);
},
itemCount: a.length,
),
);
}
}
I was writing a code for my flutter application. In that I needed to get size of screen of mobile so I used Media Query but error occurred saying "MediaQuery was called with no context" but I was passing the context.
Here is the code
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double width = MediaQuery.of(context).size.width;
return MaterialApp(
title: 'Text',
home: Scaffold(
body: Container(
height: height,
width: width,
),
),
);
}
}
How can I solve this error. Please help. Thanks in advance.
You can't do the MediaQuery.of(context) call in the MaterialApp create Methode, because the app does not know anything about the Media. The error message is
MediaQuery.of() call with a context that does not contain a MediaQuery
You can use this call after the app is running but not in the start up sequence.
Maybe you create an extra widget underneath the MaterialApp. Then it would work.
It should be working by the way anyways run flutter clean from cmd or your console and then try running the app.
If it is not working then directly give the height and the width of the scaffold,MediaQuery.of(context).size.height, and MediaQuery.of(context).size.width, respectively and some modifications like this:
import 'package:flutter/material.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(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('text'),
),
body: Container(
color:Colors.red, //Only to se if the container is being displayed.
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
),
);
}
}
This would definitely work.
if you still face any difficulties then you can ask in the comments.