Saturday, April 28, 2018

Motivational Quote App: Structure Flutter app code using Dart files and classes Ep. 9 Pt. 6



In this blog post we are going to structure our code. In previous few blog post and in videos, I did all in main.dart file and in the build method. That because I did not want you to be complicated. So, we are going to separate our code into three files except main.dart. And create some classes. That way we will get code reuse from avoiding duplicating code, readability and lot more.

Here is our main.dart file code(that we code so far),

As you can see, that is completely unreadable. Also if we have another route/page or screen, and if we want to add a background image to that page, we have to rewrite code for background image(simply you can copy paste and change the image). Sounds like we are duplicating the same code. So, we can do better than those things.

So, if you want to write better Dart codes for Flutter app, you can go to Flutter repo in github and study example app because these projects are written by Flutter, Dart developers and Contributors.

Also you can find more blog post about best practices:
One medium post.
thomasdavis.
Wikipedia.
From one of my favorite website.
softwareengineering.stackexchange.com

I recommend you to read this Effective Dart posts.

Now, let's get into our topic structuring our app code:

We can use variable to hold our background image  like below:

var background = new Container(
  decoration: new BoxDecoration(
    image: new DecorationImage(
      image: new AssetImage('assets/images/bill-gates.jpg'),
      fit: BoxFit.cover,
    ),
  ),
  child: new BackdropFilter(
    filter: new ImageFilter.blur(
      sigmaX: 3.0,
      sigmaY: 3.0,
    ),
    child: new Container(
      decoration: new BoxDecoration(
        color: Colors.black.withOpacity(0.2),
      ),
    ),
  ),
);

var is the data type that I used but you can use Widget or Container also.

But this will not give you code reuse or readability for our app. So, you can use a function and then you can get the image as a argument like below:

Widget buildBackground(String imageURL) {
  return new Container(
    decoration: new BoxDecoration(
      image: new DecorationImage(
        image: new AssetImage(imageURL),
        fit: BoxFit.cover,
      ),
    ),
    child: new BackdropFilter(
      filter: new ImageFilter.blur(
        sigmaX: 3.0,
        sigmaY: 3.0,
      ),
      child: new Container(
        decoration: new BoxDecoration(
          color: Colors.black.withOpacity(0.2),
        ),
      ),
    ),
  );
}

Also we can create a class and pass image url through constructor argument. Actually I am going to use a class for our app, so that we can create object and pass image as constructor arg. for every page/screen/route that we create for our app.

First create a file called background.dart and then improt material.dart and create stateless class called ApplyBackground and create a variable for imageURL as a member of the class(Because our class is a stateless widget we have to add final keyword. Otherwise you will see an warning underline the class name that saying "[dart] This class inherits from a class marked as @immutable, and therefore should be immutable (all instance fields must be final).").

class ApplyBackground extends StatelessWidget {
  final String imageURL;
  @override
  Widget build(BuildContext context) {
    return new Container(
      decoration: new BoxDecoration(
        image: new DecorationImage(
          image: new AssetImage(imageURL),
          fit: BoxFit.cover,
        ),
      ),
      child: new BackdropFilter(
        filter: new ImageFilter.blur(
          sigmaX: 3.0,
          sigmaY: 3.0,
        ),
        child: new Container(
          decoration: new BoxDecoration(
            color: Colors.black.withOpacity(0.2),
          ),
        ),
      ),
    );
  }
}

Now you will see an error underline ImageFilter that because we have to import
dart:ui package in this file and delete that import in main.dart file.

import 'dart:ui';

Now we can create a anonymous object(Object that do not have declaration/object name) of ApplyBackground and pass image url through constructor arg. inside the Stack widget as the first child. Of course, you have to import background.dart. Like below:

