Welcome to Tikal's Flutter Workshop

Flutter is a great framework for creating beautiful apps both for Android and IOS, and in the future, for more platforms.

Flutter apps are based on a single codebase developed in Google's Dart programming language.

Flutter Key Features:

Flutter Home Page

Dart Home Page

What you will build:

In this workshop - codelab, you will create your first Flutter app, learn the basic concepts of Flutter such as Widget, Context and State

Your app will:

Final App

What you will learn:

Get the complete Source Code Here

What you will do here:

Create New Flutter Project

  1. Open Android Studio
  2. In the welcome screen select: Create new Flutter Project
  3. In the next screen select Flutter Application
  4. Set project name and location
  5. Accept all the defaults selections and complete the project initialization

Explore Flutter Project Structure

Create an Android Emulator

  1. Click on the AVD manager icon:
  2. Click on +Create Virtual Device button

  1. Select the default emulator and click Next
  2. Select a System Image, download it if required (Recommended for this tutorial, 27 - Oreo)

  1. Continue to configuration, set the emulator name,
  2. Click Finish.
  3. Click the run button to start the emulator
  4. Once the emulator is running, run flutter doctor command, notice device connected.

Run the default app

  1. Run the app from the Run icon
  2. This is the Flutter default demonstration app
  3. Check out the counter button operation
  4. In the project file, under the MaterialApp modify the theme color from blue to green
  5. Press Ctrl+S to save, this will do Hot Reload, notice the color change on the screen.

Run the App from the command line

  1. Stop the app from the Stop button
  2. Open IDE terminal
  3. Run flutter run -d all command
  4. Wait for the app to run
  5. Change the _counter++ to decrement _counter-- (on line 59)
  6. In the terminal, press r to do hot reload, press the + button, the counter should now decrease.

What you will do here:

Create the Application file

  1. Delete all code in main.dart which given by the flutter sample project
  2. Under the lib folder, create new application.dart file
  3. Open the application file, import the package:flutter/material.dart package
  4. Create MyApp class which extends StatelessWidget
  5. Override the build()method and return a MaterialApp Widget
  6. In the MaterialApp constructor, add title: "Flutter Workshop"
  7. For now, set the home: argument to be Container()
//application.dart
import 'package:flutter/material.dart';

class MyApp extends StatelessWidget{
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: "Flutter workshop",
     home: Container(),
   );
 }
}
  1. In the empty main.dart file, add void main() function and call the runApp function with MyApp() as a parameter:
  2. Import the material package as necessary
//main.dart
import 'package:flutter/material.dart';
import 'package:flutter_workshop/application.dart';

void main() => runApp(MyApp());
  1. Open Android Studio terminal, run the app by typing:
    flutter run -d all

What you will do here

Create The HomePage

  1. Under the lib folder, create a new pages package
  2. Inside the new pages package, create home_page package
  3. Inside the home_page package, create a new home_page.dart file
  4. Open the home_page.dart file, create the HomePage class, extends StatelessWidget, override the build()method and return a Container()
    (Import the material package as necessary)
//home_page.dart
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Container();
 }
}
  1. Go back to the application.dart replace the Container() with the new HomePage()
    Make import as necessary
//application.dart
return MaterialApp(
 title: "Flutter workshop",
 home: HomePage(),
)
  1. Do Hot Reload - In the terminal type r, app still has black screen.

Home Page Body

  1. In the build method of the HomePage class, replace the Container() widget with a Scaffold() widget
    The Scaffold widget represents a Material Design page structure that can contain various components such as:
  1. Inside the Scaffold constructor, add the body: argument and set it to a Center() Widget
  2. Set the Center child: widget to Text("Hello Flutter")
//main_page.dart
...
return Scaffold(
   body: Center(
     child: Text("Hello Flutter"),
   ),
 );
}
  1. Do hot reload, you should now see the homepage now:

What you will do here

  • Add a RaisedButton widget to the homepage
  • Design button color and style
  • Handle button click event
  • Use IDE shortcuts

  1. In the home_page.dart file, click the Text() child widget inside the Center widget, press Alt+Enter
  2. Select ‘Wrap with new widget'

  1. Rename the new widget to RaisedButton(),
    In the RaisedButton constructor, set the child: widget to Text("Login")
  2. Add a comma after the button Text(), press Alt+Enter to reformat the code
    Notice the difference with and without the ,
