Varun Sharma
Varun Sharma

Reputation: 175

How to add new items in the Flutter grid view builder from the api when the scroll view reaches the middle

I am trying to update the list of the models when the scroll view reaches the end, while I was able to do so, I am unable to do it smoothly. I want to load the new images from the API into the List of models whenever the user reaches the middle of the scroll so that the scrolling feels smooth as in the current scenario, there is a second or two of pause when the user hits the end after which the new elements will be added so the user won't know if the new elements are added. Also, my current approach is putting too much load on the main thread and is crashing the app.

here's the code below:

home.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:wallpaper_app/data/data.dart';
import 'package:wallpaper_app/model/categories.dart';
import 'package:wallpaper_app/model/wallpaper_model.dart';
import 'package:wallpaper_app/network/network.dart';
import 'package:wallpaper_app/widgets/branding.dart';
import 'package:wallpaper_app/widgets/list_tile.dart';
import 'package:wallpaper_app/widgets/wallpaper_tile.dart';
var pageIndex=1;
Data data = Data();
List<CategoriesModel> categories;
List<WallpaperModel> wallpapers =[];

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _controller = ScrollController();
    _controller.addListener(() {
      if (_controller.position.atEdge) {
        if (_controller.position.pixels == 0)
          print('top');
      // you are at top position
      else{
          print('end');
          incrementIndex();
          getWallpapers(pageIndex);
        }

      // you are at bottom position
    }
    });
    categories = data.getCategories();
    getWallpapers(pageIndex);
  }
  ScrollController _controller;

  void incrementIndex(){
    setState(() {
      pageIndex++;
    });
  }

  void getWallpapers(var index) async {
    try {
      Network network = Network();
      Map<String, dynamic> jsonData = await network.getCuratedWallpapers(
          index, 80);
      jsonData["photos"].forEach((element) {
        WallpaperModel wallpaperModel = WallpaperModel.fromMap(element);
        setState(() {
          wallpapers.add(wallpaperModel);
        });
//      for (var wallpaper in wallpapers){
//        print(wallpaper.src.small);
//      }
      });
    }
    catch(e){
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
      systemNavigationBarColor: Color(0xff999999), // navigation bar color
    ));
    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        backgroundColor: Colors.white,
        title: Brand(),
      ),
      body: SingleChildScrollView(
        controller: _controller,
        child: Container(
          child: Column(
            children: [
              Container(
                height: 100,
                child: ListView.builder(
//                  controller: _controller,
                    padding: EdgeInsets.all(10),
                    itemCount: categories.length,
//                  shrinkWrap: true,
                    scrollDirection: Axis.horizontal,
                    itemBuilder: (context, index) {
                      return CategoriesTile(
                        imageUrl: categories[index].imageUrl,
                        title: categories[index].categoriesName,
                      );
                    }),
              ),
              SizedBox(
                height: 20,
              ),
              Container(
                padding: EdgeInsets.symmetric(horizontal: 10),
                child: GridView.builder(
                  physics: ScrollPhysics(),
                  shrinkWrap: true,
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                        crossAxisCount: 2,
                        crossAxisSpacing: 6.0,
                        mainAxisSpacing: 6.0,
                        childAspectRatio: 0.6),
                    itemBuilder: (context,index){
//                      if(index==50){
////                        incrementIndex();
//                        getWallpapers(pageIndex+1);
//                      }
                      return WallpaperTile(small: wallpapers[index].src.portrait,);
                    },
                  itemCount: wallpapers.length,
                    ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

wallpaper_model.dart

class WallpaperModel {
  String photographer;
  String photographerUrl;
  int photographerId;
  SrcModel src;
  WallpaperModel(
      {this.photographer, this.photographerUrl, this.photographerId, this.src});
  factory WallpaperModel.fromMap(Map<String,dynamic> jsonData){
    return WallpaperModel(
      photographer: jsonData["photographer"],
      photographerId: jsonData["photographer_id"],
      photographerUrl: jsonData["photographer_url"],
      src: SrcModel.fromMap(jsonData["src"])
    );
  }
}

class SrcModel {
  String original;
  String small;
  String portrait;
  SrcModel({this.original, this.small, this.portrait});
  factory SrcModel.fromMap(Map<String, dynamic> jsonData) {
    return SrcModel(
        original: jsonData["original"],
        small: jsonData["small"],
        portrait: jsonData["portrait"]);
  }
}

wallpaper_tile.dart

import 'package:flutter/material.dart';

class WallpaperTile extends StatelessWidget {
  final String small;
  WallpaperTile({this.small});
  @override
  Widget build(BuildContext context) {
    return Container(
      child: ClipRRect(
        borderRadius: BorderRadius.circular(20),
          child:
          Image.network(
        small,
        fit: BoxFit.cover,
      )),
    );
  }
}

network.dart

import 'dart:convert';

import 'package:http/http.dart' as http;
const apiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
class Network{
 Future<dynamic> getCuratedWallpapers(var pageNo, var maxWallpapers) async{

   var response = await http.get("https://api.pexels.com/v1/curated/?page=$pageNo&per_page=$maxWallpapers",
       headers: {
     "Authorization":apiKey
   });
   return jsonDecode(response.body);
 }
}

the top list view is just for categories and can be ignored. it's the grid view.builder that has the core functionality

Upvotes: 3

Views: 3129

Answers (3)

Ravin Laheri
Ravin Laheri

Reputation: 822

try SliverGrid instead of GridView.builder in your home.dart.

because when you will using GridView.builder it will load it's all possible result at once instead of this when you use SliverGrid it will load only those data which is currently present in screen.

SliverGrid(delegate: SliverChildBuilderDelegate((context, int index) {
          final _wallpaper = wallpaper.photos ? [index];
          return Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(10.0),), height: MediaQuery
              .of(context)
              .size
              .height / 2, child: GestureDetector(onTap: () {
            //Wallpaper Preview
          },
            child: Hero(tag: '${_wallpaper?.id}',
              child: ClipRRect(borderRadius: BorderRadius.circular(10.0),
                child: CachedNetworkImage(fit: BoxFit.cover,
                  imageUrl: _wallpaper!.src!.large!,
                  filterQuality: FilterQuality.high,
                  progressIndicatorBuilder: (context, url, progress) => Center(child: CircularProgressIndicator()),),),),),);
        }, childCount: wallpaper.photos?.length),
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 0.5, crossAxisSpacing: 5.0 mainAxisSpacing: 5.0,),),

note: Remove extra that you don't required.

Upvotes: 0

Varun Sharma
Varun Sharma

Reputation: 175

While I couldn't solve the main issue(loading every time the scroll reached the mid of the screen so that the user doesn't have to wait and see the progress bar) but I was able to solve all the other issues besides that.

  1. too many images(>3000) was causing the app to crash without a stack trace. Solution-> Instead of using a grid view inside a singleChildScrollView, I added it in an Expanded widget which solved that crashing and sluggish scroll behaviour.
  2. after a certain number of pages I have started to clear ImageCache because from what I was searching I found that after compressing the images, they can still stay in memory(I still don't know how true that is as that is internal flutter process).
  3. I also have used Provider alongside ChangeNotifier instead of setState to avoid extra rebuilding of the widgets. Result:- Though the UI isn't super smooth, scrolling is happening seamlessly, images are cleared automatically when they are not in view. There is a second of progress bar after which the next page from the API call is loaded into the list. This is my implementation of lazy loading in a way. I am not calling an async call when another is running by using a boolean.
  4. I also added cacheheight and cachewidth to each image to just load compressed image sizes in the grid view and not load full sized images in it.
  5. Also, I used the scroll controller to load more elements into the list every time scroll reaches the bottom of the page. Here are the main changes.

main.dart

import 'package:flutter/material.dart';
import 'package:wallpaper_app/data/provider_data.dart';
import 'package:wallpaper_app/screens/home.dart';
import 'package:provider/provider.dart';
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context)=>ProviderData(),
      child: MaterialApp(
        theme: ThemeData.dark().copyWith(
          scaffoldBackgroundColor: Colors.black,
        ),
        title: 'Wallpaper App',
        home: Home()
      ),
    );
  }
}

