How to proper load async data

216 Views Asked by At

I am quite new in Flutter and am wondering how to properly load async data in a Widget. This is my code so far:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';

import '../../../api/api_result.dart';
import '../../../common/application.dart';
import '../../../models/survey.dart';
import '../../common/list_material_ink_well.dart';
import '../../common/loading_widget.dart';
import '../../common/main_layout.dart';
import '../../common/status_bar.dart';
import '../../common/status_bar_icon.dart';
import 'survey_poll.dart';

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

  @override
  State<StatefulWidget> createState() => SurveysPollsWidgetState();
}

class SurveysPollsWidgetState extends State<SurveysPollsWidget> {
  bool isEditMode = false;
  Map<String, bool> activity = <String, bool>{};

  Future<List<Survey>?> getSurveys(Application application) async {
    ApiResult<List<Survey>>? apiResult =
        await application.repositories?.survey.getSurveys();

    if (apiResult?.error != null) {
      await application.auth.logout();
    }

    if (apiResult?.value != null && application.caches != null) {
      application.caches?.survey.saveSurveys(apiResult!.value!);
    }

    return apiResult?.value;
  }

  Future<List<Survey>?> getSurveysFromCache(Application application) async {
    List<Survey>? result = await application.caches?.survey.getSurveys();

    return result;
  }

  final MaterialStateProperty<Icon?> thumbIcon =
      MaterialStateProperty.resolveWith<Icon?>(
    (Set<MaterialState> states) {
      if (states.contains(MaterialState.selected)) {
        return const Icon(Icons.check);
      }
      return const Icon(Icons.close);
    },
  );

  Widget _drawItem(Survey survey) => isEditMode
      ? ListMaterialInkWell(
          survey.name,
          navItem: Switch(
            thumbIcon: thumbIcon,
            value: activity[survey.id] ?? false,
            onChanged: (bool value) {
              setState(() {
                activity[survey.id] = value;
              });
            },
          ),
        )
      : ListMaterialInkWell(
          survey.name,
          onTap: () => Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => SurveyPollWebViewWidget(survey: survey),
            ),
          ),
        );

  void toggleEditMode() => setState(() {
        isEditMode = !isEditMode;
      });

  Widget _draw(BuildContext context, AsyncSnapshot<List<Survey>?> cachedData) =>
      Material(
        child: MainLayout(
            title: AppLocalizations.of(context)!.surveysAndPolls,
            iconLeft: isEditMode
                ? StatusBarIcon(
                    StatusBar.goBackIcon.icon, (context) => toggleEditMode())
                : StatusBar.goBackIcon,
            iconRight: !isEditMode
                ? StatusBarIcon(
                    Icons.edit_outlined, (context) => toggleEditMode())
                : null,
            showLoading: !cachedData.hasData,
            children: cachedData.hasData ? cachedData.data!.map(_drawItem).toList() : []),
      );

  @override
  Widget build(BuildContext context) => Consumer<Application>(
      builder: (_, application, __) => () {
            if (application.repositories == null) {
              return const LoadingWidget();
            }

            return FutureBuilder<List<Survey>?>(
                    future: getSurveysFromCache(application),
                    builder: (context, cachedData) => cachedData
                                .connectionState !=
                            ConnectionState.done
                        ? Container()
                        : cachedData.data != null
                            ? _draw(
                                context, cachedData)
                            : FutureBuilder<List<Survey>?>(
                                future: getSurveys(application),
                                builder: (context, snapshot) => _draw(
                                    context, snapshot),
                              ));
          }());
}

the problem here is that every time the state changes, by toggling the editMode or the switches, the screen flashes black (default background), because the entire widget is re-rendered and for short amount of time when getSurveysFromCache is called, Container() is rendered, so black.

A dirty solution is to add a state for the surveys:

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';

import '../../../api/api_result.dart';
import '../../../common/application.dart';
import '../../../models/survey.dart';
import '../../common/list_material_ink_well.dart';
import '../../common/loading_widget.dart';
import '../../common/main_layout.dart';
import '../../common/status_bar.dart';
import '../../common/status_bar_icon.dart';
import 'survey_poll.dart';

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

  @override
  State<StatefulWidget> createState() => SurveysPollsWidgetState();
}

