6

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

Moor 查询实现

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 端

基础视图模型

class BaseViewModel extends ChangeNotifier {

  ViewState _state = ViewState.IDLE;

  ViewState get state => _state;

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

任务视图模型

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(),
    ),
  );
}

任务视图

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 也会出现错误。

正如我之前提到的,当应用程序重新启动时,所有数据库值都被加载到 listview 中。但是通过放置一个调试点,我注意到第一次_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 错误。并且渲染的 List 在任务名称之前也有一个边距。列表视图不适合手机屏幕。看看下面的截图。

在此处输入图像描述

我做了一些关于在 Flutter 中处理流的研究。这个例子出现了。根据this我的语法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

4 回答 4

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"),
    }
)
于 2020-10-26T12:26:34.777 回答
0

流提供者示例

我假设大多数到达这个问题的人都想知道为什么 StreamProvider 不会导致 UI 的重建。因此,这里有一个更通用的 StreamProvider 示例来说明 StreamProvider 使用来自 Stream 的新数据更新 UI。

Github 上的完整代码示例。

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

它使用 Provider 和 english_words 包。

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

发布规范.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

主要.dart

导入提供者和english_words:

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

然后用下面的替换_MyHomePageStateState 类,加上下面的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();
}
于 2020-11-01T22:12:17.070 回答
0

因为您提供的代码是分数,我只能从中猜测问题。

小部件消费者部分在TaskView.

context.watch<List<DisplayTaskData>>()

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

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

TaskRepository 中的 getAllTask​​s()

从更新的代码和原始代码来看,我认为您想在流数据进入时进行一些数据修改。原始代码更可能是您想要的,但也许您想放在formattedTasksmap 函数中。

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在主函数中使用。请检查提供者的文件

final taskViewModel = TaskViewModel();
return MultiProvider(
  providers: [
    ChangeNotifierProvider.value(value: taskViewModel),
    StreamProvider.value(value: taskViewModel.getAllTasks()),
  ],
  child: ...
于 2020-11-01T14:47:04.843 回答
-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(所有输出都将在此处列出)

希望这将为您的学习增添更多子弹。

于 2020-10-26T17:01:39.697 回答