Routing and Local Storage

1 App Development

1.1 A New Page on Battles

We want to create a new page that should open when the user wants to enquire more about a portfolio work i.e. a battle in our case. So, we need to add a button, and create a route for this.

Another way is to make the whole list item clickable. This is achieved using GestureDetector or InkWell. Both provide the click/tap functionality, but GestureDetector has more options, and InkWell has a nice ripple effect.

We change the file battle_view.dart to include an InkWell.

    import 'package:flutter/material.dart';
    import 'package:course_app/utils/battle.dart';
    
    class BattleView extends StatelessWidget {
      final List<Battle> battles;
    
      BattleView(this.battles);  
    
      @override
      Widget build(BuildContext context) {
        return Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 8.0),
            child: ListView.builder(
              itemCount: battles.length,
              itemBuilder: (context, index) {
                final battle = battles[index];
                // wrap the container with InkWell for click handling
                return InkWell(
                  onTap: () {
                    // Handle the tap event here
                    // Here, I decided to show a SnackBar at the bottom
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('Tapped on ${battle.name}')),
                    );
                  },
                  // this part is the same as before
                  // we made the container a chold of InkWell
                  child: Container(
                    margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                    padding: EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: Colors.grey[200],
                      border: Border.all(color: Colors.grey),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          battle.name!,
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                        ),
                        SizedBox(height: 5),
                        Text(battle.date!, style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic)),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        );
      }
    }

The following screenshot shows what happens after clicking on one of the elements.

image

Next, we need to actually move to a new page when we click on a portfolio item. For this, we create routes in the main function as follows.

    import 'package:flutter/material.dart';
    import 'package:course_app/pages/home.dart';
    import 'package:course_app/pages/battle_screen.dart';
    
    void main() {
      runApp(MaterialApp(
        home: Home(),
        routes: {      
          '/battle': (context) => BattleScreen(),
        },
      ));
    }

We then create the BattleScreen class. Right now, it does say much.

    import 'package:flutter/material.dart';
    
    class BattleScreen extends StatelessWidget
    {
      Widget build(BuildContext context)
      {
        return Scaffold(
          appBar: AppBar(
              title: Text('Some Battle'),
              backgroundColor: Colors.red,
              centerTitle: true,
            ),
            body: Text('Battle Info'),
        );
      }
    }

We then update BattleViewer as follows. We use Navigator.pushNamed() to navigate to target page.

    import 'package:flutter/material.dart';
    import 'package:course_app/utils/battle.dart';
    
    class BattleView extends StatelessWidget {
      final List<Battle> battles;
    
      BattleView(this.battles);  
    
      @override
      Widget build(BuildContext context) {
        return Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 8.0),
            child: ListView.builder(
              itemCount: battles.length,
              itemBuilder: (context, index) {
                final battle = battles[index];
                // wrap the container with InkWell for click handling
                return InkWell(
                  onTap: () {
                    // Handle the tap event here
                    // Navigate the page situated at '/battle'
                    Navigator.pushNamed(context, '/battle');
                  },
                  // this part is the same as before
                  // we made the container a chold of InkWell
                  child: Container(
                    margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                    padding: EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: Colors.grey[200],
                      border: Border.all(color: Colors.grey),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          battle.name!,
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                        ),
                        SizedBox(height: 5),
                        Text(battle.date!, style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic)),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        );
      }
    }

The new lib folder should look like:

image

So far, our app is structured as follows.

Image

The following are the listings of the new files.

1.1.1 main.dart

    import 'package:flutter/material.dart';
    import 'package:course_app/pages/home.dart';
    import 'package:course_app/pages/battle_screen.dart';
    void main() {
      runApp(MaterialApp(
        home: Home(),
        routes: {      
          '/battle': (context) => BattleScreen(),
        },
      ));
    }

