Flutter game tutorial: Fruit Ninja Clone

Flutter game tutorial: Fruit Ninja Clone
This article has been updated! Tap to see
  • April 30, 2023:
    • Adapted the code in the article to work with latest SDK versions: Flutter 3.7.8 and Dart 2.19.5.
    • Updated the linked GitHub repository to reflect the very same adaptions.

The goal

The goal of this tutorial is to develop a clone of the game Fruit Ninja in a basic way. We will not use any frameworks so that you as a reader can learn from scratch how things work.

Flutter Fruit Ninja animation
The resulting app

What you will learn

After having completed this tutorial, you will be able to

  • Use a GestureDetector
  • Draw on the screen
  • Implement basic collision checks
  • Implement a basic gravity simulation

The implementation

For the basic version of our game, there are the following problems to be solved:

  • Implementing a “slicer” that follows the path we create by swiping with our finger
  • Implementing the appearance of fruits
  • Implementing gravity that pulls the fruits down
  • Checking for collision of the slicer and the fruits

The slicer

Let’s start with the slicer that is supposed to appear when we drag across the screen:

 1class SlicePainter extends CustomPainter {
 2  const SlicePainter({required this.pointsList});
 3
 4  final List<Offset> pointsList;
 5  final Paint paintObject = Paint();
 6
 7  @override
 8  void paint(Canvas canvas, Size size) {
 9    _drawPath(canvas);
10  }
11
12  void _drawPath(Canvas canvas) {
13    final Path path = Path();
14
15    paintObject.color = Colors.white;
16    paintObject.strokeWidth = 3;
17    paintObject.style = PaintingStyle.fill;
18
19    if (pointsList.length < 2) {
20      return;
21    }
22
23    paintObject.style = PaintingStyle.stroke;
24
25    path.moveTo(pointsList[0].dx, pointsList[0].dy);
26
27    for (int i = 1; i < pointsList.length - 1; i++) {
28      if (pointsList[i] == null) {
29        continue;
30      }
31
32      path.lineTo(pointsList[i].dx, pointsList[i].dy);
33    }
34
35    canvas.drawPath(path, paintObject);
36  }
37
38  @override
39  bool shouldRepaint(SlicePainter oldDelegate) => true;
40}

The SlicePainter is something that expects a number of points and draws them on the screen with a connecting line in between them. For this, we create a Path, move the starting point to the coordinates of the first element of the point list and then iterate over each element, starting with the second one and draw a line from the previous point to the current point.

The CustomPainter itself has no value if it is not used anywhere. That’s why we need a canvas that recognizes the finger swipes, captures the points and puts them into the constructor of our newly created CustomPainter so that the path is actually drawn.

1List<Widget> _getStack() {
2  List<Widget> widgetsOnStack = <Widget>[];
3
4  widgetsOnStack.add(_getSlice());
5  widgetsOnStack.add(_getGestureDetector());
6
7  return widgetsOnStack;
8}

Our widget consists of a Stack. At the bottom there will be the slice that is produced by our swipe gestures, on top of that we want to have the GestureDetector because we do not want anything to block the detection.

1class TouchSlice {
2    TouchSlice({required this.pointsList});
3    
4    final List<Offset> pointsList;
5}

First, we create a model class, representing the slice. We call it TouchSlice and let it expect a list of Offsets as the only parameter.

 1Widget _getSlice() {
 2  if (touchSlice == null) {
 3    return Container();
 4  }
 5
 6  return CustomPaint(
 7    size: Size.infinite,
 8    painter: SlicePainter(
 9      pointsList: touchSlice!.pointsList,
10    )
11  );
12}

We then implement the _getSlice() method which returns a CustomPaint that paints the slice we created before based on the pointlist of the TouchSlice instance of the CanvasArea widget. The TouchSlice is always null. Let’s do something about it by adding a GestureDetector.

