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





Я думаю, что вы правильно подходите к примеру, но в реальном проекте я рекомендую хранить это состояние внутри ViewModel.
Но если круг слишком мал, pinch_zoom будет работать только с InteractiveViewer, а не с размером круга, не так ли?
Это зависит от того, в каком представлении вы добавляете жест. Если ваш вид может быть слишком маленьким, я думаю, что всплывающее меню или плавающая кнопка (как вы упомянули) должны быть лучше.
Я бы предложил это следующим образом:
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 ) ), чтобы функция перетаскивания выполнялась внутри родителя. Но до сих пор у меня было много проблем с переводом координат при перетаскивании...
У вас есть предложение по сохранению состояния приложения между запусками приложения? Я чувствую, что самым простым было бы использовать SharedPreferences или локальный файл, у вас есть какие-либо мысли?
Я думаю, это зависит от размера и сложности вашей информации о состоянии. Для сохранения множества простых пар ключ-значение shared_preferences кажется хорошим выбором.
Если у вас есть список позиций кругов для сохранения, возможно, стоит использовать SQLite (sqflite и/или sqflite_common_ffi, поддерживающие также Linux, Win). В отличие от других СУБД SQLite является очень легким и простым SQL-решением, например, в нем всего пять типов данных.
Я использую Bloc для управления состоянием, и он работает хорошо. В большинстве случаев вы можете использовать кубиты, которые упрощают часть стандартного кода. Если вы хотите, чтобы ваше состояние сохранялось между перезапусками приложения, проверьте Hydrated Bloc.