Comments (6)
@subzero911 Can you give me the code to reproduce it?
from mobx.dart.
I can't share the whole project as it's under NDA, but I can share some files which are related to error:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:get_it/get_it.dart';
import 'package:mobx/mobx.dart';
import '../../../i18n/strings.g.dart';
import '../controllers/task_controller.dart';
import '../widgets/task_list/no_tasks_placeholder.dart';
import '../widgets/task_list/task_list_loader.dart';
import '../widgets/task_list/task_list_view.dart';
import '../widgets/task_list/tasks_done_placeholder.dart';
import '../widgets/tasks_error_state.dart';
/// экран задач
class TasksScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _TasksScreenState();
}
class _TasksScreenState extends State<TasksScreen> {
late final TaskController taskController;
@override
void initState() {
super.initState();
taskController = GetIt.I<TaskController>();
}
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
return Scaffold(
body: SafeArea(
child: RefreshIndicator(
onRefresh: () async {
await taskController.fetchTodaysTasks();
await taskController.fetchTaskStats();
},
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: [
Observer(
builder: (context) {
// если ошибка одновременно при загрузке заданий на сегодня и стат по заданиям - показ ошибки на весь экран
if (taskController.fetchTodaysTasksFuture.status == FutureStatus.rejected &&
taskController.fetchTaskStatsByStatusFuture.status == FutureStatus.rejected) {
return TasksErrorState(
errorText: t.tasks.task_list.errors.task_loading_failed,
onRetry: () {
unawaited(taskController.fetchTodaysTasks());
unawaited(taskController.fetchTaskStats());
},
);
}
return Column(
children: [
// баннер Джуниор подписки - Временно отключаем
// JuniorTasksBanner(),
// список заданий
Padding(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10),
child: Observer(
builder: (context) {
// ошибки загрузки отдельно задач на сегодня
if (taskController.fetchTodaysTasksFuture.status == FutureStatus.rejected) {
return TasksErrorState(
errorText: t.tasks.task_list.errors.task_loading_failed,
onRetry: taskController.fetchTodaysTasks,
);
}
// ждем, когда загрузятся задачи
if (taskController.fetchTodaysTasksFuture.status == FutureStatus.pending) {
return TaskListLoader();
}
// если список задач пустой, а статы еще не прогрузились, показываем лоадер
if (taskController.todaysTodoTasksSource.isEmpty &&
taskController.fetchTaskStatsByStatusFuture.status == FutureStatus.pending) {
return TaskListLoader();
}
// если никогда не создавали задач
if (taskController.neverHadTasks) {
return Text('There will be tasks here');
}
// если на сегодня нет задач
if (taskController.noTodoTasksToday) {
// показываем заглушку "Сегодня дел нет"
return NoTasksPlaceholder();
}
// если на сегодня есть задачи
if (taskController.hasTodoTasksToday) {
// показываем список "Дела на сегодня"
return TaskListView(tasks: taskController.todaysTodoTasksWindow.toList());
}
// если на сегодня задач нет, но есть задачи в списке выполненных
if (taskController.allTodaysTasksDone) {
// показываем заглушку "Все дела сделаны"
return TasksDonePlaceholder();
}
return TasksDonePlaceholder();
},
),
),
],
);
},
),
],
),
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get_it/get_it.dart';
import '../../../../i18n/strings.g.dart';
import '../../controllers/task_controller.dart';
import '../../models/task.dart';
import 'task_tile.dart';
class TaskListView extends StatelessWidget {
const TaskListView({super.key, required this.tasks});
final List<Task> tasks;
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final ctrl = GetIt.I<TaskController>();
final showMoreButtonStyle = TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size(50, 36),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
alignment: Alignment.centerLeft,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.tasks.task_list.header, // Дела на сегодня
),
SizedBox(
height: 12.0,
width: 1,
),
AnimatedSize(
alignment: Alignment.topCenter,
duration: 300.ms,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListView.separated(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: tasks.length,
itemBuilder: (ctx, i) {
return TaskTile(
task: tasks[i],
);
},
separatorBuilder: (_, __) => SizedBox(height: 14.0),
),
if (ctrl.showCollapseButton) ...[
if (!ctrl.endReached)
// Показать ещё
TextButton(
onPressed: ctrl.todoTasksAddMore,
style: showMoreButtonStyle,
child: Text(t.tasks.task_list.load_more),
)
else
// Свернуть всё
TextButton(
onPressed: ctrl.collapseTasks,
style: showMoreButtonStyle,
child: Text(t.tasks.task_list.collapse),
),
]
],
),
),
],
);
}
}
// ignore_for_file: library_private_types_in_public_api
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:mobx/mobx.dart';
import '../../../graphql_generated/graphql/task.graphql.dart';
import '../../../utils/logger.dart';
import '../../auth/controllers/user_controller.dart';
import '../../task_tracker/models/task.dart';
import '../../wallet/controllers/wallet_controller.dart';
import '../models/status_to_task_stats.dart';
import '../models/task_decision.dart';
import '../models/task_input.dart';
import '../repositories/task_repository.dart';
part 'task_controller.g.dart';
/// контроллер главной страницы с задачами: "Дела на сегодня" и карусель выполненных
class TaskController = _TaskControllerBase with _$TaskController;
abstract class _TaskControllerBase with Store {
_TaskControllerBase({
required this.taskRepository,
required this.userController,
required this.walletController,
});
final TaskRepository taskRepository;
final UserController userController;
final WalletController walletController;
// * Observables
/// все задачи на сегодня
@observable
ObservableList<Task> _todaysTasks = ObservableList.of([]);
@observable
List<StatusToTaskStats> taskStatsByStatus = <StatusToTaskStats>[];
// * Computeds
@computed
TaskStats get allTaskStats {
int total = 0;
int unseen = 0;
for (var s in taskStatsByStatus) {
total += s.stats.total;
unseen += s.stats.unseen;
}
return TaskStats(total: total, unseen: unseen);
}
// выборка только задач в статусе Todo из сегодняшних
@computed
ObservableList<Task> get todaysTodoTasksSource {
final tasks = _todaysTasks.where((e) => e.status.value == TaskStatus.created).toList();
tasks.sort(createdSortFunction);
return tasks.asObservable();
}
// выборка только задач в статусе Done из сегодняшних
@computed
ObservableList<Task> get todaysDoneTasks {
final tasks = _todaysTasks.where((e) => e.status.value == TaskStatus.done).toList();
tasks.sort(doneSortFunction);
return tasks.reversed.toList().asObservable();
}
/// задачи на сегодня, с пагинацией по $limit шт.
@observable
var todaysTodoTasksWindow = <Task>[].asObservable();
int _offset = 0;
final int _limit = 10;
// показывать кнопку "Показать ещё" только если число задач превышает 1 страничку
@computed
bool get showCollapseButton => todaysTodoTasksSource.length > _limit;
// дошли до конца списка
@computed
bool get endReached => _offset >= todaysTodoTasksSource.length;
// если задачи отсутствуют и статы по ним отсутствуют - никогда не создавали задач
@computed
bool get neverHadTasks => _todaysTasks.isEmpty && taskStatsByStatus.isEmpty;
// если задачи к выполнению отсутствуют, но статы есть - значит, нет задач на сегодня (иначе показываем neverHadTasks)
@computed
bool get noTodoTasksToday => todaysTodoTasksSource.isEmpty && todaysDoneTasks.isEmpty && taskStatsByStatus.isNotEmpty;
@computed
bool get hasTodoTasksToday => todaysTodoTasksSource.isNotEmpty;
@computed
bool get allTodaysTasksDone => todaysTodoTasksSource.isEmpty && todaysDoneTasks.isNotEmpty;
// * Reactions
// * Futures
@observable
ObservableFuture<List<Task>> fetchTodaysTasksFuture = tasksEmptyResponse;
static ObservableFuture<List<Task>> tasksEmptyResponse = ObservableFuture.value([]);
@observable
ObservableFuture<List<StatusToTaskStats>> fetchTaskStatsByStatusFuture = taskStatsEmptyResponse;
static ObservableFuture<List<StatusToTaskStats>> taskStatsEmptyResponse = ObservableFuture.value([]);
// * Actions
/// получает список задач на сегодня
@action
Future<void> fetchTodaysTasks() async {
fetchTodaysTasksFuture = ObservableFuture(
taskRepository.getTasks(
user: userController.user!,
baseDate: DateTime.now(),
statuses: [
Enum_TaskStatus.CREATED,
Enum_TaskStatus.DONE,
],
// 1 день, начиная с сегодняшнего дня == "сегодня"
direction: Enum_TaskOffsetDirection.AFTER,
limit: 1,
),
);
final tasks = await fetchTodaysTasksFuture;
if (tasks.isNotEmpty) {
_todaysTasks = tasks.asObservable();
log.d('Сегодняшние задачи загрузились успешно: ${todaysTodoTasksSource.length} шт.');
// добавляем первые 10 задач в "План на сегодня"
_initTasks();
} else {
log.d('Список задач на сегодня пуст');
}
}
/// получает количество задач по статусам
@action
Future<void> fetchTaskStats() async {
final userId = userController.user?.id;
if (userId == null) {
throw Exception('userId == null');
}
fetchTaskStatsByStatusFuture = ObservableFuture(
taskRepository.getTaskStatsByStatus(userId),
);
taskStatsByStatus = await fetchTaskStatsByStatusFuture;
}
@action
Future<void> createTask(TaskInput input) async {
Task createdTask = await taskRepository.createTask(input.toDto());
log.d('Задача ${createdTask.toString()} создана');
// обновить статы (чтобы появились кнопки фильтров)
await fetchTaskStats();
// добавляем новую задачу в начало списка
_todaysTasks.add(createdTask);
_todaysTasks.sort(createdSortFunction);
todaysTodoTasksWindow.add(createdTask); // отражаем в "План на сегодня"
todaysTodoTasksWindow.sort(createdSortFunction);
_offset++;
}
@action
Future<Task?> findTaskById(String id) {
return taskRepository.findTaskById(taskId: id, user: userController.user!);
}
@action
Future<void> editTask(String taskId, TaskInput input) async {
Task editedTask = await taskRepository.editTask(
taskId,
userController.user!.id,
input.toDto(),
);
log.d('Задача ${editedTask.toString()} изменена');
// удалить старую задачу из списка и добавить отредактированную
int index = _todaysTasks.indexWhere((e) => e.id == taskId);
_todaysTasks.removeAt(index);
_todaysTasks.insert(index, editedTask);
// если задача отображается в делах на сегодня, заменим её тоже
final target = todaysTodoTasksWindow.singleWhereOrNull((e) => e.id == taskId);
if (target != null) {
final todoTasksWindowIndex = todaysTodoTasksWindow.indexWhere((e) => e.id == taskId);
todaysTodoTasksWindow.removeAt(todoTasksWindowIndex);
todaysTodoTasksWindow.insert(todoTasksWindowIndex, editedTask);
}
}
@action
Future<void> deleteTaskById(String taskId) async {
await taskRepository.deleteTask(taskId, userController.user!.id);
_todaysTasks.removeWhere((e) => e.id == taskId);
// отражаем в "Делах на сегодня"
final target = todaysTodoTasksWindow.singleWhereOrNull((e) => e.id == taskId);
if (target != null) {
todaysTodoTasksWindow.remove(target);
}
_offset--;
}
/// отклонить или принять задачу (взрослый)
@action
Future<TaskStatus> takeDecision(String taskId, TaskDecision decision) async {
final newStatus = await taskRepository.takeDecision(
taskId,
userController.user!.id,
decision.toDto(),
);
// Обновляем таску и статы и баланс при необходимости
await updateTask(taskId, newStatus);
return newStatus;
}
/// пометить задачу выполненной (ребенок)
@action
Future<TaskStatus> markAsDone(String taskId, String doneBy) async {
final newStatus = await taskRepository.markAsDone(taskId, doneBy);
// Обновляем таску и статы и баланс при необходимости
await updateTask(taskId, newStatus);
return newStatus;
}
@action
Future<void> updateTask(String taskId, TaskStatus newStatus) async {
// Обновляем текущую таску
final task = _todaysTasks.singleWhereOrNull((t) => t.id == taskId);
if (task != null) {
if (newStatus == TaskStatus.verify) {
task.verifyAt.value = DateTime.now();
}
if (newStatus == TaskStatus.done) {
task.doneAt.value = DateTime.now();
// обновляем баланс
unawaited(walletController.refetchCurrentAccount());
}
task.status.value = newStatus;
}
// рефетчим статы
await fetchTaskStats();
}
// при получении задач с сервера
@action
void _initTasks() {
todaysTodoTasksWindow.clear();
_offset = 0;
todoTasksAddMore();
}
/// по нажатию на кнопку "Показать ещё"
@action
void todoTasksAddMore() {
if (_offset + _limit > todaysTodoTasksSource.length) {
todaysTodoTasksWindow.addAll(todaysTodoTasksSource.getRange(_offset, todaysTodoTasksSource.length));
_offset = todaysTodoTasksSource.length;
} else {
todaysTodoTasksWindow.addAll(todaysTodoTasksSource.getRange(_offset, _offset + _limit));
_offset += _limit;
}
}
/// по нажатию на кнопку "Свернуть"
@action
void collapseTasks() {
_initTasks();
}
int createdSortFunction(Task a, Task b) {
if (a.endAt == null) {
return 1;
}
if (b.endAt == null) {
return -1;
}
final dateDiff = a.endAt!.difference(b.endAt!).inMinutes;
if (dateDiff != 0) {
return dateDiff;
} else {
return b.cost - a.cost;
}
}
int doneSortFunction(Task a, Task b) {
if (a.doneAt.value == null) {
return 1;
}
if (b.doneAt.value == null) {
return -1;
}
final dateDiff = a.doneAt.value!.difference(b.doneAt.value!).inMinutes;
if (dateDiff != 0) {
return dateDiff;
} else {
return b.cost - a.cost;
}
}
}
from mobx.dart.
@subzero911 Shouldn't _offset
be observable
?
When showCollapseButton and endReached are changing, UI rebuilds successfully.
But if I wrap inner TextButton in additional Observer, for some reason it doesn't rebuild properly.
Observer->Column->TaskListView->Column->Observer->Button
In this case, it seems to be because the computed used by the button is not changing properly. When the button is not wrapped in an observer, a rebuild occurs in the parent widget and it changes properly.
from mobx.dart.
I'm still struggling with this behaviour.
If I wrap the whole TaskListView into Observer, it's working
If I remove .toList()
, then no errors are logged, but the button stops working (but in theory it should log “no observables found”, as we do nothing on todaysTodoTasksWindow
and pass it to the child parameter - it should be out of immediate context).
If I wrap the root of TaskListView widget into Observer, it won't work:
It either glitches when I derive a computed from computed (endReached
derived from todaysTodoTasksSource
which is derived from _todaysTasks
), or when I put an Observer into Observer.
But I tested it on a simple "counter" app and can't find the bug.
from mobx.dart.
import 'package:mobx/mobx.dart';
import 'package:test/test.dart';
part 'nested_store.g.dart';
// ignore: library_private_types_in_public_api
class NestedStore = _NestedStore with _$NestedStore;
abstract class _NestedStore with Store {
@observable
ObservableList<int> list = ObservableList();
int offset = 0;
@computed
bool get endReached => list.length >= offset;
}
class NestedStore2 = _NestedStore2 with _$NestedStore2;
abstract class _NestedStore2 with Store {
@observable
ObservableList<int> list = ObservableList();
@observable
int offset = 0;
@computed
bool get endReached => list.length >= offset;
}
void main() {
test('offset', () {
final store = NestedStore();
var f;
reaction((p0) => store.endReached, (value) {
f = value;
});
store.offset = 1;
expect(f, null);
runInAction(() => store.list.add(1));
expect(f, null);
runInAction(() => store.list.add(1));
expect(f, null);
});
test('offset observable', () {
final store = NestedStore2();
var f;
reaction((p0) => store.endReached, (value) {
f = value;
});
store.offset = 1;
expect(f, false);
runInAction(() => store.list.add(1));
expect(f, true);
runInAction(() => store.list.add(1));
expect(f, true);
});
}
from mobx.dart.
Yes, you were right, I forgot to mark _offset
as @observable
! That's why computed wasn't recalculated.
Thank you @amondnet ! I wrestled with it for a lot of time, and thought that there's some issue with MobX, but it was a problem on my side.
So I'll close the issue.
from mobx.dart.
Related Issues (20)
- Unable to use MultiReactionBuilder HOT 3
- [question] when is it mandatory to use runInAction
- Issue In ObservableList while using addAll method with iterables.
- Feature request: static analysis for empty `Observer`
- [Codegen] name used in generated mixin has to be constant to reduce compiled app size (especially important for Flutter Web) HOT 1
- [Question] No observables detected when checking in a ternary operator inside of a child parameter HOT 1
- Make the Observer to rebuild when no changes in immediate context HOT 2
- `ObservableSet` and `ObservableMap` notify all listeners when one is added with `fireImmediately: true`
- Observer widget doesnt observe the new state (Flutter web)
- late reactions not working HOT 5
- Error happened when building Observer, but it was captured since disableErrorBoundaries==true HOT 1
- Can't use nullable type alias in computed; `null check operator used on null value` HOT 2
- ObservableMap not notifying Observer mobx 2.2.3 it was woking fine in 2.2.1 HOT 6
- feat: add requiresReaction to `Computed`
- feat: add keepAlive to Computed
- feat: add scheduler option to autorun and reaction HOT 1
- feat: add signal option to reactions
- How to wait ObservableFuture.status in function?
- testWidgets with Store not working properly HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from mobx.dart.