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.
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.
Wrap directly ListView in a StreamBuilder listening to the interested stream to listen to