How to dynamically build rich text with FutureBuilder in Flutter
Obviously, your snapshot.data
was null when you returned it.
I would just skip the connection states and just query snapshot.hasData
and snapshot.hasError
to find out whether data is available.
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());
}
That said, you should improve two things here: Your future should be a field in your state. If you just recreate it every build cycle, it will be recreated in situations you do not need to recreate it. For example it will be run again and again and again if the user changes landscape to portrait mode, although that will not change their display name at all. But you will query it every time, because that is how you set up your build function.
The second thing is you are mixing data retrieval and UI code. Your future is getting data. But you mixed it in with creating the control. Put your UI code in the build function and move your async data retrieval code out of there. That data retrieval code is the future you should have in your builder.
This is the function that actually does the async task of getting your data:
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;
}
Now your futureBuilder could look like this:
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());
}
Where gettingUserData
is a Future<Map<string, string>>
field in your state class, that you set whenever data['comment']
changes:
gettingUserData = CreateUserNameMap(data['comment'], context.read<UserDataModel>());
A good place would probably be initState
. But maybe you have a different setup.
khagen
Updated on December 19, 2022Comments
-
khagen over 1 year
I am trying to look for '@' in a piece of text and then asynchronously gather a displayName for that tag. The problem I am running into is that my FutureBuilder keeps returning null.
The error I get is this.
════════ 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.
My code is this:
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, ); }
There should probably be a simple fix but I can't find it
-
pskink about 3 yearsbefore
return snapshot.data
try toprint(snapshot.data)
- what yoiu see on the logs? -
khagen about 3 years@pskink it prints null twice
-
pskink about 3 yearsand what about
print(snapshot.error)
? -
khagen about 3 years@pskink
type '(dynamic) => Future<TextSpan>' is not a subtype of type '(String) => InlineSpan' of 'f'
-
pskink about 3 yearsso
snapshot.error
is not null - thats why yourbuilder
returns null - you have to fix that error first (located inbuildSelectableText
) -
khagen about 3 years@pskink I can't make heads or tails out of this error and I honestly have no idea where I've gone wrong. It doesn't give any linenumber either. Do you have any idea?
-
-
khagen about 3 yearsI can see that I am getting my snapshot data is null, hence I am asking this question. Initially I tried to only return TextSpan, but FutureBuilder wouldn't allow it because it wasn't a widget. Would you care to show me how I would be able to improve this code?
-
nvoigt about 3 yearsThat's why I called it "obvious". Did you try the code changes I suggested?
-
khagen about 3 yearsI see what you're saying. Though I am still unsure how to proceed. The best place for FutureBuilder would be around the second TextSpan, but it doesn't allow that so I had to put around SelectableText
-
nvoigt about 3 yearsWell first you find out what your error is. Then, you can probably create a future that returns all your needed data in a form that you can use it syncronized. For example, you could build a map of (words starting with an @)/displayname in your future and then use the map in your builder, all perfectly syncronous.
-
nvoigt about 3 years@khagen I added more code how to change your logic and not mix UI and business logic.