如何在Flutter中使用FutureBuilder动态构建富文本
How to dynamically build rich text with FutureBuilder in Flutter
我正在尝试在一段文本中查找“@”,然后异步收集该标签的显示名称。我 运行 遇到的问题是我的 FutureBuilder 一直返回 null。
我得到的错误是这样的。
════════ Exception caught by widgets library ════════
The following assertion was thrown building FutureBuilder(dirty, state: _FutureBuilderState#151f7):
A build function returned null.
The offending widget is: FutureBuilder
Build functions must never return null.
我的代码是这样的:
Container(
width: double.infinity,
child: FutureBuilder(
initialData: SelectableText(''),
future: buildSelectableText(context),
builder: (BuildContext context, AsyncSnapshot<SelectableText> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(child: CircularProgressIndicator());
return snapshot.data;
}
),
),
Future<SelectableText> buildSelectableText(BuildContext context) async {
return SelectableText.rich(
TextSpan(
text:'',
children: data["comment"].split(' ').map<InlineSpan>((word) async {
return word.startsWith('@') && word.length > 1 ?
TextSpan(
text: ' '+ (await context.read<UserDataModel>().fetchUser(word)).displayName,
style: TextStyle(color: Colors.blue),
): TextSpan(text: ' ' + word);
}).toList()
),
textAlign: TextAlign.justify,
);
}
可能应该有一个简单的修复方法,但我找不到
显然,您返回时 snapshot.data
为空。
我会跳过连接状态,只查询 snapshot.hasData
和 snapshot.hasError
以查明数据是否可用。
builder: (BuildContext context, AsyncSnapshot<SelectableText> snapshot) {
if (snapshot.hasData) {
return snapshot.data;
}
// what if snapshot.hasError?
// you don't want to stay in loading mode forever in case of errors
return Center(child: CircularProgressIndicator());
}
也就是说,你应该在这里改进两件事:你的未来应该是你所在州的一个领域。如果您只是在每个构建周期重新创建它,那么在您不需要重新创建它的情况下会重新创建它。例如,如果用户将横向模式更改为纵向模式,它将一次又一次地 运行 ,尽管这根本不会改变他们的显示名称。但是你每次都会查询它,因为那是你设置构建函数的方式。
第二件事是您混合了数据检索和 UI 代码。您的未来正在获取数据。但是您将它与创建控件混合在一起。将您的 UI 代码放入构建函数中,并将您的异步数据检索代码移出那里。 数据检索代码是您构建器中应该拥有的未来。
这是实际执行获取数据的异步任务的函数:
Future<Map<String, String>> CreateUserNameMap(String data, UserDataModel model) async {
var userNames = data.split(' ')
.where((word)=> word.startsWith('@') && word.length > 1)
.toSet();
var result = Map<String, String>();
for(var name in userNames) {
var user = await model.fetchUser(name);
result[name] = user.displayName;
}
return result;
}
现在您的 futureBuilder 可能如下所示:
FutureBuilder(
future: gettingUserData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableText.rich(
TextSpan(
text:'',
children: data["comment"].split(' ').map<InlineSpan>((word) {
return word.startsWith('@') && word.length > 1 ?
TextSpan(
text: ' ' + snapshot.data[word]
style: TextStyle(color: Colors.blue),
) : TextSpan(text: ' ' + word);
}).toList()
),
textAlign: TextAlign.justify,
);
}
if(snapshot.hasError) {
// you decide...
}
return Center(child: CircularProgressIndicator());
}
其中 gettingUserData
是您所在州 class 中的一个 Future<Map<string, string>>
字段,您在 data['comment']
更改时设置:
gettingUserData = CreateUserNameMap(data['comment'], context.read<UserDataModel>());
一个好的地方可能是 initState
。但也许您有不同的设置。
我正在尝试在一段文本中查找“@”,然后异步收集该标签的显示名称。我 运行 遇到的问题是我的 FutureBuilder 一直返回 null。
我得到的错误是这样的。
════════ Exception caught by widgets library ════════
The following assertion was thrown building FutureBuilder(dirty, state: _FutureBuilderState#151f7): A build function returned null.
The offending widget is: FutureBuilder Build functions must never return null.
我的代码是这样的:
Container(
width: double.infinity,
child: FutureBuilder(
initialData: SelectableText(''),
future: buildSelectableText(context),
builder: (BuildContext context, AsyncSnapshot<SelectableText> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(child: CircularProgressIndicator());
return snapshot.data;
}
),
),
Future<SelectableText> buildSelectableText(BuildContext context) async {
return SelectableText.rich(
TextSpan(
text:'',
children: data["comment"].split(' ').map<InlineSpan>((word) async {
return word.startsWith('@') && word.length > 1 ?
TextSpan(
text: ' '+ (await context.read<UserDataModel>().fetchUser(word)).displayName,
style: TextStyle(color: Colors.blue),
): TextSpan(text: ' ' + word);
}).toList()
),
textAlign: TextAlign.justify,
);
}
可能应该有一个简单的修复方法,但我找不到
显然,您返回时 snapshot.data
为空。
我会跳过连接状态,只查询 snapshot.hasData
和 snapshot.hasError
以查明数据是否可用。
builder: (BuildContext context, AsyncSnapshot<SelectableText> snapshot) {
if (snapshot.hasData) {
return snapshot.data;
}
// what if snapshot.hasError?
// you don't want to stay in loading mode forever in case of errors
return Center(child: CircularProgressIndicator());
}
也就是说,你应该在这里改进两件事:你的未来应该是你所在州的一个领域。如果您只是在每个构建周期重新创建它,那么在您不需要重新创建它的情况下会重新创建它。例如,如果用户将横向模式更改为纵向模式,它将一次又一次地 运行 ,尽管这根本不会改变他们的显示名称。但是你每次都会查询它,因为那是你设置构建函数的方式。
第二件事是您混合了数据检索和 UI 代码。您的未来正在获取数据。但是您将它与创建控件混合在一起。将您的 UI 代码放入构建函数中,并将您的异步数据检索代码移出那里。 数据检索代码是您构建器中应该拥有的未来。
这是实际执行获取数据的异步任务的函数:
Future<Map<String, String>> CreateUserNameMap(String data, UserDataModel model) async {
var userNames = data.split(' ')
.where((word)=> word.startsWith('@') && word.length > 1)
.toSet();
var result = Map<String, String>();
for(var name in userNames) {
var user = await model.fetchUser(name);
result[name] = user.displayName;
}
return result;
}
现在您的 futureBuilder 可能如下所示:
FutureBuilder(
future: gettingUserData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return SelectableText.rich(
TextSpan(
text:'',
children: data["comment"].split(' ').map<InlineSpan>((word) {
return word.startsWith('@') && word.length > 1 ?
TextSpan(
text: ' ' + snapshot.data[word]
style: TextStyle(color: Colors.blue),
) : TextSpan(text: ' ' + word);
}).toList()
),
textAlign: TextAlign.justify,
);
}
if(snapshot.hasError) {
// you decide...
}
return Center(child: CircularProgressIndicator());
}
其中 gettingUserData
是您所在州 class 中的一个 Future<Map<string, string>>
字段,您在 data['comment']
更改时设置:
gettingUserData = CreateUserNameMap(data['comment'], context.read<UserDataModel>());
一个好的地方可能是 initState
。但也许您有不同的设置。