Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter StreamProvider not pushing data into UI

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.

like image 587
Chathuranga Shan Jayarathna Avatar asked Oct 20 '20 15:10

Chathuranga Shan Jayarathna


1 Answers

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"),
    }
)
like image 149
Apoleo Avatar answered Oct 25 '22 13:10

Apoleo