Detecting the swipe gesture

 1Widget _getGestureDetector() {
 2  return GestureDetector(
 3    onScaleStart: (ScaleStartDetails details) {
 4      setState(() => _setNewSlice(details));
 5    },
 6    onScaleUpdate: (ScaleUpdateDetails details) {
 7      setState(() => _addPointToSlice(details));
 8    },
 9    onScaleEnd: (details) {
10      setState(() { touchSlice = null; });
11    }
12  );
13}

The GestureDetector listens to three events:

  • onScaleStart is the event that is triggered when we start swiping. This should add a new TouchSlice to the state that has a single point
  • onScaleUpdate gets called when we move our finger while it’s on the screen. This should add a new point to the existing point list of our TouchSlice
  • onScaleEnd is called when we release the finger from the screen. This should set the TouchSlice to null in order to let the slice disappear

Let’s implement the methods!

 1void _setNewSlice(details) {
 2  touchSlice = TouchSlice(pointsList: <Offset>[details.localFocalPoint]);
 3}
 4
 5void _addPointToSlice(ScaleUpdateDetails details) {
 6  if (_touchSlice?.pointsList == null || _touchSlice!.pointsList.isEmpty) {
 7    return;
 8  }
 9  touchSlice.pointsList.add(details.localFocalPoint);
10}
11
12void _resetSlice() {
13  touchSlice = null;
14}

Testing time!

Let’s have a look at how this looks in action by building and starting the app.

Flutter draw on screen
That’s a long line

Oh! We forgot to limit the length of the line we can draw. Let’s correct it by limiting the amount of points of the line to 16.

 1void _addPointToSlice(ScaleUpdateDetails details) {
 2  if (_touchSlice?.pointsList == null || _touchSlice!.pointsList.isEmpty) {
 3    return;
 4  }
 5  
 6  if (touchSlice.pointsList.length > 16) {
 7    touchSlice.pointsList.removeAt(0);
 8  }
 9  touchSlice.pointsList.add(details.localFocalPoint);
10}

Okay, if we have more than 16 points, we remove the first one before adding the last one. This way we draw a snake.

Colorful background

White line on a black background looks quite boring. Let’s create a more appealing look by using a colorful background.

 1List<Widget> _getStack() {
 2  List<Widget> widgetsOnStack = <Widget>[];
 3  
 4  widgetsOnStack.add(_getBackground());
 5  widgetsOnStack.add(_getSlice());
 6  widgetsOnStack.add(_getGestureDetector());
 7  
 8  return widgetsOnStack;
 9}
10
11Container _getBackground() {
12  return Container(
13    decoration: BoxDecoration(
14      gradient: RadialGradient(
15        stops: <double>[0.2, 1.0],
16        colors: <Color>[Color(0xffFFB75E), Color(0xffED8F03)],
17      )
18    ),
19  );
20}

A radial gradient should make the whole thing a little bit less gloomy.

Flutter draw on radial gradient
Colors!

Fruits

Okay, let’s come to the part that creates the fun! We are going to be adding fruits to the game.

 1class Fruit {
 2  Fruit({
 3    required this.position,
 4    required this.width,
 5    required this.height
 6  });
 7
 8  Offset position;
 9  final double width;
10  final double height;
11
12  bool isPointInside(Offset point) {
13    if (point.dx < position.dx) {
14      return false;
15    }
16
17    if (point.dx > position.dx + width) {
18      return false;
19    }
20
21    if (point.dy < position.dy) {
22      return false;
23    }
24
25    if (point.dy > position.dy + height) {
26      return false;
27    }
28
29    return true;
30  }
31}

