skip to Main Content

I am currently working on a simple application to learn more about Flutter. I can now play around with the UI elements, and have reached the point where I need to communicate around between parents and children and save away the state. But I’m unsure whether there is a ‘right answer here’…

Question is, what is the right way to implement this functionality?

The body of the page looks like:

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))));
  }

The idea is you have the assets/image.jpg displayed on the screen, you can zoom in/out and pan around the image, then when you tap on the image it will create a circle where you tap with a default size and colour.

That all works fine…

My next job is to have some kind of method of changing the size of the circle. Now, one thing I can do is to show a modal dialog when you tap on a circle, this would then be implemented inside the Circle class (much like the Draggable inside the class is used to change the position of the circle). Or I could create (at the parent level) a bottom navigation bar to have a + and – button to change the size and push this change downwards, such that when a child is tapped the parent is notified (with a callback) that it is the currently selected child such that an onTap() handler (of the plus button) can call into the selected child to increment or decrement the size.

   // 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);

I feel like the right answer to this is probably something of a UI question (how should the user change the size of the circle?) which will then lend to one of two solutions (handling it entirely inside the circle class or having this concept of a notification callback to select a circle then calling into the circle to adjust its size).

Is there a ‘right’ way of doing something like this?

Finally, if I want to save all this information away (number of circles, and for each circle the position, size and colour) so that the next time I start my app it always remembers and recreates the same state. Is there a ‘correct’ way of doing this? Should my statefull circle class be able to handle this, or do I need to be able to extract the state of the circle from the parent and save it away? I’ve seen many references to many different packages to handle this kind of stuff, but unsure what would be the best place to start? Maybe Bloc?

2

Answers


  1. I think you approach is not wrong with an example, but in a real project i recommend to storage these state inside ViewModel.

    • You can or Block,Flutter_RiverPod or any architecture to manage state of view (eg: size of your circle) >> and Widget will listen to your ViewModel changes to render the right representation.
    • If you no need to store size of circle, just want to change the size. You could add animation to CirCle Widget to enable user pinch_zoom or scale by your hand.
    Login or Signup to reply.
  2. I would suggest it as follows:

    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)
          )
        );
      }
    }
    

    Of course a circle class can handle resizing on its own, but I think in this scenario if you’re building a kind of a custom canvas, the information where and which kind of circles exists belong to the canvas. So we have different variables that (can) change at runtime: Where does a circle exist? Which size? In which mode I am? (adding new circles vs. resizing them). I would store all these info just in a stateful widget. (without using BLoC or what’soever…) When creating a new circle (in the code above I’ve used boxes) I would just pass a handler to the circle object. The object will invoke the handler on being selected and the handler is implemented in the parent stateful widget.

    Regarding persistency: I would implement the procedure for storing the circles’ state persistently outside the canvas component. It should be possible to create a canvas component with given circles. Maybe the parent of the canvas component, in my example MyPainterComponent, just wraps this class + functionality how to store the state info to a local database/Firebase etc. Again, I would just pass an "on storing" handler to MyPainterComponent. Using BLoC or any other state management lib is IMHO also absolutely dispensable in this case.

    Maybe using BLoC is helpful when building a really complex app, when there’s is really complex "business logic" needed for processing data from the backend. Like a complex news app that receives push notifications. But in my opinion, even for just a simple chat functionality BLoC and other state management "solutions" are absolutely dispensable.

    Maybe you like to watch this vid: After 4 YEARS as a Flutter instructor, here are my 5 tips for newcomers [for 2022]. His tip #2: "stop learning what the best "State Management" package is."

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search