1.1.2 home.dart

    import 'package:flutter/material.dart';
    import 'package:course_app/widgets/battle_view.dart';
    import 'package:course_app/utils/battle_data.dart';
    
    class Home extends StatefulWidget {
      @override
      State<Home> createState() => _HomeState();
    }
    
    class _HomeState extends State<Home> {
      int likes = 0;  
    
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text('My Portfolio'),
              backgroundColor: Colors.red,
              centerTitle: true,
            ),
            body: Column(
              children: [
                SizedBox(height: 20),
                Container(
                  decoration: BoxDecoration(
                      border: Border.all(color: Colors.black),
                      color: Colors.grey[100]),
                  padding: EdgeInsets.all(10),
                  margin: EdgeInsets.only(left: 10, right: 10),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      Image.asset(
                        'lib/assets/images/salah.jpeg',
                        width: 100,
                      ),
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text('Name'),
                          Text(
                            'Salaheddine Al-Ayyoubi',
                            style: TextStyle(fontWeight: FontWeight.bold),
                          ),
                        ],
                      ),
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text('DoB'),
                          Text(
                            '1138',
                            style: TextStyle(fontWeight: FontWeight.bold),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                SizedBox(height: 20),
                Container(
                  padding: EdgeInsets.only(left: 10, right: 10),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text('Likes: $likes'),
                      IconButton(
                        iconSize: 30,
                        icon: const Icon(Icons.favorite),
                        color: Colors.red,
                        onPressed: () {
                          setState(() {
                            likes++;
                          });
                        },
                      )
                    ],
                  ),
                ),
                // this is the widget responsible for showing the 
                // portfolio items
                BattleView(battles),
              ],
            ));
      }
    }

1.1.3 battle_view.dart

    import 'package:flutter/material.dart';
    import 'package:course_app/utils/battle.dart';
    
    class BattleView extends StatelessWidget {
      final List<Battle> battles;
    
      BattleView(this.battles);  
    
      @override
      Widget build(BuildContext context) {
        return Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 8.0),
            child: ListView.builder(
              itemCount: battles.length,
              itemBuilder: (context, index) {
                final battle = battles[index];
                return InkWell(
                  onTap: () {
                    Navigator.pushNamed(context, '/battle');
                  },
                  child: Container(
                    margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                    padding: EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: Colors.grey[200],
                      border: Border.all(color: Colors.grey),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          battle.name!,
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                        ),
                        SizedBox(height: 5),
                        Text(battle.date!, style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic)),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        );
      }
    }

1.1.4 battle_screen.dart

    import 'package:flutter/material.dart';
    
    class BattleScreen extends StatelessWidget
    {
      Widget build(BuildContext context)
      {
        return Scaffold(
          appBar: AppBar(
              title: Text('Some Battle'),
              backgroundColor: Colors.red,
              centerTitle: true,
            ),
            body: Text('Battle Info'),
        );
      }
    }

1.1.5 battle.dart

    class Battle
    {
      String? date;
      String? name;
    
      Battle(String date, String name)
      {
        this.date = date;
        this.name = name;
      }
    }

1.1.6 battle_data.dart

    import 'package:course_app/utils/battle.dart';
    final List<Battle> battles = [
        Battle('1183', 'Siege of Al-Kark'),
        Battle('1187', 'Siege of Al-Quds'),
        Battle('1187', 'Battle of Hittin'),
        Battle('1187', 'Siege of Sour'),
        Battle('1187', 'Battle of Marj Oyoun'),
        Battle('1188', 'Siege of Safd'),    
        Battle('1189', 'Siege of Akka'),
        Battle('1188', 'Siege of Borzia Fortress'),
        Battle('1188', 'Siege of Sohyoun Fortress'),    
      ];

1.2 Passing Data with Routes

Our application has some routing and it can go to another page on a click of a button. However, right now, it displays constant content only, we need it to display content according to some parameters. We need to pass data with our route.

Right now, we pass data from class BattleView in file battle_view.dart, to class BattleScreen in battle_screen.dart.

In BattleView, we add the parameter arguments that specifies map of items that we want to pass.

    onTap: () {                
     // Navigate the page situated at '/battle'
     // We add a new parameter: arguments which expects a map
     // We are free to name the arguments the way we want
     // This will send a battle object battles[index]
     // with the key 'battle'
     // We could have also used two parameters 'name' and
     // 'date' and sent them to the other end of the route
     Navigator.pushNamed(context, '/battle', arguments: {
       'battle': battles[index]
     });
   },

Then, in BattleScreen, we receive these arguments:

    Widget build(BuildContext context)
      {
        // We capture the arguments in the variable args
        final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
        // We then get our data, in this case, a Battle object
        // If we received name and date, we will catch
        // everyone in a different variable
        Battle battle = args?['battle'];
        // ....
      }

