In my simple Flutter desktop image browser I use arrow keys for traversing items in a folder (actually ZIP archive). As loading large images is slower, if arrow keys are pressed multiple times until the image is fully loaded, some images are skipped.
I'd rather waited for all images until fully loaded and to queue key events to some limit (e.g. up to 5 key events).
The actual core snippets:
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: RawKeyboardListener(
focusNode: focusNode,
autofocus: true,
onKey: handleKey,
child: InteractiveViewer(
child: image,
),
),
);
}
void handleKey(RawKeyEvent keyEvent) async {
if (keyEvent.isKeyPressed(LogicalKeyboardKey.arrowLeft) ||
keyEvent.isKeyPressed(LogicalKeyboardKey.arrowRight) ||
keyEvent.isKeyPressed(LogicalKeyboardKey.home) ||
keyEvent.isKeyPressed(LogicalKeyboardKey.end)) {
int newImageIndex = currentImageIndex;
if (keyEvent.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
newImageIndex = max(currentImageIndex - 1, 0);
} else if (keyEvent.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
newImageIndex = min(currentImageIndex + 1, widget.fileNameMap.length - 1);
}
if (newImageIndex != currentImageIndex) {
updateImage(newImageIndex);
}
}
void updateImage(int index) {
setState(() {
File file = new File(paths[index]);
image = new Image.file(file);
});
}
I've found that image skipping on subsequent key pressing can be avoided when the image widget is updated after image data is fully loaded, which can be achieved by ImageProvider this way:
void updateImage(int index) {
File file = new File(paths[index]);
var imageData = new FileImage(file);
imageData
.resolve(ImageConfiguration())
.addListener(ImageStreamListener((ImageInfo _, bool __) {
if (mounted) {
setState(() {
image = new Image(image: imageData);
});
}
}));
}`
Related
I use StreamProvider to receive firestore data in my app. And I use lazy_load_scrollview package for the pagination in the image gridview. In the StreamProvider I have to pass context to listen to data streams. Just like Provider.of<List>(context) . So I have to define it inside the build. But in the _loadMore() method I have defined in the code I need images(this is where I listen to the Stream) list to update the data list for pagination. Pagination works fine but when I first launch the app it only shows the loading indicator and does not load anything. When I swipe down the screen it starts loading and pagination works fine. To load the grid items when I first start, I need to call _loadMore() method in the initState(). I can't call it because it is inside the build. But I can't define that method outside of the build because it needs to define Stream listener(which is images). I can't get the context outside from the build to do that. Is there any way to get the context outside of the build ? or is there any better solution for pagination ? I would be grateful if you can suggest me a solution. here is my code,
class ImageGridView extends StatefulWidget {
#override
_ImageGridViewState createState() => _ImageGridViewState();
}
class _ImageGridViewState extends State<ImageGridView> {
List<GridImage> data = [];
int currentLength = 0;
final int increment = 10;
bool isLoading = false;
// I need to call _loadMore() method inside the initState
/*#override
void initState() {
_loadMore();
super.initState();
}*/
#override
Widget build(BuildContext context) {
// listening to firebase streams
final images = Provider.of<List<GridImage>>(context) ?? [];
Future _loadMore() async {
print('_loadMore called');
setState(() {
isLoading = true;
});
// Add in an artificial delay
await new Future.delayed(const Duration(seconds: 1));
for (var i = currentLength; i < currentLength + increment; i++) {
if (i >= images.length) {
setState(() {
isLoading = false;
});
print( i.toString());
} else {
data.add(images[i]);
}
}
setState(() {
print('future delayed called');
isLoading = false;
currentLength = data.length;
});
}
images.forEach((data) {
print('data' + data.location);
print(data.url);
//print('images length ' + images.length.toString());
});
try {
return LazyLoadScrollView(
isLoading: isLoading,
onEndOfPage: () {
return _loadMore();
},
child: GridView.builder(
itemCount: data.length + 1,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
if (index == data.length) {
return CupertinoActivityIndicator();
}
//passing images stream with the item index to ImageGridItem
return ImageGridItem(gridImage: data[index],);
},
),
);
} catch (e) {
return Container(
child: Center(
child: Text('Please Upload Images'),
)
);
}
}
}
I am trying to implement a listView single selection in my app such that once an item in the list is tapped such that pressed item color state is different from the others. I have done all I know but it does not work well. The problem is that even though my implementation updates each item state when pressed, it doesn't reset the others to their initial state.
class BoxSelection{
bool isSelected;
String title;
String options;
BoxSelection({this.title, this.isSelected, this.options});
}
class _AddProjectState extends State<AddProject> {
List<BoxSelection> projectType = new List();
#override
void didChangeDependencies() {
super.didChangeDependencies();
projectType
.add(BoxSelection(title: "Building", isSelected: false, options: "A"));
projectType
.add(BoxSelection(title: "Gym House", isSelected: false, options: "B"));
projectType
.add(BoxSelection(title: "School", isSelected: false, options: "C"));
}
child: ListView.builder(
itemCount: projectType.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
//here am trying to implement single selection for the options in the list but it don't work well
for(int i = 0; i < projectType.length; i++) {
if (i == index) {
setState(() {
projectType[index].isSelected = true;
});
} else {
setState(() {
projectType[index].isSelected = false;
});
}
}
});
},
child: BoxSelectionButton(
isSelected: projectType[index].isSelected,
option: projectType[index].options,
title: projectType[index].title,
),
);
},
),
Your problem is that you're using index to access projectType elements but you should be using i
if (i == index) {
setState(() {
projectType[i].isSelected = true;
});
} else {
setState(() {
projectType[i].isSelected = false;
});
}
In any case I think your code can be improved since it's not as efficient as it could be. You're iterating over the entire list and calling setState twice in every iteration, recreating the widget tree a lot of times unnecessarily when it can be done in one shoot.
Save your current selection in a class level variable
BoxSelection _selectedBox
Simplify your code to act directly over the current selection insted of iterating over the entire list
onTap: () =>
setState(() {
if (_selectedBox != null) {
_selectedBox.isSelected = false;
}
projectType[index].isSelected = !projectType[index].isSelected;
_selectedBox = projectType[index];
});
I have a widget A that has a nested widget B. I need to update a field on B (maybe by calling a function of B) when user click on a button in A. How can I do it?
EDIT: more details
Widget B is a widget that I use across my app so I want to leave him as a separate widget. Widget A is composed of widget B plus another button and I want that click on the button will change the the text that inside Widget B
EDIT2 - what I want to achieve:
Widget B is actually a Container that contains TextFormField with some other widgets. I use it for 5 form fields. now I want to add a field for location. the field for location consists of B plus a list that open when user type and there are results from google geocode. When the user clicks on one of the results, I want to set the TextFormField controller text to the value the user type.
So, I don't want to copy all logic and widgets for the text field from B to A.
In my code widget B is FormTextField and A is LocationField
class LocationField extends StatefulWidget {
Location meetingPointData;
LocationField(this.meetingPointData): super();
#override
_LocationFieldFieldState createState() => new _LocationFieldFieldState();
}
class _LocationFieldFieldState extends State<LocationField> {
OverlayEntry _overlayEntry;
var addresses;
final LayerLink _layerLink = LayerLink();
#override
void initState() {
super.initState();
}
onLocationInputChanged(text) {
this.findLocations(text);
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: this._layerLink,
child: FormTextField('meetingPoint', 'Meeting Point', "", "enter the name of a place or an address", TextInputType.text, onLocationInputChanged, true , {"empty": "Please enter the meeting point"})
);
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var addressesWidget = <Widget>[];
for(var i=0; i< addresses.length; i++) {
addressesWidget.add( ListTile(
title: Text(addresses[i].addressLine),
onTap: () {
widget.meetingPointData = Location(addresses[i].addressLine);
setState(() {});
this._overlayEntry.remove();
this._overlayEntry = null;
},
),);
}
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(10.0, size.height - 45.0),
child: Material(
elevation: 4.0,
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: addressesWidget
),
),
),
)
);
}
Future findLocations(text) async {
if(text == "") {
addresses = [];
return;
}
// From a query
final query = text;
try {
addresses = await Geocoder.local.findAddressesFromQuery(query);
var first = addresses.first;
print("${first.featureName} : ${first.coordinates}");
} catch (err) {
}
if(addresses.length > 0) {
if(this._overlayEntry != null) {
this._overlayEntry.remove();
this._overlayEntry = null;
}
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
} else {
}
}
}
Assuming that the widget tree you're thinking of is:
A(
child: B(),
);
Then A should not be able to modify the state of B. Even if it is technically possible through global variables/GlobalKey, it is anti-pattern to do so.
Instead, you should move the state up in the tree and store your field inside A instead of B.
I am coding a chess game in flutter.
and this is the relevant bits of my code :
class Rank extends StatelessWidget {
final _number;
Rank(this._number);
#override
Widget build(BuildContext context) {
var widgets = <Widget>[];
for (var j = 'a'.codeUnitAt(0); j <= 'h'.codeUnitAt(0); j++) {
widgets
.add(
DroppableBoardSquare(String.fromCharCode(j) + this._number.toString())
);
//
}
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgets);
}
}
class DroppableBoardSquare extends StatelessWidget {
final String _coordinate;
const DroppableBoardSquare(this._coordinate) ;
#override
Widget build(BuildContext context) {
return DragTarget(
builder:(BuildContext context, List candidate, List rejectedData){
return BoardSquare(_coordinate);
},
onAccept: (data ) {
print('Accepted');
},
onWillAccept: (data){
return true;
},
onLeave: (data) => print("leave"),);
}
}
class BoardSquare extends StatelessWidget {
final String _coordinate;
BoardSquare(this._coordinate);
#override
Widget build(BuildContext context) {
ChessBloc bloc = ChessBlocProvider.of(context);
return
StreamBuilder<chess.Chess>(
stream: bloc.chessState,
builder: (context, AsyncSnapshot<chess.Chess> chess) {
return DraggablePieceWidget(chess.data.get(_coordinate), _coordinate);
});
}
}
class DraggablePieceWidget extends StatelessWidget {
final chess.Piece _piece;
final String _coordinate;
DraggablePieceWidget(this._piece, String this._coordinate);
#override
Widget build(BuildContext context) {
return Draggable(
child: PieceWidget(_piece),
feedback: PieceWidget(_piece),
childWhenDragging: PieceWidget(null),
data: {"piece": _piece, "origin": _coordinate} ,
);
}
}
Now the problem is that I can drag the piece fine, but cannot drop them. None of the methods on DragTarget is getting called.
what I am doing wrong?
I developed a drag-n-drop photos grid, where you can drag photos to reorder them based on numeric indexes.
Essentially, I assume, it is the same thing as the chessboard concept you have.
The problem possibly occurs due to Draggable (DraggablePieceWidget) element being inside of DragTarget (DroppableBoardSquare).
In my app I made it the other way around - I placed DragTarget into Draggable.
Providing some pseudo-code as an example:
int _dragSelectedIndex;
int _draggingIndex;
// Basically this is what you'd use to build every chess item
Draggable(
maxSimultaneousDrags: 1,
data: index,
onDragStarted: () { _draggingIndex = index; print("Debug: drag started"); }, // Use setState for _draggingIndex, _dragSelectedIndex.
onDragEnd: (details) { onDragEnded(); _draggingIndex = null; print("Debug: drag ended; $details"); },
onDraggableCanceled: (_, __) { onDragEnded(); _draggingIndex = null; print("Debug: drag cancelled."); },
feedback: Material(type: MaterialType.transparency, child: Opacity(opacity: 0.85, child: Transform.scale(scale: 1.1, child: createDraggableBlock(index, includeTarget: false)))),
child: createDraggableBlock(index, includeTarget: true),
);
// This func is used in 2 places - Draggable's `child` & `feedback` props.
// Creating dynamic widgets through functions is a bad practice, switch to StatefulWidget if you'd like.
Widget createDraggableBlock(int index, { bool includeTarget = true }) {
if (includeTarget) {
return DragTarget(builder: (context, candidateData, rejectedData) {
if (_draggingIndex == index || candidateData.length > 0) {
return Container(); // Display empty widget in the originally selected cell, and in any cell that we drag the chess over.
}
// Display a chess, but wrapped in DragTarget widget. All chessboard cells will be displayed this way, except for the one you start dragging.
return ChessPiece(..., index: index);
}, onWillAccept: (int elemIndex) {
if (index == _draggingIndex) {
return false; // Do not accept the chess being dragged into it's own widget
}
setState(() { _dragSelectedIndex = index; });
return true;
}, onLeave: (int elemIndex) {
setState(() { _dragSelectedIndex = null; });
});
}
// Display a chess without DragTarget wrapper, e.g. for the draggable(feedback) widget
return ChessPiece(..., index: index);
}
onDragEnded() {
// Check whether _draggingIndex & _dragSelectedIndex are not null and act accordingly.
}
I assume if you change index system to custom objects that you have - this would work for you too.
Please let me know if this helped.
my use case is to create a list view of articles (each item have the same look, there could be huge amount of articles, e.g. > 10000). I tried with
- ListView with ListView.builder: it supposes only to render the item when the item is displayed
- ScrollController: to determine when to load the next items (pagination)
- then I use List to store the data fetched from restful API using http, by adding the data from http to the List instance
this approach is OK, but in case the user keeps on scrolling pages, the List instance will have more and more items, it can crash with stack Overflow error.
If I don't call List.addAll(), instead I assign the data fetched from api, like: list = data;
I have problem that when the user scroll up, he/she won't be able to see the previous items.
Is there a good approach to solve this? Thanks!
import 'package:flutter/material.dart';
import 'package:app/model.dart';
import 'package:app/components/item.dart';
abstract class PostListPage extends StatefulWidget {
final String head;
DealListPage(this.head);
}
abstract class PostListPageState<T extends PostListPage> extends State<PostListPage> {
final int MAX_PAGE = 2;
DealListPageState(String head) {
this.head = head;
}
final ScrollController scrollController = new ScrollController();
void doInitialize() {
page = 0;
try {
list.clear();
fetchNextPage();
}
catch(e) {
print("Error: " + e.toString());
}
}
#override
void initState() {
super.initState();
this.fetchNextPage();
scrollController.addListener(() {
double maxScroll = scrollController.position.maxScrollExtent;
double currentScroll = scrollController.position.pixels;
double delta = 200.0; // or something else..
if ( maxScroll - currentScroll <= delta) {
fetchNextPage();
}
});
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
void mergeNewResult(List<PostListItem> result) {
list.addAll(result);
}
Future fetchNextPage() async {
if (!isLoading && mounted) {
page++;
setState(() {
isLoading = true;
});
final List<PostListItem> result = await doFetchData(page);
setState(() {
if (result != null && result.length > 0) {
mergeNewResult(result);
} else {
//TODO show notification
}
isLoading = false;
});
}
}
Future doFetchData(final int page);
String head;
List<PostListItem> list = new List();
var isLoading = false;
int page = 0;
int pageSize = 20;
final int scrollThreshold = 10;
Widget buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isLoading ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
#override
Widget build(BuildContext context) {
ListView listView = ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
return buildProgressIndicator();
}
if (index > 0) {
return Column(
children: [Divider(), PostListItem(list[index])]
);
}
return PostListItem(list[index]);
},
controller: scrollController,
itemCount: list.length
);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(head),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {
},
),
// action button
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {
},
),
]
),
body: new RefreshIndicator(
onRefresh: handleRefresh,
child: listView
),
);
}
Future<Null> handleRefresh() async {
doInitialize();
return null;
}
}
in my case, when the list length is 600, I start to get stack overflow error like:
I/flutter ( 8842): Another exception was thrown: Stack Overflow
I/flutter ( 8842): Another exception was thrown: Stack Overflow
screen:
enter image description here
somehow flutter doesn't show any more details of the error.
I wrote some sample code for a related question about paginated scrolling, which you could check out.
I didn't implement cache invalidation there, but it would easily be extendable using something like the following in the getPodcast method to remove all items that are more than 100 indexes away from the current location:
for (key in _cache.keys) {
if (abs(key - index) > 100) {
_cache.remove(key);
}
}
An even more sophisticated implementation could take into consideration the scroll velocity and past user behavior to lay out a probability curve (or a simpler Gaussian curve) to fetch content more intelligently.