//home_page.dart
//...
body: Center(
child: RaisedButton(
  child: Text("Login"),
  ),
)
  1. Do hot reload, notice that the button is wrapped and disabled yet, that's because we didn't set an onPressed callback. We will do it in the next following steps.

Add Padding and Enabling

  1. Click on the RaisedButton, wrap it with Container as done above
  2. Set the container width: double.infinity,
  3. Set the height to 50
  4. Do hot reload, the button is now centered again and stretched to all screen width
  5. A Container widget can also set padding to its child.
  6. In the Container, add padding, set the padding to padding: const EdgeInsets.symmetric(horizontal: 20.0)
  7. Do hot reload, the button should now have some horizontal padding
  8. Let's enable the button by adding onPressed callback
  9. In the button constructor, under the child: Text(), add onPressed:(){},
  10. Add log statement print("Button clicked");
//home_page.dart
//...
child: RaisedButton(
 child: Text("Login"),
 onPressed: (){
  print("Button Clicked");
 }, 
), //RaisedButton
  1. Do hot reload, the button should be enabled, click the button and watch the log messages in the console.

Styling the button and text

  1. In the RaisedButton constructor add a color: argument - color: Theme.of(context).primaryColor
  2. Set the button text color: textColor: Theme.of(context).primaryTextTheme.button.color
  3. Do hot reload, see the button and text color.
  4. Don't forget commas, press Alt+Cmd+L to reformat the code if required.
//home_page.dart
//...
Container(
 height: 50,
 width: double.infinity,
 padding: const EdgeInsets.symmetric(horizontal: 20.0),
 child: RaisedButton(
   color: Theme.of(context).primaryColor,
   textColor: Theme.of(context).primaryTextTheme.button.color,
   child: Text("Hello Flutter"),
   onPressed: (){
     print("Button clicked");
   },
 ),
)

What you will do here

Add new package and page Dart file

  1. Under the pages package, create new page package - gallery_page

  1. In the new package create new dart file: gallery_page.dart
  2. In the new file, create GalleryPage class, extends StatefulWidget
  3. In the new class, override the createState() method
  4. In the ImagesPage, under the GalleryPage class, create another class _GalleryPageState class that extends State<GalleryPage>
  5. In the State class, override the build() method
  1. In the createState() method, return a new _GalleryPageState()
//gallery_page.dart
class GalleryPage extends StatefulWidget {
 @override
 State<StatefulWidget> createState() =>  _ImagesPagesState();
}

class _GalleryPageState extends State<GalleryPage>{
 @override
 Widget build(BuildContext context) {
   return null;
 }
}

ImagesPage body

  1. In the State build() method, return Scaffold() widget.
  2. Set an appBar: AppBar() with title: Text("Gallery Page")
  3. Set appBar elevation: 0
  4. Set the page body: top widget to be Container() with padding of 8.0 points, this widget will wrap the whole page body and allows you to add padding, and other decorations such as background color and border.
//gallery_page.dart
//...
body: Container(
   padding: const EdgeInsets.all(8.0),
)
  1. Next, set the Container child: widget to Column()
  1. Set the Column attributes to:
    mainAxisAlignment: MainAxisAlignment.center,
    mainAxisSize: MainAxisSize.max,
    crossAxisAlignment: CrossAxisAlignment.stretch,
  2. Add an children: <Widget>[] array, this array will contain the column child widgets vertically.
  3. The Scaffold() should now looks like this:
//gallery_page.dart
//...
Scaffold(
 appBar: AppBar(
   title: Text("Images page"),
   elevation: 0,
 ),
 body: Container(
   padding: const EdgeInsets.all(8.0),
   child: Column(
     mainAxisSize: MainAxisSize.max,
     mainAxisAlignment: MainAxisAlignment.start,
     crossAxisAlignment: CrossAxisAlignment.stretch,
     children: <Widget>[

     ],
   ),
 ),
);

Open the GalleryPage

  1. In the home_page.dart file, select the Login RaisedButton
  2. In the onPressed: (){} callback, use the Flutter Navigator to open the new GalleryPage():
    Navigator.of(context).push(MaterialPageRoute(builder: (context){ return GalleryPage();}));