home.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:wallpaper_app/data/data.dart';
import 'package:wallpaper_app/data/provider_data.dart';
import 'package:wallpaper_app/model/categories.dart';
import 'package:wallpaper_app/model/wallpaper_model.dart';
import 'package:wallpaper_app/network/network.dart';
import 'package:wallpaper_app/widgets/branding.dart';
import 'package:wallpaper_app/widgets/list_tile.dart';
import 'package:wallpaper_app/widgets/wallpaper_tile.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
import 'package:flutter_pagewise/flutter_pagewise.dart';
Data data = Data();
List<CategoriesModel> categories;
class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    categories = data.getCategories();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      _controller = ScrollController();
      _controller.addListener(() {
        if (_controller.position.atEdge) {
          if (_controller.position.pixels == 0)
            print('top');
          // you are at top position
          else{
            print('end');
//          incrementIndex();
            if(Provider.of<ProviderData>(context,listen: false).pageIndex>5){
              print('image cache: ${imageCache.currentSize}');
              imageCache.clear();
            }
            Provider.of<ProviderData>(context,listen: false).incrementPage();
            getWallpapers(Provider.of<ProviderData>(context,listen: false).pageIndex);
          }

          // you are at bottom position
        }
      });
      getWallpapers(Provider.of<ProviderData>(context,listen: false).pageIndex);
    });

  }
  ScrollController _controller;

  void getWallpapers(var index) async {
    if(!Provider.of<ProviderData>(context,listen: false).isPerformingRequest){
      Provider.of<ProviderData>(context,listen: false).togglePerformingRequest();

      try {
        Network network = Network();
        Map<String, dynamic> jsonData = await network.getCuratedWallpapers(
            index, 80);
        jsonData["photos"].forEach((element) {
          WallpaperModel wallpaperModel = WallpaperModel.fromMap(element);
          setState(() {
            Provider.of<ProviderData>(context,listen: false).addWallpapers(wallpaperModel);
          });
        });
      }
      catch(e){
        print(e);
      }
      Provider.of<ProviderData>(context,listen: false).togglePerformingRequest();
    }


  }

  @override
  Widget build(BuildContext context) {
  SystemChrome.setEnabledSystemUIOverlays([]);
    return Scaffold(
//      appBar: AppBar(
//        backgroundColor: Colors.black,
//        elevation: 0,
////        backgroundColor: Colors.white,
//        title: Brand(),
//      ),
      body:
//      SingleChildScrollView(
//        controller: _controller,
//        child:
        Container(
          child: Column(
            children: [
//              Container(
//                height: 100,
//                child: ListView.builder(
////                  controller: _controller,
//                    padding: EdgeInsets.all(10),
//                    itemCount: categories.length,
//                  shrinkWrap: true,
//                    scrollDirection: Axis.horizontal,
//                    itemBuilder: (context, index) {
//                      return CategoriesTile(
//                        imageUrl: categories[index].imageUrl,
//                        title: categories[index].categoriesName,
//                      );
//                    }),
//              ),
//              SizedBox(
//                height: 20,
//              ),
              Expanded(
//                padding: EdgeInsets.symmetric(horizontal: 10),
                child: GridView.builder(
                  controller: _controller,
                  physics: ScrollPhysics(),
                  shrinkWrap: true,
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2,
                      crossAxisSpacing: 6.0,
                      mainAxisSpacing: 6.0,
                      childAspectRatio: 0.6),
                      addAutomaticKeepAlives: false,
                  itemBuilder: (context,index){
                    if(index==(Provider.of<ProviderData>(context,listen: false).wallpapers!=null?Provider.of<ProviderData>(context,listen: false).wallpapers.length+0:80)){
                      return _buildProgressIndicator();
                    }
                    else
                    return WallpaperTile(small: Provider.of<ProviderData>(context,listen: false).wallpapers[index].src.medium,);
                  },
                  itemCount: Provider.of<ProviderData>(context,listen: false).wallpapers!=null?Provider.of<ProviderData>(context,listen: false).wallpapers.length+1:81,
                ),
              )
            ],
          ),
        ),
//      ),
    );
  }
}

