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
runAppwithMyAppas 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, andquantitymust always be provided when creating aProductModelobject.
- Ensures required fields like
copyWithMethod:- Provides a convenient way to create a modified copy of a
ProductModelobject without mutating the original.
- Provides a convenient way to create a modified copy of a
toMapMethod:- Transforms the
ProductModelobject into aMap<String, dynamic>. This format is ideal for storing in SQLite, Firebase, or other databases.
- Transforms the
fromMapMethod:- Reconstructs a
ProductModelobject from aMap<String, dynamic>. This is particularly useful when retrieving data from a database or deserializing JSON responses.
- Reconstructs a
- Date Handling:
- The
datefield 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 theProductControlleris 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 aproductstable.fetchProducts()Method: This method queries the database for all products and updates theproductslist, 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
TextEditingControllerobjects are defined for each field (e.g.,nameController,priceController, etc.). - The
TextFormFieldwidget 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
_buildTextFieldwidget. This widget accepts aTextEditingController, alabel,keyboardType, and an optionalvalidatorfunction. - The fields include
Product Name,Category,Brand,Price,Quantity,Manufacture,Purchase Price, andMargin. - The fields for numeric values (like price, quantity, etc.) use
TextInputType.numberto 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
ProductModelobject using the data entered in the form fields and then calls theaddProduct()method on theProductControllerto 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
ProductModelconstructor 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
dateis set to the current timestamp (DateTime.now()).
- The
- UI Design:
- The
Scaffoldprovides the basic structure of the page with anAppBartitled “Add Product” and a body containing aForm. - The
TextFormFieldwidgets have a consistent style with a purple theme for labels, borders, and a rounded shape. - The
ElevatedButtonis styled with a deep purple background, rounded corners, and padding for a modern look.
- The
Detailed Explanation of AddProductScreen:
- Form Structure:
- The
Formwidget contains aListViewto 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 Namefield, 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.deepPurplefor 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:
ProductControllerInitialization:- The
ProductControlleris instantiated usingGet.put(ProductController()). This makes the controller available for the screen and manages the state of products. TheProductControlleris responsible for fetching and deleting products from the database.
- The
- Reactive State with
Obx:Obx()is used to observe thecontroller.productslist. It automatically rebuilds the widget whenever theproductslist is updated (e.g., when a product is added or deleted).- If the
controller.productslist is empty, a message (“No Products Found.”) is displayed usingCenter(). - Otherwise, a
ListView.builderis used to display the list of products.
- ListView to Display Products:
- Each product is displayed inside a
Cardwidget with aListTile. - The
titleof theListTiledisplays the product’s name (product.productName). - The
subtitlecontains the product’s price and category. - The
trailingsection of theListTilehas anIconButtonwith 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
FloatingActionButtonis placed at the bottom right of the screen, which navigates to theAddProductScreenwhen clicked. This allows the user to add new products. - The
Get.to(() => AddProductScreen())is used to navigate to theAddProductScreenwhen the button is pressed.
- A
Detailed Explanation:
- State Management:
ProductControlleris used to manage the state of products. The list of products is observed by theObxwidget, which triggers a UI update whenever the product list changes.Obxensures that only the part of the widget tree that depends oncontroller.productsis rebuilt, making the UI more efficient.
- Product List Display:
- If there are products, the
ListView.buildercreates a list item for each product. TheListTilewidget 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.deleteicon). Clicking this button triggers thedeleteProductmethod 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 theAddProductScreenwhere 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.