//home_page.dart
//...
onPressed: () {
 print("Button Pressed");
 Navigator.of(context).push(
   MaterialPageRoute(
     builder: (context) {
       return GalleryPage();
     },
   ),
 );
}
  1. Import the GalleryPage() class, use Alt+Enter and do import
  2. Do hot reload r or Hot Restart shift+r
  3. Click the login button to open the ImagesPage
  4. You should now see an empty screen:


As you can see, the app has a default theme with default colors. Let's set a custom theme with some better colors for the entire app.

What you do here

App Theme

  1. Open the application.dart file
  2. In the MaterialApp constructor, add theme property
  3. Set its value to
    theme : ThemeData(
    primaryColor: Colors.white,
    buttonColor: Colors.lightBlue,
    )
  4. Do hot reload

Fix Button color

  1. Open the home_page.dart file
  2. Set the RaisedButton color to:
    color: Theme.of(context).buttonColor
    textColor: Theme.of(context).accentTextTheme.button.color

  1. Later on, we will improve the look of the buttons in the app.

What you do here

Image Widget

  1. Open the gallery_page.dart file
  2. At the top of the _GalleryPageState class, declare a String member variable:

final String _imageUrl = "https://image.tmdb.org/t/p/w500/xvx4Yhf0DVH8G4LzNISpMfFBDy2.jpg";

  1. Under the Column children widgets array, add the Image.network() widget
  2. Set the URL to _imageUrl variable
  3. Do hot reload or hot restart
  1. Your page should look like this now:
  2. It might show a yellow and black error, this might happen in small screens as the image overlapping the screen size. We will fix it in the next step.

Fix Image Overlap

  1. Select the Container of the Image widget.
  2. Click Alt+Enter
  3. Select Wrap with new widget
  4. Set the name of the widget to Expanded
  5. Set the image fit to fit: BoxFit.fill
Expanded(
 child: Container(
   padding: const EdgeInsets.all(8),
   child: Image.network(_imageUrl, fit: BoxFit.fill,),
 ),
)

Use SafeArea Widget

On some devices, the layout may extend to the safe area, like this:

We can use the SafeArea widget to prevent layout to overlap the safe area zone.

  1. Select the Saffold Widget, and press Alt+Enter or Options+Enter
  2. Select wrap with a widget option
  3. Set the new widget to SafeArea widget
  4. Set the safe area properties:
    top: false
    bottom: true

Add Two Buttons

  1. Under the Expanded widget add a Row() widget.
  2. Set the Row attributes and add an empty children<Widget>[] array:
//galerry_page.dart
...
Expanded(
 child: Container(
   padding: EdgeInsets.all(8),
   child: Image.network(_imageUrl, fit: BoxFit.fill,),
 ), //Image.nework
), //Expanded
Row(
 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 mainAxisSize: MainAxisSize.max,
 crossAxisAlignment: CrossAxisAlignment.center,
 children: <Widget>[
 ]
), //Row Always add a comma after the last widget
  1. In the empty <Widget>[] array add two RaisedButton widgets
  2. Set the first button child: to Text("Previous")
  3. Set the text style to: Text("Previous", style: TextStyle(fontSize: 25))
  4. Set the second button child: to Text("Next")
//galerry_page.dart
...
 children: <Widget>[
   RaisedButton(
        child: Text("Previous", style: TextStyle(fontSize: 25)),
   ),
   RaisedButton(
        child: Text("Next"), style: TextStyle(fontSize: 25)
   ), 
 ]
), //Row Always add a comma after the last widget
  1. Do hot reload, watch the page with an image and two disabled buttons under it.
  2. The buttons are wrapped, let's make them stretched equally
  3. Select the first RaisedButton, press Alt+Enter select, Wrap with widget
  4. Set the widget name to Expanded
  5. Repeat steps 8-9 for the second button
  6. Do hot reload, the buttons should now be stretched horizontal equally

Add Buttons Padding

  1. Select the first RaisedButton, click on the Flutter Outline tab at the right side of the screen:
  2. Press the Add padding button
  3. Set the padding: padding: const EdgeInsets.symmetric(horizontal: 8.0)
  4. Repeat steps 2-3 for Second button
  5. Do hot reload, buttons should now have some padding,

