DEV Community

Cover image for Flutter app basics: Building a quotes app
Francis
Francis

Posted on

Flutter app basics: Building a quotes app

Introduction.

Flutter is a cross-platform app development framework developed by google used to build beautiful UI. The most exciting thing about flutter is that dart language which is it programming language is relatively easy to learn. It can be used to develop applications for 6 different platforms android, iOS, web, MacOS, Windows and Linux. So this series of articles serves as a building block for learning how to work with JSON data which is the most popular way in which most API return data.

Prerequisite

In order to follow along with this tutorial you must have installed Flutter version 2.10 or higher, android studio and setup up the necessary environment to run your flutter app. To get started you can read the flutter documentation. flutter. After installation run flutter doctor on your command prompt. To get a details of how to install flutter checkout the link
I am using visual studio code as my IDE because it's light weight although you can use android studio or intelliJ IDE to write your codes. Just make sure you have Dart and Flutter plugin installed. For step by step instruction on how to install click on the link below depending on your operating system

What we will explore

For this project we will learn how to build a simple quotes app. Things to learn.

  • folder management
  • working with models.
  • Simple UI.
  • Working with local JSON file.
  • Simple navigation

Lets start

  1. The app will be named motivoapp. In your command prompt run
flutter create motivoapp
Enter fullscreen mode Exit fullscreen mode
C:\app>flutter create motivoapp
Creating project motivoapp...
Running "flutter pub get" in motivoapp...                        2,607ms
Wrote 96 files.

All done!
In order to run your application, type:

  $ cd motivoapp
  $ flutter run

Your application code is in motivoapp\lib\main.dart.
Enter fullscreen mode Exit fullscreen mode
  1. Let's look at the type of json data we are going to work with.
 {
         "text":"As soon as beauty is sought not from religion and love, but for pleasure, it degrades the seeker.",
         "author":"Annie Dillard",
         "tag":"beauty"
      },
Enter fullscreen mode Exit fullscreen mode
  1. Next We create our folders. For this app I will create three folders in the lib folder and name them models, screens, services. This will help us organize our app better. Also we create an assets folder in the motivoapp folder to store our different assets like the images and json file we are using for this app.

  2. Then we go to your pubspec.yaml file and declare our assets like this. The asset also contains images folder so we also declare this too.

  # To add assets to your application, add an assets section, like this:
  assets:
    - assets/
    - assets/images/
Enter fullscreen mode Exit fullscreen mode

We have everything sorted and we are good to go.

  1. In our screen folder we create a dart file named home.dart. Then we create a stateless widget and import material.dart package.
import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar());
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. In our main.dart file we set our home as HomeScreen.
import 'package:flutter/material.dart';
import 'package:motivoapp/screens/home.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomeScreen(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Let's work on our model file. Since our data is already fixed I will use the required key word which makes our data non-nullable instead of the nullable datatypes. Also, the Quotes.fromJson helps us to Map our returned JSON data to our Quotes model. While the toJson() method is used while pushing data from our app to database or backend. But for this app we didn't use the toJson() method because of

Note: If a variable is not marked as required, we include the ?. The purpose of this ? is to make our datatype nullable incase our API or data source returns a null value. This is null safety. Also note String and String? are not of the same datatype. String? is a superset of String.

class Quotes {
  Quotes({
    required this.text,
    required this.author,
    required this.tag,
  });

  String text;
  String author;
  String tag;

  factory Quotes.fromJson(Map<String, dynamic> json) => Quotes(
        text: json["text"],
        author: json["author"],
        tag: json["tag"],
      );

  Map<String, dynamic> toJson() => {
        "text": text,
        "author": author,
        "tag": tag,
      };
}


Enter fullscreen mode Exit fullscreen mode
  1. Then we create a dart file called services.dart in our services folder. This is where we create our readJsonData() function to consume the data in our quotes.json. The function is of the type Future which returns a list of Quotes which is the class we created in our model. The variable jsondata helps us to load our file. The list variable helps us to decode our json file and returns it's values as List type. Finally we return our data and map the json keys to the variable we created in our quotes model class
import 'dart:convert';
import 'package:motivoapp/models/quotes.dart';
import 'package:flutter/services.dart';

Future<List<Quotes>> readJsonData() async {
  final jsondata = await rootBundle.loadString('assets/quotes.json');
  final list = json.decode(jsondata) as List<dynamic>;
  return list.map((e) => Quotes.fromJson(e)).toList();
}
Enter fullscreen mode Exit fullscreen mode
  1. Let get into the UI part. The Homescreen returns a futurebuildern widget which accepts the future readJsonData() we created in the services.dart file. The widget then returns a Listview of cards. We use Listview.builder because the number of data we want to display is unknown.
