Как сохранить и получить состояние?

В настоящее время я работаю над простым приложением, чтобы узнать больше о Flutter. Теперь я могу поиграть с элементами пользовательского интерфейса и достиг точки, когда мне нужно общаться между родителями и детьми и сохранять состояние. Но я не уверен, есть ли здесь «правильный ответ»...

Вопрос в том, как правильно реализовать эту функцию?

Тело страницы выглядит так:

Widget _buildBody() {
    return GestureDetector(
        onTapUp: (details) {
          setState(() {
            // Use the transformation controller to translate the zoomed/panned
            // coordinates to the real image coordinates
            Offset pos = _controller.toScene(details.localPosition);
            _circles.add(Circle(Position: pos, size: 20, colour: Colors.Red));
          });
        },
        child: InteractiveViewer(
            minScale: 0.1,
            maxScale: 1.6,
            transformationController: _controller,
            child: Container(
                constraints: const BoxConstraints.expand(),
                decoration: const BoxDecoration(
                  image: DecorationImage(
                    image: AssetImage('assets/image.jpg'),
                    fit: BoxFit.cover,
                  ),
                ),
                child: Stack(children: _circles))));
  }

Идея заключается в том, что у вас есть файлы assets/image.jpg, отображаемые на экране, вы можете увеличивать/уменьшать масштаб и перемещаться по изображению, а затем, когда вы нажимаете на изображение, оно создает круг, в котором вы нажимаете размер и цвет по умолчанию.

То все нормально работает...