//galerry_page.dart
...
Expanded(
 child: Padding(
   padding: const EdgeInsets.symeetric(horizontal: 8.0),
   child: RaisedButton(
     child: Text("Previous"),
   ),
 ),
)

Enabling the buttons

  1. The buttons still disabled, this because we haven't set an onPressed callback
  2. Inside the first buttons constructor, add onPressed: argument:

onPressed: (){}

  1. Repeat the last step for the second button
  2. Do hot reload, the buttons should be enabled and clickable
  3. Lets style the buttons
  4. In the first button constructor, right above the child: argument, set button color:
    color: Theme.of(context).primaryColor
  5. Next, under the color: argument, set the textColor:
    textColor: Theme.of(context).primaryTextTheme.button.color
//gallery_page.dart
...
RaisedButton(
 color: Theme.of(context).primaryColor
 textColor: Theme.of(context).primaryTextTheme.button.color
 child: .... ,
 onPressed: (){}
)

Extract buttons build to a method

  1. Lets extract button creating to a method
  2. Select the Padding of the first button (Use Option+UpArrow for easy selection)
  3. Press Command+Alt+M, ExtractMethod dialog open, set the method name to _buildButton
  4. Set the method return type to Widget
  5. Set the method to accept BuildContext, String and Function() callback

Widget _buildButton( {BuildContext context, String title, VoidCallback onPressed})

  1. Replace the RaisedButton parameters inside the method with the method arguments:
Widget _buildButton(
   {BuildContext context, String title, VoidCallback onPressed}) {
 return Padding(
   padding: const EdgeInsets.symmetric(horizontal: 4),
   child: RaisedButton(
     color: Theme.of(context).primaryColor, //Set the context
     textColor: Theme.of(context).primaryTextTheme.button.color,
     child: Text(title) //Set the title,
     onPressed: onPressed, //Set the callback
   ),
 );
  1. Replace the RaisedButton child in the Buttons Row by calling the _buildButton method for each button.
  2. At the end, the buttons Row should look like this:
children: <Widget>[
 Expanded(
   child: _buildButton(
     context: context,
     title: "Previous",
     onPressed: () {},
   ),
 ),
 Expanded(
   child: _buildButton(
     context: context,
     title: "Next",
     onPressed: () {},
   ),
 ),

What you do here

Add Images List

  1. In the State class, add a list of 3 images URLs:
  2. Next, under the images list, add an index variable: int index = 0;
List<String> _images = [
"https://image.tmdb.org/t/p/w500/xvx4Yhf0DVH8G4LzNISpMfFBDy2.jpg",
"https://image.tmdb.org/t/p/w500/svIDTNUoajS8dLEo7EosxvyAsgJ.jpg",
"https://image.tmdb.org/t/p/w500/iiZZdoQBEYBv6id8su7ImL0oCbD.jpg"
];
int _index = 0;
  1. We will use the index to load an image from the list in the Image.network widget
  2. At the bottom of the State class, add two methods for increment and decrement
void _handleNext() {
      setState(() {
      index++;
    });
   }

void _handlePrevious() {
   setState(() {
   index--;
  });
 }
  1. Notice that in these methods we call the setState((){}).
    This method is the Flutter framework that causes our widget to rebuild with the new state.
  2. In the Image.network widget, replace the _imageUrl with _images[_index]
//gallery_page.dart
//...
Image.network(
   images[index],
   fit: BoxFit.fill,
  ),

Call Methods from buttons onPressed

  1. We need to prevent the user from click Next when reaching the last image in the list by setting the onPressed callback to null
  2. We should do the same for the Previous button when reaching the first image in the list.
  3. In the ‘Next' RaisedButton, set the onPressed: callback to null as following:
//gallery_page.dart
//...
_buildButton(
    context: context,
    title: "Next",
    onPressed: _index < (_images.length - 1) ? () {
    _handleNext();
   } : null,
  )
  1. In the ‘Previous' RaisedButton, set the onPressed: callback to null as following:
//gallery_page.dart
//...
_buildButton(
 context: context,
 title: "Previous",
 onPressed: _index > 0 ? () {
   _handlePrevious();
 } : null,
)
  1. You should now be able to navigate between images which are downloaded from the network!

What you do here

Create custom button widget

  1. Under the lib folder create ui package
  2. In the new package, create a custom_raised_button.dart file
  3. In the new file, create a CustomRaisedButton class extends StatelessWidger
//custom_raised_button.dart
...
class CustomRaisedButton extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Container();
 }
}
  1. Go to the app home_page.dart file
  2. Copy the content of the login RaisedButton (use Option+UpArrow for easy selection)
  3. Go back to the new CustomRaisedButton class and paste it instead the Container widget
  4. At the top of the class declare the following class members:
    final VoidCallback onPressed;
    final Widget child;
  5. Create a named constructor for these members:
