Flutter FutureBuilder выдает ошибку, но при моделировании работает должным образом - PullRequest
0 голосов
/ 18 июня 2020

Я новичок во Flutter, и в настоящее время у меня проблема с правильной реализацией FutureBuilder во flutter. Я пытаюсь создать страницу пользователя, на которой моя пользовательская информация хранится в Firebase, и каждый раз, когда я обращаюсь к странице пользователя, она извлекает текущие пользовательские данные и показывает данные на странице. Вот код, который я написал для реализации:

class UserPage extends StatefulWidget{
  @override
  UserPageState createState() => UserPageState();
}

class UserPageState extends State<UserPage>{
  String userName;
  String userEmail;
  String collegeName;

  Future _infoInit() async {
    userName = await HelperFunctions.getUserNamePreference();
    userEmail = await HelperFunctions.getUserEmailPreference();
    collegeName = await HelperFunctions.getUserCollegePreference();
  }

  Widget userScaffold(BuildContext context, AsyncSnapshot snapshot){
    return Scaffold(
        appBar: AppBar(
          elevation: 0,
          title: Text(
              userName
          ),
          backgroundColor: Colors.lightBlue,
        ),
        body:Center(
            child: Text("This is User Page")
        )
    );
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: _infoInit(),
        builder: (context,AsyncSnapshot snapshot) => userScaffold(context, snapshot)
    );
  }
}

На данный момент единственная часть, которую я написал, показывает текущего пользователя, вошедшего в систему, на панели приложения, и когда я запускаю код, он кажется, что он успешно работает. Однако, когда я смотрю на студийную консоль android, я вижу, что она действительно сталкивается с ошибкой, которая, как мне кажется, связана с функцией asyn c, выполняемой в виджете FutureBuilder.

enter image description here


Error Message:
Performing hot reload...
Syncing files to device iPhone 11...
Reloaded 7 of 650 libraries in 397ms.

