This will add a line like this to your package’s pubspec.yaml (and run an implicit flutter pub get
):
dependencies:
sqflite: ^2.4.1
get: ^4.6.6
Main class
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'productviewscreen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Flutter Demo', theme: ThemeData(), home: ProductDetailScreen());
}
}
Explanation of Components:
main
Function
- Purpose: The entry point of the app, calling
runApp
withMyApp
as the root widget. - Significance: Starts the Flutter app and initializes the widget tree.
MyApp
Class
- Type: StatelessWidget.
- Purpose: The root widget of the application that provides global configurations like theme and navigation.
- Key Features:
- Wraps the app with
GetMaterialApp
, a GetX-provided widget for enhanced navigation, routing, and state management.
- Wraps the app with
Imports
addproduct.dart
: Manages the form and logic for adding new products to the app.product_viewmodel.dart
: Contains theProductController
, a ViewModel for handling product data and business logic.productviewscreen.dart
: Represents the screen where users can view and manage the list of products.
GetMaterialApp
- Purpose: A specialized version of Flutter’s
MaterialApp
, offering built-in features for GetX state management and routing. - Key Properties:
title
: Sets the app’s title displayed in the task manager or system-level UI.theme
: Configures the visual appearance of the app.home
: Specifies the initial screen (ProductDetailScreen
) displayed when the app launches.
Summary
This app uses the GetX package for:
- Streamlined Navigation: Simplifies routing without boilerplate.
- Reactive State Management: Automatically updates the UI based on state changes in the
ProductController
. - Dependency Injection: Manages dependencies efficiently, ensuring scalability and maintainability.
Product Model
class ProductModel {
// Fields to represent the attributes of a product
int? id; // Unique identifier for the product, can be null for new products
int? itemCode; // Optional item code for the product
String productName; // Name of the product
String category; // Category to which the product belongs
String brand; // Brand of the product
double price; // Selling price of the product
double quantity; // Quantity available for the product
String manufacture; // Manufacturer or manufacturing details
double purchasePrice; // Purchase price of the product
double margin; // Profit margin for the product
DateTime date; // Date associated with the product (e.g., added date)
// Constructor to initialize the fields
ProductModel({
this.id,
this.itemCode,
required this.productName,
required this.category,
required this.brand,
required this.price,
required this.quantity,
required this.manufacture,
required this.purchasePrice,
required this.margin,
required this.date,
});
/// `copyWith` Method:
/// This creates a copy of the current `ProductModel` with updated values.
/// Any field not provided in the parameters will retain its current value.
ProductModel copyWith({
int? id,
int? itemCode,
String? productName,
String? category,
String? brand,
double? price,
double? quantity,
String? manufacture,
double? purchasePrice,
double? margin,
DateTime? date,
}) {
return ProductModel(
id: id ?? this.id, // Use new `id` if provided, otherwise retain current value
itemCode: itemCode ?? this.itemCode,
productName: productName ?? this.productName,
category: category ?? this.category,
brand: brand ?? this.brand,
price: price ?? this.price,
quantity: quantity ?? this.quantity,
manufacture: manufacture ?? this.manufacture,
purchasePrice: purchasePrice ?? this.purchasePrice,
margin: margin ?? this.margin,
date: date ?? this.date,
);
}
/// Converts the `ProductModel` object into a `Map<String, dynamic>`.
/// Useful for saving the object to a database or serializing it.
Map<String, dynamic> toMap() {
return {
'id': id,
'itemCode': itemCode,
'productName': productName,
'category': category,
'brand': brand,
'price': price,
'quantity': quantity,
'manufacture': manufacture,
'purchasePrice': purchasePrice,
'margin': margin,
'date': date.toIso8601String(), // Converts `DateTime` to ISO8601 string format
};
}
/// Converts a `Map<String, dynamic>` back into a `ProductModel` object.
/// Useful for reading the object from a database or deserializing it.
static ProductModel fromMap(Map<String, dynamic> json) {
return ProductModel(
id: json['id'] as int?, // Extract `id` as an integer (nullable)
itemCode: json['itemCode'] as int?, // Extract `itemCode` as an integer (nullable)
productName: json['productName'] as String, // Extract `productName` as a string
category: json['category'] as String, // Extract `category` as a string
brand: json['brand'] as String, // Extract `brand` as a string
price: json['price'] as double, // Extract `price` as a double
quantity: json['quantity'] as double, // Extract `quantity` as a double
manufacture: json['manufacture'] as String, // Extract `manufacture` as a string
purchasePrice: json['purchasePrice'] as double, // Extract `purchasePrice` as a double
margin: json['margin'] as double, // Extract `margin` as a double
date: DateTime.parse(json['date'] as String), // Parse `date` from a string
);
}
}
class ProductModel {
// Fields to represent the attributes of a product
int? id; // Unique identifier for the product, can be null for new products
int? itemCode; // Optional item code for the product
String productName; // Name of the product
String category; // Category to which the product belongs
String brand; // Brand of the product
double price; // Selling price of the product
double quantity; // Quantity available for the product
String manufacture; // Manufacturer or manufacturing details
double purchasePrice; // Purchase price of the product
double margin; // Profit margin for the product
DateTime date; // Date associated with the product (e.g., added date)
// Constructor to initialize the fields
ProductModel({
this.id,
this.itemCode,
required this.productName,
required this.category,
required this.brand,
required this.price,
required this.quantity,
required this.manufacture,
required this.purchasePrice,
required this.margin,
required this.date,
});
/// `copyWith` Method:
/// This creates a copy of the current `ProductModel` with updated values.
/// Any field not provided in the parameters will retain its current value.
ProductModel copyWith({
int? id,
int? itemCode,
String? productName,
String? category,
String? brand,
double? price,
double? quantity,
String? manufacture,
double? purchasePrice,
double? margin,
DateTime? date,
}) {
return ProductModel(
id: id ?? this.id, // Use new `id` if provided, otherwise retain current value
itemCode: itemCode ?? this.itemCode,
productName: productName ?? this.productName,
category: category ?? this.category,
brand: brand ?? this.brand,
price: price ?? this.price,
quantity: quantity ?? this.quantity,
manufacture: manufacture ?? this.manufacture,
purchasePrice: purchasePrice ?? this.purchasePrice,
margin: margin ?? this.margin,
date: date ?? this.date,
);
}
/// Converts the `ProductModel` object into a `Map<String, dynamic>`.
/// Useful for saving the object to a database or serializing it.
Map<String, dynamic> toMap() {
return {
'id': id,
'itemCode': itemCode,
'productName': productName,
'category': category,
'brand': brand,
'price': price,
'quantity': quantity,
'manufacture': manufacture,
'purchasePrice': purchasePrice,
'margin': margin,
'date': date.toIso8601String(), // Converts `DateTime` to ISO8601 string format
};
}
/// Converts a `Map<String, dynamic>` back into a `ProductModel` object.
/// Useful for reading the object from a database or deserializing it.
static ProductModel fromMap(Map<String, dynamic> json) {
return ProductModel(
id: json['id'] as int?, // Extract `id` as an integer (nullable)
itemCode: json['itemCode'] as int?, // Extract `itemCode` as an integer (nullable)
productName: json['productName'] as String, // Extract `productName` as a string
category: json['category'] as String, // Extract `category` as a string
brand: json['brand'] as String, // Extract `brand` as a string
price: json['price'] as double, // Extract `price` as a double
quantity: json['quantity'] as double, // Extract `quantity` as a double
manufacture: json['manufacture'] as String, // Extract `manufacture` as a string
purchasePrice: json['purchasePrice'] as double, // Extract `purchasePrice` as a double
margin: json['margin'] as double, // Extract `margin` as a double
date: DateTime.parse(json['date'] as String), // Parse `date` from a string
);
}
}
Explanation of Key Components:
- Fields:
- Represent the product’s attributes such as
id
,itemCode
,price
, etc.
- Represent the product’s attributes such as
- Constructor:
- Ensures required fields like
productName
,price
, andquantity
must always be provided when creating aProductModel
object.
- Ensures required fields like
copyWith
Method:- Provides a convenient way to create a modified copy of a
ProductModel
object without mutating the original.
- Provides a convenient way to create a modified copy of a
toMap
Method:- Transforms the
ProductModel
object into aMap<String, dynamic>
. This format is ideal for storing in SQLite, Firebase, or other databases.
- Transforms the
fromMap
Method:- Reconstructs a
ProductModel
object from aMap<String, dynamic>
. This is particularly useful when retrieving data from a database or deserializing JSON responses.
- Reconstructs a
- Date Handling:
- The
date
field usestoIso8601String()
for serialization andDateTime.parse()
for deserialization, ensuring compatibility with various storage and API formats.
- The
ProductController
import 'package:get/get.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'model.dart';
class ProductController extends GetxController {
late Database db;
var products = <ProductModel>[].obs;
@override
void onInit() {
super.onInit();
_initDatabase();
}
Future<void> _initDatabase() async {
try {
String path = join(await getDatabasesPath(), 'products.db');
db = await openDatabase(
path,
version: 1,
onCreate: (db, version) {
return db.execute('''
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
itemCode INTEGER,
productName TEXT,
category TEXT,
brand TEXT,
price REAL,
quantity REAL,
manufacture TEXT,
purchasePrice REAL,
margin REAL,
date TEXT
)
''');
},
);
fetchProducts();
} catch (e) {
print('Database Initialization Error: $e');
}
}
Future<void> fetchProducts() async {
try {
final List<Map<String, dynamic>> maps = await db.query('products');
products.value = maps.map((map) => ProductModel.fromMap(map)).toList();
} catch (e) {
print('Error Fetching Products: $e');
}
}
Future<void> addProduct(ProductModel product) async {
try {
await db.insert('products', product.toMap());
fetchProducts();
} catch (e) {
print('Error Adding Product: $e');
}
}
Future<void> deleteProduct(int id) async {
try {
await db.delete('products', where: 'id = ?', whereArgs: [id]);
fetchProducts();
} catch (e) {
print('Error Deleting Product: $e');
}
}
}
This code snippet defines a ProductController
class that uses the GetX
package for state management and sqflite
for local database management in a Flutter application. Here’s an overview of what each part does:
onInit()
Method: This is called when theProductController
is initialized. It calls_initDatabase()
to set up the local database._initDatabase()
Method: It initializes the SQLite database. ThegetDatabasesPath()
method gets the path where the database will be stored, andopenDatabase()
is used to open the database. If the database doesn’t exist, it will create a new one with aproducts
table.fetchProducts()
Method: This method queries the database for all products and updates theproducts
list, which is an observable (RxList<ProductModel>
). TheProductModel.fromMap()
method is used to convert the database result into product objects.addProduct(ProductModel product)
Method: This method inserts a new product into the database and then callsfetchProducts()
to reload the product list.deleteProduct(int id)
Method: This method deletes a product based on the provided ID and reloads the product list by callingfetchProducts()
.
add Product Screen
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'model.dart';
import 'product_viewmodel.dart';
class AddProductScreen extends StatelessWidget {
final ProductController controller = Get.find<ProductController>();
final _formKey = GlobalKey<FormState>();
final TextEditingController nameController = TextEditingController();
final TextEditingController categoryController = TextEditingController();
final TextEditingController brandController = TextEditingController();
final TextEditingController priceController = TextEditingController();
final TextEditingController quantityController = TextEditingController();
final TextEditingController manufactureController = TextEditingController();
final TextEditingController purchasePriceController = TextEditingController();
final TextEditingController marginController = TextEditingController();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text('Add Product'),
backgroundColor: Colors.deepPurple,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: [
Text(
'Fill in the details below to add a new product',
style: theme.textTheme.titleLarge!
.copyWith(color: Colors.grey[700]),
),
SizedBox(height: 20),
_buildTextField(
controller: nameController,
label: 'Product Name',
validator: (value) => value == null || value.isEmpty
? 'Enter product name'
: null,
),
_buildTextField(
controller: categoryController,
label: 'Category',
),
_buildTextField(
controller: brandController,
label: 'Brand',
),
_buildTextField(
controller: priceController,
label: 'Price',
keyboardType: TextInputType.number,
),
_buildTextField(
controller: quantityController,
label: 'Quantity',
keyboardType: TextInputType.number,
),
_buildTextField(
controller: manufactureController,
label: 'Manufacture',
),
_buildTextField(
controller: purchasePriceController,
label: 'Purchase Price',
keyboardType: TextInputType.number,
),
_buildTextField(
controller: marginController,
label: 'Margin',
keyboardType: TextInputType.number,
),
SizedBox(height: 30),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
controller.addProduct(ProductModel(
productName: nameController.text,
category: categoryController.text,
brand: brandController.text,
price: double.tryParse(priceController.text) ?? 0,
quantity: double.tryParse(quantityController.text) ?? 0,
manufacture: manufactureController.text,
purchasePrice:
double.tryParse(purchasePriceController.text) ?? 0,
margin: double.tryParse(marginController.text) ?? 0,
date: DateTime.now(),
));
Get.back();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
padding: EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
'Add Product',
style: TextStyle(fontSize: 18),
),
),
],
),
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
TextInputType keyboardType = TextInputType.text,
String? Function(String?)? validator,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
validator: validator,
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.deepPurple),
filled: true,
fillColor: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.deepPurple, width: 2),
borderRadius: BorderRadius.circular(10),
),
),
),
);
}
}
This Flutter code defines a screen (AddProductScreen
) for adding a product to a local database using a form. It employs the GetX
package for state management and organizes the input fields using a Form
widget to handle validation.
Key Parts of the Code:
- Form and Validation:
- A
GlobalKey<FormState>
is used to manage the form state and trigger validation. - Several
TextEditingController
objects are defined for each field (e.g.,nameController
,priceController
, etc.). - The
TextFormField
widget is used for each input, with validation functions to ensure required fields are filled. For example, the product name field ensures that the user doesn’t leave it empty.
- A
- Text Fields:
- Each text field is wrapped in a custom
_buildTextField
widget. This widget accepts aTextEditingController
, alabel
,keyboardType
, and an optionalvalidator
function. - The fields include
Product Name
,Category
,Brand
,Price
,Quantity
,Manufacture
,Purchase Price
, andMargin
. - The fields for numeric values (like price, quantity, etc.) use
TextInputType.number
to make sure the keyboard is appropriate.
- Each text field is wrapped in a custom
- Form Submission:
- When the “Add Product” button is pressed, it checks if the form is valid by calling
_formKey.currentState!.validate()
. - If the form is valid, it creates a new
ProductModel
object using the data entered in the form fields and then calls theaddProduct()
method on theProductController
to add the product to the database. - After adding the product, the screen is popped from the navigation stack (
Get.back()
), returning to the previous screen.
- When the “Add Product” button is pressed, it checks if the form is valid by calling
- Product Model:
- The
ProductModel
constructor is used to create a new product object. It includes properties likeproductName
,category
,brand
,price
, etc., that are populated with the data from the controllers. - The
date
is set to the current timestamp (DateTime.now()
).
- The
- UI Design:
- The
Scaffold
provides the basic structure of the page with anAppBar
titled “Add Product” and a body containing aForm
. - The
TextFormField
widgets have a consistent style with a purple theme for labels, borders, and a rounded shape. - The
ElevatedButton
is styled with a deep purple background, rounded corners, and padding for a modern look.
- The
Detailed Explanation of AddProductScreen
:
- Form Structure:
- The
Form
widget contains aListView
to enable scrolling, especially if the keyboard is visible. - Each input field is placed inside
_buildTextField()
, which applies consistent styling. - A validator is provided for the
Product Name
field, ensuring the user cannot submit an empty name.
- The
- Button Handling:
- The “Add Product” button performs form validation. If all fields are valid, it adds the product to the database via
controller.addProduct()
. - After the product is added, the user is returned to the previous screen using
Get.back()
.
- The “Add Product” button performs form validation. If all fields are valid, it adds the product to the database via
- UI Styling:
- Colors are defined using
Colors.deepPurple
for the button and label colors. - The form fields are styled with a light background (
Colors.grey[100]
), rounded borders, and focused borders that change color when selected.
- Colors are defined using
ProductViewScreen
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'addproduct.dart';
import 'product_viewmodel.dart';
class ProductDetailScreen extends StatelessWidget {
final ProductController controller = Get.put(ProductController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product List')),
body: Obx(() {
if (controller.products.isEmpty) {
return Center(child: Text('No Products Found.'));
}
return ListView.builder(
itemCount: controller.products.length,
itemBuilder: (context, index) {
final product = controller.products[index];
return Card(
child: ListTile(
title: Text(product.productName),
subtitle: Text(
'Price: ${product.price}\nCategory: ${product.category}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
controller.deleteProduct(product.id!);
},
),
),
);
},
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
Get.to(() => AddProductScreen());
},
child: Icon(Icons.add),
),
);
}
}
This Flutter code defines a ProductDetailScreen
that displays a list of products fetched from a local database, using GetX
for state management and reactive updates. The screen provides the ability to view, delete, and add products.
Key Components:
ProductController
Initialization:- The
ProductController
is instantiated usingGet.put(ProductController())
. This makes the controller available for the screen and manages the state of products. TheProductController
is responsible for fetching and deleting products from the database.
- The
- Reactive State with
Obx
:Obx()
is used to observe thecontroller.products
list. It automatically rebuilds the widget whenever theproducts
list is updated (e.g., when a product is added or deleted).- If the
controller.products
list is empty, a message (“No Products Found.”) is displayed usingCenter()
. - Otherwise, a
ListView.builder
is used to display the list of products.
- ListView to Display Products:
- Each product is displayed inside a
Card
widget with aListTile
. - The
title
of theListTile
displays the product’s name (product.productName
). - The
subtitle
contains the product’s price and category. - The
trailing
section of theListTile
has anIconButton
with a delete icon. When clicked, it callscontroller.deleteProduct(product.id!)
to delete the product from the database.
- Each product is displayed inside a
- Floating Action Button to Add Products:
- A
FloatingActionButton
is placed at the bottom right of the screen, which navigates to theAddProductScreen
when clicked. This allows the user to add new products. - The
Get.to(() => AddProductScreen())
is used to navigate to theAddProductScreen
when the button is pressed.
- A
Detailed Explanation:
- State Management:
ProductController
is used to manage the state of products. The list of products is observed by theObx
widget, which triggers a UI update whenever the product list changes.Obx
ensures that only the part of the widget tree that depends oncontroller.products
is rebuilt, making the UI more efficient.
- Product List Display:
- If there are products, the
ListView.builder
creates a list item for each product. TheListTile
widget shows the product name, price, and category. - If the product list is empty, a message is shown telling the user that no products are available.
- If there are products, the
- Product Deletion:
- Each product has a delete button (represented by the
Icons.delete
icon). Clicking this button triggers thedeleteProduct
method from theProductController
, which deletes the product from the database by using the product’sid
.
- Each product has a delete button (represented by the
- Navigation to Add Product Screen:
- The floating action button allows the user to add a new product by navigating to the
AddProductScreen
. This is done usingGet.to(() => AddProductScreen())
, which triggers navigation to theAddProductScreen
where users can input product details.
- The floating action button allows the user to add a new product by navigating to the
UI Components:
- AppBar: Displays the title “Product List” at the top of the screen.
- Card: Used to style the individual product entries in the list, giving them a material design look.
- ListTile: Used to display the product’s name and other details like price and category.
- IconButton: Allows the user to delete a product from the list.
- FloatingActionButton: A button that allows the user to navigate to the “Add Product” screen.