Hello. I been working on a widget for a infinity listview base on LoadMore (https://pub.dev/packages/loadmore). I am using Symfony 5 and API platform as a source of data, and also using vnd.api+json format. EUREKA!!!! 03/08/2022 Current limitations: *Cant make listview refresh when a LocalState var is modify outside the scope of the custom widget. ///////////////////////////// V2 FF API CALLS ////////////////////////// I manage to get it to work with in a custom widget and using the own FF code for API CALLS! NO MORE GITHUB AND LOCAL NONSENS 🙂 I'll post the code and explain step by step. First, is important to check the exclude checkbox.
[image.png][image.png]
V2 CODE (COMMENTED)
//Add imports for cutom widget to work with FF own code
import '../../video_detail/video_detail_widget.dart';
import '../../backend/api_requests/api_calls.dart';
import '../../directorio/directorio_widget.dart';
import '../../entrada_detail/entrada_detail_widget.dart';
import 'dart:async';
import 'package:flutter/scheduler.dart';
import '../../flutter_flow/flutter_flow_animations.dart';//Had to readd this import, custom widget add it automatically but it does not add the as functions part.//Some lines of code se this alias and you could just delete this line and remove the alias references in//code calls, using only the name of the function. But im lazy and i want to keep code as FF intendet to be used.
import '../../flutter_flow/custom_functions.dart' as functions;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:google_fonts/google_fonts.dart';
// Begin custom widget code imports, remember to add dependecies
import 'package:loadmore/loadmore.dart';
import 'dart:convert' as convert;
import 'package:http/http.dart' as http;//No changes here, only if you add or remove parameters or change names.
class InfinityeListView extends StatefulWidget {
const InfinityeListView({
Key key,
this.width,
this.height,
}) : super(key: key);
final double width;
final double height;
@override
_InfinityeListViewState createState() => _InfinityeListViewState();
}//This next code I mostly copy paste from a homepage widget. I just ajust it to work inside custom widget.//Thats why i added all those imports above.
class _InfinityeListViewState extends State
with TickerProviderStateMixin {
ApiCallResponse meta;
Completer _apiRequestCompleter;
final animationsMap = {
'listViewOnPageLoadAnimation': AnimationInfo(
trigger: AnimationTrigger.onPageLoad,
duration: 600,
hideBeforeAnimating: true,
fadeIn: true,
initialState: AnimationState(
offset: Offset(0, 0),
scale: 1,
opacity: 0,
),
finalState: AnimationState(
offset: Offset(0, 0),
scale: 1,
opacity: 1,
),
),
};
///Added local variables for use in my custom code.
static var itemCount = 0;
//List of instances, used to load the list with items. These are requested from API below.
List entradas = [];
//The page being fetch from API. Using API platform in my case and i get pages.
int page = 1;
//item count for the loadmore, this will trigger the comparation for loadmore has to fetch the next batch.
//In my case is 30 at a time.
int get count => entradas.length;
//Automatic code from FF to fetch onload page some data, I think this is not being use.
//This is in case the page where i put this widget needs this data to be fetch. I think it can be deleted.
@override
void initState() {
super.initState();
// On page load action.
SchedulerBinding.instance.addPostFrameCallback((_) async {
// API call
meta = await GetCharlasEntradasCall.call(
page: FFAppState().currentpage,
tipo: FFAppState().tipo,
);
// Update local currentpage
setState(() => FFAppState().currentpage =
functions.getCurrentPage(GetCharlasEntradasCall.meta(
(meta?.jsonBody ?? ''),
)));
// Update local total pages
setState(() => FFAppState().totalpages =
functions.getTotalPages(GetCharlasEntradasCall.meta(
(meta?.jsonBody ?? ''),
)));
///THIS IS IMPORTANT, IT WILL TRIGGER THE LOAD FUNCTION TO REQUEST THE API ON PAGE LOAD FOR THE
//FIRST TIME.
await load();
});
print('initload');
startPageLoadAnimations(
animationsMap.values
.where((anim) => anim.trigger == AnimationTrigger.onPageLoad),
this,
);
}
//ADDED LOAD MORE WIDGET, IF YOU NEED MORE INFOR, GO TO PUB.DEV AND SEARCH LOADMORE, IN THIS PART YOU COULD COPY AND PASTE CODE FROM A FF PAGE WIDGET CREATED WITH AUTOMATIC CODE IN FF EDITOR INTO HERE AND JUST ADD LOADMORE WIDGET WHERE YOU NEED IT. IT TAKE SOME TIME BUT IS A TIME AND LIVE SAVER BECAUSE YOUR REUSING CODE AND YOU DONT HAVE TO WRITE IT AGAIN, WHAT I DO IS A CREATE A TEMP PAGE AND BUILD THE LAYOUT AND THE FUNCTIONALITY WITH A NORMAL LISTVIEW, THEN COPY AND PASTE THE NEEDED CODE HERE AND ADD THE LOADMORE WIDGET AND METHODS (SEE DOCS) AND IT SHOULD BE PLUG AND PLAY IN THEORY.
@override
Widget build(BuildContext context) {
return Container(
child: RefreshIndicator(
child: LoadMore(
textBuilder: SpanishLoadMoreTextBuilder.spanish,
//IS IT FINISH LOADING CONDITION
isFinish: count >= itemCount,
//WHEN IT NEED TO LOAD MORE ITEMS IT RUNS THIS FUNCTION
onLoadMore: _loadMore,
whenEmptyLoad: false,
child: ListView.builder(
padding: EdgeInsets.zero,
scrollDirection: Axis.vertical,
itemCount: count,
itemBuilder: (context, entradasIndex) {
final entradasItem = entradas[entradasIndex];
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Divider(
thickness: 1,
color: Color(0xFF9E9E9E),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(0, 5, 0, 5),
child: InkWell(
onTap: () async {
if ('Video' ==
getJsonField(
entradasItem,
r'''$.attributes.tipo''',
)) {
// Navigate Video
await Navigator.push(
context,
PageTransition(
type: PageTransitionType.fade,
duration: Duration(milliseconds: 300),
reverseDuration: Duration(milliseconds: 300),
child: VideoDetailWidget(
titulo: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.titulo''',
).toString(),
'NO HAY',
),
descripcion: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.descripcion''',
).toString(),
'NO HAY',
),
texto: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.texto''',
).toString(),
'NO HAY',
),
image: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.image''',
).toString(),
'NO HAY',
),
images: getJsonField(
entradasItem,
r'''$.attributes.imagenes''',
),
autor: getJsonField(
entradasItem,
r'''$.attributes.autor.nombre''',
).toString(),
tipo: getJsonField(
entradasItem,
r'''$.attributes.tipo''',
).toString(),
ciudad: getJsonField(
entradasItem,
r'''$.attributes.ciudad.nombre''',
).toString(),
fechacaptura: getJsonField(
entradasItem,
r'''$.attributes.fechacaptura''',
).toString(),
video: getJsonField(
entradasItem,
r'''$.attributes.video''',
).toString(),
),
),
);
return;
} else {
// Navigate Entrada
await Navigator.push(
context,
PageTransition(
type: PageTransitionType.fade,
duration: Duration(milliseconds: 300),
reverseDuration: Duration(milliseconds: 300),
child: EntradaDetailWidget(
titulo: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.titulo''',
).toString(),
'NO HAY',
),
descripcion: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.descripcion''',
).toString(),
'NO HAY',
),
texto: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.texto''',
).toString(),
'NO HAY',
),
image: valueOrDefault(
getJsonField(
entradasItem,
r'''$.attributes.image''',
).toString(),
'NO HAY',
),
images: getJsonField(
entradasItem,
r'''$.attributes.imagenes''',
),
autor: getJsonField(
entradasItem,
r'''$.attributes.autor.nombre''',
).toString(),
tipo: getJsonField(
entradasItem,
r'''$.attributes.tipo''',
).toString(),
ciudad: getJsonField(
entradasItem,
r'''$.attributes.ciudad.nombre''',
).toString(),
fechacaptura: getJsonField(
entradasItem,
r'''$.attributes.fechacaptura''',
).toString(),
videourl: getJsonField(
entradasItem,
r'''$.attributes.archivovideo''',
).toString(),
visitas: getJsonField(
entradasItem,
r'''$.attributes.visitas''',
),
),
),
);
return;
}
},
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: MediaQuery.of(context).size.width * 0.45,
height: 100,
decoration: BoxDecoration(
color: Color(0xFFEEEEEE),
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Align(
alignment: AlignmentDirectional(-0.9, 0),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
2, 0, 0, 0),
child: ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(0),
topLeft: Radius.circular(8),
topRight: Radius.circular(0),
),
child: Image.network(
functions.getImageUrl(getJsonField(
entradasItem,
r'''$.attributes.image''',
).toString()),
width:
MediaQuery.of(context).size.width,
height: 100,
fit: BoxFit.cover,
),
),
),
),
],
),
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(5, 0, 0, 0),
child: Container(
width: MediaQuery.of(context).size.width * 0.53,
decoration: BoxDecoration(
color: Color(0xFFEEEEEE),
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getJsonField(
entradasItem,
r'''$.attributes.titulo''',
).toString(),
textAlign: TextAlign.start,
style: FlutterFlowTheme.of(context)
.bodyText1,
),
],
),
),
),
],
),
),
),
],
);
},
),
).animated([animationsMap['listViewOnPageLoadAnimation']]),
onRefresh: _refresh),
);
}
//LOAD MORE FUNCTION ASYNC
Future _loadMore() async {
print("onLoadMore");
await Future.delayed(Duration(seconds: 0, milliseconds: 2000));
//LOAD API FUNCTION
load();
return true;
}
// REFRESHER FUNCTION
Future _refresh() async {
print("refresh");
await Future.delayed(Duration(seconds: 0, milliseconds: 2000));
//LOAD API FUNCTION
load();
}
///Load method, it is use to query when List at the loadpage and when it reach the end of current items (30 per page)
Future load() async {
print("load-INICIO");
print(page);
print(count);
print(itemCount);
/// FF API call found on ../../backend/api_requests/api_calls.dart
// If you need more context on how this works, review code inside this file.
//when you setup an api call, this file adds all the functions you need to call the API
//It would be in theory a matter fo change name of function and parameters names and values
//for another api call configuration.
var entradaListaViewGetCharlasEntradasResponse =
await GetCharlasEntradasCall.call(
page: page,
tipo: FFAppState().tipo,
);
//THIS IS IMPORTANT TO AVOID EMPTY SCREEN AND THEN CLICK ON IT TO SHOW ITEMS.
//IT TELLS FLUTTER WHEN ARE CHANGES IN ITEMS AND RELOADS THE WIDGET
setState(() {
//CHANGES OF LOCAL VARIABLES, IT GETS IT FROM API RESPONSE
entradas.addAll(GetCharlasEntradasCall.entradas(
entradaListaViewGetCharlasEntradasResponse.jsonBody,
).toList());
itemCount = GetCharlasEntradasCall.meta(
entradaListaViewGetCharlasEntradasResponse.jsonBody,
)['totalItems'];
//NEXT PAGE
page++;
});
print("load-FIN");
print(page);
print(count);
print(itemCount);
//print(entradas);
}
}/// TEXT FOR LOADMORE, SEE LOADMORE DOCS, THIS IS FOR LOCAL TRANSLATIONS OF LOAD MORE TEXTSString _buildSpanihsText(LoadMoreStatus status) {
String text;
switch (status) {
case LoadMoreStatus.fail:
text = "Error";
break;
case LoadMoreStatus.idle:
text = "Terminado";
break;
case LoadMoreStatus.loading:
text = "Cargando...";
break;
case LoadMoreStatus.nomore:
text = "Fin";
break;
default:
text = "";
}
return text;
}/// loadmore textbuilder properties, this and the above code works together.class SpanishLoadMoreTextBuilder {
static const LoadMoreTextBuilder spanish = _buildSpanihsText;
}Any improvements are welcome. Thanks.
//////////////////////////////////// V1 HTTP ///////////////////////////////////
I manage to make it work, but, the problem is when It loads the first time the screen is empty. I have to click on the screen to make items visible. I manage to figure out it has something to do with the async and await part of the request. It loads the screen before the data can be fetch.
So is still a work in progress. Any advise and suggestion on how to fix it would be welcome. I leave the code an a demo video.
[image.png][Grabando #3.gif]
Pay no attention to errors. Those are from images sources.
// Automatic FlutterFlow imports
import '../../flutter_flow/flutter_flow_theme.dart';
import '../../flutter_flow/flutter_flow_util.dart';
import 'index.dart'; // Imports other custom widgets
import '../actions/index.dart'; // Imports custom actions
import '../../flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// Automatic FlutterFlow imports
import 'package:cached_network_image/cached_network_image.dart';
import 'package:google_fonts/google_fonts.dart';
// Begin custom widget code
import 'package:loadmore/loadmore.dart';
import 'dart:convert' as convert;
import 'package:http/http.dart' as http;///THIS WIDGETS CONSULT API PLATFORM FROM A SYMFONY 5 WEB APPLICATION.class ListViewInfinite extends StatefulWidget {
const ListViewInfinite({
Key key,
this.width,
this.height,
this.title,
this.tipo = "Noticia",
this.page = 1,
}) : super(key: key);
final double width;
final double height;
final String title;
final String tipo;
final int page;
@override
_ListViewInfiniteState createState() => _ListViewInfiniteState();
}
class _ListViewInfiniteState extends State {
List list = [];
static var itemCount = 0;
int page = 1;
int get count => list.length;
///INITIAL METOD AND REQUEST FOR API RESULTS
void initState() {
super.initState();
// Request function to access API
requestAPI(widget.page);
print('Number of ITEMS in http: $itemCount.');
print('Number of ITEMS in List: $count.');
}
///Load method, it is use to query when List reach end of current items (30 per page)
void load() {
print("load");
print("count" + count.toString());
setState(() {
//Current page + 1
page++;
requestAPI(page);
});
}
/// API CALL BLOCK ///
requestAPI(int page) async {
/// Request method
var url = Uri.http('127.0.0.1:8000', '/api/entradas',
{'page': page.toString(), 'tipo': widget.tipo});
/// Await the http get response, then decode the json-formatted response.
var response =
await http.get(url, headers: {'accept': 'application/vnd.api+json'});
///JSON DECODE
var jsonResponse = convert.jsonDecode(response.body);
///CREATE A MODEL FOR DATA STORAGE
List entradas = List.from(
jsonResponse['data'].map((i) => Entrada.fromJson(i)));
print(entradas);
///DEBUG BLOCK
if (response.statusCode == 200) {
//creando lista
_ListViewInfiniteState.itemCount = jsonResponse['meta']['totalItems'];
print('Request succeded with status: ${response.statusCode}.');
} else {
print('Request failed with status: ${response.statusCode}.');
}
///POPULATE LIST WITH MODEL DATA
for (var e in entradas) {
print('Entrada:' + e.id.toString() + '.');
list.add(e);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: RefreshIndicator(
child: LoadMore(
textBuilder: SpanishLoadMoreTextBuilder.spanish,
isFinish: count >= itemCount,
onLoadMore: _loadMore,
child: ListView.builder(
scrollDirection: Axis.vertical,
itemBuilder: (BuildContext context, int index) {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(16, 8, 16, 4),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
blurRadius: 4,
color: Color(0x32000000),
offset: Offset(0, 2),
)
],
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
ClipRRect(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(0),
bottomRight: Radius.circular(0),
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: CachedNetworkImage(
imageUrl: list[index].getImageUrl(),
width: double.infinity,
height: 190,
fit: BoxFit.cover,
),
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(16, 12, 16, 8),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
list[index].titulo,
style: FlutterFlowTheme.of(context)
.title3
.override(
fontFamily: 'Outfit',
color: Color(0xFF101213),
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
Padding(
padding:
EdgeInsetsDirectional.fromSTEB(16, 0, 16, 8),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
list[index].ciudad,
style: FlutterFlowTheme.of(context)
.bodyText2
.override(
fontFamily: 'Outfit',
color: Color(0xFF57636C),
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
),
],
),
),
Container(
height: 40,
decoration: BoxDecoration(),
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(
16, 0, 24, 12),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Icon(
Icons.star_rounded,
color: Color(0xFFFFA130),
size: 24,
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
4, 0, 0, 0),
child: Text(
list[index].fechacaptura,
style: FlutterFlowTheme.of(context)
.bodyText1
.override(
fontFamily: 'Outfit',
color: Color(0xFF101213),
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
8, 0, 0, 0),
child: Text(
list[index].tipo,
style: FlutterFlowTheme.of(context)
.bodyText2
.override(
fontFamily: 'Outfit',
color: Color(0xFF57636C),
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
),
],
),
),
),
],
),
),
),
],
);
},
itemCount: count,
),
whenEmptyLoad: false,
),
onRefresh: _refresh,
),
),
);
}
Future _loadMore() async {
print("onLoadMore");
await Future.delayed(Duration(seconds: 0, milliseconds: 2000));
load();
return true;
}
Future _refresh() async {
print("refresh");
await Future.delayed(Duration(seconds: 0, milliseconds: 2000));
list.clear();
page = 1;
load();
}
}/// TEXT FOR LOADMORE, SEE LOADMORE DOCS///String _buildSpanihsText(LoadMoreStatus status) {
String text;
switch (status) {
case LoadMoreStatus.fail:
text = "Error";
break;
case LoadMoreStatus.idle:
text = "Terminado";
break;
case LoadMoreStatus.loading:
text = "Cargando...";
break;
case LoadMoreStatus.nomore:
text = "Fin";
break;
default:
text = "";
}
return text;
}/// loadmore widget propertiesclass SpanishLoadMoreTextBuilder {
static const LoadMoreTextBuilder spanish = _buildSpanihsText;
}/// DATA MODEL ///class Entrada {
int id;
String titulo;
String image;
String ciudad;
String fechacaptura;
String tipo;
Entrada({this.id, this.titulo, this.image}) {
// TODO: implement BookData
throw UnimplementedError();
}
Entrada.fromJson(Map json) {
this.id =
json['attributes']["_id"] == null ? null : json['attributes']["_id"];
this.titulo = json['attributes']["titulo"];
this.image = json['attributes']["image"];
this.ciudad = json['attributes']["ciudad"]["nombre"];
this.fechacaptura = json['attributes']["fechacaptura"];
this.tipo = json['attributes']["tipo"];
}
Map toJson() {
final Map data = new Map();
if (this.id != null) data["_id"] = this.id;
data["titulo"] = this.titulo;
data["image"] = this.image;
return data;
}
String getImageUrl() {
// Add your function code here!
if (this.image == 'null' || this.image == null || this.image == '') {
return 'http://127.0.0.1:8000/build/images/800x600.png';
} else {
return 'http://127.0.0.1:8000/images/post/' + image;
}
}
}