//custom_raised_button.dart
...
const CustomRaisedButton({Key key, this.onPressed, this.child}) : super(key: key);
  1. Replace the button onPressed and child widget with the class members
//custom_raised_button.dart
...
class CustomRaisedButton extends StatelessWidget {

 final VoidCallback onPressed;
 final Widget child;

 const CustomRaisedButton({Key key, this.onPressed, this.child}) : super(key: key);

 @override
 Widget build(BuildContext context) {
   return RaisedButton(
     color: Theme.of(context).buttonColor,
     textColor: Theme.of(context).accentTextTheme.button.color,
     child: this.child,
     onPressed: this.onPressed,
   );
 }
}

Button Decoration

  1. Under the raised button constructor add a shape attribute as following

shape: RoundedRectangleBorder()

  1. Set the RoundedRectangleBorder
//custom_raised_button.dart
...
shape: RoundedRectangleBorder(
 borderRadius: BorderRadius.all(Radius.circular(10)),
 side: BorderSide(color: Theme.of(context).buttonColor)
)

Replace Login Button

  1. Go to the home_page.dart
  2. Replace the RaisedButton with the new CustomRaisedButton
//custom_raised_button.dart
...
CustomRaisedButton(
 child: Text(
   "Login",
   style: TextStyle(fontSize: 25),
 ),
 onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) {return GalleryPage();}))
)
  1. Do hot reload

Checkout the new Login button with rounded corners.

Use new button in gallery page

  1. In gallery_page.dart file replace the RaisedButton in the _buildGalleryButton method with the new CustomRaisedButton
  2. Remove the BuildContext parameter from the method and from method callers
//galerry_page.dart
...
Widget _buildGalleryButton(String title, VoidCallback onPressed) {
 return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 8.0),
     child: CustomRaisedButton(
       onPressed: onPressed,
       child: Text(
         title,
         style: TextStyle(fontSize: 25),
       ),
     ));
}
  1. Do hot reload and navigate to gallery page
  2. Notice the new gallery buttons



Now let's decorate the gallery image by setting rounded corners and some elevation and shadow.

What you do here

Image Rounded Corners

  1. In the gallery_page.dart, click in the Image.network widget
  2. Press Options+Enter, and select, wrap with Widget
  3. Set the ClipRRect widget
  4. Add borderRadius attribute
  5. Set the border radius: borderRadius: BorderRadius.all(Radius.circular(10))
//galerry_page.dart
...
ClipRRect(
 borderRadius: BorderRadius.all(Radius.circular(10))
  1. Do hot reload

Image Shadow

  1. Select the Image.network parent Container
  2. Add the decoration attribute
  3. Set decoration to BoxDecoration()
  4. In the BoxDecoration set the border radius:
    borderRadius: BorderRadius.all(Radius.circular(10))
  5. Add a boxShadow attribute: boxShadow: [ ]
  6. Configure the boxShadow as follow:
    boxShadow: [
    color: Colors.black38,
    offset: Offset(4.0, 4.0),
    blureRadius: 10.0,
    spreadRadius: 0.4)

    ]
//galerry_page.dart
...
decoration: BoxDecoration(
 borderRadius: BorderRadius.all(Radius.circular(10)),
 boxShadow: [
   BoxShadow(
       color: Colors.black38,
       offset: Offset(4.0, 4.0),
       blurRadius: 10.0,
       spreadRadius: 0.4)
 ]
)
  1. Do hot Reload
  2. Checkout the gallery page

What you do here