Our fruit should hold its position so that we can draw it on the screen and manipulate the position later. It should also have a sense of its boundary because we should be able to check if we hit it with our slice. In order to help us determine that, we create a public method called isPointInside that returns if a given point is inside the boundary of the fruit.

 1List<Fruit> fruits = List();
 2...
 3widgetsOnStack.addAll(_getFruits());
 4...
 5List<Widget> _getFruits() {
 6  List<Widget> list = <Widget>[];
 7
 8  for (Fruit fruit in fruits) {
 9    list.add(
10      Positioned(
11        top: fruit.position.dy,
12        left: fruit.position.dx,
13        child: Container(
14          width: fruit.width,
15          height: fruit.height,
16          color: Colors.white
17        )
18      )
19    );
20  }
21
22  return list;
23}

In order to store the data of every fruit currently on the screen, we give our widget state a new member variable called fruits which is a list of the Fruit class we have just created. We position the fruits from the list by using a Positioned widget. We could also go for a CustomPaint widget like we did with the Slice but for the sake of simplicity let’s just go for the widget tree approach.
As a first iteration we display a white square instead of an actual fruit because this step is about displaying something and checking for collision. Beautifying can be done later.

For the collision detection to work, we need to check for collision every time a point is added to our Slice.

 1...
 2onScaleUpdate: (details) {
 3  setState(() {
 4    _addPointToSlice(details);
 5    _checkCollision();
 6  });
 7},
 8
 9_checkCollision() {
10  if (touchSlice == null) {
11    return;
12  }
13
14  for (Fruit fruit in List<Fruit>.from(fruits)) {
15    for (Offset point in touchSlice.pointsList) {
16      if (!fruit.isPointInside(point)) {
17        continue;
18      }
19
20      fruits.remove(fruit);
21      break;
22    }
23  }
24}

We iterate over a new list that is derived from the fruit list. For every fruit we check for every point if it’s inside. If it is, we remove the fruit from the Stack and break the inner loop as there is no need to check for the rest of the points if there is a collision.

Now we have a list of fruits and a method that displays them, but yet there is no fruit in the list. Let’s change that by adding one Fruit to the list on initState.

1@override
2void initState() {
3  fruits.add(Fruit(
4    position: Offset(100, 100),
5    width: 80,
6    height: 80
7  ));
8  super.initState();
9}
Flutter Fruit Ninja simple collision
First collision with a “fruit”

Cool, we can draw a line on the screen and let a rectangle disappear. One thing that bothers me is the it instantly disappears once we touch it. Instead, we want the effect of cutting through it. So let’s change the _checkCollision algorithm a little bit.

 1_checkCollision() {
 2  if (touchSlice == null) {
 3    return;
 4  }
 5
 6  for (Fruit fruit in List<Fruit>.from(fruits)) {
 7    bool firstPointOutside = false;
 8    bool secondPointInside = false;
 9
10    for (Offset point in touchSlice.pointsList) {
11      if (!firstPointOutside && !fruit.isPointInside(point)) {
12        firstPointOutside = true;
13        continue;
14      }
15
16      if (firstPointOutside && fruit.isPointInside(point)) {
17        secondPointInside = true;
18        continue;
19      }
20
21      if (secondPointInside && !fruit.isPointInside(point)) {
22        fruits.remove(fruit);
23        break;
24      }
25    }
26  }
27}

The algorithm now only interprets a movement as a collision if one point of the line is outside of the fruit, a subsequent point is within the fruit and a third one is outside. This ensures that something like a cut through is happening.

Flutter Fruit Ninja advanced collision
A better collision detection that requires “cutting through”

A white rectangular fruit looks not very tasty. It also does not create the need to cut through. Let’s change that by replacing it with a more appealing image.

Flutter Fruit Ninja melon
Melon graphics created with vector tool

I don’t have a lot of talent in design and arts. I tried to create some simple vector graphics that look kind of the states we need of a melon. A whole melon, the left and right part of a melon and a splash.

