Flutter StreamProvider无法将数据推送到UI

时间:2020-10-20 15:09:14

标签: flutter dart flutter-moor

我是新手。我实现了该示例应用程序,以了解有关SQLite和提供程序状态管理的知识。这是一个简单的任务管理系统。我使用MOOR处理SQLite数据库。我遇到的问题是添加任务后任务列表没有更新。如果我重新启动该应用程序,则可以看到该任务已成功保存在数据库中。

停泊查询实现

Future<List<Task>> getAllTasks() => select(tasks).get();
Stream<List<Task>> watchAllTasks() => select(tasks).watch();
Future<int> insertTask(TasksCompanion task) => into(tasks).insert(task);

任务存储库

在这里,我将粘贴整个类以了解有关实现的想法。

class TaskRepository{

  Stream<List<DisplayTaskData>> getAllTasks(){

    var watchAllTasks = AppDatabase().watchAllTasks();
    final formattedTasks = List<DisplayTaskData>();

    return watchAllTasks.map((tasks) {

      tasks.forEach((element) {

        var date = element.dueDate;
        String dueDateInString;

        if(date != null){
          dueDateInString = ""
              "${date.year.toString()}-"
              "${date.month.toString().padLeft(2,'0')}-"
              "${date.day.toString().padLeft(2,'0')}";
        }

        var displayTaskData = DisplayTaskData(task : element.task, dueDate: dueDateInString);
        formattedTasks.add(displayTaskData);

      });

      return formattedTasks;

    });
  }

  Future<Resource<int>> insertTask(TasksCompanion task) async{

    try {

      int insertTaskId = await AppDatabase().insertTask(task);
      return Resource(DataProcessingStatus.SUCCESS, insertTaskId);

    }on InvalidDataException catch (e) {

      var displayMessage = DisplayMessage(
          title : "Data Insertion Error",
          description : "Something went wrong please try again"
      );

      return Resource.displayConstructor(DataProcessingStatus.PROCESSING_ERROR,displayMessage);
    }

  }
}

我有一个视图模型层,可将值推送到UI端

BaseViewModel

class BaseViewModel extends ChangeNotifier {

  ViewState _state = ViewState.IDLE;

  ViewState get state => _state;

  void setState(ViewState viewState) {
    _state = viewState;
    notifyListeners();
  }
}

TaskViewModel

class TaskViewModel extends BaseViewModel{

  final TaskRepository _repository = TaskRepository();
 
  DateTime _deuDate;

  Stream<List<DisplayTaskData>> getAllTasks(){
     return _repository.getAllTasks().map((formattedTasks) => formattedTasks);
  }

  Future<void> insertTask() async {

    setState(ViewState.PROCESSING);
    var tasksCompanion = TasksCompanion(task: Value(_taskValidation.value),dueDate: Value(_deuDate));
    insertTaskStatus  = await _repository.insertTask(tasksCompanion);
    setState(ViewState.IDLE);

  }

}

主要功能

void main() {
  Stetho.initialize();

  final taskViewModel = TaskViewModel();

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => taskViewModel),
        StreamProvider(create:(context) =>  taskViewModel.getAllTasks())
      ],
      child: MyApp(),
    ),
  );
}

TaskView

class TaskView extends StatelessWidget {

  DateTime selectedDate = DateTime.now();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Tasks'),
        ),
        body : ListView.builder(
            itemCount: context.watch<List<DisplayTaskData>>()?.length,
            itemBuilder: (context, index) => _taskListView(context, index),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            showMaterialModalBottomSheet(
              context: context,
              builder: (context, scrollController) =>
                  Container(
                    child: bottomSheet(context),
                  ),
            );
          },
          child: Icon(
            Icons.add,
            color: Colors.white,
          ),
          backgroundColor: Colors.blueAccent,
        ));
  }

  Widget _taskListView(BuildContext context, int index) {

    var task = context.watch<List<DisplayTaskData>>()[index];

    return Row(
      children: <Widget>[
        Align(
          alignment: Alignment.topLeft,
          child: Column(children: <Widget>[
            Text(task.task),
            SizedBox(height: 8),
            Text("hardcoded value")
    ],),
        )
      ],
    );
  }
}

我希望发生的是,当我插入任务getAllTasks()时,流应该流处理新添加的任务,并且列表应该得到自动更新。

粘贴存储库和ViewModel的整个代码的原因是为了表明我在插入和检索任务中使用了相同的类。我将它们结合在一起是因为这两个操作都与任务相关,并且计划在同一类中也具有更新和删除功能。我强调这一点是因为我怀疑main函数中的实现是否正确。但是,尽管如此,我仍在ChangeNotifierProviderStreamProvider

中使用同一对象

更新

首先,我为粘贴TaskRepository类代码两次表示歉意。在那个地方,我的ViewModel类应该已经来了,而我已经在上面进行了更正。

