(Flutter) ListView has unnecessary spaces between items containing only images - android

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

Related

cacheExtent of GridView nested in ListView is ignored

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.

Flutter: How can I add dynamic number of columns depending on device width or orientation?

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

connect Flutter code from different sources like for example youtube tutorials

Hello Guys im new to flutter.
To understand Flutter I watched a lot of videos and read blog entries.
But there is always a problem:
Each video is about a specific topic and all of them start with a new Flutter project. As long as I want to continue working on the code I can't change the code.
Below I have added a code by Hanz Müller as an example. Topic NavigationBar.
But now I want to delete the text under the icons and edit the different app pages (body) with text and images.
I can't delete the text under the icons because text can't be ''null''.
And I can't edit the diffrent body pages because I can't find the position.
i only know html and css because it is a hobby and now i search for the place where i find the body container :)
Thanks a lot for your help
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class Destination {
const Destination(this.title, this.icon, this.color);
final String title;
final IconData icon;
final MaterialColor color;
}
const List<Destination> allDestinations = <Destination>[
Destination('Home', Icons.home, Colors.teal),
Destination('Business', Icons.business, Colors.cyan),
Destination('School', Icons.school, Colors.orange),
Destination('Flight', Icons.flight, Colors.blue)
];
class DestinationView extends StatefulWidget {
const DestinationView({ Key key, this.destination }) : super(key: key);
final Destination destination;
#override
_DestinationViewState createState() => _DestinationViewState();
}
class _DestinationViewState extends State<DestinationView> {
TextEditingController _textController;
#override
void initState() {
super.initState();
_textController = TextEditingController(
text: 'sample text: ${widget.destination.title}',
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${widget.destination.title} Text'),
backgroundColor: widget.destination.color,
),
backgroundColor: widget.destination.color[100],
body: Container(
padding: const EdgeInsets.all(32.0),
alignment: Alignment.center,
child: TextField(controller: _textController),
),
);
}
#override
void dispose() {
_textController.dispose();
super.dispose();
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin<HomePage> {
int _currentIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
child: IndexedStack(
index: _currentIndex,
children: allDestinations.map<Widget>((Destination destination) {
return DestinationView(destination: destination);
}).toList(),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (int index) {
setState(() {
_currentIndex = index;
});
},
items: allDestinations.map((Destination destination) {
return BottomNavigationBarItem(
icon: Icon(destination.icon),
backgroundColor: destination.color,
title: Text(destination.title)
);
}).toList(),
),
);
}
}
void main() {
runApp(MaterialApp(home: HomePage(), debugShowCheckedModeBanner: false));
}
If you want to remove the Text under the icon Check the code where the Text widget is place.
So you have the relevant Text widget in BottomNavigationBarItem
title: Text(destination.title)
So if you don't need the Text widget you can simply replace it with Container to display nothing.
title: Text(destination.title)
I would suggest you read the code and understand it will. The better you understand how your widgets are built and rendered it will be easier to modify them.

Range error index invalid value not in range, 0..1 inclusive 2 in dart flutter app