import 'package:flutter/material.dart';
import 'package:motivoapp/models/quotes.dart';
import 'package:motivoapp/screens/details.dart';
import 'package:motivoapp/services/services.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: readJsonData(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(child: Text("${snapshot.error}"));
          } else if (snapshot.hasData) {
            var items = snapshot.data as List<Quotes>;
            return ListView.builder(
                itemCount: items.length,
                itemBuilder: (context, index) {
                  return Padding(
                    padding: const EdgeInsets.all(15.0),
                    child: Card(
                      elevation: 5,
                      margin: const EdgeInsets.symmetric(
                          horizontal: 10, vertical: 6),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          Text(
                            items[index].text,
                            style:
                                const TextStyle(fontWeight: FontWeight.bold),
                          ),
                          const SizedBox(
                            height: 10,
                          ),
                          Row(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: [
                              Text(items[index].author),
                              Text(items[index].tag)
                            ],
                          ),
                        ],
                      ),
                    ),
                  );
                });
          } else {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
        });
  }
}

Enter fullscreen mode Exit fullscreen mode

To display a single quote in a details page we create another screen Details.dart which is a stateful widget. This accepts three variables text, author and tag. We also add a container which will hold our background image. The MediaQuery.of(context).size (.height or .width) gets the entire screen size (height or width) of the device running the app hence making it default responsive. Also adding the singlechildscrollview widget makes the screen scrollable and prevent render overflow error.

import 'package:flutter/material.dart';

class Details extends StatefulWidget {
  final String text, author, tag;
  const Details({
    required this.text,
    required this.author,
    required this.tag,
    Key? key,
  }) : super(key: key);

  @override
  State<Details> createState() => _DetailsState();
}

class _DetailsState extends State<Details> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.grey,
        appBar: AppBar(
          backgroundColor: Colors.black,
        ),
        body: Center(
          child: Container(
            height: MediaQuery.of(context).size.height,
            width: MediaQuery.of(context).size.width,
            decoration: const BoxDecoration(
              image: DecorationImage(
                image: AssetImage('assets/images/bg.jpg'),
                fit: BoxFit.cover,
              ),
            ),
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(25.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Center(
                      child: Text(
                        widget.text,
                        style: const TextStyle(
                            fontSize: 30,
                            wordSpacing: 2,
                            letterSpacing: 2,
                            fontWeight: FontWeight.w900,
                            color: Colors.white),
                      ),
                    ),
                    const SizedBox(height: 50),
                    Text(
                      widget.author,
                      style: const TextStyle(
                          fontSize: 30,
                          wordSpacing: 2,
                          letterSpacing: 5,
                          fontWeight: FontWeight.w400,
                          color: Colors.white),
                    ),
                    const SizedBox(height: 50),
                  ],
                ),
              ),
            ),
          ),
        ));
  }
}
Enter fullscreen mode Exit fullscreen mode

we wrap our card with an inkwell widget to make it respond to click events also wrap the listview.builder with container which will hold our background image

import 'package:flutter/material.dart';
import 'package:motivoapp/models/quotes.dart';
import 'package:motivoapp/screens/details.dart';
import 'package:motivoapp/services/services.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: readJsonData(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(child: Text("${snapshot.error}"));
          } else if (snapshot.hasData) {
            var items = snapshot.data as List<Quotes>;
            return Container(
              decoration: const BoxDecoration(
                image: DecorationImage(
                  image: AssetImage('assets/images/bg.jpg'),
                  fit: BoxFit.cover,
                ),
              ),
              child: ListView.builder(
                  itemCount: items.length,
                  itemBuilder: (context, index) {
                    return InkWell(
                      onTap: (() {
                        Navigator.of(context).push(
                          MaterialPageRoute(
                            builder: ((context) => Details(
                                  text: items[index].text,
                                  author: items[index].author,
                                  tag: items[index].tag,
                                )),
                          ),
                        );
                      }),
                      child: Padding(
                        padding: const EdgeInsets.all(15.0),
                        child: Card(
                          elevation: 5,
                          margin: const EdgeInsets.symmetric(
                              horizontal: 10, vertical: 6),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.stretch,
                            children: [
                              Text(
                                items[index].text,
                                style: const TextStyle(
                                    fontWeight: FontWeight.bold),
                              ),
                              const SizedBox(
                                height: 10,
                              ),
                              Row(
                                mainAxisAlignment:
                                    MainAxisAlignment.spaceBetween,
                                children: [
                                  Text(items[index].author),
                                  Text(items[index].tag)
                                ],
                              ),
                            ],
                          ),
                        ),
                      ),
                    );
                  }),
            );
          } else {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
        });
  }
}

Enter fullscreen mode Exit fullscreen mode

Voila we have our quotes app ready

This is our final folder structure

This is our simple quotes app.

Final Thoughts

Working with models in flutter is essential. This article is a building block on how to create dart models. There are online tools that can help generate your models based on the Json file you have, but basic knowledge of how they are created will help you better understand how any model works.
In my next article we will cover working with network requests to get data from an API.

This article is originally posted at Code&Jobs website https://www.codenjobs.com/.

The link to the blog post is at Flutter app basics: Building a quotes app

Top comments (0)