DEV Community

loading...
Cover image for Let's create Dice Game App using Stateful Widget In Flutter

Let's create Dice Game App using Stateful Widget In Flutter

codingmonkey profile image Coding Monkey ・9 min read

Hi All,

Since the Last Blog, I am getting many followers and views(Not sure why though😅) But I am happy for it And I Thank all who decided to follow me. I will strive hard to give better content for you All.

Bowing

Introduction

As I continued to learn from the Angela Yu Udemy course In the Next chapter these are the things I have learnt that I am going to explain it to you guys as well:

  • Stateful Widget
  • Flat Button
  • Expanded widget
  • Random Function

So let's create the Dice Game along with learning new concepts in Flutter. At the end out App is going to look like this:

Alt Text

Coding Section

So let's begin with our Basic setup which has to be followed in every project. But which is so easy in Flutter since flutter provides commands to generate the basic setup.

flutter create dice_game
Enter fullscreen mode Exit fullscreen mode

Upon running the command we will get the all required files and folder get generated for us. Now let's Remove all non-required content and make changes according to our need. Such as adding the App icon and removing the content from the main.dart which got the code for the exemplary app. Also, store the dice face images which we are going to use in our app. (Which you might now how to do from my previous blog where I explained about it.😁)

MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.lightBlue,
        appBar: AppBar(
          title: Text('Dice Game'),
          backgroundColor: Colors.lightBlue,
        ),
        body: DicePage(),
      ),
    ),
  );
class DicePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
Enter fullscreen mode Exit fullscreen mode

Upon running the code we will be having a basic app with light blue background and App bar saying, Dice Game.

So now Let's add 2 dice images to the app. Using Image.asset widget which is the short and easy form of the Image widget which we previously used.

Image(image: AssetImage('images/dice1.png')) //Previous Method
Image.asset('images/dice1.png') // Simpler method 
                     //which does the same work as above one
Enter fullscreen mode Exit fullscreen mode
class DicePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Image.asset('images/dice1.png'),
        Image.asset('images/dice1.png'),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

So If you try to run this code you might come across an error saying A RenderFlex overflowed by 596 pixels on the right. which is due to not controlling the image sizes so the image is overflowing outside the phone screen. Now we have to make the image to stay inside the screen that can be done 2 ways:

  • Hard coding the image size using the width and height parameter in the Image Widget (which will not be optimal since there are so many devices with different screen sizes.)
  • Use of Expanded widget.

The Expanded widget makes the images to cover as much size is available in the screen but will not overflow outside the screen. When we have multiple images then it tries to cover the screen size using both images by dividing the available space.( which we want for our use case)

A widget that expands a child of a Row, Column, or Flex so that the child fills the available space.

Using an Expanded widget makes a child of a Row, Column, or Flex expand to fill the available space along the main axis (e.g., horizontally for a Row or vertically for a Column). If multiple children are expanded, the available space is divided among them according to the flex factor.

class DicePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(child: Image.asset('images/dice1.png')),
        Expanded(child: Image.asset('images/dice1.png')),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

So by covering the Image widget with Expanded widget the images are now not overflowing out of the screen.

Now let's add some space between the dices using the Padding widget. Also, let's bring those dices to the centre of the screen Which can be directly done by covering the Row widget
with Center widget.

class DicePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Image.asset('images/dice1.png'),
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Image.asset('images/dice1.png'),
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

Now let's Make those images get picked randomly using the Random Class which creates the random number for us.
First, let's convert the number in the image name as a variable so we can update it as we want and assign random values from 1-6 to that variable so every time we restart the app the new dice face is displayed. (Also forgot to mention I have named the all the images as dice<facevalue>.png to use like this. 😅)

class DicePage extends StatelessWidget {
  int leftDiceValue = Random().nextInt(6) + 1;
  int rightDiceValue = Random().nextInt(6) + 1;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Image.asset('images/dice$leftDiceValue.png'),
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Image.asset('images/dice$rightDiceValue.png'),
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Random().nextInt(max) Generates a non-negative random integer uniformly distributed in the range from 0, inclusive, to max, exclusive.

The Random().nextInt(max) function provides us with a random int Number from 0 to max but not the max value. for our case it's 0 - 6 but not 6 also we don't want the value 0 so let's tweak this function a little:

 int leftDiceValue = Random().nextInt(6) + 1;
Enter fullscreen mode Exit fullscreen mode

so the min value becomes 1 and max value 6.

Next to the use of images/dice$leftDiceValue.png which is called string interpolation. Where we pass the variable name using the $<variablename>. (Here the Article with Complete Dart Intro)

Once Randomise is out of the way now let's make those images a button so when we click on those images some action is performed such as updating the Dice face images. Also, as we are going to use the variables which can be mutable so we need to change the StatelessWidget to StatefulWidget.

To do that first we need to use the StatefulWidget which handles the changing the state in the state whenever there are some changes app redraw the first parent Build function it finds.

A widget that has mutable state.

State is information that (1) can be read synchronously when the widget is built and (2) might change during the lifetime of the widget. It is the responsibility of the widget implementer to ensure that the State is promptly notified when such state changes, using State.setState.

Stateful widgets are useful when the part of the user interface you are describing can change dynamically, e.g. due to having an internal clock-driven state, or depending on some system state. For compositions that depend only on the configuration information in the object itself and the BuildContext in which the widget is inflated, consider using StatelessWidget.

So let's convert our StatelessWidget to StatefulWidget which can be done by a single click in the Andriod Studio. Where you can see the some waring in it along with the solution that says convert to the stateful widget so just one click and we are now StatefulWidget from StatelessWidget.

class DicePage extends StatefulWidget {
  @override
  _DicePageState createState() => _DicePageState();
}

class _DicePageState extends State<DicePage> {
  int leftDiceValue = Random().nextInt(6) + 1;