Add a CircularProgressIndicator to the CustomRaisedButton

  1. In the CustomRaisedButton class add the isLoading boolean member
    final bool isLoading;
  2. Update the constructor to set the new parameter
    const CustomRaisedButton({Key key, this.onPressed, this.child, this.isLoading = false})
  3. Set the widget child according to the isLoading state
  4. Set the onPressed field to be null if isLoading is true (disable the button)
//custom_raised_button.dart
...
child: isLoading ? CircularProgressIndicator() : this.child,
onPressed: this.isLoading ? null : this.onPressed,

Change HomePage to a Stateful Widget

In order to use the new button functionality we need to change the HomePage to a Stateful Widget.

  1. In the home_page.dart file, select the HomePage class and press Alt+Enter
  2. From the dropdown options select Convert to Stateful Widget:
  3. Notice the generated code
  4. In the generated _HomPageState class, add a new _isLoading member: bool _isLoading = false;
  5. Add the new parameter to the CustomRaisedButton
//home_page.dart
...
child: CustomRaisedButton(
 isLoading: _isLoading,
 child: Text(
   "Login",
   style: TextStyle(fontSize: 25),
 )
...

Use a Future to simulate long operation

  1. At the end of the _HomPageState class add _login() method
  2. In the new method, add setState() statement to set the _isLoading to true
  3. In the new method, add a Future.delayed() statement with 5000 milliseconds delay and callback expression
  4. In the Future callback set _isLoading to false using setState again
  5. Move the call for navigator from the CustomRaisedButton onPressed() => to the Future callback before setting _isLoading
  6. From the CustomRaisedButton onPressed() call the new _login() method
//home_page.dart
...
void _login(){
 setState(() {
   _isLoading = true;
 });
Navigator.of(context).push(MaterialPageRoute(builder: (context) {return GalleryPage();}));
 Future.delayed(Duration(milliseconds: 5000), (){

 });
}
  1. Do Hot reload
  2. Click on the login button, notice progress indicator appears before navigation occurs

Sometimes there are cases when we want to navigate to a screen from many places in our app. Using the traditional Navigator may cause code duplications across the app. To avoid this, we can use the named routes along with the Navigator.pushNamed()method.

What you do here

Define app routes

  1. In the application.dart file go to the MaterialApp constructor
  2. Add the initialRoute and routes properties
    initialRoute: '/',

routes: {

'/': (context) => HomePage(),

'/gallery_page': (context) => GalleryPage()

}

  1. Delete the home: property as it is now defined as the initialRoute
//application.dart
...
return MaterialApp(
   ...
   initialRoute: '/',
   routes: {
     '/': (context) => HomePage(),
     '/gallery_page': (context) => GalleryPage()
   },
 );
}

Use named method to navigate

  1. In the home_page.dart file, use the Navigator.pushNamed() to navigate to the gallery page
  2. Do hot reload
  3. Check that navigation works properly
//home_page.dart
...
void _navigateToGalleryPage(BuildContext context) {
 Navigator.pushNamed(context, '/gallery_page');
}

Codelab Summary

Congratulations!! You've reached the end of this codelab !! Cheers!

Throughout this codelab we've learned the basics of Flutter such as Stateless and Stateful widgets, Column and Rows, decorations, Image download and more.

In addition, we've experienced IDE shortcuts and tips such as widget selection, converting a Stateless widget to Stateful widget, creating custom widgets and using them in code.

Last but not least, we've tasted the cool and easy to learn Dart programming language and its unique syntax which was designed especially for Flutter.

What's Next

Flutter has a lot of topics which hasn't covered in this tutorial and already well known recommended architectures and State Management patterns such as:

  1. StreamBuilder
  2. BLoc
  3. Redux

Flutter also has its own DI techniques which are based on InheritedWidget and the Provider package along with understanding the widget tree structure and how it can help us to manage our app state.

To become a professional Flutter developer, it is extremely important to learn these topics.

Recommended Flutter tutorials:

You may find great tutorials for these topics in the following playlist

Flutter Codelabs:

You may find more great Flutter codelabs in the official Flutter codelab page!

Dart pad :

An online dart playground: You can use it for coding and learning Dart online.

Flutter Studio :

An online Flutter widget editor:
This cool page lets you design and create Flutter widgets and pages, with a cool visual editor. It also generates the Flutter code for you. (Just like Android Studio XML layout editor)