The files after modification are:

battle_view.dart

    import 'package:flutter/material.dart';
    import 'package:course_app/utils/battle.dart';
    
    class BattleView extends StatelessWidget {
      final List<Battle> battles;
    
      BattleView(this.battles);  
    
      @override
      Widget build(BuildContext context) {
        return Expanded(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 8.0),
            child: ListView.builder(
              itemCount: battles.length,
              itemBuilder: (context, index) {
                final battle = battles[index];            
                return InkWell(
                  onTap: () {                
                    // Navigate the page situated at '/battle'
                    // We add a new parameter: arguments which expects a map
                    // We are free to name the arguments the way we want
                    // This will send a battle object battles[index]
                    // with the key 'battle'
                    // We could have also used two parameters 'name' and
                    // 'date' and sent them to the other end of the route
                    Navigator.pushNamed(context, '/battle', arguments: {
                      'battle': battles[index]
                    });
                  },              
                  child: Container(
                    margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                    padding: EdgeInsets.all(10),
                    decoration: BoxDecoration(
                      color: Colors.grey[200],
                      border: Border.all(color: Colors.grey),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          battle.name!,
                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                        ),
                        SizedBox(height: 5),
                        Text(battle.date!, style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic)),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        );
      }
    }

battle_screen.dart

    import 'package:course_app/utils/battle.dart';
    import 'package:flutter/material.dart';
    
    class BattleScreen extends StatelessWidget
    {  
      Widget build(BuildContext context)
      {
        // We capture the arguments in the variable args
        final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
        // We then get our data, in this case, a Battle object
        // If we received name and date, we will catch
        // everyone in a different variable
        Battle battle = args?['battle'];
        return Scaffold(
          appBar: AppBar(
              title: Text(battle.name!),
              backgroundColor: Colors.red,
              centerTitle: true,
            ),
            body: Text('Date of Battle is ${battle.date}'),
        );
      }
    }

lib-01

image

1.3 Adding More Information to Battle

Instead of storing and showing just the name of the battle and its day, we want to also store and show a summary of the battle.

We modify the Battle class to include a variable that holds a summary of the battle.

battle.dart

    class Battle
    {
      String? date;
      String? name;
      String? summary;
    
      Battle(String date, String name, String summary)
      {
        this.date = date;
        this.name = name;
        this.summary = summary;
      }
    }

Then, we modify the battle data to include summaries. Without loss of generality, we will include a brief summary for one of the battles, and we use place holders for the rest.

battle_data.dart

    import 'package:course_app/utils/battle.dart';
    final List<Battle> battles = [
        Battle('1183', 'Siege of Al-Kerak', """
    Karak was the stronghold of Raynald of Châtillon, lord of Oultrejordain, located 124 km south of Amman. The castle was built in 1142 by Pagan the Butler, lord of Shawbak. 
    
    During Raynald’s rule, many truces were signed between the Crusader and Islamic states in the Holy Land, but Raynald did not hesitate to break any of them. He raided caravans trading near Karak Castle for years. Raynald’s boldest raid was a naval expedition across the Red Sea toward Makkah and Medina in 1182. In the spring of 1183, he continued to plunder the Red Sea coast and threatened the pilgrimage routes to Mecca. He captured the city of Aqaba, giving him a base for operations against Makkah, the holiest city for Muslims. 
    
    At this point, Salaheddine Al-Ayyoubi, the leader of the Muslim forces, decided that Kerak Castle would be an ideal target for a Muslim attack, especially as it posed an obstacle on the route from Egypt to Damascus.
    """),
        Battle('1187', 'Siege of Al-Quds', 'Summary of Siege of Al-Quds'),
        Battle('1187', 'Battle of Hittin', 'Summary of Battle of Hittin'),
        Battle('1187', 'Siege of Sour', 'Summary of Siege of Sour'),
        Battle('1187', 'Battle of Marj Oyoun', 'Summary of Battle of Marj Oyoun'),
        Battle('1188', 'Siege of Safd', 'Summary of Siege of Safd'),    
        Battle('1189', 'Siege of Akka', 'Summary of Siege of Akka'),
        Battle('1188', 'Siege of Borzia Fortress', 'Summary of Siege of Borzia Fortress'),
        Battle('1188', 'Siege of Sohyoun Fortress', 'Summary of Siege of Sohyoun Fortress'),    
      ];

We then change BattleScreen to show the summary instead of the date.

battle_screen.dart

    import 'package:course_app/utils/battle.dart';
    import 'package:flutter/material.dart';
    
    class BattleScreen extends StatelessWidget
    {  
      Widget build(BuildContext context)
      {
        // We capture the arguments in the variable args
        final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
        // We then get our data, in this case, a Battle object
        // If we received name and date, we will catch
        // everyone in a different variable
        Battle battle = args?['battle'];
        return Scaffold(
          appBar: AppBar(
              title: Text(battle.name!),
              backgroundColor: Colors.red,
              centerTitle: true,
            ),
            body: Text('Date of Battle is ${battle.summary}'),
        );
      }
    }

image

1.4 Beautifying the Battle Screen

We can add a bit of formatting using padding and text styles.

battle_screen.dart

    import 'package:course_app/utils/battle.dart';
    import 'package:flutter/material.dart';
    
    class BattleScreen extends StatelessWidget
    {  
      Widget build(BuildContext context)
      {
        // We capture the arguments in the variable args
        final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
        // We then get our data, in this case, a Battle object
        // If we received name and date, we will catch
        // everyone in a different variable
        Battle battle = args?['battle'];
        return Scaffold(
          appBar: AppBar(
              title: Text(battle.name!),
              backgroundColor: Colors.red,
              centerTitle: true,
            ),
            body: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Row(          
                    children: 
                    [
                      Text('${battle.name}, ',
                        style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
                      ),
                      Text('${battle.date}',
                        style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
                      ),
                    ],
                  ),
                ),
                Divider(indent: 5, endIndent: 5, thickness: 1, color: Colors.grey[500],),
                Container(
                  padding: EdgeInsets.all(8.0),              
                  child: Text('${battle.summary}',
                  style: TextStyle(fontSize: 20)),
                ),
            ],)
            
        );
      }
    }

