flutterdartlazy-loadingsqflite

How can I make it so the loading screen is shown first, then the data gets fetched?


I am running into an issue where the amount of data that is being fetched is causing the app to lag a little bit. Nothing serious, just about half a second. How can I adjust this to where it shows the loading screen, and then starts fetching the data.

import 'package:flutter/material.dart';
import 'package:manebenefitsstudent/constants/colors.dart';
import 'package:manebenefitsstudent/services/database/business_model.dart';
import 'package:manebenefitsstudent/services/database/data_provider.dart';
import 'package:manebenefitsstudent/widgets/reusable/card_data.dart';
import 'package:manebenefitsstudent/widgets/reusable/card_widget.dart';
import 'package:manebenefitsstudent/widgets/reusable/loading/loading_screen.dart';
import 'package:provider/provider.dart';

class RestaurantsView extends StatefulWidget {
  const RestaurantsView({super.key});

  @override
  State<RestaurantsView> createState() => _RestaurantsViewState();
}

class _RestaurantsViewState extends State<RestaurantsView> {
  bool _isLoading = true;
  List<Business> _restaurants = [];

  @override
  void initState() {
    super.initState();
    _fetchBusinesses();
  }

  Future<void> _fetchBusinesses() async {
    await Future.wait([
      Future.delayed(const Duration(milliseconds: 1500)),
      Provider.of<DataProvider>(context, listen: false)
          .getBusinessesByCategory(3)
    ]).then((results) {
      setState(() {
        _restaurants = results[1] as List<Business>;
        _isLoading = false;
      });
    }).catchError((error) {
      setState(() {
        _isLoading = false;
      });
      print('Error fetching businesses: $error');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Restaurants'),
        backgroundColor: unaGrey,
      ),
      body: _isLoading
          ? const LoadingScreen(
              loadingText: 'LOADING RESTAURANTS...',
              minDisplayTime: 1500,
            )
          : ListView.builder(
              itemCount: _restaurants.length,
              itemBuilder: (context, index) {
                var business = _restaurants[index];
                return CustomCard(
                  cardData: CardData(
                    imageData: business.image,
                    facebookUrl: business.facebookUrl,
                    instagramUrl: business.instagramUrl,
                    websiteUrl: business.websiteUrl,
                    directionsUrl: business.directionsUrl,
                  ),
                );
              },
            ),
    );
  }
}

it only takes about half a second to fetch the data, but it freezes the app for that moment. Or is there a better way to fetch the data from the sqf db in the app?

Here is the loading screen:

import 'package:flutter/material.dart';
import 'package:gif/gif.dart';
import 'package:manebenefitsstudent/constants/colors.dart';

class LoadingScreen extends StatefulWidget {
  final String loadingText;
  final int minDisplayTime;

  const LoadingScreen(
      {super.key, this.loadingText = 'LOADING...', this.minDisplayTime = 1000});

  @override
  State<LoadingScreen> createState() => _LoadingScreenState();
}