════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building FutureBuilder<dynamic>(dirty, state: _FutureBuilderState<dynamic>#772f1):
A non-null String must be provided to a Text widget.
'package:flutter/src/widgets/text.dart':
Failed assertion: line 298 pos 10: 'data != null'

The relevant error-causing widget was: 
  FutureBuilder<dynamic> file:///Users/nossu3751/Downloads/flutter_project/moim_app/lib/user/profile.dart:39:12
When the exception was thrown, this was the stack: 
#2      new Text (package:flutter/src/widgets/text.dart:298:10)
#3      UserPageState.userScaffold (package:moimapp/user/profile.dart:26:18)
#4      UserPageState.build.<anonymous closure> (package:moimapp/user/profile.dart:41:54)
#5      _FutureBuilderState.build (package:flutter/src/widgets/async.dart:732:55)
#6      StatefulElement.build (package:flutter/src/widgets/framework.dart:4619:28)

Точнее, он говорит, что имя пользователя, которое я пытаюсь использовать в FutureBuilder, равно null, хотя я считаю, что присвоено значение уже через запуск метода _infoInit (), и оно действительно правильно отображается на симуляторе.

Я был бы очень признателен, если бы кто-нибудь сообщил мне, что я здесь делаю не так, и что я могу сделать, чтобы это сообщение больше не появлялось. Заранее большое спасибо!

Ответы [ 2 ]

1 голос
/ 18 июня 2020

Проблема в том, что метод builder FutureBuilder вызывается каждый раз, когда изменяется AsyncSnapshot (и изначально моментальный снимок не имеет данных). Следовательно, в первые несколько раз, когда вызывается builder, userName будет иметь значение null, что даст вам эту ошибку; но через какой-то момент имя пользователя будет получено, и при вызове функции построителя вы увидите имя пользователя на экране правильно.

Идиоматический c способ использования FutureBuilder следующий:

FutureBuilder(
  future: myFuture,
  builder: (context, AsyncSnapshot snapshot) {
    // Try adding this print to your code to see when this method is being executed!
    print('Building with snapshot = $snapshot');

    if (snapshot.hasData) {
      // return widget with data - in your case, userScaffold
    }
    else if (snapshot.hasError) {
      // return widget informing of error
    }
    else {
      // still loading, return loading widget (for example, a CircularProgressIndicator)
    }
  },
);

Итак, изначально функция построителя будет вызываться со снимком без данных (ветвь «else»). В этом случае вы, вероятно, захотите показать виджет загрузки. Затем, через некоторое время, Future завершается, и вызывается функция построителя со снимком, имеющим либо данные, либо некоторую ошибку.

Еще одна важная вещь в вашем коде - это то, что ваша функция _infoInit на самом деле не вернуть что-нибудь. Итак, на самом деле ваш FutureBuilder не использует данные из AsyncSnapshot (что означает, что приведенный выше фрагмент на самом деле не будет работать, поскольку snapshot.hasData никогда не будет истинным). С FutureBuilder вы обычно хотите создать виджет, используя данные, возвращаемые AsyncSnapshot. Вместо этого в вашем коде происходит следующее:

  1. FutureBuilder создан. Это вызывает _infoInit(), который запускает выборку данных из Firebase; вызывается метод построения
  2. FutureBuilder. Он пытается использовать userName, но он имеет значение null, поэтому Flutter показывает неудачное утверждение;
  3. _infoInit() извлекает все данные и возвращает Future (это будущее возвращается автоматически из-за предложения async в подпись метода; однако без предложения return он фактически не возвращает никаких данных). Но несмотря на то, что Future не имеет никаких данных, 3 переменные в состоянии (включая userName) были обновлены и теперь содержат некоторые данные.
  4. Поскольку будущее, переданное FutureBuilder, завершено, метод построителя вызывается снова. На этот раз в userName есть данные, поэтому он строится правильно.

Можно писать код, как вы, но в этом случае вам не нужно использовать FutureBuilder. Вы можете просто вызвать _infoInit() из метода initState() виджета (initState - это метод, вызываемый один раз при первом построении State), а после получения данных вызвать setState (). Вот как это будет выглядеть:

class UserPage extends StatefulWidget {
  @override
  UserPageState createState() => UserPageState();
}

class UserPageState extends State<UserPage> {
  String userName;
  String userEmail;
  String collegeName;
  bool loadingData = true;

  @override
  void initState() {
    _infoInit();
  }

  // This can be void now, since we're changing the state, rather than returning a Future
  void _infoInit() async {
    String userName = await HelperFunctions.getUserNamePreference();
    String userEmail = await HelperFunctions.getUserEmailPreference();
    String collegeName = await HelperFunctions.getUserCollegePreference();
    setState(() {
      // In theory, we could have just updated the state variables above, but the
      // recommended practice is to update state variables inside setState.
      this.userName = userName;
      this.userEmail = userEmail;
      this.collegeName = collegeName;
      loadingData = false;
    });
  }

  Widget userScaffold(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          elevation: 0,
          title: Text(userName),
          backgroundColor: Colors.lightBlue,
        ),
        body: Center(child: Text("This is User Page")));
  }

  @override
  Widget build(BuildContext context) {
    if (loadingData) {
      // We don't have the data yet, so return a widget to indicate some loading state
      return Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }
    return userScaffold(context);
  }
}

Приведенный выше фрагмент не обрабатывает ошибки при выборке данных, что вы, вероятно, захотите сделать. И для этого вы можете использовать флаг под названием 'hasError' или что-то в этом роде, что в конечном итоге даст очень похожий код на то, как написан метод "idiomati c" FutureBuilder builder.

Оба являются действительные подходы; FutureBuilder, возможно, использует меньше кода (и может быть проще использовать, если остальная часть вашего кода уже использует Futures), но в конечном итоге это зависит от ваших предпочтений.

1 голос
/ 18 июня 2020

Вам необходимо использовать ConnectionState внутри вашего построителя. Посмотрите на этот шаблон кода: (В настоящее время ваш конструктор возвращает userScaffold, не дожидаясь завершения работы в будущем)

return FutureBuilder(
        future: yourFuture(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // future complete
            // if error or data is false return error widget
            if (snapshot.hasError) {
              return _buildErrorWidget('SOMETHING WENT WRONG, TAP TO RELOAD');
            }

            // return data widget
            return _buildDataWidget();

            // return loading widget while connection state is active
          } else
            return _buildLoadingWidget();
        },
      );
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...