Моя следующая задача — разработать какой-то метод изменения размера круга. Теперь одна вещь, которую я могу сделать, это показать модальное диалоговое окно, когда вы нажимаете на круг, затем это будет реализовано внутри класса Circle (так же, как Draggable внутри класса используется для изменения положения круга). Или я мог бы создать (на родительском уровне) нижнюю панель навигации, чтобы иметь кнопки + и - для изменения размера и нажимать это изменение вниз, чтобы при касании дочернего элемента родитель уведомлялся (с обратным вызовом), что он текущий выбранный дочерний элемент, так что обработчик onTap() (кнопки плюс) может вызвать выбранный дочерний элемент для увеличения или уменьшения размера.

   // Inside the scaffold
   bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.add_circle_outline),
            label: 'Size up',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.remolve_circle_outline),
            label: 'Size down',
          ),

        onTap: (index) {
            if (index == 0) {
                _circles[_currentlySelectedCircle].incrementSize();
            }
            if (index == 1) {
                _circles[_currentlySelectedCircle].decrementSize();
            }
        },


   // Inside the parent class, this is called by the circle to tell the parent it is the selected one
   _notifySelected(int selectedCircle) {
       _currentlySelectedCircle = selectedCircle;
   }

   // When creating the circle, pass in the notification callback plus the array offset of the child
   _circles.add(Circle(
       Position: pos, 
       size: 20, 
       colour: Colors.Red, 
       which: _circles.length, 
       notify_select: _notifySelected);

Я чувствую, что правильный ответ на это, вероятно, является чем-то вроде вопроса пользовательского интерфейса (как пользователь должен изменить размер круга?), Который затем предоставит одно из двух решений (обработка его полностью внутри класса круга или наличие этой концепции обратный вызов уведомления для выбора круга, а затем вызов круга для настройки его размера).

Есть ли «правильный» способ сделать что-то подобное?

Наконец, если я хочу сохранить всю эту информацию (количество кругов и для каждого круга положение, размер и цвет), чтобы при следующем запуске приложения оно всегда запоминало и воссоздавало одно и то же состояние. Есть ли «правильный» способ сделать это? Должен ли мой класс круга с полным состоянием справиться с этим, или мне нужно иметь возможность извлекать состояние круга из родителя и сохранять его? Я видел много ссылок на множество различных пакетов для обработки подобных вещей, но не уверен, с чего лучше всего начать? Может Блок?

Я использую Bloc для управления состоянием, и он работает хорошо. В большинстве случаев вы можете использовать кубиты, которые упрощают часть стандартного кода. Если вы хотите, чтобы ваше состояние сохранялось между перезапусками приложения, проверьте Hydrated Bloc.

John Weidner 09.04.2023 21:13
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
62
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я думаю, что вы правильно подходите к примеру, но в реальном проекте я рекомендую хранить это состояние внутри ViewModel.

  • Вы можете использовать Block, Flutter_RiverPod или любую другую архитектуру для управления состоянием представления (например, размером вашего круга) >> и Widget будет прослушивать ваши изменения ViewModel, чтобы отобразить правильное представление.
  • Если вам не нужно хранить размер круга, просто хотите изменить размер. Вы можете добавить анимацию в CirCle Widget, чтобы разрешить пользователю масштабирование или масштабирование вручную.

Но если круг слишком мал, pinch_zoom будет работать только с InteractiveViewer, а не с размером круга, не так ли?

Gordon Hollingworth 10.04.2023 09:19

Это зависит от того, в каком представлении вы добавляете жест. Если ваш вид может быть слишком маленьким, я думаю, что всплывающее меню или плавающая кнопка (как вы упомянули) должны быть лучше.

ShinZ 10.04.2023 14:39
Ответ принят как подходящий

Я бы предложил это следующим образом:

class MyPainterComponent extends StatefulWidget {
  const MyPainterComponent({super.key, this.boxes});

  final List<DrawBox>? boxes;

  @override
  State<MyPainterComponent> createState() => _MyPainterComponentState();
}

// _MyPainterComponentState contains all the drawn boxes and which working mode is currently selected.
// It also handles selecting the mode, adding new boxes and resizing them.
class _MyPainterComponentState extends State<MyPainterComponent> {
  final TransformationController _tController =
      TransformationController();

  //list of all drawn boxes
  List<DrawBox> _boxes = [];
  int _selectedMode = 0;

  //select current working mode
  _selectMode(int index){

    setState((){
      _selectedMode = index;
    });

    //delete all boxes on reset
    if (_selectedMode == 3 ){
      _boxes = [];
    }
  }


  @override

  //if component was constructed with boxes args: init '_boxes' with it
  //otherwise: '_boxes' is empty
  void initState() {
    super.initState();
    if (widget.boxes != null){
      _boxes = widget.boxes!;
    }
  }


  //increase size or decrease size depending on selected mode
  void resizeBox(DrawBox box){
    double scale = 1;
    if (_selectedMode == 1) {
      scale *= 1.2;
    } else if (_selectedMode == 2) {
      scale /= 1.2;
    }

    int selectedBoxInd = _boxes.indexWhere((element) => element == box);

    setState((){
      _boxes[selectedBoxInd] = DrawBox(size: box.size * scale, color: box.color, posX: box.posX, posY: box.posY, handleSelect: resizeBox,);
    });    
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(

      //wrap InteractiveViewer into a GestureDetector
      body: GestureDetector(
          onTapUp: (details) {
            //adding a new box
            if (_selectedMode == 0) {
              //Tapping a spot S in GestureDetector gives you a position A.
              //If every transformation of the InteractiveViewer would be undone, the spot S will change its 
              //position to B in the GestureDetector. 
              //This maps position A --> B.
              Offset pos = _tController.toScene(details.localPosition);

              setState(() {
                _boxes.add(DrawBox(
                    size: 20, color: Colors.yellow, posX: pos.dx, posY: pos.dy, handleSelect: resizeBox));
              });

            }

          },
          child: InteractiveViewer(
              minScale: 0.5,
              maxScale: 2.5,
              transformationController: _tController,
              //the "canvas" for adding new objects
              child: Container(
                  width: 600,
                  height: 600,
                  decoration: const BoxDecoration(
                    image: DecorationImage(
                    image: AssetImage('resources/dutch_parliament.jpg'),
                    fit: BoxFit.cover,
                  )),
                  child: Stack(children: _boxes)))),
      //selecting the mode in the bottom navigation bar
      bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
                icon: Icon(Icons.mode_edit), label: 'Add Box'),         
            BottomNavigationBarItem(
                icon: Icon(Icons.add_circle_outline), label: 'Size up'),
            BottomNavigationBarItem(
                icon: Icon(Icons.remove_circle_outline), label: 'Size down'),
            BottomNavigationBarItem(
                icon: Icon(Icons.delete_outlined), label: 'Reset'),
          ],
          backgroundColor: Colors.white,
          currentIndex: _selectedMode,
          selectedItemColor: Colors.amber[800],
          onTap: _selectMode,
          type: BottomNavigationBarType.fixed),
    );
  }

}


