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:
Future<>, await, Streams, Listeners and lambda expressionsWhat 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:
Navigator to move between pagesFinal App
|
|
What you will learn:
Stateless and Stateful widgetsContext and StateWhat you will do here:
Create New Flutter Project

Explore Flutter Project Structure
Create an Android Emulator

+Create Virtual Device button
Next 



Run the default app

MaterialApp modify the theme color from blue to green
|
|
Run the App from the command line
flutter run -d all command_counter++ to decrement _counter-- (on line 59)r to do hot reload, press the + button, the counter should now decrease. What you will do here:
main() function which is the entry point of the appCreate the Application file
main.dart which given by the flutter sample projectlib folder, create new application.dart filepackage:flutter/material.dart packageMyApp class which extends StatelessWidgetbuild()method and return a MaterialApp Widgettitle: "Flutter Workshop" 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(),
);
}
}
main.dart file, add void main() function and call the runApp function with MyApp() as a parameter://main.dart
import 'package:flutter/material.dart';
import 'package:flutter_workshop/application.dart';
void main() => runApp(MyApp());
flutter run -d all What you will do here
Create The HomePage
lib folder, create a new pages package pages package, create home_page packagehome_page package, create a new home_page.dart filehome_page.dart file, create the HomePage class, extends StatelessWidget, override the build()method and return a Container()//home_page.dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
application.dart replace the Container() with the new HomePage()//application.dart
return MaterialApp(
title: "Flutter workshop",
home: HomePage(),
)
r, app still has black screen. Home Page Body
HomePage class, replace the Container() widget with a Scaffold() widgetbody: argument and set it to a Center() Widget//main_page.dart
...
return Scaffold(
body: Center(
child: Text("Hello Flutter"),
),
);
}
What you will do here
|
|
home_page.dart file, click the Text() child widget inside the Center widget, press Alt+Enter‘Wrap with new widget'
RaisedButton(),Text("Login")Text(), press Alt+Enter to reformat the code,//home_page.dart
//...
body: Center(
child: RaisedButton(
child: Text("Login"),
),
)
onPressed callback. We will do it in the next following steps. 
Add Padding and Enabling
Container as done above double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20.0)onPressed callbackchild: Text(), add onPressed:(){}, print("Button clicked");//home_page.dart
//...
child: RaisedButton(
child: Text("Login"),
onPressed: (){
print("Button Clicked");
},
), //RaisedButton
Styling the button and text
color: Theme.of(context).primaryColortextColor: Theme.of(context).primaryTextTheme.button.color//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
pages package, create new page package - gallery_page
gallery_page.dartGalleryPage class, extends StatefulWidgetcreateState() methodGalleryPage class, create another class _GalleryPageState class that extends State<GalleryPage>build() methodcreateState() 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
build() method, return Scaffold() widget.appBar: AppBar() with title: Text("Gallery Page")elevation: 0 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),
)
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,//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
home_page.dart file, select the Login RaisedButtonGalleryPage(): 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();
},
),
);
}
GalleryPage() class, use Alt+Enter and do import r or Hot Restart shift+r
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
application.dart filetheme property theme : ThemeData(
primaryColor: Colors.white,
buttonColor: Colors.lightBlue,
)Fix Button color
home_page.dart filecolor: Theme.of(context).buttonColor
textColor: Theme.of(context).accentTextTheme.button.color
|
|
What you do here
Image Widget
gallery_page.dart file_GalleryPageState class, declare a String member variable:final String _imageUrl = "https://image.tmdb.org/t/p/w500/xvx4Yhf0DVH8G4LzNISpMfFBDy2.jpg";
Image.network() widget
Fix Image Overlap
fit: BoxFit.fillExpanded(
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.
top: false
bottom: trueAdd Two Buttons
Expanded widget add a Row() widget.//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
Text("Previous")Text("Previous", style: TextStyle(fontSize: 25))//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
Wrap with widget ExpandedAdd Buttons Padding
Add padding button padding: const EdgeInsets.symmetric(horizontal: 8.0)
//galerry_page.dart
...
Expanded(
child: Padding(
padding: const EdgeInsets.symeetric(horizontal: 8.0),
child: RaisedButton(
child: Text("Previous"),
),
),
)
Enabling the buttons
onPressed: (){}
color: Theme.of(context).primaryColortextColor: 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
BuildContext, String and Function() callback Widget _buildButton( {BuildContext context, String title, VoidCallback onPressed})
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
),
);
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
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;
void _handleNext() {
setState(() {
index++;
});
}
void _handlePrevious() {
setState(() {
index--;
});
}
setState((){}). Image.network widget, replace the _imageUrl with _images[_index]//gallery_page.dart
//...
Image.network(
images[index],
fit: BoxFit.fill,
),
Call Methods from buttons onPressed
Next when reaching the last image in the list by setting the onPressed callback to nullPrevious button when reaching the first image in the list.//gallery_page.dart
//...
_buildButton(
context: context,
title: "Next",
onPressed: _index < (_images.length - 1) ? () {
_handleNext();
} : null,
)
//gallery_page.dart
//...
_buildButton(
context: context,
title: "Previous",
onPressed: _index > 0 ? () {
_handlePrevious();
} : null,
)
What you do here
Create custom button widget
lib folder create ui package 
custom_raised_button.dart file //custom_raised_button.dart
...
class CustomRaisedButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}