import 'background.dart';
//....other codes
  body: new Stack(
    children: <Widget>[
      new ApplyBackground('assets/images/bill-gates.jpg'),
//....other codes

Now we can do the same thing to the second child of Stack widget(Frosted Glass or our rectangle shape for quote).

First, Create a file called component.dart(because we have to add buttons in future tutorials). Next,  make sure to import material.dart and create a stateless class called FrostedGlass and add two instance/member variables for quote and personName. Then create a constructor to get value for those two variables. After that, add those variables for replacing the static text for quote and static text for person name like below:

import 'package:flutter/material.dart';

class FrostedGlass extends StatelessWidget {

  final String quote;
  final String personName;

  FrostedGlass(this.quote, this.personName);

  @override
  Widget build(BuildContext context) {

    MediaQueryData mediaQuery = MediaQuery.of(context);
    double screenWidth = mediaQuery.size.width;

    return new Center(
      child: new Container(
          width: screenWidth / 1.5,
         padding: const EdgeInsets.all(15.0),
          //here we can add color property but im gonna use box decoration
          decoration: new BoxDecoration(
              shape: BoxShape.rectangle,
              color: Colors.black.withOpacity(0.4),
              borderRadius: new BorderRadius.circular(10.0),
              boxShadow: [
                new BoxShadow(
                  color: Colors.black26,
                  offset: new Offset(5.0, 5.0),
                  blurRadius: 5.0,
                ),
              ]),
          child: new Column(
            mainAxisSize: MainAxisSize.min, // keep remember to add this line
            children: <Widget>[
              new Text(
                  quote,
                  style: new TextStyle(
                    color: Colors.white,
                    fontFamily: 'Gaegu',
                    fontSize: 22.0,
                  )),
              new Container(
                alignment: Alignment.bottomRight,
                padding: const EdgeInsets.only(top: 10.0),
                child: new Text('~ $personName',
                    style: new TextStyle(
                      color: Colors.white,
                      fontSize: 15.0,
                      fontFamily: 'Tajawal',
                      fontWeight: FontWeight.w300,
                      letterSpacing: 0.5,
                    )),
              ),
            ],
          )),
    );
  }
}

I used $(dolor mark) before personName because I want ~(tilde) symbol and and a space before the name. Using $ we can add variable value inside a String.
child: new Text('~ $personName',

There are may be some quotes that we do not know the person name. Since, we have to pass two argument we cannot avoid the person name. Because of that, we can make personName optional by open and close curly braces({}) in the FrostedGlass constructor like below:

FrostedGlass(this.quote, {this.personName});

Now this is a optional parameter. You can avoid personName constructor arg. But when you add constructor arg you have to add like this:

new FrostedGlass(quote, personName: personName),

Because, now person name is a property(Actually it is a Named argument).

Now, without adding personName arg. if you restart your app, you will see ~ null. Because we did not initialize our variable. So, we can add default values like below:

FrostedGlass(this.quote, [this.personName = '']);

Of course, this will not replace ~(tilde) symbol. So, what can we do?

Without adding default value, we can check if personName null, if it is we can add empty container or a SizedBox. If it is not null we can add our personName Container Widget. This is how I did it:

Widget personNameWidget() {
  if(personName == null) {
    return new SizedBox();
  } else {
    return new Container(
      alignment: Alignment.bottomRight,
      padding: const EdgeInsets.only(top: 10.0),
      child: new Text('~ $personName',
          style: new TextStyle(
            color: Colors.white,
            fontSize: 15.0,
            fontFamily: 'Tajawal',
            fontWeight: FontWeight.w300,
            letterSpacing: 0.5,
          )),
    );
  }
}

Now, we can call this function inside the Column Widget as the second child:

child: new Column(
  mainAxisSize: MainAxisSize.min, // keep remember to add this line
  children: <Widget>[
    new Text(
        quote,
        style: new TextStyle(
          color: Colors.white,
          fontFamily: 'Gaegu',
          fontSize: 22.0,
        )),
    personNameWidget(),
  ],
)),

Now, in the main.dart file you can create a anonymous object(you can also create a object and use object name) inside the Stack Widget as the second child with constructor args. like below:

body: new Stack( children: <Widget>[ new ApplyBackground('assets/images/bill-gates.jpg'), new FrostedGlass('Never give up'), ], ),

Okay...

Let's move on to the final step.

Now, our main.dart file looks like this:

import 'package:flutter/material.dart';
import 'background.dart';
import 'component.dart';

void main() {
  runApp(new MaterialApp(
      title: 'Motivational Quote App',
      home: new QuoteApp(),
  ));
}

class QuoteApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Stack(
        children: <Widget>[
          new ApplyBackground('assets/images/bill-gates.jpg'),
          new FrostedGlass('Never give up'),
        ],
      ),
    );
  }
}

I am going to create another Dart file called home_page.dart. In that file I'm gonna create a stateless widget called HomePage and cut and paste Scaffold Widget into build method return statement. We have to import background.dart and component.dart and delete unused import statement in main.dart file.

home_page.dart code should look like this:

import 'package:flutter/material.dart';
import 'background.dart';
import 'component.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Stack(
        children: <Widget>[
          new ApplyBackground('assets/images/bill-gates.jpg'),
          new FrostedGlass('Never give up'),
        ],
      ),
    );
  }
}

Inside the main.dart file, instead of passing MaterialApp Widget as argument of runApp function, add this MaterialApp Widget to the QuoteApp class return statement and create a object of QuoteApp inside the runApp method. Also for the home property of MaterialApp Widget instead of QuoteApp add HomePage.

Now, this is the main.dart should look like:

import 'package:flutter/material.dart';
import 'package:motivational_quote_app/home_page.dart';

void main() => runApp(new QuoteApp());

class QuoteApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Motivational Quote App',
      home: new HomePage(),
    );
  }
}

In this case:
import 'package:motivational_quote_app/home_page.dart';
and
import 'home_page.dart';
are same.

I separated Scaffold into home_page.dart because we can add more pages easily. And adding MaterialApp to the build method will improve readability. Also there are some other things like when we have routes, we have to use MaterialApp Widget.

I will explain those things in upcoming blog posts. This blog post is so long. Sorry! about that. If this is boring or if you did not understandplease let me know in the comment section. And I will see you in the next post until then goooood bye.

Thanks for reading!

No comments:

Post a Comment