I am following one flutter app tutorial and I encounter this error:
Range error index invalid value not in range, 0..1 inclusive 2
Any suggestions on how I can fix this?
import 'package:flutter/material.dart';
//void main() {
//runApp(MyApp());
//}
void main()=>runApp(MyApp());
class MyApp extends StatefulWidget{
#override
State<StatefulWidget> createState(){
return MyAppState();
}
}
class MyAppState extends State<MyApp>{
var questionIndex=0;
void answerQuestion(){
setState(() {
questionIndex=questionIndex+1;
});
print(questionIndex);
//print("answer choosen!");
}
#override
Widget build(BuildContext context){
var questions=['What is your favourite colour','what is favourite animal',];
return MaterialApp(home: Scaffold(
appBar:AppBar(title:Text('my first app'),),
body: Column(children:[Text(questions[questionIndex]),
//RaisedButton(child: Text("Ansewr 1"), onPressed:()=>print("Answer 1"),),
//RaisedButton(child: Text("Ansewr 1"),onPressed:answerQuestion),
RaisedButton(child: Text("Ansewr 0"), onPressed:answerQuestion,),
RaisedButton(child: Text("Ansewr 1"), onPressed:answerQuestion,),
],),
),);
}
}
I modify your code. Maybe that works for you. And don't use setState() method too much. Try learning bLoc archtecture.
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyAppState();
}
}
enum Questions { firstQuestion, secondQuestion }
class MyAppState extends State<MyApp> {
var question = Questions.firstQuestion;
#override
Widget build(BuildContext context) {
var questions = [
'What is your favourite colour',
'what is favourite animal',
];
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('my first app'),
),
body: Column(
children: [
Text(questions[question.index]),
RaisedButton(
child: Text("Ansewr 0"),
onPressed: () {
setState(() {
question = Questions.firstQuestion;
});
},
),
RaisedButton(
child: Text("Ansewr 1"),
onPressed: () {
setState(() {
question = Questions.secondQuestion;
});
},
),
],
),
),
);
}
}
Your question should contain in which moment the error occurs. But I guess the error appears after you clicked on one of your RaisedButtons for the second time in total?
Every time you click on one of your buttons you increase questionIndex and rebuild your widgets. In one part of your widget tree, you write Text(questions[questionIndex]). Your array questions contain two questions. If you start your app questionIndex=0 so the first question will be shown at Text(questions[questionIndex]). If you click a button questionIndex=1 so the second question will be shown. If you click one of your buttons again you increase questionIndex to 2. So now your widget tree tries to get the question at the third position in questions[questionIndex]. But there is no third question so your app breaks down.

Navigation to sub-screen from BottomNavigationBar-sceeen in Flutter

I´m currently working on my first simple flutter application and am trying to figure our the best approach to handle the navigation between screens.
Already Possible:
Navigation through screens with BottomNavigationBar + BottomNavigationBarItem
Navigation with Navigator.push(context,MaterialPageRoute(builder: (context) => Screen4()),);
Problem:
Having sub-screens in the screens of BottomNavigationBar
Code Example:
I want to have three main screens Screen1(), Screen2() and Screen3() accessible from the BottomNavigationBar. In Screen1() there is a button to navigate to another screen, let´s call it Screen4(), where the user can choose from a list of items. You can then add all chosen items to a list and navigate back to Screen1().
To achieve this I created the code below. The main Widget of the body will be changed according to the current index of the selected BottomNavigationItem.
main.dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const String _title = 'Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: MyApp(),
);
}
}
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _selectedIndex = 0;
static const List<Widget> _widgetOptions = <Widget>[
Screen1(),
Screen2(),
Screen3(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stackoverflow Example'),
),
body: Center(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Screen1'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Screen2'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('Screen3'),
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
);
}
}
The problem is when I navigate within the Screen1() - Widget to Screen4() by using
Navigator.push(context,MaterialPageRoute(builder: (context) => Screen4()),);
the navigation will happen outside of MyApp() and therefore there is no Scaffold.
If someone has an example, where this is achieved I´d be very happy.
Thank you for reading.
I know it's a bit late, but hopefully, it can help you.
To achieve this you can use the Offstage widget with a navigator as its child, this way you will have a persistent navigation bar throughout all of your pages.
you can follow this article for more details and how to implement it
https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf
you might need to tweak it a little to match your case.
info about the Offstage widget:
https://api.flutter.dev/flutter/widgets/Offstage-class.html
I suggest that you run a CupertinoApp instead of a MaterialApp. Use a CupertinoTabScaffold instead of Scaffold. Add a CupertinoTabBar as tabBar and return a CupertinoTabView as a tabBuilder
example of tabBuilder
tabBuilder: (context, index) {
if (index == 0) {
return CupertinoTabView(
navigatorKey: firstTabNavKey,
builder: (BuildContext context) => Screen1(),
Have a look at this great short article on how to implement a Tab Bar and also allow for Navigation between screens: link

Categories

Resources