I am sure you are enjoying this Flutter Series! In this article, we will have a closer look at some of the widgets provided by the Flutter Material package. We will also see how we can build our own custom widgets and how they are rendered on the screen. Along with that, we will build a cool app with our custom widgets. So, let's get going.
The app that we are building
Below is a preview of the app that we are building,
This is a single page/screen app and consists of only stateless widgets. What's better than creating a screen for yourself that you can put into any of your future apps as an about section.
Also, notice that when the Website
, Call Me
or Email Me
button is tapped it actually opens the default Browser, Phone app, or Email app. It is so cool, right? Well, let's see how we can build this.
The App
Creating an empty project
Create a new Flutter application by using flutter create <your_app_name>
or for further details refer to this post.
Once, the empty project has been created, erase all the contents of the main.dart
file to start with an empty canvas.
We need a third-party package so that we can launch the default native apps from our flutter application. The package is called url_launcher
. To install this package navigate to your pubspec.yaml
file and under the dependencies
section add url_launcher: ^5.7.10
like this,
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.0
url_launcher: ^5.7.10
It's a YAML file hence it is space/indentation sensitive. So make sure you have the correct indentation. More details on the package here.
Creating Basic Layout
With the package installed we can move on with creating our basic layout. In the main.dart
file import the material.dart
file from the material package. Write the main
function and create a new Stateless Widget named MyApp
. Your main.dart
file will look as below,
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Portfolio App',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Text('We are building this awesome app!Yay!!!!'),
);
}
}
This is the basic layout of our app and on running the app you will see a black screen with the text in red color. Now, we need some styling.
The Scaffold
Scaffold widget is provided by the material package which accepts certain named parameters. Some are appBar
, body
, drawer
, etc. To define an app bar we provide the AppBar
widget.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Portfolio App',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
appBar: AppBar(
title: Text('Portfolio'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(),
),
),
);
}
}
With this in place, you can already see that you have an app bar and an empty body. SingleChildScrollView
and Column
widgets are also provided by the material package. As the name suggests SingleChildScrollView
is responsible for enabling the scroll behavior to its child and Column
is an invisible widget that can have multiple widgets as its children aligned from top to bottom by default.
The Full App
With our app bar in place let's build the app body. Our fully built code looks like this,
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
_launchNativeApps(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Portfolio App',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
backgroundColor: Color.fromRGBO(227, 234, 237, 1),
appBar: AppBar(
title: Text(
'Portfolio',
),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children:[
SizedBox(height: 30),
CircleAvatar(
radius: 100,
backgroundColor: Theme.of(context).primaryColor,
child: CircleAvatar(
radius: 95,
backgroundColor: Colors.white70,
backgroundImage:
NetworkImage('https://i.ibb.co/n3RzK2L/shashank.jpg'),
),
),
SizedBox(height: 20),
Text(
'Shashank Biplav',
style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Divider(),
),
Text(
'SOFTWARE ENGINEER & TECH BLOGGER',
style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 0),
child: Divider(),
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Text(
'Hello, I am Shashank Biplav a Full-Stack developer based in India specializing in building apps for Mobile and the Web.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: RaisedButton(
onPressed: () {
_launchNativeApps(
'https://shashankbiplav.com');
},
child: Container(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.person_outline_rounded,
size: 30,
color: Colors.blue[800],
),
Text(
'Website',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
color: Colors.grey[700]),
),
],
),
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: RaisedButton(
onPressed: () {
_launchNativeApps('tel:+917004026852');
},
child: Container(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.wifi_calling_rounded,
size: 30,
color: Colors.greenAccent[700],
),
SizedBox(width: 20),
Text(
'+91- 700-402-6852',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 20,
color: Colors.grey[700]),
),
],
),
),
),
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: RaisedButton(
onPressed: () {
_launchNativeApps(
'mailto:biplavshashank7@gmail.com?subject=I would like to build an app with you!&body=Hi there,\n I have an awesome app idea,\n\n');
},
child: Container(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.email_rounded,
size: 30,
color: Colors.red[800],
),
Text(
'biplavshashank7@gmail.com',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
color: Colors.grey[700]),
),
],
),
),
),
),
SizedBox(height: 20),
],
),
),
),
);
}
}
Woah! this is a pretty big file. We will split the file into smaller files, i.e, widgets later but for now let's understand what is happening here. All the widgets used above are provided by the material package.
SizedBox Widget
This widget is invisible unless a visible child widget is provided to it. Here we are just providing it height and no width. So it acts as a widget providing space vertically with no width. We are using several of these to add spaces between our widgets.
CircleAvatar Widget
This widget forms a circle with the size that is defined by us. It expects certain named parameters such as radius
, backgroundColor
, backgroundImage
, child
, etc. Here we are using two CircleAvatar
widgets. One holds the profile image and has a smaller radius than its parent. The parent widget has a slightly bigger radius so that it spreads out and gives a circular border effect of blue color.
Text Widget
The Text
widget is used is used to display some thxt with our own styling. We are using this widget to display all our texts with varying font sizes, font weights and colors.
RaisedButton Widget
As the name suggests RaisedButton
widgets gives us a prebuilt button. It appears raised on the screen by implementing a shadow. We can provide a child widget to it. In our case we are providing it with a Container
which has a Row
as it child. The Row
also contains two nested widgets, i.e, Icon
and Text
widgets.
We can also assign any function that we would like the RaisedButton
to execute by using the onPressed
property. In this case we are providing a private function _launchNativeApps(...)
to it. This function executes every time the button is clicked.
_launchNativeApps()
function
In this function we are using a function named canLaunch(...)
which is provided to us by the url_launcher
package that we installed earlier. This is an async function that checks if the provided url
which is basically a String can be launched in the other compatible app. If such an app is available then it launched the app with our pre-defined values in the url
and if not then it throws an error.
Splitting the app into smaller widgets
This is a single page app for now but imagine if we would include routing, multiple screens, navigation drawer/ bottom nav bar, etc. Yeah! it would be huge and writing all that in a single file would be horrendous. Also, maintaining and debugging the code would be a nightmare!
To overcome this problem we can split the app into several other widgets as separate widgets. In the above code we can clearly see that the 3 buttons have almost the same code and the only thing that is different is their url
. So what we can do is that we can create a separate widget for the button and use them thrice rather than writing the same code thrice.
So, we will create a new file in the lib
folder named as custom_button.dart
. In this file we will do our regular material.dart
import and create a new StatelessWidget
and move all the Button related code in this file. It will look like this,
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class CustomButton extends StatelessWidget {
final String uri;
final IconData icon;
final String buttonText;
CustomButton({this.uri, this.icon, this.buttonText});
_launchNativeApps(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: RaisedButton(
onPressed: () {
_launchNativeApps(uri);
},
child: Container(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
icon,
size: 30,
color: Colors.blue[800],
),
Text(
buttonText,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
color: Colors.grey[700]),
),
],
),
),
),
);
}
}
Here, we have moved our function here _launchNativeApps(..){...}
from main.dart
file. Also, we are initializing this widget with all the dynamic parameters such as uri
, icon
and buttonText
which we will pass to this widget dynamically to render different content.
You will notice that I have used {..}
in the constructor CustomButton({this.uri, this.icon, this.buttonText});
. This is the shorthand constructor syntax that dart offers us. The {..}
makes this a named constructor with which w have to pass values to the instance variables referencing their names. For example, if we are using our CustomButton
widget anywhere then we have to initialize it like this,
CustomButton(
uri: 'https://shashankbiplav.com',
icon: Icons.person_outline_outlined,
buttonText: 'Website',
),
With this, we have the advantage that we have to either remember the name neither the order of the instance variables.
We can also create a separate widget for our image avatar and we will name the file profile_image.dart
in the lib
folder. We will migrate all our image related code here into this new StatelessWidget
. It will look like this,
import 'package:flutter/material.dart';
class ProfileImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: 100,
backgroundColor: Theme.of(context).primaryColor,
child: CircleAvatar(
radius: 95,
backgroundColor: Colors.white70,
backgroundImage: NetworkImage('https://i.ibb.co/n3RzK2L/shashank.jpg'),
),
);
}
}
Now, with these widgets in place all we need to do is to import these widgets in our main.dart
file and use them. This will make our code cleaner. Now, our main.dart
file will look like this,
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import './custom_button.dart';
import './profile_image.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Portfolio App',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Scaffold(
backgroundColor: Color.fromRGBO(227, 234, 237, 1),
appBar: AppBar(
title: Text(
'Portfolio',
),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(height: 30),
ProfileImage(),
SizedBox(height: 20),
Text(
'Shashank Biplav',
style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Divider(),
),
Text(
'SOFTWARE ENGINEER & TECH BLOGGER',
style: TextStyle(fontSize: 19, fontWeight: FontWeight.w700),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 0),
child: Divider(),
),
Padding(
padding: const EdgeInsets.all(30.0),
child: Text(
'Hello, I am Shashank Biplav a Full-Stack developer based in India specializing in building apps for Mobile and the Web.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
),
CustomButton(
uri: 'https://shashankbiplav.com',
icon: Icons.person_outline_outlined,
buttonText: 'Website',
),
SizedBox(height: 20),
CustomButton(
uri: 'tel:+917004026852',
icon: Icons.wifi_calling_rounded,
buttonText: '+91- 700-402-6852',
),
SizedBox(height: 20),
CustomButton(
uri:'mailto:biplavshashank7@gmail.com?subject=I would like to build an app with you!&body=Hi there,\n I have an awesome app idea,\n\n',
icon: Icons.email_rounded,
buttonText: 'biplavshashank7@gmail.com',
),
SizedBox(height: 20),
],
),
),
),
);
}
}
We can see that we have imported our custom widgets in this file. We are just placing our custom widgets in the same place as our previous widgets. Now, our main.dart
file is much cleaner, easier to maintain and debug. Also, we have greatly enhanced the readability and code reusability.
Conclusion
With the app built, just replace all the url
's with yours and you will have an awesome looking single page Portfolio App. I hope you enjoyed building this app with me. It was awesome right?
In the next article, we will have a detailed look at the State including Stateful and Stateless widgets. Till then, Happy Coding👨🏽💻👩💻!