I'm using Flutter with the package flutter_pagewise to add lazyloading to a large image grid which loads images from local assets.
I created json file which includes all paths for the images. With this file I want to work as future source in flutter_pagewise.
The function which returns the list from json for pagewise
Future<List> getDemoImagesJson(int offset, int limit) async {
String demoImages = await DefaultAssetBundle.of(context)
.loadString("assets/demo/demo.json");
final jsonResult = json.decode(demoImages);
List images = [];
for (var i in jsonResult) {
images.add({"id": 1, "title": "sample", "thumbnailUrl": i});
}
final list = List.from(images).toList();
var listWithOffset = list.skip(offset).toList();
final finalList = listWithOffset.getRange(0, limit).toList();
return ImageModel.fromJsonList(finalList);
}
The ImageModel class (copied from official documentation)
class ImageModel {
String title;
String id;
String thumbnailUrl;
ImageModel.fromJson(obj) {
this.title = obj['title'];
this.id = obj['id'].toString();
this.thumbnailUrl = obj['thumbnailUrl'];
}
static List<ImageModel> fromJsonList(jsonList) {
return jsonList.map<ImageModel>((obj) => ImageModel.fromJson(obj)).toList();
}
}
The PageWise Widget
PagewiseGridView.count(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
pageSize: 10,
crossAxisCount: 3,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
padding: EdgeInsets.all(15.0),
itemBuilder: (context, entry, index) {
return Container(
child: Image.asset(entry.thumbnailUrl,
fit: BoxFit.cover));
},
pageFuture: (pageIndex) {
return getDemoImagesJson(pageIndex * 10, 10);
})
What I expect: I expect that the Widget loads only 10 images and after I scroll down to the end of the page it loads other 10 images...until all images did load.
What I get: The Widget loads all images at once (as batch). There is no lazyload effect after reaching the bottom of the page.
Where is the issue?
Related
I am developing an app which is showing epub content. But I want to make it responsive. I am getting all tags from epub file and adding it to page. And I calculated how much chars can get into screen and controlling will content overflow. But paragraphs has too many character than maximumum char it will adding page overflowing.
This is the code that parsing paragraphs and adding to page :
readBook() async {
ByteData data = await rootBundle.load("assets/dosya4.epub");
List<int> bookBytes =
data.buffer.asInt8List(data.offsetInBytes, data.lengthInBytes);
epubBook = await EpubReader.readBook(bookBytes);
for (var element in epubBook.Content!.Html!.values) {
dom.Document doc = parse(element.Content);
tags.addAll(doc.getElementsByTagName("p"));
}
setState(() {
String page = "";
print(widget.size.height / (2 * fontSize));
maxChar =
((widget.size.width / fontSize) * (widget.size.height / (fontSize)))
.toInt();
print(maxChar);
for (dom.Element tag in tags) {
if (maxChar < page.length + tag.innerHtml.length - 200) {
pages.add(page);
page = tag.outerHtml;
} else {
page += tag.outerHtml;
}
}
});
This is the code how i show pages :
PageView.builder(
controller: pageController,
itemCount: pages.length,
itemBuilder: (context, index) {
pages.removeWhere((element) => element.isEmpty);
return SingleChildScrollView(
child: SelectableHtml(
style: {
"body": Style(
fontSize: FontSize(fontSize),
),
"span.found": Style(
display: Display.INLINE,
backgroundColor:
const Color.fromARGB(255, 255, 255, 0)),
},
data: pages[index],
),
);
},
)
I am thinking if i add to page sentence by sentence it will be good but i thought it will cause bug or is there any package for what i want?
i'm trying to fetch a list of sub categories based on category id, for example:
Category A has 3 sub categories: A1 - A2 - A3
My backend works fine, I pass the category_id to the function and it returns me a list of sub categories having the category_id.
Since i'm new to getx, I tried passing the category_id as a route parameter but i'm not able to show the list of sub categories. In fact I didn't get how to pass the category_id while in the UI.
Here is my repo:
Future<Response> getSubCategoriesListByCategory(int categ_id) async {
return await apiClient.getData('shop/category/$categ_id');
}
Here is my controller:
List<dynamic> _subCategoriesListByCategory = [];
List<dynamic> get subCategoriesListByCategory => _subCategoriesListByCategory;
bool _isLoaded = false;
bool get isLoaded => _isLoaded;
Future<void> getSubCategoriesByCategoryId(int cat_id) async {
Response response =
await subCategoriesRepo.getSubCategoriesListByCategory(cat_id);
if (response.statusCode == 200) {
_subCategoriesListByCategory = [];
_subCategoriesListByCategory.addAll(response.body);
_isLoaded = true;
//print(categoriesList);
update(); // = setState();
} else {}
}
Here is my RouteHelper:
GetPage(
name: subCategory,
page: () {
var catId = Get.parameters['catId'];
return SubCategoriesPage(catId: int.parse(catId!));
},
transition: Transition.fadeIn),
And here is my UI:
GetBuilder<SubCategoriesController>(builder: (subCategories) {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: ScrollPhysics(),
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 90 / 100,
padding: EdgeInsets.all(16),
children: List.generate(
subCategories.subCategoriesListByCategory.length, (index) {
return _buildSingleSubCategory(
index,
SubCategoryModel.fromJson(
subCategories.subCategoriesListByCategory[index]));
}),
);
})
Code from home page where i'm passing the category_id:
onTap: () {
Get.toNamed(RouteHelper.getSubCategory(category.id));
},
I'm able to print the clicked category's id in the subs page which means it's passed correctly, also i'm getting GOING TO ROUTE /sub-category?catId=3
Noting that i'm priting the specific category_id correctly in the sub categories page, I couldn't fetch the specific data related to them. Any suggestion on how to solve this?
I'm not sure if this helps you since I haven't seen your full code, but I'm guessing you want to add this as parameter to your GetBuilder
initState: (state) => state.controller?.getSubCategoriesByCategoryId(widget.cat_id),
Solved it by adding: Get.find<SubCategoriesController().getSubCategories(widget.catId); inside the GetBuilder()
Basically, I have a set of tags done as an array in firebase and want to show them as string in flutter. Is this possible? I'm completely lost here.
I've gotten this far: but I'm not sure I understand what I'm doing here and it doesn't seem to work
class Tags {
List<dynamic>? selectedItems;
Tags fromMap(Map<String, dynamic> map) {
selectedItems =
(map[selectedItems] as List).map((item) => item as String).toList();
return this;
}
}
class TagsList extends StatelessWidget {
const TagsList({super.key});
#override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: 1,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
child: Center(child: Text('${Tags}')),
);
});
}
}
I hope that I understood your question right , You want to render the items that you got from firebase in your screen? if yes then here is a code snippet .
void getDataBaseCarouselData() async {
final data = await _firestore.collection("Carousels").get();
carouselItems = [];
for (var item in data.docs) {
carouselItems.add(CarouselItem(
title: item.data()["title"],
description: item.data()["description"],
imageUrl: item.data()["imageUrl"],
id: item.id));
}
notifyListeners();
}
.get() return a Map that you can use to get data from Objects using the tags name ["field name in firebase"] and then you can use the List of object to render them into your screen .
If I didn't answer it please provide more information so I can get it clear . Thank you
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);
});
}
}));
}`
Purpose is very simple. After getting data it is filterable by specific set of Strings. So I am initially filtering with 'all' which means showing all data and when clicking any choice chips then filtering based on that specific strings. Everything working fine except not showing all data after loading it from api call. Even if I Hot Reload again its showing the full list data. So basically adding string data in Sink is not working. I think I have done some silly mistake but couldn't figure it out. Need suggestions.
BLOC Class
final Application _application;
ProductListScreenBloc(this._application);
int totalPages = 1;
final _productList = BehaviorSubject<List<Product>>();
Observable<List<Product>> _filteredProductList = Observable.empty();
final _filterName = BehaviorSubject<String>();
Stream<List<Product>> get productList => _productList.stream;
Stream<List<Product>> get filteredProductList => _filteredProductList;
Sink<String> get filterName => _filterName;
void loadBrandWiseProductList(
String categorySlug, String brandSlug, int pageNo) {
if (totalPages >= pageNo) { //for pagination
StreamSubscription subscription = _application.productListRepository
.getBrandWiseProductList(categorySlug, brandSlug, pageNo)
.listen((ProductListResponse response) {
if (_productList.value == null) {
totalPages = response.totalPage;
_productList.add(response.productList);
filterName.add('all');
_filteredProductList = Observable.combineLatest2(
_filterName, _productList, applyModelFilter)
.asBroadcastStream();
}
});
}
}
List<Product> applyModelFilter(
String filter,
List<Product> products,
) {
if (filter == 'all') {
return products;
} else {
return products
.where((seriesSLug) => seriesSLug.series.slug == filter)
.toList();
}
}
UI Widget Class
class _AllSeriesModelListScreenState extends State<AllSeriesModelListScreen> {
AllSeriesModelListScreenArguments allSeriesModelListScreenArguments;
ProductListScreenBloc bloc;
int _selectedSeriesChipValue = -1;
int _pageNo = 1;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
bloc.dispose();
}
#override
Widget build(BuildContext context) {
RouteSettings settings = ModalRoute.of(context).settings;
allSeriesModelListScreenArguments = settings.arguments;
_init();
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
StreamBuilder(
stream: bloc.filteredProductList,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Product> productList = snapshot.data;
return SliverPadding(
padding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 10.0,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 0.0,
mainAxisSpacing: 8.0,
),
delegate: SliverChildListDelegate(
buildModelGridList(productList),
),
),
);
} else {
return SliverList(
delegate: SliverChildListDelegate([
PaddingWithTitle(
title: 'No Model Available',
),
]),
);
}
})
],
),
);
}
void _init() {
if (null == bloc) {
bloc = ProductListScreenBloc(
AppProvider.getApplication(context),
);
bloc.loadBrandWiseProductList(
allSeriesModelListScreenArguments.categorySlug,
allSeriesModelListScreenArguments.brandSlug,
_pageNo);
}
}
}
I believe you have missed something in these 2 lines.
final _filterName = BehaviorSubject<String>();
Sink<String> get filterName => _filterName;
You are not exposing the sink. BehaviorSubject is just a StreamController with default value and cache for last value. So as every Stream controller it has 2 props - sink and stream. to push data you need to access the sink.
To do that you need to type
StreamSink<String> get filterName => _filterName.sink;
Plus why you do not have a seed value in the behavior subject?
It is required to have that "default" value
final _filterName = BehaviorSubject<String>(seedValue: '');
Just had to change the code into this
void loadBrandWiseProductList(
String categorySlug, String brandSlug, int pageNo) {
if (totalPages >= pageNo) { //for pagination
StreamSubscription subscription = _application.productListRepository
.getBrandWiseProductList(categorySlug, brandSlug, pageNo)
.listen((ProductListResponse response) {
if (_productList.value == null) {
totalPages = response.totalPage;
_productList.add(response.productList);
}
_filteredProductList = Observable.combineLatest2(
_filterName, _productList, applyModelFilter)
.asBroadcastStream();
});
}
}