Let’s take care that we see the whole melon when it appears and the two parts when we cut through.

 1List<Widget> _getFruits() {
 2  List<Widget> list = new List();
 3
 4  for (Fruit fruit in fruits) {
 5    list.add(
 6      Positioned(
 7        top: fruit.position.dy,
 8        left: fruit.position.dx,
 9        child: _getMelon(fruit)
10      )
11    );
12  }
13
14  return list;
15}
16
17Widget _getMelon(Fruit fruit) {
18  return Image.asset(
19      'assets/melon_uncut.png',
20      height: 80,
21      fit: BoxFit.fitHeight
22  );
23}

Let’s start with the easy part: replacing the white rectangular. Instead of returning a Container, we return the return value of getMelon() which accepts a Fruit and returns an Image, specifically the one we have created the assets for.

Okay, now we want the melon to be turned into two once we cut it.

 1class _CanvasAreaState<CanvasArea> extends State {
 2  List<FruitPart> fruitParts = <FruitPart>[];
 3  
 4  List<Widget> _getStack() {
 5    List<Widget> widgetsOnStack = <Widget>[];
 6
 7    widgetsOnStack.add(_getBackground());
 8    widgetsOnStack.add(_getSlice());
 9    widgetsOnStack.addAll(_getFruitParts());
10    widgetsOnStack.addAll(_getFruits());
11    widgetsOnStack.add(_getGestureDetector());
12
13    return widgetsOnStack;
14  }
15
16  List<Widget> _getFruitParts() {
17    List<Widget> list = <Widget>[];
18
19    for (FruitPart fruitPart in fruitParts) {
20      list.add(
21        Positioned(
22          top: fruitPart.position.dy,
23          left: fruitPart.position.dx,
24          child: _getMelonCut(fruitPart)
25        )
26      );
27    }
28
29    return list;
30  }
31
32  Widget _getMelonCut(FruitPart fruitPart) {
33    return Image.asset(
34      fruitPart.isLeft ? 'assets/melon_cut.png': 'assets/melon_cut_right.png',
35      height: 80,
36      fit: BoxFit.fitHeight
37    );
38  }
39
40  _checkCollision() {
41    
42    for (Fruit fruit in List.from(fruits)) {
43    
44        if (secondPointInside && !fruit.isPointInside(point)) {
45          fruits.remove(fruit);
46          _turnFruitIntoParts(fruit);
47          break;
48        }
49      }
50    }
51  }
52
53  void _turnFruitIntoParts(Fruit hit) {
54    FruitPart leftFruitPart = FruitPart(
55        position: Offset(
56          hit.position.dx - hit.width / 8,
57          hit.position.dy
58        ),
59        width: hit.width / 2,
60        height: hit.height,
61        isLeft: true
62    );
63
64    FruitPart rightFruitPart = FruitPart(
65        position: Offset(
66          hit.position.dx + hit.width / 4 + hit.width / 8,
67          hit.position.dy
68        ),
69        width: hit.width / 2,
70        height: hit.height,
71        isLeft: false
72    );
73
74    setState(() {
75      fruitParts.add(leftFruitPart);
76      fruitParts.add(rightFruitPart);
77      fruits.remove(hit);
78    });
79  }
80}
81
82class FruitPart {
83  FruitPart({
84    required this.position,
85    required this.width,
86    required this.height,
87    required this.isLeft
88  });
89
90  Offset position;
91  final double width;
92  final double height;
93  final bool isLeft;
94}

We introduce a new class called FruitPart, which represents both of the parts of our fruit. The properties are slightly different to those of our Fruit class. position, width and height are kept, but there is an addition bool variable called isLeft, which determines if this is the left or the right fruit part. Also, there is no need for a method to check if a point is inside.
We then add a new member variable to our state: fruitParts, which represents a list of fruit parts currently on the screen. They are added to the Stack underneath the Fruits. The isLeft property determines if we load the image asset of the left or the right cut.
When a collision between a slice and a fruit is happening, in addition to removing the fruit, we place the two fruit parts.

Flutter Fruit Ninja Cut Half
The melon is cut half on cut through