Image

1.5 Local Data (Using JSON)

Instead of defining the data we need inside the app, we can read it from local storage. Basically, we store the data in a JSON file, and then read it inside the variable of interest.

1.5.1 Create the JSON File

Inside the Flutter project, we create an assets folder for the data e.g. assets/data/battles.json.

 [
     {"date": "1183", "name": "Siege of Al-Kerak", "summary": "Karak was the stronghold of Raynald of Châtillon, lord of Oultrejordain, located 124 km south of Amman. The castle was built in 1142 by Pagan the Butler, lord of Shawbak.\n\nDuring Raynald’s rule, many truces were signed between the Crusader and Islamic states in the Holy Land, but Raynald did not hesitate to break any of them. He raided caravans trading near Karak Castle for years. Raynald’s boldest raid was a naval expedition across the Red Sea toward Makkah and Medina in 1182. In the spring of 1183, he continued to plunder the Red Sea coast and threatened the pilgrimage routes to Mecca. He captured the city of Aqaba, giving him a base for operations against Makkah, the holiest city for Muslims.\n\nAt this point, Salaheddine Al-Ayyoubi, the leader of the Muslim forces, decided that Kerak Castle would be an ideal target for a Muslim attack, especially as it posed an obstacle on the route from Egypt to Damascus."},
     {"date": "1187", "name": "Siege of Al-Quds", "summary": "Summary of Siege of Al-Quds"},
     {"date": "1187", "name": "Battle of Hittin", "summary": "Summary of Battle of Hittin"},
     {"date": "1187", "name": "Siege of Sour", "summary": "Summary of Siege of Sour"},
     {"date": "1187", "name": "Battle of Marj Oyoun", "summary": "Summary of Battle of Marj Oyoun"},
     {"date": "1188", "name": "Siege of Safd", "summary": "Summary of Siege of Safd"},
     {"date": "1189", "name": "Siege of Akka", "summary": "Summary of Siege of Akka"},
     {"date": "1188", "name": "Siege of Borzia Fortress", "summary": "Summary of Siege of Borzia Fortress"},
     {"date": "1188", "name": "Siege of Sohyoun Fortress", "summary": "Summary of Siege of Sohyoun Fortress"}
   ]

