Flutter StreamProvider 没有将数据推送到 UI
Posted
技术标签:
【中文标题】Flutter StreamProvider 没有将数据推送到 UI【英文标题】:Flutter StreamProvider not pushing data into UI 【发布时间】:2020-10-20 15:09:14 【问题描述】:我是新来的颤振。我实现了这个示例应用程序来了解 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 端
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(),
),
);
任务视图
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 函数中的实现是否正确。但是,我仍然对ChangeNotifierProvider
和StreamProvider
使用相同的对象
更新
首先,对于两次粘贴 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 错误。并且渲染的 List 在任务名称之前也有一个边距。列表视图不适合手机屏幕。看看下面的截图。
我做了一些关于在 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;
通过此更改,列表甚至不会在应用重新启动时加载。在这一点上,我不清楚错误在哪里。我试图缩小范围。我希望这些更新的信息能帮助某人查明此代码中的问题并帮助我。
感谢大家的回答。
【问题讨论】:
考虑通过删除所有不必要的细节并只留下核心问题来改进这个问题。最好能将其剥离为单个代码块。 【参考方案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"),
)
【讨论】:
在这种情况下你还不如使用快照数据而不是context.watch
?
是的,我只是包装证明将 StreamBuilder 放在哪里【参考方案2】:
因为你提供的代码是小数,我只能猜出问题所在。
TaskView
中的小部件消费者部分。
context.watch<List<DisplayTaskData>>()
您应该使用您在提供程序中放置的类。从下面的代码中,您应该使用 TaskViewModel 类在数据更改时接收通知。
providers: [
ChangeNotifierProvider(create: (_) => taskViewModel),
StreamProvider(create:(context) => taskViewModel.getAllTasks())
],
TaskRepository 中的getAllTasks()
从更新后的代码和原始代码来看,我认为您想在流数据进入时进行一些数据修改。原始的更可能是您想要的,但也许您想将formattedTasks
放在 map 函数中。
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: ...
【讨论】:
【参考方案3】: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
导入提供商和english_words:
import 'package:provider/provider.dart';
import 'package:english_words/english_words.dart';
然后将_MyHomePageState
State 类替换为下面的,加上下面的RowItem
类。
注意:
在 StreamProvider consumer
/context.watch
下方,还添加了一个 StreamBuilder 用于比较。 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();
【讨论】:
【参考方案4】:每当我在 Flutter 中工作时,我都喜欢使用 BLoC 模式来编写代码。这也很容易扩展。 这是参考的链接。 https://pub.dev/packages/flutter_bloc
一般需要创建一个widget文件夹,文件夹包含
-
Screen.dart(每个状态的纯 UI 都会转到此处)
Widget-Name.dart(您将在此处完成所有导入和块管理)
widget_bloc.dart(你们所有的企业登录都会到这里)
event.dart(用户的所有操作都会在这里列出)
state.dart(所有输出都将在此处列出)
希望这将为您的学习增添更多亮点。
【讨论】:
以上是关于Flutter StreamProvider 没有将数据推送到 UI的主要内容,如果未能解决你的问题,请参考以下文章
在 Flutter 中使用 StreamProvider 4.0.1 从 Firebase 监控身份验证状态
您是不是必须在 Flutter 中手动处理来自 streamprovider 的流?
Flutter:如何处理使用 StreamProvider、FireStore 和使用 Drawer
Flutter Riverpod:使用 StreamProvider 返回 2 个流