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 expressions
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:
Navigator
to move between pagesFinal App
What you will learn:
Stateless
and Stateful
widgetsContext
and State
What you will do here:
Create New Flutter Project
Explore Flutter Project Structure
Create an Android Emulator
+Create Virtual Device
buttonNext
Run the default app
MaterialApp
modify the theme color from blue to greenRun 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 StatelessWidget
build()
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).primaryColor
textColor: 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.dart
GalleryPage
class, extends StatefulWidget
createState()
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()
widgetFix Image Overlap
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.
top: false
bottom: true
Add 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
Expanded
Add 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).primaryColor
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
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 true
Future.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)