Load image only if it's inside viewport - flutter

I have a Scaffold with a ListView as a child to be able to scroll.
There is a header container at top of it and a column with a list of images.
I want to load this images only when they appears in viewport but all of them are starting to load at the same time.
This is not happening when I'm adding this images to the children of ListView but I'd like to put them to a separate widget to keep my code clean. Is it possible?
Here is a simplified example:
const List<String> images = [
'https://cataas.com/cat?hash=1',
'https://cataas.com/cat?hash=2',
'https://cataas.com/cat?hash=3',
'https://cataas.com/cat?hash=4',
'https://cataas.com/cat?hash=5',
'https://cataas.com/cat?hash=6',
'https://cataas.com/cat?hash=7',
'https://cataas.com/cat?hash=8',
];
// Scaffold child
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ListView(
children: [
// Header
Placeholder(
fallbackHeight: 300,
),
Category(),
SizedBox(height: 16),
Category(),
],
);
}
}
class Category extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Build category');
return Column(
children: [
Padding(
padding: EdgeInsets.all(32),
child: Text('CATEGORY TITLE'),
),
...images.map((image) => CategoryItem(image)),
],
);
}
}
class CategoryItem extends StatelessWidget {
final String imageUrl;
CategoryItem(this.imageUrl);
#override
Widget build(BuildContext context) {
print('Build image: $imageUrl');
return FadeInImage.memoryNetwork(
fit: BoxFit.cover,
placeholder: kTransparentImage,
image: imageUrl,
);
}
}

You need to use ListView.builder, because it renders its children lazily. Also you'll need to use one single list.
The problem is that you don't know the image's size before it's loaded, so all images with 0 height will be built instantly. But if you set fixed height to them, it will work as intended.
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
final children = [
// Header
Placeholder(
fallbackHeight: 300,
),
Padding(
padding: EdgeInsets.all(32),
child: Text('CATEGORY TITLE'),
),
...images.map((image) => CategoryItem(image)),
SizedBox(height: 16),
Padding(
padding: EdgeInsets.all(32),
child: Text('CATEGORY TITLE'),
),
...images.map((image) => CategoryItem(image)),
];
return Scaffold(
body: ListView.builder(
itemCount: children.length,
itemBuilder: (context, i) => children[i],
)
);
}
}

Related

flutter - chat's input box keyboard pushing message-list upwards

I have a fairly simple flutter app. It has a chat feature.
However, I have a problem with the chat feature.
It's made up of a widget does Scaffold and in it SingleChildScrollView - which has a message-list (container) and input-area (container). Code is attached.
Problem is: if I click on the input box, the keyboard opens and it pushes the message-list.
Pushing the message-list is an acceptable thing if you are already at the bottom of the message-list.
However, if the user scrolled up and saw some old messages, I don't want the message-list widget to be pushed up.
Also, I don't want the message-list to be pushed up if I have only a handful of messages (because that just makes the messages disappear when keyboard opens, and then I need to go and scroll to the messages that have been pushed [user is left with 0 visible messages until they scroll]).
I tried different approaches - like
resizeToAvoidBottomInset: false
But nothing seems to work for me, and this seems like it should be a straightforward behavior (for example, whatsapp act like the desired behavior).
My only option I fear is to listen to keyboard opening event, but I was hoping for a more elegant solution.
Here's my code:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
);
Widget buildInputArea(BuildContext context) {
return Container(
height: height * 0.1,
width: width,
child: Row(
children: <Widget>[
buildChatInput(),
buildSendButton(context),
],
),
);
}
Widget buildMessageList() {
return Container(
height: height * 0.8,
width: width,
child: ListView.builder(
controller: scrollController,
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
return buildSingleMessage(index);
},
),
);
}
This seems to work for me:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
buildMessageList(),
buildInputArea(context),
],
),
),
);
}
Widget buildInputArea(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: TextField(),
),
RaisedButton(
onPressed: null,
child: Icon(Icons.send),
),
],
);
}
Widget buildMessageList() {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: 50,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(index.toString()),
),
);
},
),
);
}
}
I think the problem is that you are using fixed sizes for all widgets. In this case it is better to use Expanded for the ListView and removing the SingleChildScrollView. That way the whole Column won't scroll, but only the ListView.
Try to use Stack:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(),
],
),
),
Positioned(
bottom: 0.0,
child: buildInputArea(context),
),
],
),
);
}
Setting resizeToAvoidBottomInset property to false in your Scaffold should work.
You can use NotificationListener to listen to scroll notifications to detect that user is at the bottom of the message-list. If you are at the bottom you can then set resizeToAvoidBottomInset to true.
Something like this should work
final resizeToAvoidBottomInset = true;
_onScrollNotification (BuildContext context, ScrollNotification scrollNotification) {
if (scrollNotification is ScrollUpdateNotification) {
// detect scroll position here
// and set resizeToAvoidBottomInset to false if needed
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: this.resizeToAvoidBottomInset,
body: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
return _onScrollNotification(context, scrollNotification);
},
child: SingleChildScrollView(
child: Column(
children: <Widget>[
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
),
);
}
this is technically already answered, and the answer is almost correct. However, I have found a better solution to this. Previously the author mentions that he wants to have a similar experience to WhatsApp. By using the previous solution, the listview would not be able to scrolldown to maxExtent when the sent button is pressed. To fix this I implemented Flex instead of Expanded, and use a singlechildscrollview for the input area
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
TextEditingController _textcontroller=TextEditingController();
List<String> messages=[];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: messages.length,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
print("From listviewbuilder: ${messages[index]}");
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(messages[index])
),
);
},
),
),
SingleChildScrollView(
child: Row(
children: <Widget>[
Expanded(
child: TextField(controller: _textcontroller),
),
RaisedButton(
onPressed: (){
Timer(Duration(milliseconds: 100), () {
_controller.animateTo(
_controller.position.maxScrollExtent,
//scroll the listview to the very bottom everytime the user inputs a message
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
});
setState(() {
messages.add(_textcontroller.text);
});
print(messages);
},
child: Icon(Icons.send),
),
],
),
),
],
),
),
);
}
}
It's better to use flex because expanded as the documentation says, expands over available space, whereas flex would resize to the appropriate proportion. This way if you are going for the "WhatsApp experience" in which the listview scrolls down once you sent a message. The listview would resize when the keyboard pops up and you will get to the bottom, instead of it not going fully to the bottom.