It’s raining fruits

Now we want the fruits to behave like in Fruit Ninja: spawned at a certain point, they are “thrown” in a certain directory and constantly pulled down by the simulated gravity.

 1class Fruit extends GravitationalObject {
 2  Fruit({
 3    required this.width,
 4    required this.height,
 5    required super.position,
 6    super.gravitySpeed = 0.0,
 7    super.additionalForce = const Offset(0,0)
 8  });
 9
10  double width;
11  double height;
12  ...
13}
14
15class FruitPart extends GravitationalObject {
16  FruitPart({
17    required this.width,
18    required this.height,
19    required this.isLeft,
20    required super.position,
21    super.gravitySpeed = 0.0,
22    super.additionalForce = const Offset(0, 0),
23  });
24
25  final double width;
26  final double height;
27  final bool isLeft;
28}
29
30abstract class GravitationalObject {
31  GravitationalObject({
32    required this.position,
33    this.gravitySpeed = 0.0,
34    this.additionalForce = const Offset(0,0)
35  });
36
37  Offset position;
38  double gravitySpeed;
39  final double _gravity = 1.0;
40  final Offset additionalForce;
41
42  void applyGravity() {
43    gravitySpeed += _gravity;
44    position = Offset(
45      position.dx + additionalForce.dx,
46      position.dy + gravitySpeed + additionalForce.dy
47    );
48  }
49}

We create a new abstract class called GravitationalObject and let both the Fruit and the FruitPart extend that class. A GravitationalObject has a position, a gravitySpeed and an additionalForce as constructor arguments. The gravitySpeed is the amount by which the the object is pulled down. Every time the applyGravity() method is called, this speed is increased by _gravity to simulate a growing force. additionalForce represents any other force that is acting upon that object. This is useful if we don’t want the fruits to just fall down, but be “thrown” up or sideways. We will also us it to let the fruit parts fall apart when cutting through the fruit.

Now, what’s left to do to make the gravitation start to have an effect is regularly applying the force to the fruits, updating their position.

 1@override
 2void initState() {
 3  fruits.add(Fruit(
 4    position: Offset(0, 200),
 5    width: 80,
 6    height: 80,
 7    additionalForce: Offset(5, -10)
 8  ));
 9  _tick();
10  super.initState();
11}
12
13void _tick() {
14  setState(() {
15    for (Fruit fruit in fruits) {
16      fruit.applyGravity();
17    }
18    for (FruitPart fruitPart in fruitParts) {
19      fruitPart.applyGravity();
20    }
21  });
22
23  Future<void>.delayed(Duration(milliseconds: 30), _tick);
24}
25
26void _turnFruitIntoParts(Fruit hit) {
27  FruitPart leftFruitPart = FruitPart(
28      
29      additionalForce: Offset(hit.additionalForce.dx - 1, hit.additionalForce.dy -5)
30  );
31
32  FruitPart rightFruitPart = FruitPart(
33      ...
34      additionalForce: Offset(hit.additionalForce.dx + 1, hit.additionalForce.dy -5)
35  );
36  ...
37}

We create a new method _tick() that is executed every 30 milliseconds and updates the position of our fruits. The initially displayed fruit gets an addition force that let it be thrown up and right. When a fruit is turned into parts, we give every part an additional force in the opposite direction.

Flutter Fruit Ninja Gravity Cut
Fruits fall down and parts are torn apart

The devil is in the details

Okay the basic game mechanic is there. Let’s improve a bunch of details.