我对原始代码所做的更改很少。首先,AppDatabase类不是单例类,因此即使应用程序没有崩溃,Logcat上也存在错误。

如前所述,在应用程序重新启动时,所有数据库值均已加载到列表视图中。但是通过放置调试点,我注意到第一次_taskListView方法触发结果列表为空,并且由于我访问该索引而给出了错误。我不知道是什么原因,但是我添加了一个空检查并解决了该问题。

进行了这些更改之后,我尝试插入一个任务,看看会发生什么。因此,在插入重新编码时,Logcat给出了以下日志。

2020-10-28 21:02:29.845 4258-4294/com.example.sqlite_test I/flutter: Moor: Sent INSERT INTO tasks (task, due_date) VALUES (?, ?) with args [Task 8, 1604082600]
2020-10-28 21:02:29.905 4258-4294/com.example.sqlite_test I/flutter: Moor: Sent SELECT * FROM tasks; with args []

因此,根据此日志watchAllTasks(),在插入重新编码后会被触发。但是UI尚未更新。我通过在视图模型中放置调试点甚至getAllTasks()来进行检查。这似乎是一个UI错误。并且渲染的列表在任务名称之前也有一个空白。并且列表视图不适合电话屏幕。请查看以下屏幕截图。

enter image description here

我做了一些关于在Flutter中处理流的研究。然后这个example出现了。据此,我的语法在存储库中的getAllTasks()中是错误的。因此,我按照如下所示更改了功能。

Stream<List<DisplayTaskData>> getAllTasks() async*{

    var watchAllTasks = AppDatabase().watchAllTasks();
    final formattedTasks = List<DisplayTaskData>();

    watchAllTasks.map((tasks) {

      tasks.forEach((element) {

        var date = element.dueDate;
        String dueDateInString;

        if(date != null){
          dueDateInString = ""
              "${date.year.toString()}-"
              "${date.month.toString().padLeft(2,'0')}-"
              "${date.day.toString().padLeft(2,'0')}";
        }

        var displayTaskData = DisplayTaskData(task : element.task, dueDate: dueDateInString);
        formattedTasks.add(displayTaskData);

      });

    });
      yield formattedTasks;
  }

进行此更改后,即使重新启动应用程序,列表也无法加载。在这一点上,我仍然不清楚错误在哪里。我试图缩小范围。我希望这些更新的信息将有助于某人在此代码中指出问题并为我提供帮助。

感谢所有答案。

4 个答案:

答案 0 :(得分:1)

将ListView直接包装在StreamBuilder中,以收听感兴趣的流以收听

StreamBuilder<List<DisplayTaskData>>(
    stream: getAllTasks(), // provide the correct reference to the stream
    builder: (context, snapshot) {
        if (snapshot.data != null)
            // here your old ListView
            return ListView.builder(
                // itemCount: snapshot.data.length, // <- this is better
                itemCount: contextsnapshot.watch<List<DisplayTaskData>>()?data.length,
                // here you probably can pass directly the element snapshot.data[index]
                itemBuilder: (context, index) => _taskListView(context, index),
            );
        else
            return Text("No Tasks"),
    }
)

答案 1 :(得分:0)

由于您提供的代码是零碎的,所以我只能从中猜测问题所在。

TaskView中的小部件使用者部分。

context.watch<List<DisplayTaskData>>()

您应该使用放置在提供程序中的类。从下面的代码,您应该使用TaskViewModel类在数据更改时接收通知。

providers: [
  ChangeNotifierProvider(create: (_) => taskViewModel),
  StreamProvider(create:(context) =>  taskViewModel.getAllTasks())
],

TaskRepository中的getAllTask​​s()

从更新的代码和原始代码中,我认为您想在流数据传入时进行一些数据修改。原始数据更可能是您想要的,但也许您想将formattedTasks放在地图内功能。