flutter, Why not render ListView<Widget> when build method called?

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: Scaffold(body: Center(child: TestWidget())));
}
}
class TestWidget extends StatefulWidget {
TestWidgetState createState() => new TestWidgetState();
}
class TestWidgetState extends State<TestWidget> {
List<Widget> _bodyItems = [];
List<Widget> _topItems = [];
final Widget _boundary = Column(
children: <Widget>[
SizedBox(
height: 10,
),
SizedBox(
child: Container(
color: Colors.black12,
),
height: 1,
),
SizedBox(
height: 10,
),
],
);
#override
void initState() {
super.initState();
Widget e = GestureDetector(key: Key("0"), onTap: () {
onChangedFunction(Key("0"));
},child: Text("This text is on body range"));
_bodyItems.add(e);
_topItems = [];
}
void onChangedFunction(Key key) async {
setState(() {
_bodyItems.removeAt(0);
_topItems.add(Text("This text is on top range."));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
SizedBox(
height: 10,
),
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100,
),
child: ListView(
scrollDirection: Axis.horizontal,
children: _topItems,
),
),
_boundary,
ConstrainedBox(
constraints: BoxConstraints(
minHeight: 450,
maxHeight: 450,
),
child: Column(
children: _bodyItems,
),
),
_boundary,
],
)));
}
}
result
top items : [Text("This text is on top range.")]
This code deletes the body item when the widget of the body item is tapped and added item at the top item.
Looking at the result, you can see that the data disappeared from the body item widget and the data was added to the top item widget.
However, the data of the top item is not render.
I want to know what the reason is.
This happens because Children is constant, you can see that if you look into definition.
You will find following line in implementation of ListView.
List<Widget> children = const <Widget>[],
but you can change widget in side children, so you have to give list as a children of list.
As Foolowing.
children: [..._topItems],
Replace _topItems and _bodyItems with below
children: [..._topItems], & children: [..._bodyItems], --inside your body

Is it possible to create links to sections in the same page in flutter web?

