Coder Social home page Coder Social logo

Comments (6)

amondnet avatar amondnet commented on June 4, 2024 1

@subzero911 Can you give me the code to reproduce it?

from mobx.dart.

subzero911 avatar subzero911 commented on June 4, 2024 1

I can't share the whole project as it's under NDA, but I can share some files which are related to error:

tasks_screen.dart:
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();
                            },
                          ),
                        ),
                      ],
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
task_list_view.dart
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),
                  ),
              ]
            ],
          ),
        ),
      ],
    );
  }
}
task_controller.dart
// 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.

amondnet avatar amondnet commented on June 4, 2024 1

@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.

subzero911 avatar subzero911 commented on June 4, 2024

I'm still struggling with this behaviour.

If I wrap the whole TaskListView into Observer, it's working
image

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:
image


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.

amondnet avatar amondnet commented on June 4, 2024
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.

subzero911 avatar subzero911 commented on June 4, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.