I have a local JSON file with a 1000 list. How to apply the Lazy Loading or scrollController function in the Listview.builder?
P/S: I have tried this Flutter ListView lazy loading, but I don't know how to modify it. For example: List items = List.generate(100, (position) => 'Hello $position'); how to change this part to the data that loaded from local JSON file?
Below is post.json file
{
"Food": [
{
"name": "Cake",
"id": "0001",
"description": [
{
"category": "AAA",
"Size": "Regular",
"topping": "Chocolate with Sprinkles"
},
{
"category": "AAA",
"Size": "Small",
"topping": "Chocolate only"
},
{
"category": "BBB",
"Size": "Regular",
"topping": "Maple"
},
{
"category": "BBB",
"Size": "Small",
"topping": "Blueberry"
},
{
"category": "CCC",
"Size": "Medium",
"topping": "Strawberry"
},
{
"category": "CCC",
"Size": "small",
"topping": "banana"
},
{
....another 1000 list
}
]
},
{
"name": "Raised",
"id": "0002",
"description": ... // same structure as above
}
]
}
Below is the Main file
void main() {
runApp(const MyApp());
}
// Load local Json file
Future<Post> getFoodList () async {
final getResponse = await rootBundle.loadString('assets/post.json');
var data = jsonDecode(getResponse);
return Post.fromJson(data);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: FutureBuilder<Post>(
future: getFoodList(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var getFoodName = snapshot.data!.food;
return SingleChildScrollView(
child: Wrap(
children: [
for (final food in getFoodName!)
GestureDetector(
child: Text(foodName.name.toString()),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => foodDetail(food: food)));
},
),
],
),
);
} else {
return Text('Loading');
}
},
),
),
],
),
);
}
}
Below is the food detail page. As there will be a 1000 lists of food details, how do I apply lazy loading, scrollController?
class FoodDetail extends StatefulWidget {
final Food food;
const FoodDetail({super.key, required this.food});
#override
State<FoodDetail> createState() => _FoodDetailState();
}
class _FoodDetailState extends State<FoodDetail> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
children: [
ListView.builder(
itemCount: widget.food.description!.length,
itemBuilder: (context, index) {
return Column(
children: [
Text(widget.food.description![index].category.toString())
Text(widget.food.description![index].topping.toString())
],
);
}
),
],
),
)));
}
}
Related
How to apply Lazy Loading or other better option to load more than 5000 of topping list in the Listview.builder inside another Listview.builder?
Below is post.json which will be loaded from local assets
{
"Food": [
{
"name": "Cake",
"id": "0001",
"description": [
{
"category": "AAA",
"Size": "Regular",
"topping": []
},
{
"category": "BBB",
"Size": "Small",
"topping": []
},
{
"category": "CCC",
"Size": "Medium",
"topping": [
{
"ingredient": "Chocolate with Sprinkles",
"price": "$70"
},
{
"ingredient": "Maple",
"price": "$99"
},
{
"ingredient": "Blueberry",
"price": "$123"
}, ... // more than 5000 of topping list
]
}
]
},
{
"name": "Raised",
"id": "0002",
"description": ... // same structure as above
}
]
}
Below is the Main file to display List of foods
void main() {
runApp(const MyApp());
}
// Load local Json file
Future<Post> getFoodList() => Future.delayed(Duration(seconds: 1), () async {
final getResponse = await rootBundle.loadString('assets/post.json');
var data = jsonDecode(getResponse);
return Post.fromJson(data);
});
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: FutureBuilder<Post>(
future: getFoodList(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var getFoodName = snapshot.data!.food;
return SingleChildScrollView(
child: Wrap(
children: [
for (final food in getFoodName!)
GestureDetector(
child: Text(foodName.name.toString()),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => foodDetail(food: food)));
},
),
],
),
);
} else {
return Text('Loading');
}
},
),
),
],
),
);
}
}
Food Detail page - display more than 5000 of topping list
class FoodDetail extends StatefulWidget {
final Food food;
const FoodDetail({super.key, required this.food});
#override
State<FoodDetail> createState() => _FoodDetailState();
}
class _FoodDetailState extends State<FoodDetail> {
late ScrollController controller;
// This example is taken from Flutter ListView lazy loading
List<String> items = List.generate(100, (position) => 'Hello $position');
#override
void initState() {
super.initState();
controller = ScrollController()..addListener(_scrollListener);
}
#override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
children: [
// This is the first ListView.build to display category and size under description
ListView.builder(
itemCount: widget.food.description!.length,
itemBuilder: (context, index) {
// IF category is AAA || BBB, display the size
if (widget.food.description![index].category == "AAA" || widget.food.description![index].category == "BBB") {
return Text(widget.food.description![index].size.toString());
}
// IF category is CCC display the List of Topping (Note: this topping list is more than 5000)
else {
// This is the second ListView.build to display the topping information
// How do I apply ScollController or Lazy Loading to load 5000 list of topping?
return ListView.builder(
controller: controller,
itemCount: widget.food.description![index].topping!.length,
itemBuilder: (context, position) {
return Column(
children: [
Text(widget.food.description![index].topping![position].ingredient.toString())
Text(widget.food.description![index].topping![position].price.toString())
],
);
});
}
}),
],
),
)));
}
// This example is taken from Flutter ListView lazy loading
void _scrollListener() {
if (controller.position.extentAfter < 500) {
setState(() {
items.addAll(List.generate(42, (index) => 'Inserted $index'));
});
}
}
}
Output of the workflow:
I think your best bet is to try to try to refactor it in a way to use only a single ListView.builder. To do this you need to flatten the data you are working with in some way. Here is an example that also uses nested data that transforms it in a way so a single ListView displays it. I'm not sure if it's the best way to do it but I think you could do something similar for your code maybe. This might give you an idea of how to do it:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
final data = const [
{
"name": "A",
"subnames": ["A1", "A2", "A3", "A4", "A5"]
},
{
"name": "B",
"subnames": ["B1", "B2", "B3", "B4", "B5"]
},
{
"name": "C",
"subnames": ["C1", "C2", "C3", "C4", "C5"]
},
];
const MyApp({super.key});
#override
Widget build(BuildContext context) {
final newData = data
.map((e) => [
{"type": 1, "name": e["name"]},
for (final name in (e["subnames"] as List))
{"type": 2, "name": name}
])
.expand((e) => e.toList())
.toList();
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
itemCount: newData.length,
itemBuilder: (context, index) {
if (newData[index]['type'] == 1) {
return Text(
newData[index]['name'],
style: const TextStyle(color: Colors.red),
);
} else {
return Text(
newData[index]['name'],
style: const TextStyle(color: Colors.blue),
);
}
}),
)),
);
}
}
Output:
I have a flutter code which runs fine, but at the same time it throws Unexpected null value error in debug console. Can someone please help why this is happening.
import 'package:flutter/material.dart';
import 'dart:convert';
//add this library to get data from the internet
import 'package:http/http.dart' as http;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _jsonString =
'{ "count": 7, "result": [ { "iconId": 1, "id": 1, "name": "Kitchen", "timestamp": 1586951631 }, { "iconId": 2, "id": 2, "name": "android", "timestamp": 1586951646 }, { "iconId": 3, "id": 3, "name": "mobile", "timestamp": 1586951654 }, { "iconId": 4, "id": 4, "name": "bathroom", "timestamp": 1586951665 }, { "iconId": 5, "id": 5, "name": "parking", "timestamp": 1586974393 }, { "iconId": 6, "id": 6, "name": "theatre", "timestamp": 1586974429 }, { "iconId": 7, "id": 7, "name": "bedroom", "timestamp": 1586974457 } ] }';
Future<String> _getDataFromWeb() async {
// http.Response response = await http.get(
// Uri.parse("http://localhost/api/Ticket/GetTickets?username=myuser"),
// headers: {
// 'Content-Type': 'application/json',
// 'Accept': 'application/json'
// },
// );
// if (response.statusCode == 200) {
// // If you are sure that your web service has json string, return it directly
// return response.body;
// } else {
// // create a fake response against any stuation that the data couldn't fetch from the web
// return _jsonString;
// }
return _jsonString;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Your App Title"),
),
body: FutureBuilder<String>(
future: _getDataFromWeb(),
builder: (context, snapshot) {
Map jsonMap = json.decode(snapshot.data!);
return GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: jsonMap['count'],
itemBuilder: (BuildContext c, int i) {
Map resultItem = jsonMap['result'][i];
return Card(
child: Center(child: Text("${resultItem['name']}")),
);
},
);
},
),
);
}
}
Please review the above code and help me find out the problem. A runtime null is generated from somewhere, and it is throwing repeated errors in console. You can run the above code and see if the problem happens.
Loading future takes some time, you need to await until the data is ready,
builder: (context, snapshot) {
if(snapshot.hasData){
Map jsonMap = json.decode(snapshot.data!);
return GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: jsonMap['count'],
itemBuilder: (BuildContext c, int i) {
Map resultItem = jsonMap['result'][i];
return Card(
child: Center(child: Text("${resultItem['name']}")),
);
},
);}
return CircularProgressIndicator();
}
Highly recommend checking this doc example handling error and other states.
in one of our mobile application we have a simple screen which we want to make TabBarView pages via result of server response, for example if server response this collection:
{
"categories": [
{
/* has 3 children */
"category_item": {
"id": 1,
"title": "sample"
},
"products": [
{
"id": 1,
"title": "sample 1"
},
{
"id": 2,
"title": "sample 1"
},
{
"id": 3,
"title": "sample 1"
}
]
},
/* has 2 children */
{
"category_item": {
"id": 2,
"title": "test"
},
"products": [
{
"id": 1,
"title": "test 1"
},
{
"id": 2,
"title": "test 1"
}
]
}
]
}
we should have 2 Tabs as sample and test with some children inside TabBarView relatives with parent category, for example first index of array has 3 children and second index of array has 2 children.
our problem is TabController that when we retrieve data from server, screen has been built and we can't manage this action. I mean this tab and TabBarView children should be built with response data. basically time I passed zero to TabController because server doesn't response in this time and after getting data I get error on length of that,
class SelectClothesScreenState extends State<SelectClothesScreen> with SingleTickerProviderStateMixin {
TabController _tabController;
CategoryProducts _cats;
List<Products> _productList = [];
#override
void initState() {
super.initState();
_getCats();
}
bool _isLoading = true;
_getCats() async {
final CategoryProducts response = await Api.apiCatList();
List<Products> _p = [];
setState(() {
_cats = response;
_productList = _p;
_isLoading = false;
});
_tabController = TabController(length: _cats == null ? 0 : _cats.categories.length ?? 0, vsync: this);
}
Widget loadingView() {
return CircularProgressIndicator();
}
#override
Widget build(BuildContext context) {
var fullWidth = MediaQuery.of(context).size.width;
return Scaffold(
resizeToAvoidBottomInset: false,
body: Column(
children: [
//address and search icon and close icon
Container(
height: 50,
child: _cats != null && _isLoading
? loadingView()
: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.all(8),
itemCount: _cats == null ? 0 : _cats.categories.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
print(_cats.categories.length);
return GestureDetector(
onTap: () {
print(index);
_tabController.animateTo(index, duration: Duration(milliseconds: 750), curve: Curves.ease);
},
child: CatItem(catView: _cats.categories[index].categoryItem),
);
},
),
),
//content grid list product and btn confirm
Container(
width: fullWidth,
child: TabBarView(controller: _tabController, children: [
//grid product list
...List.generate(_cats == null ? 0 : _cats.categories.length, (index) {
return Container(
width: fullWidth,
child: GridView.builder(
shrinkWrap: true,
itemCount: _productList.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (BuildContext context, int index) {
return Container(color: Colors.black);
//return ProductView(productView: _productList[index]);
},
),
);
}),
]),
),
],
));
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
}
how can I set correct implementation for length of TabController after retrieving data from server? thanks in advance
I have 2 JSON cases as follows
Case 1: one value of "country" is null
[
{
"id": 1,
"continent": "North America",
"country": "United States"
},
{
"id": 2,
"continent": "Europe",
"country": "Germany"
},
{
"id": 3,
"continent": "Asia",
"country": null
}
]
Case 2: one of "country" has no value
[
{
"id": 1,
"continent": "North America",
"country": "United States"
},
{
"id": 2,
"continent": "Europe",
"country": "Germany"
},
{
"id": 3,
"continent": "Asia"
}
]
I use TableRow to show "continent" and "country" in Table,
TableRow(children: [
TableCell(child: Text(continent.continent)),
TableCell(child: Text(continent.country))])
But in case there isn't "country" in List or continent.country == null => I don't want to show that TableRow, so please help me set the conditions for:
Case 1
Case 2
Case 1 + Case 2
This is the main file:
import 'package:ask/services/continent_services2.dart';
import 'package:flutter/material.dart';
import 'model/continent_model2.dart';
class TableRowDemo extends StatefulWidget {
TableRowDemo() : super();
#override
_ContinentPageState createState() => _ContinentPageState();
}
class _ContinentPageState extends State<TableRowDemo> {
List<Continent> _continent;
#override
void initState() {
super.initState();
ContinentServices2.getContinent().then((continents) {
setState(() {
_continent = continents;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('')),
body: Column(
children: <Widget>[
for (Continent continent in _continent)
SingleChildScrollView(
child: Table(children: [
continent.country == null
? Container() // Error: type 'Container' is not a subtype of type 'TableRow'
: TableRow(children: [
TableCell(child: Text(continent.continent)),
TableCell(child: Text(continent.country))]),
]),
),
],
),
);
}
}
You should rename continent to 'zone' or 'area' and you can use a simple if condition:
(...)
Table(children: [
for (Zone zone in zones)
If (zone.country != null )
TableRow(children[
TableCell(child: Text(zone.continent)),
TableCell(child: Text(zone.country)),
])
)
If you upgraded to latest Flutter SDK (Dart >=2.3) then you can you can use simple if statement to conditionally rendering TableRow or any other widget.
...
If(continent.country != null)
TableRow(children: [
TableCell(child: Text(continent.continent)),
TableCell(child: Text(continent.country))]),
]
Here is the full example with implementation of filtering:
import 'package:flutter/material.dart';
void main() {
runApp(TableExample());
}
class TableExample extends StatefulWidget {
#override
_TableExampleState createState() => _TableExampleState();
}
class _TableExampleState extends State<TableExample> {
List continents = [
{"id": 1, "continent": "North America", "country": "United States"},
{"id": 2, "continent": "Europe", "country": "Germany"},
{"id": 3, "continent": "Asia", "country": null}
];
List _continents = [];
#override
void initState() {
super.initState();
// filtering of data
setState(() {
_continents =
continents.where((element) => element["country"] != null).toList();
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Table Example"),
),
body: Column(
children: <Widget>[
for (Map<String, dynamic> continent in _continents)
SingleChildScrollView(
child: Table(children: [
TableRow(children: [
TableCell(child: Text(continent["continent"])),
TableCell(child: Text(continent["country"]))
]),
]),
),
],
),
));
}
}
Output:
How do I populate this JSON list into dropdown button?
{
"status": true,
"message": "success",
"data": {
"list": [
{
"idattribute": "2",
"attrName": "BBQ"
},
{
"idattribute": "1",
"attrName": "FRUIT JUICE"
}
]
}
}
class _YourPageState extends State<YourPage> {
Map yourJson = {
"status": true,
"message": "success",
"data": {
"list": [
{"idattribute": "2", "attrName": "BBQ"},
{"idattribute": "1", "attrName": "FRUIT JUICE"}
]
}
};
int _value = 1;
List<DropdownMenuItem<int>> _menuItems;
#override
void initState() {
super.initState();
List dataList = yourJson["data"]["list"];
_menuItems = List.generate(
dataList.length,
(i) => DropdownMenuItem(
value: int.parse(dataList[i]["idattribute"]),
child: Text("${dataList[i]["attrName"]}"),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: DropdownButton<int>(
items: _menuItems,
value: _value,
onChanged: (value) => setState(() => _value = value),
),
),
);
}
}