class SurveysPollsWidgetState extends State<SurveysPollsWidget> {
  bool isEditMode = false;
  Map<String, bool> activity = <String, bool>{};
  List<Survey>? surveys;

  Future<List<Survey>?> getSurveys(Application application) async {
    ApiResult<List<Survey>>? apiResult =
        await application.repositories?.survey.getSurveys();

    if (apiResult?.error != null) {
      await application.auth.logout();
    }

    if (apiResult?.value != null && application.caches != null) {
      application.caches?.survey.saveSurveys(apiResult!.value!);
    }

    if (apiResult?.value != null) {
      for (Survey element in apiResult!.value!) {
        activity[element.id] = element.isActive;
      }
    }

    setState(() {
      surveys = apiResult?.value;
    });

    return apiResult?.value;
  }

  Future<List<Survey>?> getSurveysFromCache(Application application) async {
    List<Survey>? result = await application.caches?.survey.getSurveys();

    setState(() {
      surveys = result;
    });

    return result;
  }

  final MaterialStateProperty<Icon?> thumbIcon =
      MaterialStateProperty.resolveWith<Icon?>(
    (Set<MaterialState> states) {
      if (states.contains(MaterialState.selected)) {
        return const Icon(Icons.check);
      }
      return const Icon(Icons.close);
    },
  );

  Widget _drawItem(Survey survey) => isEditMode
      ? ListMaterialInkWell(
          survey.name,
          navItem: Switch(
            thumbIcon: thumbIcon,
            value: activity[survey.id] ?? false,
            onChanged: (bool value) {
              setState(() {
                activity[survey.id] = value;
              });
            },
          ),
        )
      : ListMaterialInkWell(
          survey.name,
          onTap: () => Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => SurveyPollWebViewWidget(survey: survey),
            ),
          ),
        );

  void toggleEditMode() => setState(() {
        isEditMode = !isEditMode;
      });

  Widget _draw(BuildContext context, List<Survey>? surveys, bool hasData) =>
      Material(
        child: MainLayout(
            title: AppLocalizations.of(context)!.surveysAndPolls,
            iconLeft: isEditMode
                ? StatusBarIcon(
                    StatusBar.goBackIcon.icon, (context) => toggleEditMode())
                : StatusBar.goBackIcon,
            iconRight: !isEditMode
                ? StatusBarIcon(
                    Icons.edit_outlined, (context) => toggleEditMode())
                : null,
            showLoading: !hasData,
            children: hasData ? surveys!.map(_drawItem).toList() : []),
      );

  @override
  Widget build(BuildContext context) => Consumer<Application>(
      builder: (_, application, __) => () {
            if (application.repositories == null) {
              return const LoadingWidget();
            }

            return surveys != null
                ? _draw(context, surveys, true)
                : FutureBuilder<List<Survey>?>(
                    future: getSurveysFromCache(application),
                    builder: (context, cachedData) => cachedData
                                .connectionState !=
                            ConnectionState.done
                        ? Container()
                        : cachedData.data != null
                            ? _draw(
                                context, cachedData.data, cachedData.hasData)
                            : FutureBuilder<List<Survey>?>(
                                future: getSurveys(application),
                                builder: (context, snapshot) => _draw(
                                    context, snapshot.data, cachedData.hasData),
                              ));
          }());
}

A clean solution would be to call getSurveysFromCache and getSurveys in the initState, but I need my Application instance object which is returned by the Consumer<Application>.

Some hint how to do it properly and clean?

1

There are 1 best solutions below

0
tyukesz On

First of all I would recommend using Riverpod over Provider. There are couple of articles out there about pros&cons:


With Provider you are basically limited by BuildContext, but as I can see, you are using just some function calls to load data, so:

  1. I would extract those from the provider
  2. Instead mixing setState with provider, use just the provider to store states
  3. Instead Consumer use Selector
  4. Split up the code and listen to particular state changes only (with Selector)
  5. Init data with null, and while is not loaded, render the Container (or a loading indicator)
  6. Load data in initState