class _LoadingScreenState extends State<LoadingScreen>
    with SingleTickerProviderStateMixin {
  late GifController _gifController;

  @override
  void initState() {
    super.initState();
    _gifController = GifController(vsync: this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      int frameCount = 16;
      _gifController.repeat(
          min: 0, max: frameCount - 15, period: const Duration(seconds: 1));
    });
  }

  @override
  void dispose() {
    _gifController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: unaGrey,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Gif(
              controller: _gifController,
              image: const AssetImage('assets/images/lion_running.gif'),
              width: 300,
              height: 300,
            ),
            const SizedBox(height: 20),
            const CircularProgressIndicator.adaptive(
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
            ),
            const SizedBox(height: 20),
            Text(
              widget.loadingText,
              style: const TextStyle(
                color: Colors.white,
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Here is the code used to fetch all of the data form the DB:

data_initalizer:

import 'dart:io';
import 'package:manebenefitsstudent/services/database/business_model.dart';
import 'package:path/path.dart';
import 'package:http/http.dart' as http;
import 'package:manebenefitsstudent/services/database/database_helper.dart';
import 'package:sqflite/sqflite.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

class DataInitializer {
  static const String dbUrl =
      'GH REPO';
  static const String dbVersionUrl = '${dbUrl}db_version.txt';
  static const String dbFileName = 'manebenefitsdb.db';
  static String? githubToken = dotenv.env['GITHUB_TOKEN'];

  static Future<void> initializeData() async {
    print('Initializing data...');
    await _updateDatabaseIfNeeded();
  }

  static Future<void> _updateDatabaseIfNeeded() async {
    String databasesPath = await getDatabasesPath();
    String path = join(databasesPath, dbFileName);

    bool exists = await databaseExists(path);
    if (exists) {
      int localDbVersion = await _getLocalDbVersion(path);
      int remoteDbVersion = await _fetchRemoteDbVersion();
      print('Local DB version: $localDbVersion');
      print('Remote DB version: $remoteDbVersion');
      if (localDbVersion >= remoteDbVersion) {
        print('Local database is up-to-date.');
        return;
      }
    }

    print('Downloading database...');
    final response = await http.get(
      Uri.parse('${dbUrl}manebenefitsdb.db'),
      headers: {
        'Authorization': 'token $githubToken',
      },
    );
    print(
        'HTTP GET request to ${dbUrl}manebenefitsdb.db responded with status code: ${response.statusCode}');
    if (response.statusCode == 200) {
      await File(path).writeAsBytes(response.bodyBytes, flush: true);
      print('Database downloaded and saved.');
    } else {
      throw Exception('Failed to download database');
    }
  }

  static Future<int> _getLocalDbVersion(String path) async {
    final db = await openDatabase(path);
    try {
      List<Map<String, dynamic>> result =
          await db.rawQuery('SELECT version_number FROM version LIMIT 1');
      print('Local DB version query result: $result');
      return result.isNotEmpty ? result.first['version_number'] as int : 0;
    } catch (e) {
      print('Error getting local database version: $e');
      return 0;
    } finally {
      await db.close();
    }
  }

  static Future<int> _fetchRemoteDbVersion() async {
    final response = await http.get(
      Uri.parse(dbVersionUrl),
      headers: {
        'Authorization': 'token $githubToken',
      },
    );
    print(
        'HTTP GET request to $dbVersionUrl responded with status code: ${response.statusCode}');
    if (response.statusCode == 200) {
      print('Remote DB version fetched: ${response.body.trim()}');
      return int.parse(response.body.trim());
    } else {
      throw Exception('Failed to fetch remote database version');
    }
  }

  static Future<List<Business>> getBusinessesByCategory(int categoryId) async {
    DatabaseHelper dbHelper = DatabaseHelper();
    List<Map<String, dynamic>> businesses =
        await dbHelper.getBusinessesByCategory(categoryId);

    print('Fetched businesses for category $categoryId: $businesses');

    List<Business> businessList = businesses.map((map) {
      return Business.fromMap(map);
    }).toList();

    print('Converted businesses for category $categoryId: $businessList');

    businessList.sort((a, b) => a.name.compareTo(b.name));

    return businessList;
  }
}

class GlobalData {
  static List<Business> businesses = [];
}

database_helper:

import 'dart:io';
import 'package:flutter/services.dart' show ByteData, rootBundle;
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  static Database? _database;

  factory DatabaseHelper() {
    return _instance;
  }

  DatabaseHelper._internal();

  Future<Database> get database async {
    if (_database != null) return _database!;

    _database = await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    String databasesPath = await getDatabasesPath();
    String path = join(databasesPath, 'manebenefitsdb.db');

    bool exists = await databaseExists(path);
    if (!exists) {
      ByteData data = await rootBundle.load('assets/manebenefitsdb.db');
      List<int> bytes =
          data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
      await File(path).writeAsBytes(bytes, flush: true);
    }

    return await openDatabase(path);
  }

  Future<List<Map<String, dynamic>>> getBusinessesByCategory(
      int categoryId) async {
    final db = await database;
    try {
      final result = await db.rawQuery('''
        SELECT b.*
        FROM Businesses b
        INNER JOIN Business_categories bc ON b.Business_id = bc.Business_id
        WHERE bc.Category_id = ?
      ''', [categoryId]);
      print('Query result: $result'); 
      return result;
    } catch (e) {
      print('Error querying database: $e');
      return [];
    }
  }
}

data_provider:

import 'package:flutter/material.dart';
import 'package:manebenefitsstudent/services/database/data_initializer.dart';
import 'package:manebenefitsstudent/services/database/business_model.dart';

class DataProvider extends ChangeNotifier {
  final Map<int, List<Business>> _businessesByCategory = {};

  Future<List<Business>> getBusinessesByCategory(int categoryId) async {
    if (_businessesByCategory.containsKey(categoryId)) {
      return _businessesByCategory[categoryId]!;
    } else {
      List<Business> businesses =
          await DataInitializer.getBusinessesByCategory(categoryId);
      _businessesByCategory[categoryId] = businesses;
      return businesses;
    }
  }
}

I tried it like this, but I wanted it to show the gif I have.

import 'package:flutter/material.dart';
import 'package:manebenefitsstudent/constants/colors.dart';
import 'package:manebenefitsstudent/services/database/business_model.dart';
import 'package:manebenefitsstudent/services/database/data_provider.dart';
import 'package:manebenefitsstudent/widgets/reusable/card_data.dart';
import 'package:manebenefitsstudent/widgets/reusable/card_widget.dart';
import 'package:provider/provider.dart';

class RestaurantsView extends StatelessWidget {
  const RestaurantsView({super.key});

  Future<List<Business>> _fetchBusinesses(BuildContext context) async {
    await Future.delayed(const Duration(milliseconds: 1500));
    return Provider.of<DataProvider>(context, listen: false).getBusinessesByCategory(3);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Restaurants'),
        backgroundColor: unaGrey,
      ),
      body: FutureBuilder<List<Business>>(
        future: _fetchBusinesses(context),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          } else if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}'),
            );
          } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return const Center(
              child: Text('No restaurants available'),
            );
          } else {
            var restaurants = snapshot.data!;
            return ListView.builder(
              itemCount: restaurants.length,
              itemBuilder: (context, index) {
                var business = restaurants[index];
                return CustomCard(
                  cardData: CardData(
                    imageData: business.image,
                    facebookUrl: business.facebookUrl,
                    instagramUrl: business.instagramUrl,
                    websiteUrl: business.websiteUrl,
                    directionsUrl: business.directionsUrl,
                  ),
                );
              },
            );
          }
        },
      ),
    );
  }
}

Solution

  • The FutureBuilder is the right widget just put your loading view instead of the CircularProgressIndicator and to give more time to show your GIF just add the Future.delayed on the method that fetch and return the data You can use something like this..

    class _MyWidgetState extends State<MyWidget> {
      late Future<List<Business>> businessFuture; 
    
      @override
      void didChangeDependencies() {
        super.didChangeDependencies();
        businessFuture = Provider.of<DataProvider>(context, listen: false).getBusinessesByCategory(3);
      }
    
      @override
      Widget build(BuildContext context) {
        return FutureBuilder<List<Business>>(
          future: businessFuture,
          builder: ((context, snapshot) {
            if (snapshot.connectionState == ConnectionState.none ||
                snapshot.connectionState == ConnectionState.waiting) {
              return LoadingScreen(); // Your GIF
            } else {
              if (snapshot.hasError) {
                return SomeErrorWidget();
              } else {
                // Check your data here
              }
            }
          }),
        );
      }
    }