I want to create a website using flutter web but I'm unable to navigate to sections in the same page. Here's an example of what I want to achieve using flutter.
P.S. Navigator is not working:
I created an example with PageView
class MyHomePage extends StatelessWidget {
var list = ["Home","Services", "Work", "About"];
var colors = [Colors.orange, Colors.blue, Colors.red, Colors.green];
PageController controller = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Row(
children: <Widget>[
Container(
width: 50,
height: 50,
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10)
),
),
Spacer(),
Row(
children: List.generate(3, (index){
return GestureDetector(
onTap: (){
_scrollToIndex(index);
},
child: Container(
margin: EdgeInsets.all(8),
child: Text(
list[index+1]
),
),
);
}),
)
],
),
Expanded(
child : PageView(
scrollDirection: Axis.vertical,
pageSnapping: false,
controller: controller,
children: List.generate(list.length, (index){
return Container(
width: MediaQuery.of(context).size.width,
height: double.maxFinite,
color: colors[index],
child: Center(
child: Text(
list[index],
style: TextStyle(
color: Colors.white,
fontSize: 50
),
),
),
);
})
),
),
],
)
),
);
}
void _scrollToIndex(int index) {
controller.animateToPage(index + 1, duration: Duration(seconds: 2), curve: Curves.fastLinearToSlowEaseIn);
}
}
The output:
ScrollController is the thing you are looking for.
Add a new one to your ScrolView and you can set where you want it to scroll to.
Josteve mentioned a way of doing it. But I'd like to show the other way which provides more features as one would expect in the gif example you have put.
You can see the demo here: https://mohith7548.github.io/portfolio/
My project has 3 sections called About, Blog & Projects. It also has another top section called Home. So the order of screens is Home, About, Blog & Projects. Each section takes full-screen height & width. So the starting offset for these pages are [0 * screenHeight, 1 * screenHeight, 2 * screenHeight, 3 * screenHeight] respectively. screenHeight can be accessed by MediaQuery.of(context).size.height inside build method.
class Portfolio extends StatefulWidget {
#override
_PortfolioState createState() => _PortfolioState();
}
class _PortfolioState extends State<Portfolio> {
ScrollController _scrollController;
String _curNavItem;
static double offsetHome = 0;
static double offsetAbout = SizeConfig.screenHeight;
static double offsetBlog = 2 * SizeConfig.screenHeight;
static double offsetProjects = 3 * SizeConfig.screenHeight;
#override
void initState() {
super.initState();
_scrollController = ScrollController();
}
#override
void dispose() {
super.dispose();
_scrollController.dispose();
}
void scrollTo(String title) {
double offset = 0;
switch (title) {
case Constants.HOME:
offset = offsetHome;
break;
case Constants.ABOUT:
offset = offsetAbout;
break;
case Constants.BLOG:
offset = offsetBlog;
break;
case Constants.PROJECTS:
offset = offsetProjects;
break;
}
setState(() {
_curNavItem = title;
});
// animate to the pag
_scrollController.animateTo(
offset,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOutQuart,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: PageScrollPhysics(), // use NeverScrollableScrollPhysics() to block user scrolling
controller: _scrollController,
slivers: <Widget>[
// This is just SliverAppBar wrapped in InterheritedWidget called NavState
// You can use just SliverAppBar
NavState(
curNavItem: _curNavItem,
scrollTo: scrollTo,
child: AppBanner(key: _appBannerKey), // SliverAppBar in another file
),
SliverList(
delegate: SliverChildListDelegate([
About(),
Blog(),
Projects(),
]),
)
],
),
);
}
}
You can do this in different ways:
TabBarView https://stackoverflow.com/a/60624536/10976088
PageView https://stackoverflow.com/a/60778791/10976088
NavigationRail https://api.flutter.dev/flutter/material/NavigationRail-class.html
My method: Using a state management way to keep name or index of content pages and change visible page. I do it with the Riverpod package here:
Suppose you want to have a fixed SidebarView and HeaderView in all pages and also you have a ContentPage that will be changed.
So you can have a RootPage including these 3 sections and change ContentPage by the riverpod, so that only ContentPage will be changed.
class RootPage extends StatelessWidget {
const RootPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: SidebarView(),
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (Responsive.isDesktop(context))
const Expanded(
flex: 1,
child: SidebarView(),
),
Expanded(
flex: 5,
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
HeaderView(),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Consumer(
builder: (context, ref, _) {
var watch = ref.watch(pageVisibleStateProvider);
return contentPageSelection(watch.state);
},
),
),
),
],
),
),
),
],
),
);
}
}
simply change content page:
Widget contentPageSelection(String pageName){
switch(pageName){
case "page1":
return Page1();
case "page2":
return Page2();
case "page3":
return Page3();
default:
return DefaultPage();
}
}
where:
final pageVisibleStateProvider = StateProvider<String>((_) => "defaultPage");
and:
class SidebarView extends StatelessWidget {
const SidebarView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text("sidebar content"),
);
}
}
class HeaderView extends StatelessWidget {
const HeaderView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text("HeaderView content"),
);
}
}
Now you can change content page. for example you want to show Page2:
ElevatedButton(
onPressed: (){
ref.read(pageVisibleStateProvider.notifier).state = "page2";
},
child: Text("go to page 2"),
)
where page2 and other content pages only includes content not sidebar or header:
class Page2 extends StatelessWidget {
const Page2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text("page2 content");
}
}

How to shrink images in ListTile but not expand it?

