Single Child Widgets are those that can contain only one child. These widgets are used to modify or control the behavior of a single widget
Container(
padding: EdgeInsets.all(16.0),
margin: EdgeInsets.all(8.0),
color: Colors.blue,
child: Text('Single Child Widget'),
);
Padding(
padding: EdgeInsets.all(16.0),
child: Text('Padded Text'),
);
Align(
alignment: Alignment.center,
child: Text('Centered Text'),
);
Center(
child: Text('Centered Text'),
);
SizedBox(
width: 100,
height: 100,
child: Text('Sized Box'),
);
Expanded(
child: Text('Expanded Text'),
);
Multiple Child Widgets can contain more than one child. These widgets are often used to create complex layouts by arranging multiple widgets in a specific order or alignment.
Column(
children: <Widget>[
Text('First'),
Text('Second'),
Text('Third'),
],
);
Row(
children: <Widget>[
Icon(Icons.star),
Icon(Icons.star),
Icon(Icons.star),
],
);
Stack(
children: <Widget>[
Container(
width: 100,
height: 100,
color: Colors.red,
),
Positioned(
top: 20,
left: 20,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
),
],
);
ListView(
children: <Widget>[
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
],
);
GridView.count(
crossAxisCount: 2,
children: <Widget>[
Container(color: Colors.red, height: 100),
Container(color: Colors.blue, height: 100),
Container(color: Colors.green, height: 100),
Container(color: Colors.yellow, height: 100),
],
);
Wrap(
spacing: 8.0,
runSpacing: 4.0,
children: <Widget>[
Chip(label: Text('Chip 1')),
Chip(label: Text('Chip 2')),
Chip(label: Text('Chip 3')),
],
);
Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(child: Container(color: Colors.red, height:
50)),
Expanded(child: Container(color: Colors.blue, heigh
t: 50)),
],
);
In this section, let us learn how to create a complex user interface of product listing with custom design using both single and multiple child layout widgets.
For this purpose, follow the sequence given below:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Product layout demo home page'),
);
}
}
import 'package:flutter/material.dart';
class ProductBox extends StatelessWidget {
const ProductBox({
Key? key,
this.name = '',
this.description = '',
this.price = 0,
this.image = '',
}) : super(key: key);
final String name;
final String description;
final int price;
final String image;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(2),
height: 120,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset(
'assets/appimages/' + image,
width: 100, // Adjust width as needed
height: 100, // Adjust height as needed
fit: BoxFit.cover, // Adjust BoxFit as needed
),
Expanded(
child: Container(
padding: const EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
description,
maxLines: 2, // Adjust as needed
overflow: TextOverflow.ellipsis,
),
Text(
'Price: \$${price.toString()}',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, this.title = ''});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Product Listing")),
body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: <Widget>[
ProductBox(
name: "iPhone",
description: "iPhone is the stylist phone ever",
price: 1000,
image: "iPhone.jpg"),
ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone ever",
price: 800,
image: "pixel.png"),
ProductBox(
name: "Laptop",
description: "Laptop is most productive development tool",
price: 2000,
image: "laptop.jpg"),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device ever for meeting",
price: 1500,
image: "tablet.jpg"),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful rescue storage medium",
price: 20,
image: "floppy.jpg"),
],
),
);
}
}
import 'package:flutter/material.dart';
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, this.title = ''});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Product Listing")),
body: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0),
children: const <Widget>[
ProductBox(
name: "iPhone",
description: "iPhone is the most stylish phone ever.",
price: 1000,
image: "iPhone.jpg",
),
ProductBox(
name: "Pixel",
description: "Pixel is the most featureful phone.",
price: 800,
image: "pixel.png",
),
ProductBox(
name: "Laptop",
description: "Laptop is the most productive development tool.",
price: 2000,
image: "laptop.jpg",
),
ProductBox(
name: "Tablet",
description: "Tablet is the most useful device for meetings.",
price: 1500,
image: "tablet.jpg",
),
ProductBox(
name: "Floppy Drive",
description: "Floppy drive is useful for rescue storage.",
price: 20,
image: "floppy.jpg",
),
],
),
);
}
}
class ProductBox extends StatelessWidget {
const ProductBox({
super.key,
this.name = '',
this.description = '',
this.price = 0,
this.image = '',
});
final String name;
final String description;
final int price;
final String image;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(2),
height: 120,
child: Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Image.asset(
"assets/appimages/$image",
width: 100,
height: 100,
fit: BoxFit.cover,
),
Expanded(
child: Container(
padding: const EdgeInsets.all(5),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Text(
"Price: \$${price.toString()}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
],
),
),
);
}
}
In Flutter, gestures are a way to detect and respond to user interactions like taps, swipes, and drags. Flutter provides a robust system to handle gestures, making it easy to create interactive and responsive user interfaces. The primary widget used for handling gestures in Flutter is the GestureDetector
.
GestureDetector
: The GestureDetector
widget is used to capture a wide range of gestures. It wraps around another widget and intercepts gestures like taps, double taps, long presses, swipes, and more.GestureDetector
: The GestureDetector
widget has properties like onTap
, onDoubleTap
, onLongPress
, onPanUpdate
, and others, each corresponding to a specific gesture.Let's create a simple example where a container changes its color when the user taps on it
import 'package:flutter/material.dart';
void main() {
runApp(GestureExample());
}
class GestureExample extends StatefulWidget {
@override
_GestureExampleState createState() => _GestureExampleState();
}
class _GestureExampleState extends State<GestureExample> {
Color _color = Colors.blue;
void _changeColor() {
setState(() {
_color = _color == Colors.blue ? Colors.red : Colors.blue;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Gesture Example'),
),
body: Center(
child: GestureDetector(
onTap: _changeColor,
child: Container(
width: 200,
height: 200,
color: _color,
child: Center(
child: Text(
'Tap me',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
),
),
);
}
}
GestureDetector
wraps a Container widget. The onTap
callback is used to change the color of the container when the user taps on it._changeColor
method is called when the container is tapped, toggling its color between blue and red.onPanUpdate
callback. This is useful for interactive elements like sliders or custom scroll views.GestureDetector(
onPanUpdate: (details) {
// Handle drag
},
child: Container(
color: Colors.green,
width: 200,
height: 200,
),
);
onHorizontalDragEnd
or onVerticalDragEnd
callbacks. These are triggered when the user swipes in a particular direction.dartCopy code
GestureDetector(
onHorizontalDragEnd: (details) {
// Handle horizontal swipe
},
child: Container(
color: Colors.orange,
width: 200,
height: 200,
),
);
onScaleUpdate
callback.dartCopy code
GestureDetector(
onScaleUpdate: (details) {
// Handle pinch-to-zoom
},
child: Container(
color: Colors.purple,
width: 200,
height: 200,
),
);
PageView
.State
class
Example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Ephemeral State Example'),
),
body: CounterWidget(),
),
);
}
}
class CounterWidget extends StatefulWidget {
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text('$_counter', style: Theme.of(context).textTheme.headline4),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
),
);
}
}
_counter
variable is managed within the CounterWidget
's state class ( _CounterWidgetState
).CounterWidget
and is not needed elsewhere in the app.setState
is called, which tells Flutter to rebuild the widget with the updated state.InheritedWidget
, Provider
, Bloc
, Riverpod
, and more.StatefulWidget
to hold the application state (in this case, a counter) and manage its changes.import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App State Management',
theme: ThemeData(primarySwatch: Colors.blue),
home: HomeScreen(
counter: _counter,
incrementCounter: _incrementCounter,
),
);
}
}
_counter
is our application-wide state. We have a function _incrementCounter()
that updates the state. setState()
is used to rebuild the widget tree when the state changes.HomeScreen
and Pass the StateHomeScreen
widget will display the counter value and allow navigation to a second screen where the counter can be updated.import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
final int counter;
final VoidCallback incrementCounter;
HomeScreen({
required this.counter,
required this.incrementCounter,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Counter value:',
style: TextStyle(fontSize: 20),
),
Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(
counter: counter,
incrementCounter: incrementCounter,
),
),
);
},
child: const Text('Go to Second Screen'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class SecondScreen extends StatelessWidget {
final int counter;
final VoidCallback incrementCounter;
SecondScreen({
required this.counter,
required this.incrementCounter,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Counter value on Second Screen:',
style: TextStyle(fontSize: 20),
),
Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Back to Home Screen'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
HomeScreen
takes the current counter value and the incrementCounter
function as parameters.SecondScreen
while passing the same state and callback function to allow state management across screens.SecondScreen
will display the counter value and allow the user to increment the counter.import 'package:flutter/material.dart';
class SecondScreen extends StatelessWidget {
final int counter;
final VoidCallback incrementCounter;
SecondScreen({
required this.counter,
required this.incrementCounter,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Counter value:',
style: TextStyle(fontSize: 20),
),
Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
CounterModel
is a ChangeNotifier
that holds the application state (in this case, the counter value).ChangeNotifierProvider
makes CounterModel
available to the entire widget tree. CounterWidget
listens to changes in CounterModel
using context.watch()
and updates the UI accordingly.FloatingActionButton
increments the counter, demonstrating how application state can be modified and shared across different parts of the app.Route
in Flutter is an abstraction for a screen or page. When you navigate to a new screen, you are pushing a new Route
onto the navigation stack.Navigator
is a widget that manages a stack of Route
objects. It provides methods to navigate between routes, such as push
, pop
, and others.Navigator.push
method. This method takes a Route and adds it to the navigation stack, displaying the new screen.import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: ElevatedButton(
child: Text('Go to Second Screen'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: ElevatedButton(
child: Text('Go Back'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
Navigator.push
: This method is used to navigate to a new screen by adding a new route to the stack.MaterialPageRoute
: This creates a route that uses a platform-specific animation to transition between screens.Navigator.pop
: This removes the top route from the stack, returning to the previous screen.import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// Define the named routes
routes: {
'/': (context) => FirstScreen(),
'/second': (context) => SecondScreen(),
},
initialRoute: '/',
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: ElevatedButton(
child: Text('Go to Second Screen'),
onPressed: () {
Navigator.pushNamed(context, '/second');
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: ElevatedButton(
child: Text('Go Back'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
MaterialApp
widget using the routes
property.Navigator.pushNamed
: This method is used to navigate to a route using its name, which simplifies the navigation process, especially in larger apps.import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: ElevatedButton(
child: Text('Go to Second Screen with Data'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
SecondScreen(data: 'Hello from First Screen!'),
),
);
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
final String data;
SecondScreen({required this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: Text(data),
),
);
}
}
SecondScreen
through its constructor. This allows the SecondScreen
to access and display the data.ScopedModel
is a state management solution in Flutter that allows you to share state across your application in a way that is simple, efficient, and effective.Provider
and Riverpod
became mainstream, but it’s still useful for understanding how state can be managed in a structured manner.Model
is a class that holds the application state. It extends the Model
class from the scoped_model
package. Any changes to the model can trigger a rebuild of widgets that depend on it.ScopedModel
widget is used to provide the Model
to the widget tree. It acts as a provider of the state and can be accessed by any widget in the subtree.ScopedModelDescendant
is a widget that listens to changes in the Model
. When the state in the Model
changes, only those ScopedModelDescendant
widgets that depend on the model will rebuild.Let’s create a simple counter application using ScopedModel
scoped_model
DependencyFirst, add the scoped_model
package to your pubspec.yaml
file:
yamlCopy code
dependencies:
flutter:
sdk: flutter
scoped_model: ^1.0.1
Then, run flutter pub get
to install the package.
Model
Create a CounterModel
class that will hold the counter value and provide methods to modify it.
import 'package:scoped_model/scoped_model.dart';
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners(); // Notifies all listening widgets to rebuild
}
}
Model
Using ScopedModel
Wrap your MaterialApp
or Scaffold with a ScopedModel
to provide the model to the widget tree.
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'counter_model.dart'; // Import the model
void main() {
runApp(
ScopedModel<CounterModel>(
model: CounterModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: CounterScreen(),
);
}
}
ScopedModelDescendant
Use ScopedModelDescendant
to access and react to changes in the model.
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'counter_model.dart';
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Scoped Model Example'),
),
body: Center(
child: ScopedModelDescendant<CounterModel>(
builder: (context, child, model) {
return Text(
'Counter: ${model.counter}',
style: Theme.of(context).textTheme.headline4,
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ScopedModel.of<CounterModel>(context).increment();
},
child: Icon(Icons.add),
),
);
}
}
CounterModel
class holds the counter value and a method to increment it. It extends Model
and calls notifyListeners()
whenever the counter value changes. This ensures that all widgets listening to this model get rebuilt with the updated state.ScopedModel
widget provides the CounterModel
to the entire widget tree. Any widget within this tree can access the model and react to changes.CounterModel
. When the CounterModel
updates, the ScopedModelDescendant
will rebuild, displaying the new counter value.increment()
method in the CounterModel
to increase the counter value and notify listeners to update the UI.ScopedModel
can become difficult to manage in larger applications with more complex state requirements.Provider
has become the preferred state management solution, so ScopedModel
has less community support and fewer resources.Made By SOU Student for SOU Students