Search code examples
flutterdartflutter-futurebuilder

How to narrow `snapshot` inside `FutureBuilder.builder()` without using null-check (!)


Problem

I am currently learning Flutter and familiar with TS. While Flutter/Dart is known for being strongly-typed, I'm encountering difficulties trying to narrowing down my nullable types (which would be possible in TS).

Specifically, I used FutureBuilder and an async function to fetch data, and tried to build widgets based on those data. The data is List<{ path: String, ... }> for brevity. However, I cannot access snapshot.data:

Problem

My approach looks dumb. I looked up the docs and the closest solution I found to my problem is type promotion, but I believe this is not what I'm looking for.

Code

child: FutureBuilder(
    future: _selectedImages,
    builder: (context, snapshot) {
        // error: dart(unchecked_use_of_nullable_value)
        if (snapshot.hasData) {
            log(snapshot.data[0].path);
        }

        // error: dart(unchecked_use_of_nullable_value)
        if (snapshot.hasData && snapshot.data.isNotEmpty) {
            log(snapshot.data[0].path);
        }

        // this works
        if (snapshot.hasData) {
            final tmp = snapshot.data?[0].path;
            if (tmp != null) {
                log(tmp);
            }
        }
    },
),

Editted for @pskink

I believe I covered all the cases, why the errors still pop up, do you know why ?

pskink

                // this works
                // builder: ((context, snapshot) =>
                //     switch ((snapshot.connectionState, snapshot.data)) {
                //       // TODO: Handle this case.
                //       (ConnectionState.none, _) =>
                //         Text('null ${snapshot.data.toString()}'),
                //       (ConnectionState.waiting, _) => const Text('text'),
                //       (ConnectionState.active, _) => const Text('text'),
                //       (ConnectionState.done, null) => const Text('text'),
                //       (ConnectionState.done, final data?) => const Text('text'),
                //     }),

                builder: (context, snapshot) {
                  switch ((snapshot.connectionState, snapshot.data)) {
                    case (ConnectionState.none, _):
                      return Text('null ${snapshot.data.toString()}');
                    case (ConnectionState.waiting, _):
                      return const Text('text');
                    case (ConnectionState.active, _):
                      return const Text('text');
                    case (ConnectionState.done, null):
                      return const Text('text');
                    case (ConnectionState.done, final data?):
                      return const Text('text');
                    // no error if have this
                    // default:
                    //   return const Text('text');
                  }
                }

Solution

  • snapshot.hasData already mean that snapshot.data != null. You can look at source code of async.dart file:

    bool get hasData => data != null;

    But unfortunately, Dart Analytic doesn't have enough smart to know about a param inside a class isn't null like that.

    Dart can only promote local variables to null safety. The field data of an AsyncSnapshot object can theoretically return anything the next time you read it, so it can't be promoted based on checking that the value you read in one place is not null. Even if that data is final and cannot be setter but promoted, it means that the author can’t change that field in the future. So, promotion is only for local variables.

    If you want a quick fix then you can just put ! behind the data after you check snapshot.hasData and it will be fine:

    if (snapshot.hasData) {
       final data = snapshot.data!;
       // from here data can use without null error
    }
    

    And below is some code I usually use with FutureBuilder:

        FutureBuilder(
          future: _selectedImages,
          builder: (context, snapshot) {
            final data = snapshot.data;
            
            if (data == null) {
              return YourLoadingWidget();
            }
    
            // do whatever you want with non-nullable data here
    
          },
        )