I need to display images of several different sizes in a ListView.
When the image is larger than screen.width, I'd like it to shrink to fit width.
But when the image is shorter, I'd like it to keep its original size.
How can I do it? Thanks in advance.
I tried putting Image inside Flex, but couldn't "stop" the small one to expand.
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:firebase_database/firebase_database.dart';
void main() => runApp(MyApp());
const _imagesDir = "images";
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Image List',
theme: ThemeData(primarySwatch: Colors.blue,),
home: MyListPage(title: 'Image List Page'),
);
}
}
class MyListPage extends StatefulWidget {
MyListPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyListPageState createState() => _MyListPageState();
}
class _MyListPageState extends State<MyListPage> {
Widget build1(BuildContext context, AsyncSnapshot snapshot) {
Widget _tileImagem(BuildContext context, String imageName) {
imageName = _imagesDir + "/"+ imageName;
return Padding(padding:EdgeInsets.all(2.0),
child: Flex(
direction: Axis.vertical,
children: <Widget>[
Image.asset(imageName),
]
),
);
}
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(40.0),
child: AppBar(
title: Row(
children: <Widget> [
Padding(padding: EdgeInsets.only(right: 20.0),),
Text( 'Duda'),
]),
)
),
body: ListView(
shrinkWrap: true,
children: <Widget>[
Container(),
_tileImagem(context, 'flutter_big_medium.png'),
Container(), //My App have some different widgets
Container(),
Container(), //I kept them here just as place holder
Container(),
Container(),
Container(),
Container(),
Container(),
Divider(),
TileTexts(),
Divider(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () { },
child: Icon(Icons.skip_next),
),
);
}
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future:
FirebaseDatabase.instance.reference()
.child('Testing')
.once(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState){
case ConnectionState.done: return build1(context, snapshot);
case ConnectionState.waiting: return CircularProgressIndicator();
default:
if (snapshot.hasError) {
return Text("hasError: ${snapshot.error}");
} else {
return Text("${snapshot.data}");
}
}
}
);
}
}
class TileTexts extends StatefulWidget {
TileTexts() : super();
#override
_TileTextsState createState() => _TileTextsState();
}
class _TileTextsState extends State<TileTexts> {
#override
void initState() {
super.initState();
}
Widget text1(String title, String imageName, TextStyle style) {
return Expanded(
child:Container(
margin: const EdgeInsets.only(left: 10.0),
child: Column(
children: <Widget>[
Html(data: title,
useRichText: true,
defaultTextStyle: style,
),
((imageName == null))
? Container()
: Image.asset(_imagesDir + "/"+imageName),
]
),
),
);
}
Widget _tileDetail(BuildContext context, String imageName) {
return Container(
padding: EdgeInsets.fromLTRB(5.0,0.0,10.0,0.0),
child: Row(
children: <Widget>[
Material(
shape: RoundedRectangleBorder(borderRadius:BorderRadius.circular(22.0) ),
clipBehavior: Clip.antiAlias,
child: MaterialButton(
child: Text('X'),
color: Theme.of(context).accentColor,
elevation: 8.0,
height: 36.0,
minWidth: 36.0,
onPressed: () {
//
},
),
),
text1('<body>veja a imagem</body>', imageName, Theme.of(context).textTheme.caption),
],
),
);
}
//_TileTexts
#override
Widget build(BuildContext context) {
print('_TileTexts build');
return Column(
children: <Widget>[
_tileDetail(context, 'flutter_med_medium.png'),
Divider(),
_tileDetail(context, 'flutter_med_medium.png'),
Divider(),
_tileDetail(context, 'flutter_med_medium.png'),
],
);
}
}
Create an method,getTitleImage(imageName), that returns Flex if image is bigger then screen-with, else return the image inside an container or in other widget of choice.
....
return Padding(padding:EdgeInsets.all(2.0),
child: getTitleImage(imageName)
),
);
....
Here is some other tips and tricks using Flex
Please check the doc, it says:
The heights of the leading and trailing widgets are constrained according to the Material spec. An exception is made for one-line ListTiles for accessibility. Please see the example below to see how to adhere to both Material spec and accessibility requirements.
after reading docs, you should achieve what you want :)

How Can I add onTap to image category

I want to add onTap to each category in my horizontal list view so how can I do it?
class HorizontalList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 80.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
Category(
image_location: "catogories/name.png",
),
],
),
);
}}
You can wrap your Category widget with GestureDetector widget.
class HorizontalList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 80.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
GestureDetector(
onTap: () {
//This will be called on tap
},
child:Category(
image_location: "catogories/name.png",
),
),
],
),
);
}}
If you are using small sized images, you can use IconButton().
class HorizontalList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
height: 80.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
IconButton(icon: Image.asset(<name_here(or any other optoin such as Image.network()>)), onPressed: <function_here:fox example clickEvent()>)
],
),
);
}
}
You can also use
icon: Icon(icon: Icons.<icon_name>, color: Colors.<colors_name>)
, if you're using Icons.
And if you're trying to do something else, comment below !