Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter, render widget after async call

I would like to render a widget that needs an HTTP call to gather some data.

Got the following code (simplified)

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");
    });
  }
}

My code works like this : the widget using async data takes a collector (which make the http call) and a renderer which will render the widgets with the http data. I create an instance of this widget on the initState() and then I make my async call.

I found some documentation saying that we should use the setState() method to update the widget with the new data but this doesn't work for me.

However when I put some logs, i see the HTTP call is done and the setState() method is called, but the widget does not update.

like image 675
Alexi Coard Avatar asked Apr 19 '18 21:04

Alexi Coard


People also ask

How do I get my Future widget back in Flutter?

How to Assign Future to a Widget In Flutter? StatefulWidget can be used for this purpose. Declare a member variable String _textFromFile = “”; in your State class and update its value on future resolve by using setState() method.

Do not use Buildcontexts across async gaps Flutter?

DON'T use BuildContext across asynchronous gaps. Storing BuildContext for later usage can easily lead to difficult to diagnose crashes. Asynchronous gaps are implicitly storing BuildContext and are some of the easiest to overlook when writing code.

How do you use stateful widget Flutter?

The name of the Stateful Widget is MyApp which is called from the runApp() and extends a stateful widget. In the MyApp class, we override the create state function. This createState() function is used to create a mutable state for this widget at a given location in the tree.

Why is the build () method on state and not StatefulWidget?

Why is the build method on State, and not StatefulWidget? Putting a Widget build(BuildContext context) method on State rather than putting a Widget build(BuildContext context, State state) method on StatefulWidget gives developers more flexibility when subclassing StatefulWidget.


2 Answers

The best way to do this is to use a FutureBuilder.

From the FutureBuilder documentation:

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

The other thing is that you're building your widget outside of the State.build method and saving the widget itself, which is an anti-pattern. You should be actually building the widgets each time in the build method.

You could get this to work without FutureBuilder, but you should save the result of the http call (processed appropriately) and then use the data within your build function.

See this, but note that using a FutureBuilder is a better way to do this and I'm just providing this for you to learn.

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(),
          ),
        ),
      );
    }
  }
}
like image 165
rmtmckenzie Avatar answered Oct 18 '22 02:10

rmtmckenzie


Full Example

Best way for rander widget after async call is using FutureBuilder()

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

In future builder, it calls the future function to wait for the result, and as soon as it produces the result it calls the builder function where we build the widget.


AsyncSnapshot has 3 state:

1. connectionState.none -- In this state future is null
2. connectionState.waiting -- [future] is not null, but has not yet completed
3. connectionState.done -- [future] is not null, and has completed. If the future completed successfully, the [AsyncSnapshot.data] will be set to the value to which the future completed. If it completed with an error, [AsyncSnapshot.hasError] will be true
like image 36
Sanjayrajsinh Avatar answered Oct 18 '22 03:10

Sanjayrajsinh