Widget _buildProgressIndicator() {
  return new Padding(
    padding: const EdgeInsets.all(8.0),
    child: new Center(
      child: new Opacity(
      opacity: 1,
        child: new CircularProgressIndicator(),
      ),
    ),
  );
}

wallpaper_model.dart

class WallpaperModel {
  String photographer;
  String photographerUrl;
  int photographerId;
  SrcModel src;
  WallpaperModel(
      {this.photographer, this.photographerUrl, this.photographerId, this.src});
  factory WallpaperModel.fromMap(Map<String,dynamic> jsonData){
    return WallpaperModel(
      photographer: jsonData["photographer"],
      photographerId: jsonData["photographer_id"],
      photographerUrl: jsonData["photographer_url"],
      src: SrcModel.fromMap(jsonData["src"])
    );
  }
}

class SrcModel {
  String original;
  String small;
  String portrait;
  String medium;
  SrcModel({this.original, this.small, this.portrait,this.medium});
  factory SrcModel.fromMap(Map<String, dynamic> jsonData) {
    return SrcModel(
        original: jsonData["original"],
        small: jsonData["small"],
        medium: jsonData["medium"],
        portrait: jsonData["portrait"]);
  }
}

network.dart

import 'dart:convert';

import 'package:http/http.dart' as http;
const apiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
class Network{
 Future<dynamic> getCuratedWallpapers(var pageNo, var maxWallpapers) async{

   var response = await http.get("https://api.pexels.com/v1/curated/?page=$pageNo&per_page=$maxWallpapers",
       headers: {
     "Authorization":apiKey,
   });
   print(response.statusCode);
//   print(response.body);
   return jsonDecode(response.body);
 }
}