  int rightDiceValue = Random().nextInt(6) + 1;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Image.asset('images/dice$leftDiceValue.png'),
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Image.asset('images/dice$rightDiceValue.png'),
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Once all this is done now let's make those images as a flat button so when clicked on that button we change the Dice's face.

FlatButton widget provides us with a button which is almost like anchor text of the web development. Now that I see it the FlatButton is updated to as TextButton widget.

A Material Design "Text Button".

Use text buttons on toolbars, in dialogues, or inline with other content but offset from that content with padding so that the button's presence is obvious. Text buttons do not have visible borders and must therefore rely on their position relative to other content for context. In dialogues and cards, they should be grouped together in one of the bottom corners. Avoid using text buttons where they would blend in with other content, for example in the middle of lists.

A text button is a labelled child displayed on a (zero elevation) Material widget. The label's Text and Icon widgets are displayed in the style's ButtonStyle.foregroundColor. The button reacts to touches by filling with the style's ButtonStyle.backgroundColor.

class _DicePageState extends State<DicePage> {
  int leftDiceValue = Random().nextInt(6) + 1;

  int rightDiceValue = Random().nextInt(6) + 1;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: TextButton(
                onPressed: () {},
                child: Image.asset('images/dice$leftDiceValue.png'),
              ),
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: TextButton(
                onPressed: () {},
                child: Image.asset('images/dice$rightDiceValue.png'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Once we run the code we can see that the padding got increased which is due to TextButton also comes with its own padding of 16px so we'll remove our custom Padding which we added previously. (Remove widget is also easy job in Andriod Studion we need to click on the widget we want to remove then click on the context action there select remove the widget. 🙂)

Also TextButton comes with onPressed parameter which takes a callback that is called when the button is tapped or otherwise activated. To test the click let's just add a print statement which is be run when we click on the Dice image.

class _DicePageState extends State<DicePage> {
  int leftDiceValue = Random().nextInt(6) + 1;

  int rightDiceValue = Random().nextInt(6) + 1;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        children: <Widget>[
          Expanded(
            child: TextButton(
              onPressed: () {print('Left Dice is clicked')},
              child: Image.asset('images/dice$leftDiceValue.png'),
            ),
          ),
          Expanded(
            child: TextButton(
              onPressed: () {print('Right Dice is clicked')},
              child: Image.asset('images/dice$rightDiceValue.png'),
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

So now when we click on the dice now we will see the print statements in our terminal now. Next, we'll make the click to change the dice face value.

For now, let's create a function which is to be called when we click on the dice. Upon calling the function changes the face value using the Random function which we previously used.

To show that there are some changes in the state of the widget we have to add the change face value code inside the setState() function which tell the StatefulWidget that there are some changes in the widget so we need to rebuild the app.
If we do not use the setState() function the changes will only be reflected only when we reload the app in future.

class _DicePageState extends State<DicePage> {
  int leftDiceValue = Random().nextInt(6) + 1;
  int rightDiceValue = Random().nextInt(6) + 1;

  void changeDiceFace() {
    setState(() {
      rightDiceValue = Random().nextInt(6) + 1;
      leftDiceValue = Random().nextInt(6) + 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        children: <Widget>[
          Expanded(
            child: TextButton(
              onPressed: () {
                changeDiceFace();
              },
              child: Image.asset('images/dice$leftDiceValue.png'),
            ),
          ),
          Expanded(
            child: TextButton(
              onPressed: () {
                changeDiceFace();
              },
              child: Image.asset('images/dice$rightDiceValue.png'),
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We are updating both dice face on clicking any one of the dice or else if we want to roll dice one at a time we just need to make the changes depending on the on clicked dice. But then we just add the change in the onPressed parameter rather than making new method for it.

Now you finally have running dice game app which rolls the dice when you click on the dice. (Nothing fancy 😅) Hope you like it.

Alt Text

Also here is the Widget tree:

Alt Text

Summary and Challenges

By creating this app we have learned to use several new widgets and techniques can be used with flutter app development such as StatefulWidget which is used to build a dynamic app, Image widget used to display images in the app, an Expanded widget which is used to control the size of the images displayed, Text button which provide us with text as a button and many more concepts.

challenges we faced during the creation of this app was to make the images to change when we clicking the image which we handled using the StatefulWidget and Random function offered by Dart, Then we had image overflow issue which we solved using the Expanded widget. Displaying images side by side which was done by using the Row widget.

Thank You

If you have followed the blog and you also got your own running dice game app in your phone (Which is nothing fancy 😣) Congratulate yourself on achieving it. Also thanks from my side as well for reading the whole blogs as well. If you liked the blog leave like and leave a comment. Same goes for if you find any mistakes do let me know So that I can correct myself 😔.

You can use the same code for displaying the images with some tweaks here and there to make revolving image gallery or can add some scoring system to make the app more engaging or any ideas that are coming to mind as well.

Auf Wiedersehen👋

Discussion (0)

pic
Editor guide