How can i approach to update an attribute in a Future Builder after receiving the snapshot / Using statemanagement

22 Views Asked by At

I am building an interactive SVG, so far the paths are clickable and the onTap print function is doing as it should. Also i want to change the color of the tapped element between red and the original color. My approach was to add a tapCounter to the class and make the color dependent of whether the tapCounter value is even (red) or not (orig. color from XML).

I've now run into the problem that a Future Builder will not accept changes to the attribute after the snapshot has been received and setState is not an option.

How would i solve this issue? My current attempst were to transform the FutureBuilder to a StreamBuilder but that caused initialization issues, wrap the FuturBuilder with a StatefulBuilder(context, setState)

This was the point where I started to try using a Statemanagement package and went with the easiest solution, for now, getX.

This is my code:

import 'dart:async';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:xml/xml.dart';
import 'package:path_drawing/path_drawing.dart';

import 'BodyPageController.dart';

class BodyPage extends StatefulWidget {
  const BodyPage({super.key});

  @override
  State<BodyPage> createState() => _BodyPageState();
}

class Clipper extends CustomClipper<Path> {
  Clipper({
    required this.svgPath,
  });

  String svgPath;

  @override
  Path getClip(Size size) {
    var path = parseSvgPathData(svgPath);
    final Matrix4 matrix4 = Matrix4.identity();

    matrix4.scale(0.5, 0.5);

    return path.transform(matrix4.storage).shift(const Offset(70, 180));
  }

  @override
  bool shouldReclip(CustomClipper oldClipper) {
    return false;
  }
}

Widget _getClippedImage({
  required Clipper clipper,
  required Color color,
  required MuscleClass muscleObject,
  required VoidCallback onTapCallback,
  required MuscleController musclecontroller,
}) {
  return ClipPath(
    clipper: clipper,
    child: GestureDetector(
      onTap:
      onTapCallback,
      child: Container(
        color: muscleObject.tapCounter % 2 == 0
            ? Colors.red
            : Color(
            int.parse('FF${muscleObject.color}', radix: 16)),
      ),
    ),
  );
}

class MuscleClass {
  final String id;
  final String path;
  String color;
  final String title;
  final String group;
  int tapCounter;

  MuscleClass({
    required this.id,
    required this.path,
    required this.color,
    required this.title,
    required this.group,
    this.tapCounter = 1,
  });
}

///MuscleClass is a Class holding the attributes
///muscles is the List holding Muscle objects
///muscleObject is each of a iteration of Muscleclass in muscles

class _BodyPageState extends State<BodyPage> {
  late List<MuscleClass> muscles;
  final MuscleController muscleController = Get.put(MuscleController());

  @override
  void initState() {
    super.initState();
    muscles = [];
    loadSvgImage(svgImage:'assets/images/TrackingPageImages/SportImages/ImagesWorkout/muscleMap.svg'); // Load SVG data when the widget initializes
  }


  Future<List<MuscleClass>> loadSvgImage({required String svgImage}) async {
    List<MuscleClass> muscles = [];
    String generalString = await rootBundle.loadString(svgImage);
    svgImage ='assets/images/TrackingPageImages/SportImages/ImagesWorkout/muscleMap.svg';
    XmlDocument document = XmlDocument.parse(generalString);
    final paths = document.findAllElements('path');

    for (var element in paths) {
      String partId = element.getAttribute('id').toString();
      String partPath = element.getAttribute('d').toString();
      String title = element.getAttribute('title').toString();
      String color = element.getAttribute('fill')?.toString() ?? 'D7D3D2';
      String group = element.getAttribute('group')?.toString() ?? 'noclick';

      muscles.add(MuscleClass(
          id: partId,
          path: partPath,
          color: color,
          title: title,
          group: group));
    }

    return muscles;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GetBuilder<MuscleController>(
        builder: (controller) {
          if (controller.muscles.isEmpty) {
            return const Center(child: CircularProgressIndicator());
          } else {
            return Stack(
              children: [
                for (var muscleObject in controller.muscles)
                  _getClippedImage(
                    clipper: Clipper(
                      svgPath: muscleObject.path,
                    ),
                    color: muscleObject.tapCounter % 2 == 0
                        ? Colors.red
                        : Color(
                        int.parse('FF${muscleObject.color}', radix: 16)),
                    muscleObject: muscleObject,
                    onTapCallback: () {
                      controller.incrementTapCounter(muscleObject);
                      },
                     musclecontroller: muscleController,
                  ),
              ],
            );
          }
        },
      ),
    );
  }
}

class MuscleController extends GetxController {
  RxList<MuscleClass> muscles = <MuscleClass>[].obs;

  @override
  void onInit() {
    super.onInit();
  }

  Future<void> loadSvgImage() async {

  }

  void incrementTapCounter(MuscleClass muscle) {
    muscle.tapCounter++;
  }
}

The GetBuilder was my FutureBuilder before.

Here my previous version:

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<List<MuscleClass>>(
          future: loadSvgImage(
              svgImage:
                  'assets/images/TrackingPageImages/SportImages/ImagesWorkout/muscleMap.svg'),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              var muscles = snapshot.data!;
              return Stack(
                children: [
                  for (var muscleObject in muscles)
                    _getClippedImage(
                      clipper: Clipper(
                        svgPath: muscleObject.path,
                      ),
                      color: muscleObject.tapCounter % 2 == 0
                          ? Colors.red
                          : Color(
                              int.parse('FF${muscleObject.color}', radix: 16)),
                      muscleObject: muscleObject,
                      onTapCallback: () {
                        print(muscleObject.title);
                        setState(() {
                          print('Increment tapCounter');
                          muscleObject.tapCounter++;
                        });
                        print(muscleObject.tapCounter);
                      },
                    ),
                ],
              );
            } else if (snapshot.hasError) {
              return const Center(child: Text('Error loading muscle data'));
            } else {
              return const Center(child: CircularProgressIndicator());
            }
          }),
    );
  }

So the color of the element should iterate between two colors based on the current value of the tapoCounter, so that one element should be "redrawn" in the best case, while others aren't affected. Anything i found here or through google search did not adress how to manipulate the attribute, so i dared to put up a new question. I hope I could provide as much information as needed, kind regards :)

0

There are 0 best solutions below