Flutter,异步调用后渲染小部件
Posted
技术标签:
【中文标题】Flutter,异步调用后渲染小部件【英文标题】:Flutter, render widget after async call 【发布时间】:2018-09-30 11:52:06 【问题描述】:我想渲染一个需要 HTTP 调用来收集数据的小部件。
得到以下代码(简体)
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");
);
我的代码是这样工作的:使用异步数据的小部件需要一个收集器(进行 http 调用)和一个渲染器,它将使用 http 数据渲染小部件。 我在 initState() 上创建了这个小部件的一个实例,然后进行异步调用。
我发现一些文档说我们应该使用 setState() 方法用新数据更新小部件,但这对我不起作用。
但是,当我放置一些日志时,我看到 HTTP 调用已完成并调用了 setState() 方法,但小部件没有更新。
【问题讨论】:
Firestore async load and populate listview flutter的可能重复 【参考方案1】:最好的方法是使用FutureBuilder。
来自 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');
,
)
另一件事是您在 State.build 方法之外构建小部件并保存小部件本身,这是一种反模式。您实际上应该每次都在 build 方法中构建小部件。
你可以在没有 FutureBuilder 的情况下让它工作,但你应该保存 http 调用的结果(经过适当处理),然后在你的构建函数中使用数据。
请参阅此内容,但请注意,使用 FutureBuilder 是执行此操作的更好方法,我只是提供此内容供您学习。
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(),
),
),
);
【讨论】:
我的代码中的 Futures 确实搞砸了,你的例子更简单。谢谢,将来会尝试使用futureBuilder;) 考虑存在网络错误的情况也很重要,例如没有互联网连接,如果 (snapshot?.data?.exception?.clientException 是 NetworkException,您可以在 switch 语句中添加检查) return buildEmptyPlaceHolder(); Future Builder 完成后我们是否可以更改状态(SetState)? 如果你打算这样做,你不妨遵循第二个例子,它只是调用 setState 并使用调用的结果。如果你真的想在 FutureBuilder 中做这件事,你有几个选择;一种是简单地将您想要发生的任何事情添加到 Future 的末尾(即Future(...).then((result) doWhatever(); return result;);
。或者您可以在 FutureBuilder 的构建器中执行此操作,但要知道这可以称为多个次。【参考方案2】:
完整示例
Best way在异步调用后使用 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
在future builder中,它调用future函数来等待结果,一旦产生结果,它就会调用我们构建widget的builder函数。
AsyncSnapshot 有 3 个状态: 1. connectionState.none -- 在这种状态下,future 为空 2. connectionState.waiting -- [future] 不为空,但尚未完成 3. connectionState.done -- [future] 不为空,并且已经完成。如果未来成功完成,[AsyncSnapshot.data] 将设置为未来完成的值。如果它以错误完成,[AsyncSnapshot.hasError] 将为真【讨论】:
这不是违反 FutureBuiler 文档中说的 - 未来必须更早获得,例如在 State.initState、State.didUpdateConfig 或 State.didChangeDependencies 期间。在构造 FutureBuilder 时,不能在 State.build 或 StatelessWidget.build 方法调用期间创建它。如果future与FutureBuilder同时创建,那么每次FutureBuilder的parent重建时,异步任务都会重启。【参考方案3】:要添加到@rmtmckenzie 接受的答案,处理出现网络错误时的情况很重要,因为snapshot.data
将有数据但错误将在snapshot.data.exception
所以
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');
,
)
【讨论】:
你在这里做了一些不必要甚至是错误的事情 - 快照不是可选的,所以使用没有意义?在它之后,'data' 在这种情况下应该是一个字符串,所以 data?.exception?实际上是无效的。如果您查看我的答案的第一部分,它已经显示了如何使用snapshot.hasError
和snapshot.error
正确执行此操作。
我试图处理的是snapshot.hasData
为真而snapshot.hasError
为假但snapshot.data
中有错误的情况,即snapshot.data.exception.clientException
不为空的情况
但snapshot.data.exception
无效,因为它是一个字符串,并且字符串没有成员.exception
。也许如果您使用的是具有exception
成员的其他类型,这会更有效,但我仍然不建议这样做。以上是关于Flutter,异步调用后渲染小部件的主要内容,如果未能解决你的问题,请参考以下文章
如何测试 initState() 中有异步调用的 Flutter 应用程序?
当 onDatasetChanged 方法包含异步网络调用 Android 时,小部件 ListView 不刷新