1.5.2 Register the File in pubspec.yaml

 flutter: 
   #...     
   assets:
     - lib/assets/images/
     - lib/assets/data/

1.5.3 Modify the Battle Class

We update the Battle class to support JSON deserialization using the fromJson() Method.

class Battle
{
  String? date;
  String? name;
  String? summary;

  Battle(String date, String name, String summary)
  {
    this.date = date;
    this.name = name;
    this.summary = summary;
  }

  // Deserializing from JSON
  Battle.fromJson(Map<String, dynamic> json)
      : date = json['date'],
        name = json['name'],
        summary = json['summary'];
}

1.5.4 Load and Parse the JSON File

Use Flutter’s rootBundle to load the JSON file and parse it into a list of Battle objects. The rootBundle contains the resources that were packaged with the application when it was built.

import 'package:course_app/utils/battle.dart';
// Provides tools for encoding and decoding JSON data
import 'dart:convert'; 
// Allows access to asset files
// The word "show" is used for convenience and it means
// we are only interested in that part of the package
// This is done to minimize naming conflicts
// The word "hide" can also be used which means
// {All Package} - {Hidden Classes}
import 'package:flutter/services.dart' show rootBundle; 

// Define a function to load battle data from a JSON file in the assets folder
Future<List<Battle>> loadBattlesFromFile() async 
{
  // Load the JSON file as a string from the assets folder.
  // The 'rootBundle' provides a way to access files bundled with the app.
  // 'loadString' reads the file and returns its content as a String.
  final String jsonString = await rootBundle.loadString('lib/assets/data/battles.json');

  // Decode the JSON string into a list of dynamic objects (maps).
  // The 'json.decode' function converts the JSON string into a list of maps
  // or any other structure as described in the JSON data.
  final List<dynamic> jsonData = json.decode(jsonString);

  // Convert the decoded JSON list into a list of Battle objects.
  // Each JSON item (map) is passed to the 'fromJson' constructor of the Battle class
  // to create a new Battle instance. 'toList()' converts the mapped iterable to a List.
  return jsonData.map((jsonItem) => Battle.fromJson(jsonItem)).toList();
}

Put this file in battle_loader.dart and use it when needed.

The reason we use Future<List<Battle>> instead of List<Battle> is because the function is asynchronous (async), meaning it performs operations that may take time, such as loading a file, fetching something over the network, etc. Here’s a detailed breakdown of why:

  • Asynchronous Function (async):
    When you declare a function as async, it means the function may contain await statements, which will pause the function’s execution until the awaited operation completes. Here, await rootBundle.loadString(’assets/battles.json’) is an asynchronous operation because loading a file can take time. An async function must always return a Future, which represents a value that will be available at some point in the future, once the asynchronous operations are done.

  • Future Return Type (Future<List<Battle>>):
    Since await is used to read the file content, loadBattlesFromFile does not return the result immediately. Instead, it returns a Future<List<Battle>>, meaning it will eventually (in the future) complete and return a List<Battle> once all asynchronous tasks are done.

  • Why Not List<Battle> Directly?
    If we declared List<Battle> loadBattlesFromFile(), it would imply that the function returns a List<Battle> right away. However, since file loading is asynchronous, there is no List<Battle> available immediately. Returning List<Battle> directly would not allow the function to handle asynchronous file loading correctly, and will produce a compilation error. In Dart, any function marked with async must return a Future type.

In short, Future<List<Battle>> tells the Flutter framework and any code calling this function to expect a List<Battle> at some future point once the data loading is finished.

1.5.5 Use the Loaded Data inside the BattleView Widget

import 'package:flutter/material.dart';
import 'package:course_app/utils/battle.dart';
// Add this file so that we can use the function
// loadBattlesFromFile() that loads data.
import 'package:course_app/utils/battle_loader.dart';