wallpaper_tile.dart

import 'package:flutter/material.dart';

class WallpaperTile extends StatelessWidget {
  final String small;
  WallpaperTile({this.small});
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(5),
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(25),
          color: Colors.grey
      ),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(25),
          child:
          Image.network(
        small,
        fit: BoxFit.cover,
        cacheHeight: 400,
        cacheWidth: 225,
      )
      ),
    );
  }
}

provider_data.dart

import 'package:flutter/foundation.dart';
import 'package:wallpaper_app/model/wallpaper_model.dart';

class ProviderData extends ChangeNotifier{
  List<WallpaperModel> wallpapers =[];
  bool isPerformingRequest = false;
  var pageIndex=1;

  void togglePerformingRequest(){
    isPerformingRequest = !isPerformingRequest;
    notifyListeners();
  }
  void incrementPage(){
    pageIndex++;
    print(pageIndex);
    notifyListeners();
  }

  void addWallpapers(WallpaperModel wallpaperModel){
    wallpapers.add(wallpaperModel);
    notifyListeners();
  }
}

Upvotes: 5

Akif
Akif

Reputation: 7640

You need to use NotificationListener with ScrollNotification. Modify this with your requirements.


import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  int present = 0;
  int perPage = 15;

  final originalItems = List<String>.generate(10000, (i) => "Item $i");
  var items = List<String>();


  @override
  void initState() {
    super.initState();
    setState(() {
      items.addAll(originalItems.getRange(present, present + perPage));
      present = present + perPage;
    });
  }

  void loadMore() {
    setState(() {
      if((present + perPage )> originalItems.length) {
        items.addAll(
            originalItems.getRange(present, originalItems.length));
      } else {
        items.addAll(
            originalItems.getRange(present, present + perPage));
      }
      present = present + perPage;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification scrollInfo) {
          if (scrollInfo.metrics.pixels ==
              scrollInfo.metrics.maxScrollExtent / 2) {
            loadMore();
          }
        },
        child: ListView.builder(
          itemCount: (present <= originalItems.length) ? items.length + 1 : items.length,
          itemBuilder: (context, index) {
            return (index == items.length ) ?
            Container(
              color: Colors.greenAccent,
              child: FlatButton(
                child: Text("Load More"),
                onPressed: () {
                  loadMore();
                },
              ),
            )
                :
            ListTile(
              title: Text('${items[index]}'),
            );
          },
        ),
      ),
    );
  }
}

Read more.

Upvotes: 1

Related Questions