//drawing a box around the center ('posX', 'posY'), length of edge is 'size'
class DrawBox extends StatelessWidget {
  const DrawBox(
      {required this.size,
      required this.color,
      required this.posX,
      required this.posY,
      required this.handleSelect});

  final double size;
  final Color color;
  final double posX;
  final double posY;

  final Function(DrawBox) handleSelect;

  @override
  Widget build(BuildContext context) {
    
    //position boxes with margin property
    //
    //prevent negative margin, which would throw an exeption
    double marginL = (posX - size/2) > 0 ? posX - size/2 : 0;
    double marginT = (posY - size/2) > 0 ? posY - size/2 : 0;

    return GestureDetector(
      onTap: () {
        handleSelect(this);
      },
      child: Container(
          margin: EdgeInsets.only(
            left: marginL, 
            top: marginT),
          width: size,
          height: size,
          color: color,
          //problem: transformation won't transform the gesture detector widget, 
          // this would stay always in default position
          //transform:
          //  Matrix4.translationValues(posX - size / 2, posY - size / 2, 0)
      )
    );
  }
}

Конечно, класс круга может обрабатывать изменение размера сам по себе, но я думаю, что в этом сценарии, если вы создаете своего рода пользовательский холст, информация о том, где и какие круги существуют, принадлежит холсту. Итак, у нас есть разные переменные, которые (могут) изменяться во время выполнения: Где существует круг? Какого размера? В каком я режиме? (добавление новых кругов или изменение их размера). Я бы сохранил всю эту информацию только в виджете с отслеживанием состояния. (без использования BLoC или чего-то еще...) При создании нового круга (в приведенном выше коде я использовал блоки) я просто передал бы обработчик объекту круга. Объект будет вызывать обработчик при выборе, и обработчик реализован в родительском виджете с отслеживанием состояния.

Что касается постоянства: я бы реализовал процедуру постоянного хранения состояния кругов вне компонента холста. Должна быть возможность создать компонент холста с заданными кругами. Возможно, родитель компонента холста, в моем примере MyPainterComponent, просто обертывает этот класс + функциональность, как хранить информацию о состоянии в локальной базе данных/Firebase и т. д. Опять же, я бы просто передал обработчик «хранения» в MyPainterComponent. Использование BLoC или любой другой библиотеки управления состоянием, ИМХО, также абсолютно необязательно в этом случае.

Возможно, использование BLoC полезно при создании действительно сложного приложения, когда для обработки данных из серверной части требуется действительно сложная «бизнес-логика». Как сложное новостное приложение, которое получает push-уведомления. Но, на мой взгляд, даже для простого функционала чата BLoC и другие «решения» по управлению состоянием совершенно не нужны.

Возможно, вам понравится это видео: После 4 ЛЕТ работы инструктором по Flutter, вот мои 5 советов для новичков [на 2022 год]. Его совет № 2: «Перестаньте изучать, какой лучший пакет «Управление государством»».

Спасибо, моя текущая реализация дочернего элемента (здесь DrawBox) также реализует Draggable, что упрощает управление положением элемента. Но помещает право собственности на позицию внутри дочернего элемента... Думаю, я мог бы попробовать добавить что-то вроде Stack( _boxes.map( (box) => Draggable( onDragEnd: _onDragEnd, child: box ) ), чтобы функция перетаскивания выполнялась внутри родителя. Но до сих пор у меня было много проблем с переводом координат при перетаскивании...

Gordon Hollingworth 11.04.2023 19:25

У вас есть предложение по сохранению состояния приложения между запусками приложения? Я чувствую, что самым простым было бы использовать SharedPreferences или локальный файл, у вас есть какие-либо мысли?

Gordon Hollingworth 13.04.2023 21:00

Я думаю, это зависит от размера и сложности вашей информации о состоянии. Для сохранения множества простых пар ключ-значение shared_preferences кажется хорошим выбором.

DaenMine 14.04.2023 11:13

Если у вас есть список позиций кругов для сохранения, возможно, стоит использовать SQLite (sqflite и/или sqflite_common_ffi, поддерживающие также Linux, Win). В отличие от других СУБД SQLite является очень легким и простым SQL-решением, например, в нем всего пять типов данных.

DaenMine 14.04.2023 11:20

Другие вопросы по теме