class BattleView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 8.0),
        // We wrap the ListView.builder inside the FutureBuilder        
        child: FutureBuilder<List<Battle>>(
          // Specify the asynchronous operation to load the list of Battle objects
          // This will call the loadBattlesFromFile() function which reads data from the JSON file
          future: loadBattlesFromFile(),          
          builder: (context, snapshot) {
            // Check if the Future is still in a loading state
            // When waiting, show a loading indicator to let the user know data is being fetched
            if (snapshot.connectionState == ConnectionState.waiting) {
              // Show loading indicator while data is loading
              return Center(child: CircularProgressIndicator());
            // Check if there was an error while loading data from the Future
            // If an error occurs, display the error message to the user  
            } else if (snapshot.hasError) {
              // Display an error message if data loading fails
              return Center(child: Text('Error: ${snapshot.error}'));
            // Check if the data is successfully loaded and available in snapshot  
            } else if (snapshot.hasData) {
              // Retrieve the list of battles from snapshot.data, using an empty list if data is null
              final battles = snapshot.data ?? [];
              // The rest is as was
              return ListView.builder(
                itemCount: battles.length,
                itemBuilder: (context, index) {
                  final battle = battles[index];
                  return InkWell(
                    onTap: () {                      
                      Navigator.pushNamed(context, '/battle', arguments: {
                        'battle': battle
                      });
                    },
                    child: Container(
                      margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                      padding: EdgeInsets.all(10),
                      decoration: BoxDecoration(
                        color: Colors.grey[200],
                        border: Border.all(color: Colors.grey),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            battle.name!,
                            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                          ),
                          SizedBox(height: 5),
                          Text(
                            battle.date!,
                            style: TextStyle(fontSize: 14, fontStyle: FontStyle.italic),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            } else {              
              return Center(child: Text('No battles found.'));
            }
          },
        ),
      ),
    );
  }
}

In the context of a FutureBuilder, context and snapshot serve different purposes:

context:
  • context is a BuildContext object.

  • It provides information about the location of a widget within the widget tree.

  • Through context, widgets can access the nearest instance of any widget that provides resources or settings, such as themes, inherited widgets, or navigation.

  • context is generally used to access things like theme data, show dialogs, navigate, or retrieve other widget information in the tree.

snapshot:
  • snapshot is an AsyncSnapshot object provided specifically by the FutureBuilder (or StreamBuilder).

  • It contains the current state of the asynchronous operation.

  • snapshot has several properties that let you determine if the Future is still loading, completed successfully, or resulted in an error:

    • snapshot.connectionState: Indicates the current state of the Future (e.g., waiting, done).

    • snapshot.hasData: Returns true if the Future has completed successfully and there’s data to display.

    • snapshot.hasError: Returns true if the Future completed with an error.

    • snapshot.data: Contains the data returned by the Future if it completed successfully.

    • snapshot.error: Contains error information if the Future failed.

In FutureBuilder, snapshot helps decide what UI to display at different stages of the asynchronous process.

Our files are now organised as follows.

Image

1.6 Local Data (Using SQLite)

As we said, using JSON is good for loading read-only data, but in the general case, we need a better solution. Using SQLite, we can have a local database that stores data that can be modified by the app.

We shall first revert to our version of the code before using the JSON solution.

1.6.1 Prepare the Database

Use a tool such as DB Browser for SQLite (https://sqlitebrowser.org) to create your database.

  • Download and open the tool.

  • Create a new database and choose a file for it e.g. battles.db.

  • Create the table structure.

        CREATE TABLE "battles" (
            "id" INTEGER,
            "name" TEXT,
            "date" TEXT,
            "summary" TEXT,
            PRIMARY KEY("id")
        );
  • Populate the table with data. In the tab Execute SQL, run the following command.

            INSERT INTO battles (id, name, date, summary) VALUES 
            ('1', 'Siege of Al-Kerak', '1183', 'Karak was the stronghold of Raynald of Châtillon, lord of Oultrejordain, located 124 km south of Amman. The castle was built in 1142 by Pagan the Butler, lord of Shawbak.\n\nDuring Raynald’s rule, many truces were signed between the Crusader and Islamic states in the Holy Land, but Raynald did not hesitate to break any of them. He raided caravans trading near Karak Castle for years. Raynald’s boldest raid was a naval expedition across the Red Sea toward Makkah and Medina in 1182. In the spring of 1183, he continued to plunder the Red Sea coast and threatened the pilgrimage routes to Mecca. He captured the city of Aqaba, giving him a base for operations against Makkah, the holiest city for Muslims.\n\nAt this point, Salaheddine Al-Ayyoubi, the leader of the Muslim forces, decided that Kerak Castle would be an ideal target for a Muslim attack, especially as it posed an obstacle on the route from Egypt to Damascus.'),
            ('2', 'Siege of Al-Quds', '1187', 'Summary of Siege of Al-Quds'),
            ('3', 'Battle of Hittin', '1187', 'Summary of Battle of Hittin'),
            ('4', 'Siege of Sour', '1187', 'Summary of Siege of Sour'),
            ('5', 'Battle of Marj Oyoun', '1187', 'Summary of Battle of Marj Oyoun'),
            ('6', 'Siege of Safd', '1188', 'Summary of Siege of Safd'),
            ('7', 'Siege of Akka', '1189', 'Summary of Siege of Akka'),
            ('8', 'Siege of Borzia Fortress', '1188', 'Summary of Siege of Borzia Fortress'),
            ('9', 'Siege of Sohyoun Fortress', '1188', 'Summary of Siege of Sohyoun Fortress');     
  • Add the database file battles.db to the project folder (drag and drop in VS Code), and add it to the pubspec.yaml file.

         flutter: 
           #...     
           assets:
             - lib/assets/images/
             - lib/assets/data/

1.6.2 Change the Files

The files will be organised as follows. 

Image

pubspec.yaml

# ...
dependencies:
  flutter:
    sdk: flutter
  sqflite: ^2.0.0+4
  path_provider: ^2.0.10
  path: ^1.8.3
# ...
flutter:
# ..
  assets:
    - lib/assets/images/
    - lib/assets/data/

main.dart

import 'package:flutter/material.dart';
import 'package:course_app/pages/home.dart';
import 'package:course_app/pages/battle_screen.dart';

void main() async {
  runApp(MaterialApp(
    home: Home(),
    routes: {      
      '/battle': (context) => BattleScreen(),
    },
  ));
}

home.dart

import 'package:flutter/material.dart';
import 'package:course_app/widgets/battle_view.dart';

class Home extends StatefulWidget {
  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int likes = 0;

  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('My Portfolio'),
          backgroundColor: Colors.red,
          centerTitle: true,
        ),
        body: Column(
          children: [
            SizedBox(height: 20),
            Container(
              decoration: BoxDecoration(
                  border: Border.all(color: Colors.black),
                  color: Colors.grey[100]),
              padding: EdgeInsets.all(10),
              margin: EdgeInsets.only(left: 10, right: 10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  Image.asset(
                    'lib/assets/images/salah.jpeg',
                    width: 100,
                  ),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Name'),
                      Text(
                        'Salaheddine Al-Ayyoubi',
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('DoB'),
                      Text(
                        '1138',
                        style: TextStyle(fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            SizedBox(height: 20),
            Container(
              padding: EdgeInsets.only(left: 10, right: 10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text('Likes: $likes'),
                  IconButton(
                    iconSize: 30,
                    icon: const Icon(Icons.favorite),
                    color: Colors.red,
                    onPressed: () {
                      setState(() {
                        likes++;
                      });
                    },
                  )
                ],
              ),
            ),
            // this is the widget responsible for showing the
            // portfolio items
            BattleView(),
          ],
        ));
  }
}

battle_view.dart

import 'package:flutter/material.dart';
import 'package:course_app/utils/battle.dart';
import 'package:course_app/utils/database_helper.dart';

class BattleView extends StatelessWidget {
  BattleView();

  // Function to load battles from the database
  Future<List<Battle>> fetchBattles() async {
    final dbHelper = DatabaseHelper.instance;
    final battles = await dbHelper.getAllBattles();

    //print('Fetched ${battles.length} battles from the database'); // Add this line to check the number of battles
    return battles;
  }

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 8.0),
        child: FutureBuilder<List<Battle>>(
          future: fetchBattles(), // Load data using FutureBuilder
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              // Show loading indicator while data is being fetched
              return Center(child: CircularProgressIndicator());
            } else if (snapshot.hasError) {
              // Show an error message if fetching data failed
              return Center(child: Text('Error: ${snapshot.error}'));
            } else if (snapshot.hasData) {
              final battles = snapshot.data ?? []; // Retrieve the loaded data

              return ListView.builder(
                itemCount: battles.length,
                itemBuilder: (context, index) {
                  final battle = battles[index];
                  return InkWell(
                    onTap: () {
                      Navigator.pushNamed(
                        context,
                        '/battle',
                        arguments: {'battle': battle},
                      );
                    },
                    child: Container(
                      margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
                      padding: EdgeInsets.all(10),
                      decoration: BoxDecoration(
                        color: Colors.grey[200],
                        border: Border.all(color: Colors.grey),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            battle.name,
                            style: TextStyle(
                                fontWeight: FontWeight.bold, fontSize: 16),
                          ),
                          SizedBox(height: 5),
                          Text(
                            battle.date,
                            style: TextStyle(
                                fontSize: 14, fontStyle: FontStyle.italic),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            } else {
              // Show an empty state if there is no data available
              return Center(child: Text('No battles available.'));
            }
          },
        ),
      ),
    );
  }
}

battle_screen.dart

import 'package:course_app/utils/battle.dart';
import 'package:flutter/material.dart';

class BattleScreen extends StatelessWidget
{  
  Widget build(BuildContext context)
  {
    // We capture the arguments in the variable args
    final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
    // We then get our data, in this case, a Battle object
    // If we received name and date, we will catch
    // everyone in a different variable
    Battle battle = args?['battle'];
    return Scaffold(
      appBar: AppBar(
          title: Text(battle.name),
          backgroundColor: Colors.red,
          centerTitle: true,
        ),
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Row(          
                children: 
                [
                  Text('${battle.name}, ',
                    style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
                  ),
                  Text('${battle.date}',
                    style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
                  ),
                ],
              ),
            ),
            Divider(indent: 5, endIndent: 5, thickness: 1, color: Colors.grey[500],),
            Container(
              padding: EdgeInsets.all(8.0),              
              child: Text('${battle.summary}',
              style: TextStyle(fontSize: 20)),
            ),
        ],)
        
    );
  }
}