First of all, the slice doesn’t look very appealing as it’s only a line. Let’s create an actual blade!

 1void _drawBlade(Canvas canvas, Size size) {
 2  final Path pathLeft = Path();
 3  final Path pathRight = Path();
 4  final Paint paintLeft = Paint();
 5  final Paint paintRight = Paint();
 6
 7  if (pointsList.length < 3) {
 8    return;
 9  }
10
11  paintLeft.color = Color.fromRGBO(220, 220, 220, 1);
12  paintRight.color = Colors.white;
13  pathLeft.moveTo(pointsList[0].dx, pointsList[0].dy);
14  pathRight.moveTo(pointsList[0].dx, pointsList[0].dy);
15
16  for (int i = 0; i < pointsList.length; i++) {
17    if (pointsList[i] == null) {
18      continue;
19    }
20
21    if (i <= 1 || i >= pointsList.length - 5) {
22      pathLeft.lineTo(pointsList[i].dx, pointsList[i].dy);
23      pathRight.lineTo(pointsList[i].dx, pointsList[i].dy);
24      continue;
25    }
26
27    double x1 = pointsList[i-1].dx;
28    double x2 = pointsList[i].dx;
29    double lengthX = x2 - x1;
30
31    double y1 = pointsList[i-1].dy;
32    double y2 = pointsList[i].dy;
33    double lengthY = y2 - y1;
34
35    double length = sqrt((lengthX * lengthX) + (lengthY * lengthY));
36    double normalizedVectorX = lengthX / length;
37    double normalizedVectorY = lengthY / length;
38    double distance = 15;
39
40    double newXLeft = x1 - normalizedVectorY * (i / pointsList.length * distance);
41    double newYLeft = y1 + normalizedVectorX * (i / pointsList.length * distance);
42    
43    double newXRight = x1 - normalizedVectorY * (i / pointsList.length  * -distance);
44    double newYRight = y1 + normalizedVectorX * (i / pointsList.length * -distance);
45
46    pathLeft.lineTo(newXLeft, newYLeft);
47    pathRight.lineTo(newXRight, newYRight);
48  }
49
50  for (int i = pointsList.length - 1; i >= 0; i--) {
51    if (pointsList[i] == null) {
52      continue;
53    }
54
55    pathLeft.lineTo(pointsList[i].dx, pointsList[i].dy);
56    pathRight.lineTo(pointsList[i].dx, pointsList[i].dy);
57  }
58
59  canvas.drawShadow(pathLeft, Colors.grey, 3.0, false);
60  canvas.drawShadow(pathRight, Colors.grey, 3.0, false);
61  canvas.drawPath(pathLeft, paintLeft);
62  canvas.drawPath(pathRight, paintRight);
63}

This looks more complicated than it is. What we are doing here is drawing two paths that are parallel to the one that follows our finger. This is achieved by using some geometry. Given a point, we calculate the distance to the previous one using Pythagoras. We then divide the components by the length. This gives us the orthogonal vector between the center line and the left side. The negated value is the respective vector for the right side.
We multiply it by the current index divided by the number of points times the distance we set to 15. This way there are not two parallel curves but rather two curves that grow in their distance to the middle line.
In order to close both of the paths we then iterate from the last point to the first and draw lines from point to point until we reach the first point again.

