I'm making a e-book application and user can draw lines on the WorkbookDrawingPage Widget by Stack layer.
I want to make the drawn lines saved to the device and books by using shared preference. I tried to use a Json encoding, but couldn't apply to my code.
How to save List<Object> to SharedPreferences in Flutter?
Here's my code
WorkbookViewPage ( PdfViewer and DrawingPage widget)
class WorkbookViewPage extends StatefulWidget {
String downloadedURL;
String workbookName;
WorkbookViewPage(this.downloadedURL, this.workbookName, {super.key});
#override
State<WorkbookViewPage> createState() => _WorkbookViewPageState();
}
class _WorkbookViewPageState extends State<WorkbookViewPage> {
/// Widget Key
GlobalKey _viewKey = GlobalKey();
final GlobalKey<SfPdfViewerState> _pdfViewerKey = GlobalKey();
/// Controller
PdfViewerController _pdfViewerController = PdfViewerController();
ScrollController _scrollController = ScrollController();
/// Variables
int _lastClosedPage = 1;
int _currentPage = 1;
bool _memoMode = false;
int drawingPage = 1;
bool isPenTouched = false;
/// pen size control widget Entry
bool isOverlayVisible = false;
OverlayEntry? entry;
/// Hide Overlay Widget
void hideOverlay() {
entry?.remove();
entry = null;
isOverlayVisible = false;
}
/// Count the Total Pages of the Workbook
int _countTotalPages(){
int _totalPages = _pdfViewerController.pageCount;
return _totalPages;
}
/// get Last closed page of workbook
void _loadLastClosedPage() async {
final pagePrefs = await SharedPreferences.getInstance();
_lastClosedPage = pagePrefs.getInt('${widget.workbookName}') ?? 1;
}
#override
void initState() {
super.initState();
// Load Pdf with landScape Mode
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft]);
_pdfViewerController = PdfViewerController();
String workbookName = '${widget.workbookName}';
_loadLastClosedPage();
_countTotalPages();
print("======== Loaded workbook name = ${workbookName} ==========");
}
#override
Widget build(BuildContext context) {
/// Drawing Provider
var p = context.read<DrawingProvider>();
/// set _lastClosedPage
void countLastClosedPage() async {
final pagePrefs = await SharedPreferences.getInstance();
setState(() {
pagePrefs.setInt('${widget.workbookName}', _lastClosedPage);
_lastClosedPage = (pagePrefs.getInt('${widget.workbookName}') ?? 1);
});
}
return Scaffold(
appBar: AppBar(
centerTitle: true,
backgroundColor: Colors.white,
elevation: 1,
),
body: Stack(
children: [
InteractiveViewer(
panEnabled: _memoMode ? false : true,
scaleEnabled: _memoMode ? false : true,
maxScale: 3,
child: Stack(
children: [
IgnorePointer(
ignoring: true,
child: SfPdfViewer.network(
widget.downloadedURL,
controller: _pdfViewerController,
key: _pdfViewerKey,
pageLayoutMode: PdfPageLayoutMode.single,
enableDoubleTapZooming: false,
// Save the last closed page number
onPageChanged: (details) {
_lastClosedPage = details.newPageNumber;
_currentPage = details.newPageNumber;
countLastClosedPage();
},
onDocumentLoaded: (details) {
_pdfViewerController.jumpToPage(_lastClosedPage);
_pdfViewerController.zoomLevel = 0;
print("totalpages = ${_countTotalPages().toInt()}");
},
canShowScrollHead: false,
),
),
SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: WorkbookDrawingPage(widget.workbookName, _countTotalPages().toInt(),),
),
],
),
),
WorkbookDawingPage.dart
class WorkbookDrawingPage extends StatefulWidget {
String workbookName;
int countTotalPages;
WorkbookDrawingPage(this.workbookName, this.countTotalPages,{super.key});
#override
State<WorkbookDrawingPage> createState() => _WorkbookDrawingPageState();
}
class _WorkbookDrawingPageState extends State<WorkbookDrawingPage> {
// OverlayEntry widget key
GlobalKey _overlayViewKey = GlobalKey();
Key _viewKey = UniqueKey();
// pen size control widget Entry
bool isOverlayVisible = false;
OverlayEntry? entry;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
var p = context.read<DrawingProvider>();
void hideOverlay() {
entry?.remove();
entry = null;
isOverlayVisible = false;
}
return Container(
width: MediaQuery.of(context).size.width * (widget.countTotalPages),
height: MediaQuery.of(context).size.height * 1,
child: CustomPaint(
painter: DrawingPainter(p.lines),
child: Listener(
behavior: HitTestBehavior.translucent,
// Draw lines when stylus hit the screen
onPointerDown: (s) async {
if (s.kind == PointerDeviceKind.stylus) {
setState(() {
p.penMode ? p.penDrawStart(s.localPosition) : null;
p.highlighterMode
? p.highlighterDrawStart(s.localPosition)
: null;
p.eraseMode ? p.erase(s.localPosition) : null;
});
}
/// Stylus with button pressed touched the Screen
else if (s.kind == PointerDeviceKind.stylus ||
s.buttons == kPrimaryStylusButton) {
setState(() {
p.changeEraseModeButtonClicked = true;
});
}
},
onPointerMove: (s) {
if (s.kind == PointerDeviceKind.stylus) {
setState(() {
p.penMode ? p.penDrawing(s.localPosition) : null;
p.highlighterMode
? p.highlighterDrawing(s.localPosition)
: null;
p.eraseMode ? p.erase(s.localPosition) : null;
});
} else if (s.kind == PointerDeviceKind.stylus ||
s.buttons == kPrimaryStylusButton) {
setState(() {
p.changeEraseModeButtonClicked = true;
});
}
},
),
),
);
}
}
The application uses provider for drawing lines and here's provider class
Drawing Provider
Class DrawingProvider extends ChangeNotifier {
/// line List **/
final lines = <List<DotInfo>>[];
Color _selectedColor = Colors.black;
Color get selectedColor => _selectedColor;
/** function method to change Color **/
set changeColor(Color color) {
_selectedColor = color;
notifyListeners();
}
/** Mode Selection **/
bool _penMode = false;
bool get penMode => _penMode;
bool _highlighterMode = false;
bool get highlighterMode => _highlighterMode;
bool _eraseMode = false;
bool get eraseMode => _eraseMode;
bool _memoMode = false;
bool get memoMode => _memoMode;
set changeEraseModeButtonClicked(bool eraseMode) {
eraseMode = true;
_eraseMode = eraseMode;
print("eraseMode가 On ");
notifyListeners();
}
/** 지우개 선택 모드 **/
void changeEraseMode() {
_eraseMode = !_eraseMode;
print("eraseMode is : ${eraseMode}");
_penMode = false;
_highlighterMode = false;
notifyListeners();
}
/** 일반 펜 선택 모드 **/
void changePenMode() {
_penMode = !_penMode;
// _selectedColor = _selectedColor.withOpacity(1);
print("penMode is called : ${penMode} ");
_eraseMode = false;
_highlighterMode = false;
notifyListeners();
}
/** 형광펜 선택 모드 **/
void changeHighlighterMode() {
_highlighterMode = !_highlighterMode;
// _selectedColor = _selectedColor.withOpacity(0.3);
print("Highlighter Mode : ${highlighterMode}");
_eraseMode = false;
_penMode = false;
_opacity = 0.3;
notifyListeners();
}
/** Pen Draw Start **/
void penDrawStart(Offset offset) async {
var oneLine = <DotInfo>[];
oneLine.add(DotInfo(offset, penSize, _selectedColor,));
lines.add(oneLine);
notifyListeners();
}
/** Pen Drawing **/
void penDrawing(Offset offset) {
lines.last.add(DotInfo(offset, penSize, _selectedColor));
notifyListeners();
}
/** highlighter Start **/
void highlighterDrawStart(Offset offset) {
var oneLine = <DotInfo>[];
oneLine
.add(DotInfo(offset, highlighterSize, _selectedColor.withOpacity(0.3),));
lines.add(oneLine);
notifyListeners();
}
/** 터치 후 형광 펜 그리기 **/
void highlighterDrawing(Offset offset) {
lines.last
.add(DotInfo(offset, highlighterSize, _selectedColor.withOpacity(0.3)));
notifyListeners();
}
/** 선 지우기 메서드 **/
void erase(Offset offset) {
final eraseRange = 15;
for (var oneLine in List<List<DotInfo>>.from(lines)) {
for (var oneDot in oneLine) {
if (sqrt(pow((offset.dx - oneDot.offset.dx), 2) +
pow((offset.dy - oneDot.offset.dy), 2)) <
eraseRange) {
lines.remove(oneLine);
break;
}
}
}
notifyListeners();
}
}
and the Dotinfo save the information of the lines
DotInfo.dart
class DotInfo {
final Offset offset;
final double size;
final Color color;
DotInfo(this.offset, this.size, this.color);
}
in the WorkbookDrawingPage.dart , CustomPaint.painter method get DrawingPainter Class
DrawingPainter.dart
class DrawingPainter extends CustomPainter {
final List<List<DotInfo>> lines;
DrawingPainter(this.lines);
#override
void paint(Canvas canvas, Size size) {
for (var oneLine in lines) {
Color? color;
double? size;
var path = Path();
var l = <Offset>[];
for (var oneDot in oneLine) {
color ??= oneDot.color;
size ??= oneDot.size;
l.add(oneDot.offset);
}
path.addPolygon(l, false);
canvas.drawPath(
path,
Paint()
..color = color!
..strokeWidth = size!
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..isAntiAlias = true
..strokeJoin = StrokeJoin.round);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
The Drawing function is related to the provider.
I have no idea how to save the drawn lines into the device. My idea was saving the WorkbookDrawingPage widget itself to the device by using shared preference but, shared preference only can save Int, String, double... so, I tried to encode the DotInfo Class but it cannot be encoded because constructors value is not initialized in the DotInfo Class.
how to save the drawn lines to the device and make them pair with the book?
is saving the widget to the device is possible?? or can you give me any hint or answer?
I have a listview in which the text of items are animated when they first appear - and when they reappear after enough scrolling. When the list grows to certain size and the user scrolls back far enough items are animated again - presumably they've been removed from the widget tree and are now being re-inserted and thus get re-initiated etc. I want to prevent this from happening so that they only animate the first time they appear.
I think this means I need to have state stored somewhere per item that keeps track and tells the individual items whether they should animate on them being built or not. I am not sure where to put and how to connect that though, partly because it seems to overlap between presentation and business logic layers. I think perhaps it should be a variable in the list items contained in the list object that the listview builder is constructing from - or should it somehow be in the actual widgets in the listview?
class _StockListViewBuilderState extends State<StockListViewBuilder> with asc_alertBar {
final ScrollController _scrollController = ScrollController();
late double _scrollPosition;
late double _maxScrollExtent;
late bool isThisTheEnd = false;
_scrollListener() async {
setState(() {
_scrollPosition = _scrollController.position.pixels;
_maxScrollExtent = _scrollController.position.maxScrollExtent;
});
if (!isThisTheEnd && _scrollPosition / _maxScrollExtent > 0.90) {
isThisTheEnd = true;
if (widget.stockListicle.getIsRemoteEmpty()) {
alertBar('No more items available', /* null,*/ context: context);
} else {
await widget.stockListicle.fetch(numberToFetch: 5);
}
}
if (isThisTheEnd && _scrollPosition / _maxScrollExtent <= 0.90) {
isThisTheEnd = false;
}
}
#override
void initState() {
super.initState();
late String? userFullName = GetIt.I.get<Authenticate>().user?.fullName;
developer.log('Authenticated user $userFullName', name: '_StockListViewBuilderState');
developer.log("init ", name: "_StockListViewBuilderState ");
int listCount;
_scrollController.addListener(_scrollListener);
WidgetsBinding.instance.addPostFrameCallback((_) async {
//developer.log("stckLtcl init pf con ");
listCount = widget.stockListicle.items.length;
if (listCount < 10 && !widget.stockListicle.getIsRemoteEmpty()) {
try {
await widget.stockListicle.fetch(numberToFetch: 10);
} catch (e) {
super.setState(() {
//developer.log("Can't load stock:$e");
alertBar(
"Couldn't load from the internet.",
context: context,
backgroundColor: Colors.purple,
);
});
}
}
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
final ConnectionNotifier connectionNotifier = context.read<ConnectionNotifier>();
if (connectionNotifier.isConnected() != true) {
await connectionNotifier.check();
if (connectionNotifier.isConnected() != true) {
alertBar("Please check the internet connection.", context: context);
}
}
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection: Axis.vertical,
controller: _scrollController,
shrinkWrap: true,
key: widget.theKey,
itemCount: widget.stockListicle.items.length + 1,
itemBuilder: (context, index) {
if (index <= widget.stockListicle.items.length - 1) {
return InkWell(
onTap: (() => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(widget.stockListicle.items[index] as Stock),
)),
child: StockListItem(
stock: widget.stockListicle.items[index] as Stock,
));
} else {
return LoadingItemNotifier(
isLoading: widget.stockListicle.getIsBusyLoading(),
);
}
},
);
}
}
//...
Currently StockListItem extends StatelessWidget and returns a 'ListTile' which as its title parameter has ...title: AnimatedText(textContent: stock.title),...
I was trying to keep track of first-time-animation inside AnimatedText widget until I realized from an OOP & Flutter perspective, it's probably wrong place...
class AnimatedText extends StatefulWidget {
final bool doShowMe;
final String textContent;
final Duration hideDuration;
final double durationFactor;
const AnimatedText({
Key? key,
this.doShowMe = true,
this.textContent = '',
this.hideDuration = const Duration(milliseconds: 500),
this.durationFactor = 1,
}) : super(key: key);
#override
State<AnimatedText> createState() => _AnimatedTextState();
}
class _AnimatedTextState extends State<AnimatedText> with SingleTickerProviderStateMixin {
late AnimationController _appearanceController;
late String displayText;
late String previousText;
late double durationFactor;
late Duration buildDuration = Duration(
milliseconds: (widget.textContent.length / 15 * widget.durationFactor * 1000).round());
#override
void initState() {
super.initState();
developer.log('init ${widget.textContent}', name: '_AnimatedTextState');
displayText = '';
previousText = widget.textContent;
_appearanceController = AnimationController(
vsync: this,
duration: buildDuration,
)..addListener(
() => updateText(),
);
if (widget.doShowMe) {
_doShowMe();
}
}
void updateText() {
String payload = widget.textContent;
int numCharsToShow = (_appearanceController.value * widget.textContent.length).ceil();
if (widget.doShowMe) {
// make it grow
displayText = payload.substring(0, numCharsToShow);
// developer.log('$numCharsToShow / ${widget.textContent.length} ${widget.textContent}');
} else {
// make it shrink
displayText = payload.substring(payload.length - numCharsToShow, payload.length);
}
}
#override
void didUpdateWidget(AnimatedText oldWidget) {
super.didUpdateWidget(oldWidget);
if ((widget.doShowMe != oldWidget.doShowMe) || (widget.textContent != oldWidget.textContent)) {
if (widget.doShowMe) {
_doShowMe();
} else {
_doHideMe();
}
}
if (widget.doShowMe && widget.textContent != previousText) {
previousText = widget.textContent;
developer.log('reset');
_appearanceController
..reset()
..forward();
}
}
#override
void dispose() {
_appearanceController.dispose();
displayText = '';
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _appearanceController,
builder: (context, child) {
return Text(displayText);
});
}
void _doShowMe() {
_appearanceController
..duration = buildDuration
..forward();
}
void _doHideMe() {
_appearanceController
..duration = widget.hideDuration
..reverse();
}
}
It's been a long time since I started to search for a Flutter ListView library that will allow me to use pagination in a smart way. Sadly I haven't found anything that meets my criteria:
Smart pagination: the library should't simply increase a list page-by-page but must have a fixed size cache which load and keep in memory only the needed pages in the moment.
Async loading: the library should basically accept a function which returns a future of a list representing a page.
Real-time invalidation: Dart has streams, so somehow the library should use their power to handle invalidation and reload everything needed when data changes in a reactive way.
Basically I wanted something that acted like PagedListAdapter + DataSource.Factory + LiveData in the standard Android library.
I came up with the widget PagedListView:
import 'dart:math';
import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget WaitBuilder(BuildContext context);
typedef Widget PlaceholderBuilder(BuildContext context);
typedef Widget EmptyResultBuilder(BuildContext context);
typedef Widget ErrorBuilder(BuildContext context);
class PagedListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final WaitBuilder waitBuilder;
final PlaceholderBuilder placeholderBuilder;
final EmptyResultBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
PagedListView(
{#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.waitBuilder,
#required this.placeholderBuilder,
#required this.emptyResultBuilder,
#required this.errorBuilder});
#override
_PagedListView<T> createState() => _PagedListView<T>();
}
class _PagedListView<T> extends State<PagedListView<T>> {
/// Represent the number of cached pages before and after the current page.
/// If edgeCachePageCount = 1 the total number of cached pages are 3 (one before + current + one after).
/// TODO calculate from pageSize
final int edgeCachePageCount = 2;
int get maxCachedPageCount => (edgeCachePageCount * 2) + 1;
int currentPage = 0;
List<T> items;
Object error;
int totalCount = -1;
/// Contains the page indexes which the fetching is started but not completed.
final progressPages = Set<int>();
/// Contains the page indexes already retrieved.
final cachedPages = Set<int>();
int limitStartIndex = -1;
int limitEndIndex = -1;
#override
void initState() {
super.initState();
items = List.filled(widget.pageSize * maxCachedPageCount, null);
widget.countStream.listen((int count) {
Fimber.i("Total count changed: $count");
totalCount = count;
// Invalidate.
cachedPages.clear();
if (count > 0) {
_fetchPages(PageRequest.SAME);
}
setState(() {});
});
}
void _fetchPages(PageRequest pageRequest) {
Set<int> refreshIndexes = _getRefreshIndexes();
//Fimber.i("Refresh indexes are $refreshIndexes");
refreshIndexes.forEach((pageIndex) => _fetchPage(pageIndex, pageRequest));
}
Set<int> _getRefreshIndexes() {
return getRefreshIndexes(maxCachedPageCount, edgeCachePageCount, currentPage, widget.pageSize, totalCount);
}
_fetchPage(int index, PageRequest request) {
if (cachedPages.contains(index)) {
// We already have this page.
return;
}
if (!progressPages.contains(index)) {
//Fimber.i("Fetch page $index start");
progressPages.add(index);
widget.pageFuture(index).asStream().map((list) => PageResult<T>(index, request, list)).listen(_onData, onError: _onError);
}
}
void _onData(PageResult<T> data) {
if (data.items != null) {
if (!_getRefreshIndexes().contains(data.index)) {
progressPages.remove(data.index);
//Fimber.i("Skipping invalid page ${data.index}, currentPage = $currentPage, refreshIndexes = ${_getRefreshIndexes()}");
return;
}
//Fimber.i("Fetch page ${data.index} end");
if (cachedPages.length == maxCachedPageCount) {
// The cached page count is exceeded, remove the smallest / greatest page.
if (data.request == PageRequest.NEXT) {
int smallestPage = cachedPages.reduce(min);
cachedPages.remove(smallestPage);
//Fimber.i("Smallest page $smallestPage removed");
} else if (data.request == PageRequest.PREVIOUS) {
int greatestPage = cachedPages.reduce(max);
cachedPages.remove(greatestPage);
//Fimber.i("Greatest page $greatestPage removed");
} else {
int smallestPage = cachedPages.reduce(min);
int greatestPage = cachedPages.reduce(max);
int smallestPageDistance = currentPage - smallestPage;
int greatestPageDistance = greatestPage - currentPage;
if (smallestPageDistance >= greatestPageDistance) {
//Fimber.i("Smallest page $smallestPage removed, smallestPageDistance = $smallestPageDistance, greatestPageDistance = $greatestPageDistance");
cachedPages.remove(smallestPage);
} else {
//Fimber.i("Greatest page $greatestPage removed, smallestPageDistance = $smallestPageDistance, greatestPageDistance = $greatestPageDistance");
cachedPages.remove(greatestPage);
}
}
}
Set<int> tempCachedPages = cachedPages.toSet()..add(data.index);
// Put the result in the correct position.
int startIndex = widget.pageSize * (data.index % maxCachedPageCount);
items.setAll(startIndex, data.items);
//Fimber.i("Fetch page ${data.index} end, startIndex = $startIndex");
limitStartIndex = cachedPages.isEmpty ? 0 : tempCachedPages.reduce(min) * widget.pageSize;
//Fimber.i("limitStartIndex set to $limitStartIndex");
limitEndIndex = cachedPages.isEmpty ? -1 : (widget.pageSize * tempCachedPages.reduce(max)) + data.items.length - 1;
//Fimber.i("limitEndIndex set to $limitEndIndex");
cachedPages.add(data.index);
progressPages.remove(data.index);
//Fimber.i("Fetch page ${data.index} end, startIndex = $startIndex, cached pages ${cachedPages.toList()..sort()}, currentPage = $currentPage");
setState(() {});
}
}
void _onError(error) {
this.error = error;
setState(() {});
}
_fetchNewPage(int index) {
int newPage = index ~/ widget.pageSize;
PageRequest pageRequest = newPage > currentPage ? PageRequest.NEXT : (newPage < currentPage ? PageRequest.PREVIOUS : PageRequest.SAME);
/*pageRequest == PageRequest.NEXT
? Fimber.i("Fetch next page $newPage")
: (pageRequest == PageRequest.PREVIOUS ? Fimber.i("Fetch previous page $newPage") : null);*/
currentPage = newPage;
_fetchPages(pageRequest);
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
if (error != null) {
return widget.errorBuilder(context);
}
if (totalCount == -1) {
return widget.waitBuilder(context);
}
if (totalCount == 0) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
key: Key("listView"),
itemCount: totalCount,
itemBuilder: (context, index) {
if (index < limitStartIndex || index > limitEndIndex) {
_fetchNewPage(index);
}
return _getListItem(context, index);
},
);
}
Widget _getListItem(BuildContext context, int realIndex) {
int pageIndex = realIndex ~/ widget.pageSize;
if (!cachedPages.contains(pageIndex)) {
return widget.placeholderBuilder(context);
}
int cachePageIndex = pageIndex % maxCachedPageCount;
int cacheIndex = (cachePageIndex * widget.pageSize) + (realIndex % widget.pageSize);
return widget.itemBuilder(context, realIndex, items[cacheIndex]);
}
}
enum PageRequest { NEXT, PREVIOUS, SAME }
class PageResult<T> {
/// Page index of this data.
final int index;
/// Represent the direction from the current page when the request was made.
final PageRequest request;
final List<T> items;
PageResult(this.index, this.request, this.items);
}
Set<int> getRefreshIndexes(int maxCachedPageCount, int edgeCachePageCount, int currentPage, int pageSize, int totalCount) {
List<int> temp = List.generate(min(maxCachedPageCount, (totalCount ~/ pageSize) + 1), (index) => index + (currentPage - edgeCachePageCount));
int minIndex = temp.reduce(min);
if (minIndex < 0) {
return temp.map((index) => index + minIndex.abs()).toSet();
}
int maxIndex = temp.reduce(max);
int maxPage = totalCount ~/ pageSize;
if (maxIndex > maxPage) {
return temp.map((index) => index - (maxIndex - maxPage)).toSet();
}
return temp.toSet();
}
Since I need to know the total amount of items and handle invalidation I figured to accept a Stream<int> which returns the real list size every time data is modified.
This is an example of how it's used:
class MyHomePage extends StatelessWidget {
final MyDatabase database = MyDatabase();
MyHomePage({Key key}) : super(key: key);
Random random = Random.secure();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: PagedListView(
pageSize: 10,
pageFuture: (pageIndex) =>
Future.delayed(Duration(milliseconds: (random.nextDouble() * 5000).toInt()), () => database.getCategories(10, 10 * pageIndex)),
countStream: database.countCategories().watchSingle(),
itemBuilder: _itemBuilder,
waitBuilder: _waitBuilder,
placeholderBuilder: _placeholderBuilder,
emptyResultBuilder: _emptyResultBuilder,
errorBuilder: _errorBuilder,
),
);
}
Widget _itemBuilder(BuildContext context, int index, Category item) => Container(
height: 60,
child: Center(
child: ListTile(
key: Key(item.id.toString()),
title: Text(item.description),
subtitle: Text("id = ${item.id}, index = $index")
),
),
);
Widget _waitBuilder(BuildContext context) => Center(child: CircularProgressIndicator());
Widget _placeholderBuilder(BuildContext context) => Container(
height: 60,
margin: EdgeInsets.all(8),
child: Center(
child: CircularProgressIndicator(),
));
Widget _emptyResultBuilder(BuildContext context) => Container(
margin: EdgeInsets.all(8),
child: Center(
child: Text("Empty"),
));
Widget _errorBuilder(BuildContext context) => Container(
color: Colors.red,
margin: EdgeInsets.all(8),
child: Center(
child: Text("Error"),
));
}
I'm using SQLite with Moor to retrieve data (https://moor.simonbinder.eu/docs/).
database.getCategories(10, 10 * pageIndex)) is a method returning the Future<List<Category>> representing a page
database.countCategories().watchSingle() is the Stream emitting the list size at every add/update/delete
What do you think?
Am I missing some bugs? Would you done things differently? Maybe in a more simple / elegant / performant way?
Thanks
UPDATE #1
I made a new version based on pskink suggestion using LruMap.
import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget WaitBuilder(BuildContext context);
typedef Widget PlaceholderBuilder(BuildContext context);
typedef Widget EmptyResultBuilder(BuildContext context);
typedef Widget ErrorBuilder(BuildContext context);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final WaitBuilder waitBuilder;
final PlaceholderBuilder placeholderBuilder;
final EmptyResultBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
LazyListView(
{#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.waitBuilder,
#required this.placeholderBuilder,
#required this.emptyResultBuilder,
#required this.errorBuilder});
#override
_LazyListView<T> createState() => _LazyListView<T>();
}
class _LazyListView<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
Object error;
int totalCount = -1;
int currentPage = 0;
#override
void initState() {
super.initState();
map = LruMap<int, PageResult<T>>(maximumSize: 500 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
widget.countStream.listen((int count) {
Fimber.i("Total count changed: $count");
totalCount = count;
map.clear();
setState(() {});
});
}
#override
Widget build(BuildContext context) {
if (error != null) {
return widget.errorBuilder(context);
}
if (totalCount == -1) {
return widget.waitBuilder(context);
}
if (totalCount == 0) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
key: Key("listView"),
itemCount: totalCount,
itemBuilder: (context, index) {
currentPage = index ~/ widget.pageSize;
final pageResult = map[currentPage];
final value = pageResult == null ? null : pageResult.items[index % widget.pageSize];
final loading = (value == null);
if (loading) {
cache.get(currentPage, ifAbsent: _loadPage).then(reload);
return widget.placeholderBuilder(context);
}
return widget.itemBuilder(context, index, value);
},
);
}
Future<PageResult<T>> _loadPage(int index) {
Fimber.i("Start fetch page $index");
return widget.pageFuture(index).then((list) => PageResult(index, list));
}
reload(PageResult<T> value) {
// Avoid calling setState if it's not needed.
if ((value.index - currentPage).abs() > 2) {
// ATTENTION: 2 is an arbitrary value, the distance between the current page and the page in the future result should ensure correct refreshing.
// It should be greater if item widgets have a smaller height, can be smaller if item widgets have a greater height.
// TODO: make it configurable?
Fimber.i("Skipping refreshing for result of page ${value.index}, currentPage = $currentPage");
return;
}
setState(() {});
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
PageResult(this.index, this.items);
}
UPDATE #2 based on pskink new comment
import 'package:fimber/fimber.dart';
import 'package:flutter/material.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget WaitBuilder(BuildContext context);
typedef Widget PlaceholderBuilder(BuildContext context);
typedef Widget EmptyResultBuilder(BuildContext context);
typedef Widget ErrorBuilder(BuildContext context);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final WaitBuilder waitBuilder;
final PlaceholderBuilder placeholderBuilder;
final EmptyResultBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
LazyListView(
{#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.waitBuilder,
#required this.placeholderBuilder,
#required this.emptyResultBuilder,
#required this.errorBuilder});
#override
_LazyListView<T> createState() => _LazyListView<T>();
}
class _LazyListView<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
Object error;
int totalCount = -1;
#override
void initState() {
super.initState();
map = LruMap<int, PageResult<T>>(maximumSize: 50 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
widget.countStream.listen((int count) {
Fimber.i("Total count changed: $count");
totalCount = count;
map.clear();
setState(() {});
});
}
#override
Widget build(BuildContext context) {
if (error != null) {
return widget.errorBuilder(context);
}
if (totalCount == -1) {
return widget.waitBuilder(context);
}
if (totalCount == 0) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
key: Key("listView"),
itemCount: totalCount,
itemBuilder: (context, index) {
int currentPage = index ~/ widget.pageSize;
final pageResult = map[currentPage];
final value = pageResult == null ? null : pageResult.items[index % widget.pageSize];
final loading = (value == null);
if (loading) {
cache.get(currentPage, ifAbsent: _loadPage).then(_reload);
return widget.placeholderBuilder(context);
}
return widget.itemBuilder(context, index, value);
},
);
}
Future<PageResult<T>> _loadPage(int index) {
Fimber.i("Start fetch page $index");
return widget.pageFuture(index).then((list) => PageResult(index, list));
}
_reload(PageResult<T> value) {
if (value.refreshed) {
// Avoid calling setState if already called.
Fimber.i("Skipping refreshing for result of page ${value.index}");
return;
}
setState(() {
value.refreshed = true;
});
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
bool refreshed = false;
PageResult(this.index, this.items);
}
What dou you think?
This is the last version thanks to some very useful suggestions
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget ErrorBuilder(BuildContext context, dynamic error);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final IndexedWidgetBuilder placeholderBuilder;
final WidgetBuilder waitBuilder;
final WidgetBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
final double velocityThreshold;
LazyListView({
#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.placeholderBuilder,
this.waitBuilder,
this.emptyResultBuilder,
this.errorBuilder,
this.velocityThreshold = 128,
}) : assert(pageSize > 0),
assert(pageFuture != null),
assert(countStream != null),
assert(itemBuilder != null),
assert(placeholderBuilder != null),
assert(velocityThreshold >= 0);
#override
_LazyListViewState<T> createState() => _LazyListViewState<T>();
}
class _LazyListViewState<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
dynamic error;
int totalCount = -1;
bool _frameCallbackInProgress = false;
#override
void initState() {
super.initState();
_initCache();
widget.countStream.listen((int count) {
totalCount = count;
_initCache();
setState(() {});
});
}
#override
Widget build(BuildContext context) {
//debugPrintBeginFrameBanner = true;
//debugPrintEndFrameBanner = true;
//print('build');
if (error != null && widget.errorBuilder != null) return widget.errorBuilder(context, error);
if (totalCount == -1 && widget.waitBuilder != null) return widget.waitBuilder(context);
if (totalCount == 0 && widget.emptyResultBuilder != null) return widget.emptyResultBuilder(context);
return ListView.builder(
physics: _LazyListViewPhysics(velocityThreshold: widget.velocityThreshold),
itemCount: max(totalCount, 0),
itemBuilder: (context, index) {
// print('builder $index');
var page = index ~/ widget.pageSize;
final pageResult = map[page];
final value = pageResult?.items?.elementAt(index % widget.pageSize);
if (value != null) {
return widget.itemBuilder(context, index, value);
}
// print('$index ${Scrollable.of(context).position.activity.velocity}');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
cache.get(page, ifAbsent: _loadPage).then(_reload).catchError(_error);
} else if (!_frameCallbackInProgress) {
_frameCallbackInProgress = true;
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context));
}
return widget.placeholderBuilder(context, index);
},
);
}
Future<PageResult<T>> _loadPage(int index) async {
print('load $index');
var list = await widget.pageFuture(index);
return PageResult(index, list);
}
void _initCache() {
map = LruMap<int, PageResult<T>>(maximumSize: 50 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
}
void _error(dynamic e, StackTrace stackTrace) {
if (widget.errorBuilder == null) {
throw e;
}
setState(() => error = e);
}
void _reload(PageResult<T> value) => _doReload(value.index);
void _deferredReload(BuildContext context) {
print('_deferredReload');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
_frameCallbackInProgress = false;
_doReload(-1);
} else {
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context), rescheduling: true);
}
}
void _doReload(int index) {
// print('reload $index');
setState(() {});
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
PageResult(this.index, this.items);
}
class _LazyListViewPhysics extends AlwaysScrollableScrollPhysics {
final double velocityThreshold;
_LazyListViewPhysics({
#required this.velocityThreshold,
ScrollPhysics parent,
}) : super(parent: parent);
#override
recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
// print('velocityThreshold: $velocityThreshold');
return velocity.abs() > velocityThreshold;
}
#override
_LazyListViewPhysics applyTo(ScrollPhysics ancestor) {
// print('applyTo($ancestor)');
return _LazyListViewPhysics(velocityThreshold: velocityThreshold, parent: buildParent(ancestor));
}
}
UPDATE #1
This is a new version which ensures futures don't call setState if the widget is unmounted.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:quiver/cache.dart';
import 'package:quiver/collection.dart';
typedef Future<List<T>> PageFuture<T>(int pageIndex);
typedef Widget ItemBuilder<T>(BuildContext context, int index, T entry);
typedef Widget ErrorBuilder(BuildContext context, dynamic error);
class LazyListView<T> extends StatefulWidget {
final int pageSize;
final PageFuture<T> pageFuture;
final Stream<int> countStream;
final ItemBuilder<T> itemBuilder;
final IndexedWidgetBuilder placeholderBuilder;
final WidgetBuilder waitBuilder;
final WidgetBuilder emptyResultBuilder;
final ErrorBuilder errorBuilder;
final double velocityThreshold;
LazyListView({
#required this.pageSize,
#required this.pageFuture,
#required this.countStream,
#required this.itemBuilder,
#required this.placeholderBuilder,
this.waitBuilder,
this.emptyResultBuilder,
this.errorBuilder,
this.velocityThreshold = 128,
}) : assert(pageSize > 0),
assert(pageFuture != null),
assert(countStream != null),
assert(itemBuilder != null),
assert(placeholderBuilder != null),
assert(velocityThreshold >= 0);
#override
_LazyListViewState<T> createState() => _LazyListViewState<T>();
}
class _LazyListViewState<T> extends State<LazyListView<T>> {
Map<int, PageResult<T>> map;
MapCache<int, PageResult<T>> cache;
dynamic error;
int totalCount = -1;
bool _frameCallbackInProgress = false;
StreamSubscription<int> countStreamSubscription;
#override
void initState() {
super.initState();
_initCache();
countStreamSubscription = widget.countStream.listen((int count) {
totalCount = count;
print('totalCount = $totalCount');
_initCache();
setState(() {});
});
}
#override
void dispose() {
countStreamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
//debugPrintBeginFrameBanner = true;
//debugPrintEndFrameBanner = true;
//print('build');
if (error != null && widget.errorBuilder != null) {
return widget.errorBuilder(context, error);
}
if (totalCount == -1 && widget.waitBuilder != null) {
return widget.waitBuilder(context);
}
if (totalCount == 0 && widget.emptyResultBuilder != null) {
return widget.emptyResultBuilder(context);
}
return ListView.builder(
physics: _LazyListViewPhysics(velocityThreshold: widget.velocityThreshold),
itemCount: max(totalCount, 0),
itemBuilder: (context, index) {
// print('builder $index');
final page = index ~/ widget.pageSize;
final pageResult = map[page];
final value = pageResult?.items?.elementAt(index % widget.pageSize);
if (value != null) {
return widget.itemBuilder(context, index, value);
}
// print('$index ${Scrollable.of(context).position.activity.velocity}');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
cache.get(page, ifAbsent: _loadPage).then(_reload).catchError(_error);
} else if (!_frameCallbackInProgress) {
_frameCallbackInProgress = true;
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context));
}
return widget.placeholderBuilder(context, index);
},
);
}
Future<PageResult<T>> _loadPage(int index) async {
print('load $index');
var list = await widget.pageFuture(index);
return PageResult(index, list);
}
void _initCache() {
map = LruMap<int, PageResult<T>>(maximumSize: 512 ~/ widget.pageSize);
cache = MapCache<int, PageResult<T>>(map: map);
}
void _error(dynamic e, StackTrace stackTrace) {
if (widget.errorBuilder == null) {
throw e;
}
if (this.mounted) {
setState(() => error = e);
}
}
void _reload(PageResult<T> value) => _doReload(value.index);
void _deferredReload(BuildContext context) {
print('_deferredReload');
if (!Scrollable.recommendDeferredLoadingForContext(context)) {
_frameCallbackInProgress = false;
_doReload(-1);
} else {
SchedulerBinding.instance.scheduleFrameCallback((d) => _deferredReload(context), rescheduling: true);
}
}
void _doReload(int index) {
print('reload $index');
if (this.mounted) {
setState(() {});
}
}
}
class PageResult<T> {
/// Page index of this data.
final int index;
final List<T> items;
PageResult(this.index, this.items);
}
class _LazyListViewPhysics extends AlwaysScrollableScrollPhysics {
final double velocityThreshold;
_LazyListViewPhysics({
#required this.velocityThreshold,
ScrollPhysics parent,
}) : super(parent: parent);
#override
recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
// print('velocityThreshold: $velocityThreshold');
return velocity.abs() > velocityThreshold;
}
#override
_LazyListViewPhysics applyTo(ScrollPhysics ancestor) {
// print('applyTo($ancestor)');
return _LazyListViewPhysics(velocityThreshold: velocityThreshold, parent: buildParent(ancestor));
}
}
Anyone with a better idea?
I'm brand new to Flutter / Dart and I'm trying to build a reusable infinite scroller with placeholder loading. The class is as follows:
import 'dart:async';
import 'package:flutter/material.dart';
class PagedScroller<T> extends StatefulWidget {
final int limit;
final Future<List<T>> Function(int, int) getDataFunction;
final Widget Function(T) renderFunction;
final Widget Function() renderPlaceholderFunction;
PagedScroller(
{#required this.limit,
#required this.getDataFunction,
#required this.renderFunction,
#required this.renderPlaceholderFunction});
#override
_PagedScrollerState<T> createState() => _PagedScrollerState<T>();
}
class _PagedScrollerState<T> extends State<PagedScroller> {
int _offset = 0;
int _lastDataLength = 1; // Init to one so the first call can happen
List<dynamic> _items = [];
Future<List<dynamic>> _future;
bool _isInitializing = false;
bool _isInitialized = false;
bool _isLoading = false;
ScrollController _controller =
ScrollController(initialScrollOffset: 0.0, keepScrollOffset: true);
_PagedScrollerState();
void _init() {
_isInitializing = true;
_reset();
_controller.addListener(() {
bool loadMore = false;
if (_controller.position.maxScrollExtent == double.infinity) {
loadMore = _controller.offset == _controller.position.maxScrollExtent;
} else {
loadMore =
_controller.offset >= _controller.position.maxScrollExtent * 0.85;
}
// Only load more if it's not currently loading and we're not on the last page
// _lastDataLength should be 0 if there are no more pages
if (loadMore && !_isLoading && _lastDataLength > 0) {
_offset += widget.limit;
_load();
}
});
_load();
_isInitializing = false;
_isInitialized = true;
}
void _reset() {
// Clear things array and reset inital get-things link (without paging)
setState(() {
_future = _clearThings();
});
// Reload things
// Reset to initial GET link
_offset = 0;
}
void _load() {
setState(() {
_future = _loadPlaceholders();
_future = _loadData();
});
}
Future<List<dynamic>> _clearThings() async {
_items.clear();
return Future.value(_items);
}
Future<List<dynamic>> _loadPlaceholders() async {
// Add 20 empty placeholders to represent stuff that's currently loading
for (var i = 0; i < widget.limit; i++) {
_items.add(_Placeholder());
}
return Future.value(_items);
}
List<dynamic> _getInitialPlaceholders() {
var placeholders = List<dynamic>();
for (var i = 0; i < widget.limit; i++) {
placeholders.add(_Placeholder());
}
return placeholders;
}
Future<List<dynamic>> _loadData() async {
_setLoading(true);
var data = await widget.getDataFunction(widget.limit, _offset);
// When loading data is done, remove any placeholders
_items.removeWhere((item) => item is _Placeholder);
// If 0 items were returned, it's probably the last page
_lastDataLength = data.length;
for (var item in data) {
_items.add(item);
}
_setLoading(false);
return Future.value(_items);
}
void _setLoading(bool isLoading) {
if (!mounted) {
return;
}
setState(() {
_isLoading = isLoading;
});
}
Future<void> _refreshThings() async {
_reset();
_load();
return Future;
}
#override
Widget build(BuildContext context) {
if (!_isInitializing && !_isInitialized) {
_init();
}
return FutureBuilder(
future: _future,
initialData: _getInitialPlaceholders(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
List<dynamic> loadedItems = snapshot.data;
return RefreshIndicator(
onRefresh: _refreshThings,
child: ListView.builder(
itemCount: loadedItems.length,
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
var item = loadedItems[index];
if (item is _Placeholder) {
return widget.renderPlaceholderFunction();
} else if (item is T) {
// THIS IS THE LINE THAT FAILS
return widget.renderFunction(item);
}
return Text('Unknown item type');
},
),
);
}
return Container();
},
);
}
}
class _Placeholder {}
The line that fails above:
return widget.renderFunction(item);
Fails with the following:
type '(MyModel) => Widget' is not a subtype of type '(dynamic) => Widget'
I understand why this is happening. The compiler can't know that type T from my PagedScroller<T> is the same as type T from _PagedScrollerState<T>. As a result, Dart tries to be helpful and converts my callback function of type Widget Function(T) to Widget Function(dynamic).
I then figured "maybe I can fake it out" with the following since I know the T in PagedScroller<T> and _PagedScrollerState<T> are always the same:
var renderFunction = widget.renderFunction as Widget Function(T);
return renderFunction(item);
Interestingly, this gives me a warning:
Unnecessary cast.
Try removing the cast.
Yet it won't even run that line (crashes) with the following:
Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
https://github.com/flutter/flutter/issues/new?template=BUG.md
Changing everything to dynamic works a charm, but I really don't want to lose the readability of generics here if I don't have to.
Despite extensive searching, I can't find the equivalent of C#'s Convert.ChangeType where you can provide types at runtime so I can just do the cast I want and be done with it.
This seems like a really simple thing to achieve, but I'm stuck.
You can consume the scroller with this simple main.dart copy/pasted:
import 'package:flutter/material.dart';
import 'package:minimal_repros/paged_scroller.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: 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> {
#override
Widget build(BuildContext context) {
Future<List<MyModel>> getDataFunction(int limit, int offset) async {
var myModels = List<MyModel>();
// Simulate API call
await Future.delayed(Duration(milliseconds: 1000));
for (int i = 0; i < limit; i++) {
var myModel = MyModel();
myModel.count = i + offset;
myModel.firstName = 'Bob';
myModels.add(myModel);
}
return myModels;
}
Widget renderFunction(MyModel myModel) {
return Text(myModel.firstName);
}
Widget renderPlaceholderFunction() {
return Text('Loading');
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PagedScroller(
getDataFunction: getDataFunction,
renderFunction: renderFunction,
renderPlaceholderFunction: renderPlaceholderFunction,
limit: 20));
}
}
class MyModel {
int count;
String firstName;
}
In the declaration of your State class, you forgot to specify the generic parameter of the widget.
Instead of:
class _PagedScrollerState<T> extends State<PagedScroller> {
do:
class _PagedScrollerState<T> extends State<PagedScroller<T>> {
I am trying to implement a Column with a Text:
Column(
children: <Widget>[
Text('data from future function')
],
),
I can't get the data from initState() cause initState() it's only void
If I get data directly from the function
Text(function)
I get
instance of function
The function:
Future<double> calculate(int index) async {
LocData _getUser = await getLoc();
double uLat = _getUser.latitude;
double uLng = _getUser.latitude;
double pLat = parks[data].loc.lat;
double pLng = parks[data].loc.lng;
double dis = await Geolocator()
.distanceBetween(uLat , uLng, uLng , pLat );
return dis ;
}
Any idea what can i do to get this data from the function directly to the text wigdet?
There 2 ways to get data from a future.
Option #1:
(with 2 suboptions)
class MyWidgetState extends State<MyWidget> {
String _someAsyncData;
#override
void initState() {
super.initState();
// opt 1.
aDataFuture.then((val) {
setState(() {
_someAsyncdata = val;
});
});
// opt 2.
_setAsyncData(aDataFuture);
}
void _setAsyncData(Future<String> someFuture) async {
// void + async is considered a "fire and forget" call
// part of opt. 2
_someAsyncData = await someFuture;
// must trigger rebuild with setState
setState((){});
}
Widget build(BuildContext context) {
return _someAsyncData == null ? Container() : Text('$_someAsyncData');
}
}
Option #2
Use FutureBuilder
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _someFuture,
builder: (ctx, snapshot) {
// can also check for snapshot.hasData or snapshot.hasError to
// provide more user feedback eg.
if(snapshot.connectionState == ConnectionState.done)
return Text('${snapshot.data}');
return Text('No data available yet...');
}
);
}
}
Here is the full working code.
class _InfoPageState extends State<InfoPage> {
String _text = "";
#override
void initState() {
super.initState();
calculate(10).then((value) {
setState(() {
_text = value.toString();
});
});
}
Future<double> calculate(int index) async {
LocData _getUser = await getLoc();
double uLat = _getUser.latitude;
double uLng = _getUser.latitude;
double pLat = parks[data].loc.lat;
double pLng = parks[data].loc.lng;
double dis = await Geolocator().distanceBetween(uLat, userLng, uLng, pLat);
return dis;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(children: <Widget>[Text(_text)]),
);
}
}