battle.dart

class Battle {
  final int? id; // optional id, set automatically by SQLite
  final String name;
  final String date;
  final String summary;

  Battle({this.id, required this.name, required this.date, required this.summary});

  // Convert a Battle object to a Map
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'date': date,
      'summary': summary,
    };
  }

  // Create a Battle object from a Map
  factory Battle.fromMap(Map<String, dynamic> map) {
    return Battle(
      id: map['id'],
      name: map['name'],
      date: map['date'],
      summary: map['summary'].replaceAll(r'\n', '\n'),
    );
  }
}

database_helper.dart

import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:course_app/utils/battle.dart';
import 'package:flutter/services.dart'; // Import services for rootBundle

class DatabaseHelper {

  final String myDatabaseName = "battles.db";
  final String myTableName = "battles";
  final String myDatabasePath = "lib/assets/data/battles.db";

  // Make this a singleton class
  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  // Database reference
  static Database? _database;

  // Getter to initialize and retrieve the database
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  // Initialize the database
  Future<Database> _initDatabase() async {
    final dbPath = await getDatabasesPath();
    String path = join(dbPath, myDatabaseName);

    // Check if the database already exists
    if (!await File(path).exists()) {
      print('Database does not exist, copying from assets...');
      await _copyDatabaseFromAssets(path);
    } else {
      print('Database already exists at $path');
    }

    // Open the database
    return await openDatabase(
      path,
      version: 1,
      //onCreate: _onCreate,
    );
  }

  Future<void> _copyDatabaseFromAssets(String path) async {
    try {
      print('Attempting to copy database from assets...');
      ByteData data = await rootBundle.load(myDatabasePath);
      List<int> bytes = data.buffer.asUint8List();

      File file = File(path);
      await file.writeAsBytes(bytes);
      print('Database copied successfully!');
    } catch (e) {
      print('Error copying database: $e');
    }
  }

  // Insert a new battle
  Future<int> insertBattle(Battle battle) async {
    final db = await database;
    return await db.insert(myTableName, battle.toMap());
  }

  // Fetch all battles
  Future<List<Battle>> getAllBattles() async {
    final db = await database;
    final maps = await db.query(myTableName);

    // Convert List<Map<String, dynamic>> to List<Battle>
    return List.generate(maps.length, (i) {
      return Battle.fromMap(maps[i]);
    });
  }

  Future<void> logBattleCount() async {
    final db = await database;
    final result = await db.rawQuery('SELECT COUNT(*) AS count FROM battles');
    print('Number of battles in database: ${result[0]['count']}');
  }
}

Last modified: Tuesday, 3 December 2024, 5:20 AM