Stream<List<DisplayTaskData>> getAllTasks(){
  var watchAllTasks = AppDatabase().watchAllTasks();
  return watchAllTasks.map((tasks) {
    final formattedTasks = List<DisplayTaskData>();
    tasks.forEach((element) {
    var date = element.dueDate;
    String dueDateInString;
    if(date != null){
      dueDateInString = ""
          "${date.year.toString()}-"
          "${date.month.toString().padLeft(2,'0')}-"
          "${date.day.toString().padLeft(2,'0')}";
    }
    var displayTaskData = DisplayTaskData(task : element.task, dueDate: dueDateInString);
    formattedTasks.add(displayTaskData);
  });
  return formattedTasks;
}

我不得不提的其他建议:

*不要使用默认构造函数重用现有的ChangeNotifier

如果已创建,则不应在主函数中使用create: (_) => variable。 请检查document in provider

final taskViewModel = TaskViewModel();
return MultiProvider(
  providers: [
    ChangeNotifierProvider.value(value: taskViewModel),
    StreamProvider.value(value: taskViewModel.getAllTasks()),
  ],
  child: ...

答案 2 :(得分:0)

StreamProvider示例

我以为大多数人都想知道为什么StreamProvider不会导致UI的重建。因此,这是StreamProvider的一个更通用的示例,用于说明StreamProvider用来自Stream的新数据更新UI。

完整代码示例here on Github

当您在Android Studio中创建新的Flutter项目时,这是基于默认的Counter示例的。

它使用Provider和english_words软件包。

在Android Studio中,您需要编辑pubspec.yaml和main.dart。

pubspec.yaml

pubspec.yaml中,为该示例添加几个程序包依赖项:

  provider: ^4.3.2+2
  english_words: 3.1.5

例如

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.3
  provider: ^4.3.2+2
  english_words: 3.1.5

main.dart

导入提供程序和英语单词:

import 'package:provider/provider.dart';
import 'package:english_words/english_words.dart';

然后将_MyHomePageState状态类替换为下面的类,并在其下方替换RowItem类。

注意: 在StreamProvider consumer / context.watch下,还添加了 Stream Builder 用于比较。 SteamProvider和StreamBuilder都将在UI上显示相同的数据。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  List<RowItem> row;
  StreamController<List<RowItem>> streamController =
      StreamController.broadcast();

  @override
  void initState() {
    super.initState();
    row = [RowItem(_counter)];
    streamController.add(row);
  }

  @override
  void dispose() {
    streamController.close();
    super.dispose();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
      row.add(RowItem(_counter));
      streamController.add(row);
    });
  }

  @override
  Widget build(BuildContext context) {
    /// STREAM*PROVIDER* HERE
    /// - wrapped Scaffold in Builder to make consuming widgets "children" of StreamProvider
    /// instead of siblings. InheritedWidgets like StreamProvider are only useful
    /// to children widgets who inherit its context from below in the widget tree hierarchy.
    /// Often you'd wrap your entire MyApp with a Provider, but this keeps this example
    /// more concise.
    /// https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple#changenotifierprovider
    return StreamProvider<List<RowItem>>.value(
      initialData: row,
      value: streamController.stream,
      child: Builder( // <-- Added to make everything below
        builder: (context) { //<-- this context, children of/inherit from StreamProvider
          return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Expanded(
                    flex: 1,
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          'You have pushed the button this many times:',
                        ),
                        Text(
                          '$_counter',
                          style: Theme.of(context).textTheme.headline4,
                        )
                      ],
                    ),
                  ),
                  Expanded(
                    flex: 2,
                    child: Container(
                      color: Colors.lightBlueAccent,

/// CONSUMER / CONTEXT.WATCH of STREAM*PROVIDER* HERE
                      child: ListView.builder(
                        itemCount: context.watch<List<RowItem>>().length,
                        itemBuilder: (context, index) {
                          List<RowItem> _row = context.watch<List<RowItem>>();
                          return ListTile(
                            title: Text(
                                '[${_row[index].num} | ${_row[index].name}]'),
                          );
                        },
                      ),
                    ),
                  ),
                  Expanded(
                    flex: 2,
                    child: Container(
                      alignment: Alignment.center,
                      color: Colors.lightGreenAccent,

/// STREAM_BUILDER_ for contrast HERE
                      child: StreamBuilder(
                        initialData: row,
                        stream: streamController.stream,
                        builder: (context, snapshot) {
                          if (snapshot.hasData) {
                            List<RowItem> _row = snapshot.data;
                            return ListView.builder(
                              itemCount: _row.length,
                              itemBuilder: (context, index) {
                                return ListTile(
                                  title: Text(
                                      '[${_row[index].num} | ${_row[index].name}]'),
                                );
                              },
                            );
                          }
                          return Text('Waiting on data...');
                        },
                      ),
                    ),
                  ),
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: _incrementCounter,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
          );
        },
      ),
    );
  }
}

class RowItem {
  int num;
  String name;

  RowItem(this.num) {
    name = WordPair.random().asPascalCase;
  }
}

现在停止/重新启动项目,然后您可以单击FloatingActionButton来增加计数器并将事件添加到Stream中。

RowItem对象应同时出现在StreamProvider和StreamBuilder中

对于那些不使用Android Studio的人来说,StatefulWidget是这样的:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

答案 3 :(得分:-1)

每当我在Flutter工作时,我都喜欢使用BLoC模式作为代码。这也很容易扩展。 这是参考的链接。 https://pub.dev/packages/flutter_bloc

通常,您需要创建一个小部件文件夹,并且该文件夹包含

  1. Screen.dart(每个状态的纯UI都将移至此处)
  2. Widget-Name.dart(您将在此处进行所有导入和块管理)
  3. widget_bloc.dart(您所有的公司登录都将在此处)
  4. event.dart(所有用户操作将在此处列出)
  5. state.dart(所有输出将在此处列出)

希望这会为您的学习增加更多的项目符号。