Flutter Basics - Splitting Widgets

Flutter Basics - Splitting Widgets

Building a Portfolio App

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,

portfolio-app-preview-by-shashank-biplav.gif 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👨🏽‍💻👩‍💻!