How to dynamically build rich text with FutureBuilder in Flutter

1,169

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.

Share:
1,169
khagen
Author by

khagen

Updated on December 19, 2022

Comments

  • khagen
    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
      pskink about 3 years
      before return snapshot.data try to print(snapshot.data) - what yoiu see on the logs?
    • khagen
      khagen about 3 years
      @pskink it prints null twice
    • pskink
      pskink about 3 years
      and what about print(snapshot.error) ?
    • khagen
      khagen about 3 years
      @pskink type '(dynamic) => Future<TextSpan>' is not a subtype of type '(String) => InlineSpan' of 'f'
    • pskink
      pskink about 3 years
      so snapshot.error is not null - thats why your builder returns null - you have to fix that error first (located in buildSelectableText)
    • khagen
      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
    khagen about 3 years
    I 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
    nvoigt about 3 years
    That's why I called it "obvious". Did you try the code changes I suggested?
  • khagen
    khagen about 3 years
    I 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
    nvoigt about 3 years
    Well 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
    nvoigt about 3 years
    @khagen I added more code how to change your logic and not mix UI and business logic.