If we were to spawn multiple fruits at once, every object would have the same rotation. Let’s change that by giving it a random rotation.

 1List<Widget> _getFruits() {
 2    List<Widget> list = new List();
 3
 4    for (Fruit fruit in fruits) {
 5      list.add(
 6        Positioned(
 7          top: fruit.position.dy,
 8          left: fruit.position.dx,
 9          child: Transform.rotate(
10            angle: fruit.rotation * pi * 2,
11            child: _getMelon(fruit)
12          )
13        )
14      );
15    }
16
17    return list;
18  }
19
20  Widget _getMelonCut(FruitPart fruitPart) {
21    return Transform.rotate(
22      angle: fruitPart.rotation * pi * 2,
23     
24    );
25  }
26
27  void _turnFruitIntoParts(Fruit hit) {
28    FruitPart leftFruitPart = FruitPart(
29        
30        rotation:  hit.rotation
31    );
32
33    FruitPart rightFruitPart = FruitPart(
34      
35      rotation:  hit.rotation
36    );
37
38class Fruit extends GravitationalObject {
39  Fruit({
40    
41    super.rotation = 0.0,
42  });
43}
44
45class FruitPart extends GravitationalObject {
46  FruitPart({
47    
48    super.rotation = 0.0,
49  });
50}
51
52abstract class GravitationalObject {
53  GravitationalObject({
54    
55    required this.rotation
56  });
57
58  
59  final double rotation;
60  
61}

We add a new field to our GravitationalObject: a rotation. The rotation is a double determining the number of 360 ° rotations. We then wrap the lines where we display the fruit and the fruit parts with a Transform.rotate widget whose angle is the rotation times pi * 2 because it expects the rotation to be given as a radian (in which 2 * pi is a 360 ° rotation). In _turnFruitIntoParts() we take care of the parts having the same rotation as the original fruit to make it look more natural.

Flutter Fruit Ninja animation
The resulting app

After having changed the background color a bit, displaying a score and triggering the spawn of a melon every now and then, we are finished for now. It’s up to your imagination where to go from here.

Final thoughts

Without the usage of a framework, we implemented a very basic version of the game Fruit Ninja. Yet, it’s only slicing and collecting points, but I am sure you guys have plenty of ideas about how to continue from here. Adding more fruit types, splashes, bombs, levels, high scores, a start screen etc. could be the next steps. You can find the full source on GitHub:

GET FULL CODE

Comments (11) ✍️

interested reader

the assets are missing from github
Reply to interested reader

Marc
In reply to interested reader's comment

You are right, sorry! I added the missing assets. Now the project should compile and run.
Reply to Marc

Ar Kar

thanks for sharing :)
Reply to Ar Kar

Marc
In reply to Ar Kar's comment

Thanks for appreciating :)
Reply to Marc

Cold Stone

Cool! Thx
Reply to Cold Stone

Sunnatillo Shavkatov

Great!
Reply to Sunnatillo Shavkatov

peter

thanks for this , but can you tell me which part of the code that makes the fruit to come down faster plus i want 5 fruit to spawn at once .and i think the gravity speed of the fruit reduces slowly as the time on the app increase
Reply to peter

Marc
In reply to peter's comment

Hey peter,

to make the fruits come down faster (apply a stronger gravity to all objects), you can just edit the GravitationalObject class: https://github.com/flutter-clutter/flutter-fruit-ninja-clone/blob/master/lib/canvas_area/models/gravitational_object.dart here, you could set double _gravity = 1.0; to something higher.

In order to spawn more than one fruit, you could just call the respective _spawnRandomFruit method in CanvasArea 5 times inside the tick and the initState. You might want to wrap it in a loop which you wrap in a function that you call at these two places: https://github.com/flutter-clutter/flutter-fruit-ninja-clone/blob/master/lib/canvas_area/canvas_area.dart

To reduce the gravity speed of the fruit over time (do you mean play time or lifetime of the fruit?), you need to track the time (either in the CanvasArea or the Fruit class, depending on what you want) and let it be a part of the applyGravity() method of the GravitationalObject.

Kind regards,
Marc

Reply to Marc

Montana

i want to dispose fruits that was never swiped . but i don’t know how it keeps making the app size bigger
Reply to Montana

Marc
In reply to Montana's comment

Hey Montana,

thank you for your suggestion. I guess if you want to dispose unswiped fruits, you’ll have to do something like this in the _tick() method with every fruit in the fruits List:

1if (fruit.position.dy >  MediaQuery.of(context).size.height) {
2  fruits.remove(fruit);
3}
Reply to Marc

Astrid

Hello, thanks for the tutorial! I am new to flutter and was wondering how to add different fruits to the fruit list. I tried to use the stack widget but it only displayed the most recent fruit that I added in. Any tips?
Reply to Astrid

Comment this 🤌

You are replying to 's commentRemove reference