76 votes

Flutter, rendre le widget après un appel asynchrone

Je voudrais rendre un widget qui a besoin d'un appel HTTP pour collecter certaines données.

J'ai obtenu le code suivant (simplifié)

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';

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: 'async demo'),
    );
  }
}

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> {

  var asyncWidget;

  @override
  initState() {
    super.initState();

    loadData().then((result) {
      print(result);
      setState(() {
       asyncWidget = result;
      });
    });
  }

  loadData() async {
    var widget = new AsyncWidget();
    return widget.build();
  }

  @override
  Widget build(BuildContext context) {

    if(asyncWidget == null) {
      return new Scaffold(
        appBar: new AppBar(
          title: new Text("Loading..."),
        ),
      );
    } else {
      return new Scaffold(
        appBar: new AppBar(
          title: new Text(widget.title),
        ),
        body: new Center(
          child: this.asyncWidget,
        ),
      );
    }
  }
}

class MyRenderer {

  MyRenderer();

  Widget render (List data) {

    List<Widget> renderedWidgets = new List<Widget>();

    data.forEach((element) {
      renderedWidgets.add(new ListTile(
        title: new Text("one element"),
        subtitle: new Text(element.toString()),
      ));
    });
    var lv = new ListView(
      children: renderedWidgets,
    );
    return lv;
  }
}

class MyCollector {

  Future gather() async {

    var response = await // do the http request here;

    return response.body;
  }
}

class AsyncWidget {

  MyCollector collector;
  MyRenderer renderer;

  AsyncWidget() {
    this.collector = new MyCollector();
    this.renderer = new MyRenderer();
  }

  Widget build() {

    var data = this.collector.gather();
    data.then((response) {
      var responseObject = JSON.decode(response);
      print(response);
      return this.renderer.render(responseObject);
    });
    data.catchError((error) {
      return new Text("Oups");
    });
  }
}

Mon code fonctionne comme suit : le widget utilisant des données asynchrones prend un collecteur (qui fait l'appel http) et un moteur de rendu qui rendra les widgets avec les données http. Je crée une instance de ce widget lors de la fonction initState() et ensuite je fais mon appel asynchrone.

J'ai trouvé de la documentation indiquant que nous devrions utiliser la méthode setState() pour mettre à jour le widget avec les nouvelles données, mais cela ne fonctionne pas pour moi.

Cependant, lorsque j'enregistre des journaux, je vois que l'appel HTTP est effectué et que la méthode setState() est appelée, mais le widget ne se met pas à jour.

1 votes

107voto

rmtmckenzie Points 10854

Le meilleur moyen d'y parvenir est d'utiliser un fichier FutureBuilder .

Extrait de la documentation de FutureBuilder :

new FutureBuilder<String>(
  future: _calculation, // a Future<String> or null
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    switch (snapshot.connectionState) {
      case ConnectionState.none: return new Text('Press button to start');
      case ConnectionState.waiting: return new Text('Awaiting result...');
      default:
        if (snapshot.hasError)
          return new Text('Error: ${snapshot.error}');
        else
          return new Text('Result: ${snapshot.data}');
    }
  },
)

Par ailleurs, vous construisez votre widget en dehors de la méthode State.build et vous sauvegardez le widget lui-même, ce qui est un anti-modèle. Vous devriez construire les widgets à chaque fois dans la méthode build.

Vous pourriez faire fonctionner cette fonction sans FutureBuilder, mais vous devriez sauvegarder le résultat de l'appel http (traité de manière appropriée) et utiliser ensuite les données dans votre fonction de construction.

Voyez ceci, mais notez que l'utilisation d'un FutureBuilder est une meilleure façon de le faire et je vous le fournis simplement pour que vous puissiez apprendre.

