Tag: flutterdev

  • Flame Engine : Unleashing Flutter’s Game Development Potential

    With Flutter, developers can leverage a single codebase to seamlessly build applications for diverse platforms, including Android, iOS, Linux, macOS, Windows, Google Fuchsia, and the web. The Flutter team remains dedicated to empowering developers of all backgrounds, ensuring effortless creation and publication of applications using this powerful multi-platform UI toolkit.
    Flutter simplifies the process of developing standard applications effortlessly. However, if your aim is to craft an extraordinary game with stunning graphics, captivating gameplay, lightning-fast loading times, and highly responsive interactions, Flames emerges as the perfect solution.s
    This blog will provide you with an in-depth understanding of Flame. Through the features provided by Flame, you will embark on a journey to master the art of building a Flutter game from the ground up. You will gain invaluable insights into seamlessly integrating animations, configuring immersive soundscapes, and efficiently managing diverse game assets.

    1. Flame engine

    Flame is a cutting-edge 2D modular game engine designed to provide a comprehensive suite of specialized solutions for game development. Leveraging the powerful architecture of Flutter, Flame significantly simplifies the coding process, empowering you to create remarkable projects with efficiency and precision.

    1.1. Setup: 

    Run this command with Flutter:

    $ flutter pub add flame

    This will add a line like this to your package’s pubspec.yaml (and run an implicit flutter pub get):

    Dependencies:
    Flame: ^1.8.1

    Import it, and now, in your Dart code, you can use:

    import 'package:flame/flame.dart';

    1.2. Assets Structure: 

    Flame introduces a well-structured assets directory framework, enabling seamless utilization of these resources within your projects.
    To illustrate the concepts further, let’s delve into a practical example that showcases the application of the discussed principles:

    Flame.images.load(‘card_sprites.png');  	
    FlameAudio.play('shuffling.mp3');

    When utilizing image and audio assets in Flame, you can simply specify the asset name without the need for the full path, given that you place the assets within the suggested directories as outlined below.

    For better organization, you have the option to divide your audio folder into two distinct subfolders: music and sfx

    The music directory is intended for audio files used as background music, while the sfx directory is specifically designated for sound effects, encompassing shots, hits, splashes, menu sounds, and more.

    To properly configure your project, it is crucial to include the entry of above-mentioned directories in your pubspec.yaml file:

    1.3. Support to other platforms: 

    As Flame is built upon the robust foundation of Flutter, its platform support is inherently reliant on Flutter’s compatibility with various platforms. Therefore, the range of platforms supported by Flame is contingent upon Flutter’s own platform support.

    Presently, Flame offers extensive support for desktop platforms such as Windows, MacOS, and Linux, in addition to mobile platforms, including Android and iOS. Furthermore, Flame also facilitates game development for the web. It is important to note that Flame primarily focuses on stable channel support, ensuring a reliable and robust experience. While Flame may not provide direct assistance for the dev, beta, and master channels, it is expected that Flame should function effectively in these environments as well.

    1.3.1. Flutter web: 

    To optimize the performance of your web-based game developed with Flame, it is recommended to ensure that your game is utilizing the CanvasKit/Skia renderer. By leveraging the canvas element instead of separate DOM elements, this choice enhances web performance significantly. Therefore, incorporating the CanvasKit/Skia renderer within your Flame-powered game is instrumental in achieving optimal performance on the web platform.

    To run your game using Skia, use the following command:

    flutter run -d chrome --web-renderer canvaskit

    To build the game for production, using Skia, use the following:

    flutter build web --release --web-renderer canvaskit

    2. Implementation

    2.1 GameWidget: 

    To integrate a Game instance into the Flutter widget tree, the recommended approach is to utilize the GameWidget. This widget serves as the root of your game application, enabling seamless integration of your game. You can incorporate a Game instance into the widget tree by following the example provided below:

    void main() {
      runApp(
        GameWidget(game: MyGame()),
      );
    }

    By adopting this approach, you can effectively add your Game instance to the Flutter widget tree, ensuring proper execution and integration of your game within the Flutter application structure.

    2.2 GameWidget:

    When developing games in Flutter, it is crucial to utilize a widget that can efficiently handle high refresh rates, speedy memory allocation, and deallocation and provide enhanced functionality compared to the Stateless and Stateful widgets. Flame offers the FlameGame class, which excels in providing these capabilities.

    By utilizing the FlameGame class, you can create games by adding components to it. This class automatically calls the update and render methods of all the components added to it. Components can be directly added to the FlameGame through the constructor using the named children argument, or they can be added from anywhere else using the add or addAll methods.

    To incorporate the FlameGame into the widget tree, you need to pass its object to the GameWidget. Refer to the example below for clarification:

    class CardMatchGame extends FlameGame {
      @override
      Future<void> onLoad() async {
        await add(CardTable());
      }
    }
    
    main() {
      final cardMatchGame = CardMatchGame(children: [CardTable]);
      runApp(
        GameWidget(
          game: cardMatchGame,
        ),
      );
    }

    2.3 Component:

    This is the last piece of the puzzle. The smallest individual components that make up the game. This is like a widget but within the game. All components can have other components as children, and all components inherit from the abstract class Component. These components serve as the fundamental entities responsible for rendering and interactivity within the game, and their hierarchical organization allows for flexible and modular construction of complex game systems in Flame. These components have their own lifecycle. 

    Component Lifecycle: 

     

    Figure 01

    2.3.1. onLoad:

    The onLoad method serves as a crucial component within the game’s lifecycle, allowing for the execution of asynchronous operations such as image loading. Positioned between the onGameResize and onMount callbacks, this method is strategically placed to ensure the necessary assets are loaded and prepared. In Figure 01 of the component lifecycle, onLoad is set as the initial method due to its one-time execution. It is within this method that all essential assets, including images, audio files, and tmx files, should be loaded. This ensures that these assets are readily available for utilization throughout the game’s progression.

    2.3.2. onGameResize:

    Invoked when new components are added to the component tree or when the screen undergoes resizing, the onGameResize method plays a vital role in handling these events. It is executed before the onMount callback, allowing for necessary adjustments to be made in response to changes in component structure or screen dimensions.

    2.3.3. onParentResize:

    This method is triggered when the parent component undergoes a change in size or whenever the current component is mounted within the component tree. By leveraging the onParentResize callback, developers can implement logic that responds to parent-level resizing events and ensures the proper rendering and positioning of the component.

    2.3.4. onMount:

    As the name suggests, the onMount method is executed each time a component is mounted into the game tree. This critical method offers an opportunity to initialize the component and perform any necessary setup tasks before it becomes an active part of the game.

    2.3.5. onRemove:

    The onRemove method facilitates the execution of code just before a component is removed from the game tree. Regardless of whether the component is removed using the parent’s remove method or the component’s own remove method, this method ensures that the necessary cleanup actions take place in a single execution.

    2.3.6. onChildrenChanged:

    The onChildrenChanged method is triggered whenever a change occurs in a child component. Whether a child is added or removed, this method provides an opportunity to handle the updates and react accordingly, ensuring the parent component remains synchronized with any changes in its children.

    2.3.7. Render & Update Loop:

    The Render method is responsible for generating the user interface, utilizing the available data to create the game screen. It provides developers with canvas objects, allowing them to draw the game’s visual elements. On the other hand, the Update method is responsible for modifying and updating this rendered UI. Changes such as resizing, repositioning, or altering the appearance of components are managed through the Update method. In essence, any changes observed in the size or position of a component can be attributed to the Update method, which ensures the dynamic nature of the game’s user interface.

    3. Sample Project

    To showcase the practical implementation of key classes like GameWidget, FlameGame, and essential Components within the Flame game engine, we will embark on the creation of a captivating action game. By engaging in this hands-on exercise, you will gain valuable insights and hands-on experience in utilizing Flame’s core functionalities and developing compelling games. Through this guided journey, you will unlock the knowledge and skills necessary to create engaging and immersive gaming experiences, while harnessing the power of Flame’s robust framework.

    Let’s start with:

    3.1. Packages & assets: 

    3.1.1. Create a project using the following command:

    flutter create flutter_game_poc

    3.1.2. Add these under dependencies of pubspec.yaml (and run command flutter pub get):

    flame: ^1.8.0

    3.1.3. As mentioned earlier in the Asset Structure section, let’s create a directory called assets in your project and include an images subdirectory within it. Download assets from here, add both the assets to this  images directory.

    Figure 02 

    Figure 03

    In our game, we’ll use “Figure 02” as the background image and “Figure 03” as the avatar character who will be walking. If you have separate images for the avatar’s different walking frames, you can utilize a sprite generator tool to create a sprite sheet from those individual images.

    A sprite generator helps combine multiple separate images into a single sprite sheet, which enables efficient rendering and animation of the character in the game. You can find various sprite generator tools available online that can assist in generating a sprite sheet from your separate avatar images.

    By using a sprite sheet, you can easily manage and animate the character’s walking motion within the game, providing a smooth and visually appealing experience for the players.

    After uploading, your asset structure will look like this: 

    Figure 04

    3.1.4. To use these assets, we have to register them into pubspect.yaml below assets section: 

    assets: 
       -  assets/images/

    3.2. Supporting code: 

    3.2.1. Create 3 directories  constants, overlays, and components inside the lib directory.

    3.2.2. First, we will start with a constants directory where we have to create 4 files as follows:

       all_constants.dart. 

    export 'asset _constants.dart';
    export 'enum_ constants.dart';
    export 'key_constants.dart';

       assets_constants.dart. 

    class AssetConstants {
     static String backgroundImage = 'background.png';
     static String avatarImage = 'avatar_sprite.png';
    }

       enum_constants.dart. 

    enum WalkingDirection {idle, up, down, left, right};

       key_constants.dart. 

    class KeyConstants {
     static String overlayKey = 'DIRECTION_BUTTON';
    }

    3.2.3. In addition to the assets directory, we will create an overlay directory to include elements that need to be constantly visible to the user during the game. These elements typically include information such as the score, health, or action buttons.

    For our game, we will incorporate five control buttons that allow us to direct the gaming avatar’s movements. These buttons will remain visible on the screen at all times, facilitating player interaction and guiding the avatar’s actions within the game environment.

    Organizing these overlay elements in a separate directory makes it easier to manage and update the user interface components that provide vital information and interaction options to the player while the game is in progress.

    In order to effectively manage and control the position of all overlay widgets within our game, let’s create a dedicated controller. This controller will serve as a centralized entity responsible for orchestrating the placement and behavior of these overlay elements. Create a file named  overlay_controller.dart.

    All the files in the overlays directory are common widgets that extend Stateless widget.

    class OverlayController extends StatelessWidget {
     final WalkingGame game;
     const OverlayController({super.key, required this.game});
    
    
     @override
     Widget build(BuildContext context) {
       return Column(children: [
         Row(children: [ButtonOverlay(game: game)])
       ]);
     }
    }

    3.2.5. In our game, all control buttons share a common design, featuring distinct icons and functionalities. To streamline the development process and maintain a consistent user interface, we will create a versatile widget called DirectionButton. This custom widget will handle the uniform UI design for all control buttons.

    Inside the overlays directory, create a directory called widgets and add a file called direction_button.dart in that directory. This file defines the shape and color of all control buttons. 

    class DirectionButton extends StatelessWidget {
     final IconData iconData;
     final VoidCallback onPressed;
    
    
     const DirectionButton(
         {super.key, required this.iconData, required this.onPressed});
    
    
     @override
     Widget build(BuildContext context) {
       return Container(
         height: 40,
         width: 40,
         margin: const EdgeInsets.all(4),
         decoration: const BoxDecoration(
             color: Colors.black45,
             borderRadius: BorderRadius.all(Radius.circular(10))),
         child: IconButton(
           icon: Icon(iconData),
           iconSize: 20,
           color: Colors.white,
           onPressed: onPressed,
         ),
       );
     }
    }

    class ButtonOverlay extends StatelessWidget {
     final WalkingGame game;
     const ButtonOverlay({Key? key, required this.game}) : super(key: key);
    
    
     @override
     Widget build(BuildContext context) {
       return SizedBox(
         height: MediaQuery.of(context).size.height,
         width: MediaQuery.of(context).size.width,
         child: Column(
           children: [
             Expanded(child: Container()),
             Row(
               children: [
                 Expanded(child: Container()),
                 DirectionButton(
                   iconData: Icons.arrow_drop_up,
                   onPressed: () {
                     game.direction = WalkingDirection.up;
                   },
                 ),
                 const SizedBox(height: 50, width: 50)
               ],
             ),
             Row(
               children: [
                 Expanded(child: Container()),
                 DirectionButton(
                   iconData: Icons.arrow_left,
                   onPressed: () {
                     game.direction = WalkingDirection.left;
                   },
                 ),
                 DirectionButton(
                   iconData: Icons.pause,
                   onPressed: () {
                     game.direction = WalkingDirection.idle;
                   },
                 ),
                 DirectionButton(
                   iconData: Icons.arrow_right,
                   onPressed: () {
                     game.direction = WalkingDirection.right;
                   },
                 ),
               ],
             ),
             Row(
               children: [
                 Expanded(child: Container()),
                 DirectionButton(
                   iconData: Icons.arrow_drop_down,
                   onPressed: () {
                     game.direction = WalkingDirection.down;
                   },
                 ),
                 const SizedBox(height: 50, width: 50),
               ],
             ),
           ],
         ),
       );
     }
    }

    3.3. Core logic: 

    Moving forward, we will leverage the code we have previously implemented, building upon the foundations we have laid thus far:

    3.3.1.  The first step is to create a component. As discussed earlier, all the individual elements in the game are considered components, so let’s create 1 component that will be our gaming avatar. For the UI of this avatar, we are going to use assets shown in Figure 03.

    For the avatar, we will be using SpriteAnimationComponents as we want this component to animate automatically.

    In the components directory, create a file called avatar_component.dart. This file will hold the logic of when and how our game avatar will move. 

    In the onLoad() method, we are loading the asset and using it to create animations, and in the update() method, we are using an enum to decide the walking animation.

    class AvatarComponent extends SpriteAnimationComponent with HasGameRef {
     final WalkingGame walkingGame;
     AvatarComponent({required this.walkingGame}) {
       add(RectangleHitbox());
     }
     late SpriteAnimation _downAnimation;
     late SpriteAnimation _leftAnimation;
     late SpriteAnimation _rightAnimation;
     late SpriteAnimation upAnimation;
     late SpriteAnimation _idleAnimation;
     final double _animationSpeed = .1;
    
    
     @override
     Future<void> onLoad() async {
       await super.onLoad();
    
    
       final spriteSheet = SpriteSheet(
         image: await gameRef.images.load(AssetConstants.avatarImage),
         srcSize: Vector2(2284 / 12, 1270 / 4),
       );
    
    
       _downAnimation =
           spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 11);
       _leftAnimation =
           spriteSheet.createAnimation(row: 1, stepTime: _animationSpeed, to: 11);
       upAnimation =
           spriteSheet.createAnimation(row: 3, stepTime: _animationSpeed, to: 11);
       _rightAnimation =
           spriteSheet.createAnimation(row: 2, stepTime: _animationSpeed, to: 11);
       _idleAnimation =
           spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 1);
       animation = _idleAnimation;
     }
    
    
     @override
     void update(double dt) {
       switch (walkingGame.direction) {
         case WalkingDirection.idle:
           animation = _idleAnimation;
           break;
         case WalkingDirection.down:
           animation = _downAnimation;
           if (y < walkingGame.mapHeight - height) {
             y += dt * walkingGame.characterSpeed;
           }
           break;
         case WalkingDirection.left:
           animation = _leftAnimation;
           if (x > 0) {
             x -= dt * walkingGame.characterSpeed;
           }
           break;
         case WalkingDirection.up:
           animation = upAnimation;
           if (y > 0) {
             y -= dt * walkingGame.characterSpeed;
           }
           break;
         case WalkingDirection.right:
           animation = _rightAnimation;
           if (x < walkingGame.mapWidth - width) {
             x += dt * walkingGame.characterSpeed;
           }
           break;
       }
       super.update(dt);
     }
    }

    3.1.2. Our Avatar is ready to walk now, but there is no map or world where he can do that. So, let’s create a game and add a background to it.  

    Create file name walking_game.dart in the lib directory and add the following code.

    class WalkingGame extends FlameGame with HasCollisionDetection {
     late double mapWidth =2520 ;
     late double mapHeight = 1300;
     WalkingDirection direction = WalkingDirection.idle;
     final double characterSpeed = 80;
     final _world = World();
    
    
     // avatar sprint
     late AvatarComponent _avatar;
    
    
     // Background image
     late SpriteComponent _background;
     final Vector2 _backgroundSize = Vector2(2520, 1300);
    
    
     // Camera Components
     late final CameraComponent _cameraComponent;
    
    
     @override
     Future<void> onLoad() async {
       await super.onLoad();
    
    
       overlays.add(KeyConstants.overlayKey);
    
    
       _background = SpriteComponent(
         sprite: Sprite(
           await images.load(AssetConstants.backgroundImage),
           srcPosition: Vector2(0, 0),
           srcSize: _backgroundSize,
         ),
         position: Vector2(0, 0),
         size: Vector2(2520, 1300),
       );
       _world.add(_background);
    
    
       _avatar = AvatarComponent(walkingGame: this)
         ..position = Vector2(529, 128)
         ..debugMode = true
         ..size = Vector2(1145 / 24, 635 / 8);
    
    
       _world.add(_avatar);
    
    
       _cameraComponent = CameraComponent(world: _world)
         ..setBounds(Rectangle.fromLTRB(390, 200, mapWidth - 390, mapHeight - 200))
         ..viewfinder.anchor = Anchor.center
         ..follow(_avatar);
    
    
       addAll([_cameraComponent, _world]);
     }
    }

    First thing in onLoad(), you can see that we are adding an overlay using a key. You can learn more about this key in the main class.

    Next is to create background components using SpriteComponent and add it to the world component. For creating the background component, we are using SpriteComponent instead of SpriteAnimationComponent because we do not need any background animation in our game.

    Then we add AvatarComponent in the same world component where we added the background component. To keep the camera fixed on the AvatarComponent, we are using 1 extra component, which is called CameraComponent.

    Lastly, we are adding both world & CameraComponents in our game by using addAll() method.

    3.1.3. Finally, we have to create the main.dart file. In this example, we are wrapping a GameWidget with MaterialApp because we want to use some features of material themes like icons, etc., in this project. If you do not want to do that, you can pass GameWidget to the runApp() method directly.
    Here we are not only adding the WalkingGame into GameWidget but also adding an overlay, which will show the control buttons. The key mentioned here for the overlay is the same key we added in walking_game.dart file’s onLoad() method.

    void main() {
     WidgetsFlutterBinding.ensureInitialized();
     Flame.device.fullScreen();
     runApp(MaterialApp(
       home: Scaffold(
         body: GameWidget(
           game: WalkingGame(),
           overlayBuilderMap: {
             KeyConstants.overlayKey: (BuildContext context, WalkingGame game) {
               return OverlayController(game: game);
             }
           },
         ),
       ),
     ));
    }

    After all this, our game will look like this, and with these 5 control buttons, we can tell your avatar to move and/or stop.

    4. Result

    For your convenience, the complete code for the project can be found here. Feel free to refer to this code repository for a comprehensive overview of the implementation details and to access the entirety of the game’s source code.

    5. Conclusion

    Flame game engine alleviates the burden of crucial tasks such as asset loading, managing refresh rates, and efficient memory management. By taking care of these essential functionalities, Flame allows developers to concentrate on implementing the core functionality and creating an exceptional game application.

    By leveraging Flame’s capabilities, you can maximize your productivity and create an amazing game application that resonates with players across various platforms, all while enjoying the benefits of a unified codebase.

    6. References

    1. https://docs.flutter.dev/
    2. https://pub.dev/packages/flame
    3. https://docs.flame-engine.org/latest
    4. https://medium.flutterdevs.com/flame-with-flutter-4c6c3bd8931c
    5. https://supabase.com/blog/flutter-real-time-multiplayer-game
    6. https://www.kodeco.com/27407121-building-games-in-flutter-with-flame-getting-started
    7. https://blog.codemagic.io/flutter-flame-game-development/
    8. https://codelabs.developers.google.com/codelabs/flutter-flame-game

  • A Primer To Flutter

    In this blog post, we will explore the basics of cross platform mobile application development using Flutter, compare it with existing cross-platform solutions and create a simple to-do application to demonstrate how quickly we can build apps with Flutter.

    Brief introduction

    Flutter is a free and open source UI toolkit for building natively compiled applications for mobile platforms like Android and iOS, and for the web and desktop as well. Some of the prominent features are native performance, single codebase for multiple platforms, quick development, and a wide range of beautifully designed widgets.

    Flutter apps are written in Dart programming language, which is a very intuitive language with a C-like syntax. Dart is optimized for performance and developer friendliness. Apps written in Dart can be as fast as native applications because Dart code compiles down to machine instructions for ARM and x64 processors and to Javascript for the web platform. This, along with the Flutter engine, makes Flutter apps platform agnostic.

    Other interesting Dart features used in Flutter apps is the just-in-time (JIT) compiler, used during development and debugging, which powers the hot reload functionality. And the ahead-of-time (AOT) compiler which is used when building applications for the target platforms such as Android or iOS, resulting in native performance.

    Everything composed on the screen with Flutter is a widget including stuff like padding, alignment or opacity. The Flutter engine draws and controls each pixel on the screen using its own graphics engine called Skia.

    Flutter vs React-Native

    Flutter apps are truly native and hence offer great performance, whereas apps built with react-native requires a JavaScript bridge to interact with OEM widgets. Flutter apps are much faster to develop because of a wide range of built-in widgets, good amount of documentation, hot reload, and several other developer-friendly choices made by Google while building Dart and Flutter. 

    React Native, on the other hand, has the advantage of being older and hence has a large community of businesses and developers who have experience in building react-native apps. It also has more third party libraries and packages as compared to Flutter. That said, Flutter is catching up and rapidly gaining momentum as evident from Stackoverflow’s 2019 developer survey, where it scored 75.4% under “Most Loved Framework, Libraries and Tools”.

     

    All in all, Flutter is a great tool to have in our arsenal as mobile developers in 2020.

    Getting started with a sample application

    Flutter’s official docs are really well written and include getting started guides for different OS platforms, API documentation, widget catalogue along with several cookbooks and codelabs that one can follow along to learn more about Flutter.

    To get started with development, we will follow the official guide which is available here. Flutter requires Flutter SDK as well as native build tools to be installed on the machine to begin development. To write apps, one may use Android Studios or VS Code, or any text editor can be used with Flutter’s command line tools. But a good rule of thumb is to install Android Studio because it offers better support for management of Android SDK, build tools and virtual devices. It also includes several built-in tools such as the icons and assets editor.

    Once done with the setup, we will start by creating a project. Open VS Code and create a new Flutter project:

    We should see the main file main.Dart with some sample code (the counter application). We will start editing this file to create our to-do app.

    Some of the features we will add to our to-do app:

    • Display a list of to-do items
    • Mark to-do items as completed
    • Add new item to the list

    Let’s start by creating a widget to hold our list of to-do items. This is going to be a StatefulWidget, which is a type of widget with some state. Flutter tracks changes to the state and redraws the widget when a new change in the state is detected.

    After creating theToDoList widget, our main.Dart file looks like this:

    /// imports widgets from the material design 
    import 'package:flutter/material.dart';
    
    void main() => runApp(TodoApp());
    
    /// Stateless widgets must implement the build() method and return a widget. 
    /// The first parameter passed to build function is the context in which this widget is built
    class TodoApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'TODO',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: TodoList(),
        );
      }
    }
    
    /// Stateful widgets must implement the createState method
    /// State of a stateless widget against has a build() method with context
    class TodoList extends StatefulWidget {
      @override
      State<StatefulWidget> createState() => TodoListState();
    }
    
    class TodoListState extends State<TodoList> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Todo'),
          ),
          body: Text('Todo List'),
        );
      }
    }

    The ToDoApp class here extends Stateless widget i.e. a widget without any state whereas ToDoList extends StatefulWidget. All Flutter apps are a combination of these two types of widgets. StatelessWidgets must implement the build() method whereas Stateful widgets must implement the createState() method.

    Some built-in widgets used here are the MaterialApp widget, the Scaffold widget and AppBar and Text widgets. These all are imported from Flutter’s implementation of material design, available in the material.dart package. Similarly, to use native looking iOS widgets in applications, we can import widgets from the flutter/cupertino.dart package.

    Next, let’s create a model class that represents an individual to-do item. We will keep this simple i.e. only store label and completed status of the to-do item.

    class Todo {
      final String label;
      bool completed;
      Todo(this.label, this.completed);
    }

    The constructor we wrote in the code above is implemented using one of Dart’s syntactic sugar to assign a constructor argument to the instance variable. For more such interesting tidbits, take the Dart language tour.

    Now let’s modify the ToDoListState class to store a list of to-do items in its state and also display it in a list. We will use ListView.builder to create a dynamic list of to-do items. We will also use Checkbox and Text widget to display to-do items.

    /// State is composed all the variables declared in the State implementation of a Stateful widget
    class TodoListState extends State<TodoList> {
      final List<Todo> todos = List<Todo>();
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Todo'),
          ),
          body: Padding(
            padding: EdgeInsets.all(16.0),
            child: todos.length > 0
                ? ListView.builder(
                    itemCount: todos.length,
                    itemBuilder: _buildRow,
                  )
                : Text('There is nothing here yet. Start by adding some Todos'),
          ),
        );
      }
    
      /// build a single row of the list
      Widget _buildRow(context, index) => Row(
            children: <Widget>[
              Checkbox(
                  value: todos[index].completed,
                  onChanged: (value) => _changeTodo(index, value)),
              Text(todos[index].label,
                  style: TextStyle(
                      decoration: todos[index].completed
                          ? TextDecoration.lineThrough
                          : null))
            ],
          );
    
      /// toggle the completed state of a todo item
      _changeTodo(int index, bool value) =>
          setState(() => todos[index].completed = value);
    }

    A few things to note here are: private functions start with an underscore, functions with a single line of body can be written using fat arrows (=>) and most importantly, to change the state of any variable contained in a Stateful widget, one must call the setState method.

    The ListView.builder constructor allows us to work with very large lists, since list items are created only when they are scrolled.

    Another takeaway here is the fact that Dart is such an intuitive language that it is quite easy to understand and you can start writing Dart code immediately.

    Everything on a screen, like padding, alignment or opacity, is a widget. Notice in the code above, we have used Padding as a widget that wraps the list or a text widget depending on the number of to-do items. If there’s nothing in the list, a text widget is displayed with some default message.

    Also note how we haven’t used the new keyword when creating instances of a class, say Text. That’s because using the new keyword is optional in Dart and discouraged, according to the effective Dart guidelines.

    Running the application

    At this point, let’s run the code and see how the app looks on a device. Press F5, then select a virtual device and wait for the app to get installed. If you haven’t created a virtual device yet, refer to the getting started guide.

    Once the virtual device launches, we should see the following screen in a while. During development, the first launch always takes a while because the entire app gets built and installed on the virtual device, but subsequent changes to code are instantly reflected on the device, thanks to Flutter’s amazing hot reload feature. This reduces development time and also allows developers and designers to experiment more frequently with the interface changes.

    As we can see, there are no to-dos here yet. Now let’s add a floating action button that opens a dialog which we will use to add new to-do items.

    Adding the FAB is as easy as passing floatingActionButton parameter to the scaffold widget.

    floatingActionButton: FloatingActionButton(
      child: Icon(Icons.add),                                /// uses the built-in icons
      onPressed: () => _promptDialog(context),
    ),

    And declare a function inside ToDoListState that displays a popup (AlertDialog) with a text input box.

    /// display a dialog that accepts text
      _promptDialog(BuildContext context) {
        String _todoLabel = '';
        return showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                title: Text('Enter TODO item'),
                content: TextField(
                    onChanged: (value) => _todoLabel = value,
                    decoration: InputDecoration(hintText: 'Add new TODO item')),
                actions: <Widget>[
                  FlatButton(
                    child: new Text('CANCEL'),
                    onPressed: () => Navigator.of(context).pop(),
                  ),
                  FlatButton(
                    child: new Text('ADD'),
                    onPressed: () {
                      setState(() => todos.add(Todo(_todoLabel, false)));
                      /// dismisses the alert dialog
                      Navigator.of(context).pop();
                    },
                  )
                ],
              );
            });
      }

    At this point, saving changes to the file should result in the application getting updated on the virtual device (hot reload), so we can just click on the new floating action button that appeared on the bottom right of the screen and start testing how the dialog looks.

    We used a few more built-in widgets here:

    • AlertDialog: a dialog prompt that opens up when clicking on the FAB
    • TextField: text input field for accepting user input
    • InputDecoration: a widget that adds style to the input field
    • FlatButton: a variation of button with no border or shadow
    • FloatingActionButton: a floating icon button, used to trigger primary action on the screen

    Here’s a quick preview of how the application should look and function at this point:

    And just like that, in less than 100 lines of code, we’ve built the user interface of a simple, cross platform to-do application.

    The source code for this application is available here.

    A few links to further explore Flutter:

    Conclusion:

    To conclude, Flutter is  an extremely powerful toolkit to build cross platform applications that have native performance and are beautiful to look at. Dart, the language behind Flutter, is designed considering the nuances of user interface development and Flutter offers a wide range of built-in widgets. This makes development fun and development cycles shorter; something that we experienced while building the to-do app. With Flutter, time to market is also greatly reduced which enables teams to experiment more often, collect more feedback and ship applications faster.  And finally, Flutter has a very enthusiastic and thriving community of designers and developers who are always experimenting and adding to the Flutter ecosystem.