final VoidCallback onPressed;
final Widget child;//custom_raised_button.dart
...
const CustomRaisedButton({Key key, this.onPressed, this.child}) : super(key: key);
//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
shape: RoundedRectangleBorder()
//custom_raised_button.dart
...
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
side: BorderSide(color: Theme.of(context).buttonColor)
)
Replace Login Button
//custom_raised_button.dart
...
CustomRaisedButton(
child: Text(
"Login",
style: TextStyle(fontSize: 25),
),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) {return GalleryPage();}))
)
Checkout the new Login button with rounded corners.

Use new button in gallery page
gallery_page.dart file replace the RaisedButton in the _buildGalleryButton method with the new CustomRaisedButton //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),
),
));
}

Now let's decorate the gallery image by setting rounded corners and some elevation and shadow.
What you do here
Image Rounded Corners
gallery_page.dart, click in the Image.network widgetClipRRect widgetborderRadius attributeborderRadius: BorderRadius.all(Radius.circular(10))//galerry_page.dart
...
ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(10))
Image Shadow
Image.network parent Container decoration attributeBoxDecoration() borderRadius: BorderRadius.all(Radius.circular(10))boxShadow attribute: boxShadow: [ ]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)
]
)

What you do here
Add a CircularProgressIndicator to the CustomRaisedButton
final bool isLoading;const CustomRaisedButton({Key key, this.onPressed, this.child, this.isLoading = false})isLoading state //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.
home_page.dart file, select the HomePage class and press Alt+Enter
_HomPageState class, add a new _isLoading member: bool _isLoading = false;//home_page.dart
...
child: CustomRaisedButton(
isLoading: _isLoading,
child: Text(
"Login",
style: TextStyle(fontSize: 25),
)
...
Use a Future to simulate long operation
_HomPageState class add _login() methodsetState() statement to set the _isLoading to trueFuture.delayed() statement with 5000 milliseconds delay and callback expression_isLoading to false using setState againCustomRaisedButton onPressed() => to the Future callback before setting _isLoading//home_page.dart
...
void _login(){
setState(() {
_isLoading = true;
});
Navigator.of(context).push(MaterialPageRoute(builder: (context) {return GalleryPage();}));
Future.delayed(Duration(milliseconds: 5000), (){
});
}
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
application.dart file go to the MaterialApp constructorinitialRoute: '/', routes: {
'/': (context) => HomePage(),
'/gallery_page': (context) => GalleryPage()
}
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
home_page.dart file, use the Navigator.pushNamed() to navigate to the gallery page//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:
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)