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.

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:

So far, our app is structured as follows.

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}'),
);
}
}


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}'),
);
}
}

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)),
),
],)
);
}
}

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 asasync, it means the function may containawaitstatements, 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. Anasyncfunction must always return aFuture, 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>>):
Sinceawaitis used to read the file content,loadBattlesFromFiledoes not return the result immediately. Instead, it returns aFuture<List<Battle>>, meaning it will eventually (in the future) complete and return aList<Battle>once all asynchronous tasks are done. -
Why Not
List<Battle>Directly?
If we declaredList<Battle> loadBattlesFromFile(), it would imply that the function returns aList<Battle>right away. However, since file loading is asynchronous, there is noList<Battle>available immediately. ReturningList<Battle>directly would not allow the function to handle asynchronous file loading correctly, and will produce a compilation error. In Dart, any function marked withasyncmust return aFuturetype.
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:
-
contextis aBuildContextobject. -
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. -
contextis generally used to access things like theme data, show dialogs, navigate, or retrieve other widget information in the tree.
snapshot:
-
snapshotis anAsyncSnapshotobject provided specifically by theFutureBuilder(orStreamBuilder). -
It contains the current state of the asynchronous operation.
-
snapshothas 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 theFuturehas completed successfully and there’s data to display. -
snapshot.hasError: Returns true if theFuturecompleted with an error. -
snapshot.data: Contains the data returned by theFutureif it completed successfully. -
snapshot.error: Contains error information if theFuturefailed.
-
In FutureBuilder, snapshot helps decide what UI to display at different stages of the asynchronous process.
Our files are now organised as follows.

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.dbto the project folder (drag and drop in VS Code), and add it to thepubspec.yamlfile.flutter: #... assets: - lib/assets/images/ - lib/assets/data/
1.6.2 Change the Files
The files will be organised as follows.

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']}');
}
}