import 'dart:async';
import 'dart:convert';

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: 'async demo'),
    );
  }
}

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> {
  List data;

  @override
  initState() {
    super.initState();

    new Future<String>.delayed(new Duration(seconds: 5), () => '["123", "456", "789"]').then((String value) {
      setState(() {
        data = json.decode(value);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    if (data == null) {
      return new Scaffold(
        appBar: new AppBar(
          title: new Text("Loading..."),
        ),
      );
    } else {
      return new Scaffold(
        appBar: new AppBar(
          title: new Text(widget.title),
        ),
        body: new Center(
          child: new ListView(
            children: data
                .map((data) => new ListTile(
                      title: new Text("one element"),
                      subtitle: new Text(data),
                    ))
                .toList(),
          ),
        ),
      );
    }
  }
}

1 votes

Je me suis certainement trompé avec les Futures dans mon code, votre exemple est beaucoup plus simple. Merci, j'essaierai d'utiliser un futureBuilder à l'avenir ;)

0 votes

Il est également important de prendre en compte le cas où il y a une erreur de réseau, par exemple l'absence de connexion Internet. Vous pouvez ajouter une vérification pour cela dans l'instruction switch if (snapshot ?.data ?.exception ?.clientException is NetworkException) { return buildEmptyPlaceHolder() ; }.

0 votes

Est-il possible de modifier l'état (SetState) lorsque le Future Builder est terminé ?

37voto

Sanjayrajsinh Points 1098

Le meilleur moyen est FutureBuilder()

Exemple complet

class _DemoState extends State<Demo> {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: downloadData(), // function where you call your api
      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {  // AsyncSnapshot<Your object type>
        if( snapshot.connectionState == ConnectionState.waiting){
            return  Center(child: Text('Please wait its loading...'));
        }else{
            if (snapshot.hasError)
              return Center(child: Text('Error: ${snapshot.error}'));
            else
              return Center(child: new Text('${snapshot.data}'));  // snapshot.data  :- get your object which is pass from your downloadData() function
        }
      },
    );
  }
  Future<String> downloadData()async{
    //   var response =  await http.get('https://getProjectList');    
    return Future.value("Data download successfully"); // return your response
  }
}

Dans le constructeur du futur, il appelle la fonction future pour attendre le résultat, et dès qu'il produit le résultat, il appelle la fonction builder où nous construisons le widget.

AsyncSnapshot a 3 états :

1. connectionState.none \= Dans cet état le futur est nul

2. état de connexion.attente \= [futur] n'est pas nul, mais n'est pas encore achevé

3. connectionState.done \= Le [futur] n'est pas nul et s'est terminé. Si le futur s'est terminé avec succès, [AsyncSnapshot.data] sera défini à la valeur à laquelle le futur s'est terminé. S'il s'est terminé avec une erreur, [AsyncSnapshot.hasError] sera vrai.

5 votes

N'est-ce pas contraire à la documentation du FutureBuiler qui dit - Le futur doit avoir été obtenu plus tôt, par exemple pendant State.initState, State.didUpdateConfig, ou State.didChangeDependencies. Il ne doit pas être créé pendant l'appel de la méthode State.build ou StatelessWidget.build lors de la construction du FutureBuilder. Si le futur est créé en même temps que le FutureBuilder, chaque fois que le parent du FutureBuilder est reconstruit, la tâche asynchrone sera redémarrée.

-2voto

Nelson Bwogora Points 461

Pour ajouter à la réponse acceptée par @rmtmckenzie, il est important de traiter le cas où il y a une erreur de réseau comme suit snapshot.data aura les données mais l'erreur se trouvera dans snapshot.data.exception donc

new FutureBuilder<String>(
future: _calculation, // a Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
  case ConnectionState.none: return new Text('Press button to start');
  case ConnectionState.waiting: return new Text('Awaiting result...');
  default:
    if (snapshot.hasError)
      return new Text('Error: ${snapshot.error}');
       if (snapshot?.data?.exception?.clientException
            is NetworkException) {
         return new 
                Text('Result:${snapshot..data?.exception?.clientException?.message}');
        }
    else
      return new Text('Result: ${snapshot.data}');
  }
 },
)

0 votes

Vous faites quelques choses d'inutile et même de faux ici - snapshot n'est pas optionnel donc il n'y a pas de raison d'utiliser ? après lui, et 'data' devrait être une chaîne dans ce cas donc data ?.exception ? est en fait invalide. Si vous regardez la première partie de ma réponse, elle montre déjà comment faire cela correctement en utilisant snapshot.hasError y snapshot.error .

0 votes

Ce que j'essayais de gérer, c'est les cas où snapshot.hasData est vrai et snapshot.hasError est faux mais il y a une erreur dans snapshot.data c'est-à-dire le cas où snapshot.data.exception.clientException n'est pas nul

0 votes

Mais snapshot.data.exception n'est pas valide, car il s'agit d'un String, et String n'a pas de membre .exception . Peut-être que si vous utilisiez un autre type qui a un système de contrôle de la qualité exception membre ce serait plus valable, mais je ne recommanderais toujours pas de le faire de cette façon.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X