Flutter StreamProvider not pushing data into UI

1.6k views Asked by At

I'm new to flutter. I implemented this sample application to get an idea about SQLite and provider state management. This is a simple task management system. I used MOOR to handle the SQLite database. The problem I'm having is tasks list is not getting updated after adding the task. If I restart the application I can see that the task has been saved in the database successfully.

Moor query implementation

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

TaskRepository

Here I'm going to paste the whole class to get the idea about my implementation.

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

  }
}

I have a view model layer to push values to the UI side

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);

  }

}

Main function

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

What I expect to happen is When I insert a task getAllTasks() stream should stream that newly added task and the list should get an automatic update.

The reason for pasting the whole code of the repository and the ViewModel is to show that I used the same class from both insert and retrieve tasks. I couple them together because both of those operations are Task-related and planning to have updated and delete functions also in the same class. I'm emphasizing this because I have a doubt whether the implementation in the main function is correct or not. But still, I'm using the same object for ChangeNotifierProvider and StreamProvider

Update

First of all, I apologize for the pasting TaskRepository class code twice. In that place, My ViewModel class should have come and I have corrected that above.

Few things I change from the original code. First, the AppDatabase class was not a singleton class so there was an error on Logcat even though the app didn't crash.

As I mentioned before when on application restart all the database value is loaded into listview. But by putting a debugging point I notice that the first time _taskListView method trigger result list is null and it was giving an error because I access that index. I don't know the reason for that but I added a null check and fixed the issue.

After doing these changes and I try to insert a task and see what happens. So on insert recode Logcat gave the following log.

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 []

So according to this log watchAllTasks() get triggered after inserting a recode. But UI is not getting updated. I check by putting debugging point even getAllTasks() in view model get triggered. This seems a UI bug. And rendered List also having a margin before the task name. And list view is not fitting the phone screen. Check out the below screenshot.

enter image description here

I did some research about handling stream in Flutter. and this example came up. According to this my syntax are wrong in getAllTasks() in the repository. So I changed the function according to that like below.

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;
  }

With this change, the list is not even loading on the app restart. At this point, I'm not clear where the error is even though. I tried to narrow it down. I hope this updated information will help someone to pinpoint the issue in this code and help me.

Thanks for all the answers.

4

There are 4 answers

2
Apoleo On

Wrap directly ListView in a StreamBuilder listening to the interested stream to listen to

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"),
    }
)
0
Piyush Dubey On

Whenever I work in Flutter, I preferred using BLoC Pattern for the Code. This is also easily Scalable. Here is the link for ref. https://pub.dev/packages/flutter_bloc

Generally you need to create a widget folder and the folder contains

  1. Screen.dart(Pure UI will go here for each state)
  2. Widget-Name.dart(you will do all import and bloc management here)
  3. widget_bloc.dart(All of you business login will go here)
  4. event.dart(All of the user's action will be listed here)
  5. state.dart (All of the output will be listed there)

Hope this will add more bullets to your learning.

0
yellowgray On

Because the code you provided is fractional, I can only guess the problem from.

Widget consumer part in TaskView.

context.watch<List<DisplayTaskData>>()

You should consume the class you put in your provider. From code below, you should use TaskViewModel class the receive notification when data change.

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

getAllTasks() in TaskRepository

From the updated code and the original code, I think you want to do some data modification when stream data come in. The original one is more likely you want but maybe you would like to put formattedTasks inside the map function.

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;
}

Other advises I have to mention:

*DON'T reuse an existing ChangeNotifier using the default constructor

You shouldn't use create: (_) => variable in the main function if it is created. Please check document in provider

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

StreamProvider Example

I'm assuming most arriving to this question are wondering why StreamProvider isn't causing rebuilds of UI. So here's a more generic example of a StreamProvider to illustrate a StreamProvider updating the UI with new data from a Stream.

Full code example here on Github.

This is built on the default Counter example when you create a new Flutter project in Android Studio.

It uses Provider and the english_words packages.

In Android Studio, you'll need to edit pubspec.yaml and main.dart.

pubspec.yaml

In pubspec.yaml, add a couple package dependencies for this example:

  provider: ^4.3.2+2
  english_words: 3.1.5

e.g.

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 Provider & english_words:

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

Then replace the _MyHomePageState State class with the below, plus the RowItem class below that.

Note: Below the StreamProvider consumer/context.watch there is also a StreamBuilder added for comparison. Both the SteamProvider and StreamBuilder will show the same data on the 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;
  }
}

Now stop/restart the project and then you can click the FloatingActionButton to increment the counter and add events to the Stream.

The RowItem objects should show up in both the StreamProvider and the StreamBuilder

For those not using Android Studio the StatefulWidget was this:

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

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