instead of instantiating the project from the first lesson I try to recreate the application from scratch
lib
models
Trip.dart
screens
details.dart
home.dart
shared
heart.dart
screenTitle.dart
tripList.dart
main.dart
Trip.dart
defines a model for the trip.
details.dart
and home.dart
describe the two routes of the application.
The shared
folder includes reusable widgets.
main.dart
kickstarts the application.
main.dart
instantiates the application by creating a material application with the Home
widget created in home.dart
.
return MaterialApp(
title: 'Trips',
home: Home()
);
home.dart
creates a stateful widget with the following tree structure.
Scaffold
Container
Column
SizedBox
SizedBox
ScreenTitle
Flexible
TripList
The Container
widget adds padding and a background image in the form of a BoxDecoration
widget.
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('images/bg.png'),
fit: BoxFit.fitWidth,
alignment: Alignment.topLeft
)
),
In the column the second SizedBox
widget wraps around the reusable component ScreenTitle
to create a container of a larger size.
The Flexible
widget wraps around the reusable component TripList
.
The reusable widget is instantiated with a text
property.
child: ScreenTitle(text: 'Trips'),
Create ScreenTitle
as a stateless widget with the input string.
In the build
function return a Text
widget with said string.
return Text(text)
tripList.dart
creates a stateful widget for the list of trips. The idea is to ultimately add the widgets through a ListView
widget.
return ListView.builder(
key: _listKey,
itemCount: _tripTiles.length,
itemBuilder: (_, i) => _tripTiles[i]
);
Create an instance of a global key to keep track of the state in the widget.
final GlobalKey _listKey = GlobalKey();
Define a list to store the different widgets.
List<Widget> _tripTiles = [];
The list is populated in two steps:
-
with
_addTrips
loop through a list ofTrip
objects to call for each instance the_buildTile
functionfor (Trip trip in _trips) { _tripTiles.add(_buildTile(trip)); }
For the list define
_trips
with a few instances of the trip model.List<Trip> _trips = [ Trip(), Trip(), ];
-
with
_buildTile
produce the widget tree for an individual tripWidget _buildTile(Trip trip) { return ListTile() }
For each trip object the ListTile
widget has several properties:
-
onTap
to consider the tap event — more of this later -
title
to add the number of nights and the title of the trip -
leading
to show the image -
trailing
to highlight the price
In the initState
method, the lifecycle function invoked as the widget is first created, call _addTrips
to populate the list of widgets.
@override
void initState() {
super.initState();
_addTrips();
}
As a ListTile
widget is tapped the idea is to create a details page for the individual trip. This is achieved with Navigator.push
and the MaterialPageRoute
object.
onTap: () {
Navigator.push();
},
As the second argument of the push
function include MaterialPageRoute
with a builder
field. It is this field which creates the second screen returning a widget.
MaterialPageRoute(
builder: (context) => Details(trip: trip)
)
As the page is pushed the screen is added above the home widget. The action also implies that an application bar in the details page will include a back arrow to pop the screen.
Create details.dart
as a stateless widget which receives a trip object.
In the build
function return a Scaffold
widget with several fields and a specific widget tree.
In a column add a ClipRRect
widget to show a clipped version of the image associated with the trip.
ClipRRect(child: Image.asset())
Add a ListTile
widget to describe the trip.
ListTile()
In the tile illustrate the trip with a title
and subtitle
. Add also an instance of the heart widget in the trailing
field.
ListTile(
trailing: Heart()
)
Finally add lorem ipsum text in a Padding
and Text
widget.
Padding(
child: Text()
)
To include random letters install flutter-lorem
— the course relies on ipsum
, but the package no longer seems to be supported.
dependencies:
flutter:
sdk: flutter
flutter_lorem: ^2.0.0
Import the module and produce the relevant string with the lorem
function.
import 'package:flutter_lorem/flutter_lorem.dart';
//
Text(
lorem(paragraphs: 1, words: 50),
)
The reusable widget is created as a stateful widget to ultimately change the state and appearance of the icon.
In the build
function return an IconButton
widget with a specific icon and an empty onPressed
callback.
return IconButton(
icon: Icon(Icons.favorite),
onPressed: () {}
);
Flutter provides widgets to implicitly animate several properties.
With AnimatedContainer
gradually change values such as margin, width and color.
With AnimatedOpacity
gradually change the opacity.
Both widgets require a duration
field.
AnimatedContainer(
duration: Duration(seconds: 1),
)
Any property set on the nested widget and updated with setState
is then automatically tweened over time.
// set up variable
_color = Colors.blue;
// use in AnimatedContainer
AnimatedContainer(
duration: Duration(seconds: 1),
color: _color,
)
// update through setState
setState(() {
_color: Colors.purple;
})
With the TweenAnimationBuilder
widget define implicit animations with a start and end value. Use the tweened result in the application.
Begin by wrapping the widgets to-be-animated in TweenAnimationBuilder
.
TweenAnimationBuilder(
child: Text()
)
The widget requires three fields: duration
, tween
and builder
.
With duration
describe the duration for the tween.
duration: const Duration(milliseconds: 500),
With tween
define the start and end value.
tween: Tween<double>(begin: 0, end: 1),
With builder
elaborate a function which receives a context, the tweened value and the widget — in the example Text
.
builder: (BuildContext context, double _tweenedValue, Widget? child) {}
In the function return a widget. For instance an Opacity
widget which wraps around the child and changes is opacity.
return Opacity(
opacity: _tweenedValue,
child: child
)
To animate multiple properties return a more complex widget tree.
return Opacity(
opacity: _tweenedValue,
child: Padding(
padding: EdgeInsets.only(top: _tweenedValue * 20),
child: child
)
)
Use the Hero
widget to animate widgets between screens.
Wrap the widget in a Hero
widget.
child: Hero(
child: Image.asset()
)
Add the required tag
field as a unique string.
child: Hero(
tag: 'location-img-${trip.img}',
child: Image.asset()
)
In the moment you repeat the same structure in a different screen, with the Hero
widget and the tag
field, animates the transition between pages.
Mixins provide an interface to include additional functionalities.
As an example, a User
class might define a method so that every instance can invoke the function.
void main() {
User().sayHello();
}
class User {
void sayHello() {
print('Hello');
}
}
A class extending User
has access to the same method.
void main() {
PoliteUser().sayHello();
}
class PoliteUser extends User {}
For additional functionalities you can specify attributes and methods in the extending class or rely on a mixin.
Define the mixin.
mixin Goodbye {
void sayGoodbye() {
print('Goodbye');
}
}
Add the functionality of the mixin to the extending class.
void main() {
PoliteUser().sayHello();
PoliteUser().sayGoodbye();
}
class PoliteUser extends User with Goodbye {}
An animation controller is how you create explicit animations. The feature is implemented in the context of the heart widget, in order to:
-
change the color of the icon as the button is toggled
-
change the size of the icon as the button is pressed — in the next section
Define a variable to store the controller.
late AnimationController _controller;
In the initState
lifecycle method initialize the controller as an instance of AnimationController
.
@override
void initState() {
_controller = AnimationController()
}
The object requires a duration
and vsync
field. The duration refers to an instance of the Duration
object.
AnimationController(
duration: const Duration(milliseconds: 300),
)
vsync
refers to a ticker. To gain access to this ticker include the TickerProviderStateMixin
mixin.
class _HeartState extends State<Heart> with TickerProviderStateMixin {}
With the mixin you gain access to the vsync value through the this
keyword.
AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this
);
Once you initialize the controller run the animation with the forward
function.
_controller.forward();
You can listen to the changing value through the addListener
method.
_controller.addListener(() {
print(_controller.value)
});
To update the color refer to the controller through a ColorTween
widget.
Define a variable for the color animation.
late Animation _colorAnimation;
As you instantiate the controller initialize the color animation as well.
With ColorTween
specify the start and end values for the animation.
ColorTween(begin: Colors.grey[400], end: Colors.red)
To obtain the animation chain the animate
function and refer to the controller.
_colorAnimation = ColorTween(begin: Colors.grey[400], end: Colors.red).animate(_controller);
In this manner _colorAnimation.value
describes a color in the selected range, animated through the controller.
Icon(
Icons.favorite,
color: _colorAnimation.value,
),
To update the widget, however, you need to update the state.
_controller.addListener(() {
setState(() {});
});
Alternatively, create an AnimatedBuilder
widget.
AnimatedBuilder()
In this widget refer to the controller through the animation
field.
AnimatedBuilder(
animation: _controller,
)
Through the builder
field describe a function which returns the desired, changing widget tree.
AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, _) {
return IconButton(
icon: Icon(
icons.faorite,
color: _colorAnimation.value
),
onPressed: () {
_controller.forwards();
}
)
}
)
The widget automatically updates the value of the affected animations.
forward
runs the animation forward, meaning the specific animation updates the color from grey to red. As the button is pressed once more, however, the color does not revert to its initial value. To achieve this tap into the status of the animation.
Create a variable to keep track of the button's state.
bool isFavorite = false;
In the addStatusListener method the controller passes the status as the animation is run.
_controller.addStatusListener((status) {}
Use the value to update the boolean considering two specific statuses: completed and dismissed.
if(status == AnimationStatus.completed) {
setState(() {
isFavorite = true;
});
} else if(status == AnimationStatus.dismissed) {
setState(() {
isFavorite = false;
});
}
With the updated boolean run the animation forward or backwards.
onPressed: () {
if(isFavorite) {
_controller.reverse();
} else {
_controller.forward();
}
}
As the widget is destroyed dispose of the animation controller to let flutter free the necessary resources.
@override void dispose() { super.dispose(); _controller.dispose(); }
Invoke the dispose method in the dispose lifecycle function.
ColorTween
interpolates between a start and end value, changing the color of the icon between grey and red.
For the size the idea is to interpolate between three values, effectively expanding the icon for a brief amount of time.
Create a variable to keep track of the animation.
late Animation _sizeAnimation;
As the controller is defined store in the variable a an instance of TweenSequence
, again animated by the controller.
TweenSequence().animate(_controller);
TweenSequence
receives a list of TweenSequenceItem
, themselves responsible for interpolating between multiple values.
TweenSequence([
TweenSequenceItem(),
TweenSequenceItem(),
])
Each item specifies a tween and weight.
tween
works similarly to the color tween, instantiating a Tween
with a begin and end value.
TweenSequenceItem(
tween: Tween(begin: 1, end: 1.5)
)
weight
describes the percentage of the animation duration. By assigning the same weight to two items the duration is split equally between the two steps.
TweenSequenceItem(
tween: Tween(begin: 1, end: 1.5),
weight: 50,
),
TweenSequenceItem(
tween: Tween(begin: 1.5, end: 1),
weight: 50,
)
With this setup _sizeAnimation
is updated similarly to _colorAnimation
. Tap in the changing value in the AnimatedBuilder
widget to modify the size of the icon.
Icon(
Icons.favorite,
color: _colorAnimation.value,
size: _sizeAnimation.value
),
The course uses the value directly in the size
field of the icon widget.
Icon(
Icons.favorite,
color: _colorAnimation.value,
size: _sizeAnimation.value
),
The scale is however updated from the top left corner. To have the icon scale from its center wrap the widget in a Transform.scale
widget and use the scale
field instead.
Transform.scale(
scale: _sizeAnimation.value,
child: Icon()
)
Specify a curve to modify the pace of an animation, its timing function.
With built-in animations add a curve directly in the animating widget.
TweenAnimationBuilder(
curve: Curves.easeIn,
)
For custom animations the process is slightly more elaborate, and relies on creating a curved animation inheriting from the controller.
Define a variable for the curved animation.
late Animation<double> _curve;
With the controller initialize the curve with the CurvedAnimation
object.
_curve = CurvedAnimation();
In the object specify the curve and the parent controller.
CurvedAnimation(parent: _controller, curve: Curves.slowMiddle)
With this setup animate the different tween through the curve.
-.animate(_controller);
+.animate(_curve);
The controller is already incorporated in the curved animation.
To illustrate how items are animated in a list I created a small playground in animatedListSample.dart
.
With a list of widgets listTiles
the ListView
widget creates multiple widgets through the itemBuilder
function.
ListView.builder(
itemCount: listTiles.length,
itemBuilder: (context, index) {
return listTiles[index];
}
),
To animate the items replace the widget with an AnimatedList
.
AnimatedList()
In the instance specify a key which is of type AnimatedListState
.
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
// later
AnimatedList(
key: _listKey
)
Instead of itemCount
describe the number of initial items with initialItemCount
.
AnimatedList(
key: _listKey
initialItemCount: listTiles.length,
)
In the itemBuilder
function structure a function which receives three arguments: the context, index as well as the animation object.
AnimatedList(
key: _listKey
initialItemCount: listTiles.length,
itemBuilder: (context, index, animation) {}
)
In the body of the function return a widget which benefits from the animation, for instance a SlideTransition
widget.
itemBuilder: (context, index, animation) {
return SlideTransition();
}
SlideTransition
wraps around the widget and animates its position through the matching field.
SlideTransition(
child: listTiles[index],
position: animation.drive(_offsetIn)
)
animation.drive
receives an instance of a Tween
, detailing the start and end value.
final Tween<Offset> _offsetIn = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));
AnimatedList
is not enough to run the animation. It is necessary to instruct Flutter of the changing state through the global key.
In the moment you add new items, for instance.
listTiles.add(listTile);
Update the key with the insertItem
method available in the currentState
object.
_listKey.currentState?.insertItem(listTiles.length - 1);
As you remove items
listTiles.remove(listTile);
Use the key with removeItem
method, this time specifying not only the index of the item, but a widget for how the widget is removed.
_listKey.currentState?.removeItem(0, (context, animation) {
return SlideTransition(
child: listTile,
position: animation.drive(_offsetOut)
);
});
When items are added in the initState
lifecycle method the animation is not run. To wait for the build
method wrap the instruction in a specific callback.
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) {
// add and animate
}
}
In the actual application the logic of the playground is included to only animate existing items. The state of the key is therefore updated in the _addTrips
function.
_tripTiles.add(_buildTile(trip));
_listKey.currentState?.insertItem(_tripTiles.length - 1);
To stagger the animation the course relies on dart's concept of futures, promises to run logic in sequence. The idea is to chain multiple futures so that Flutter waits before adding a new item and updating the key.
Define a starting, empty promise.
Future ft = Future(() {});
In the loop update the future with ft.then()
, which describes the promise itself.
ft = ft.then((_) {
}
With Future.delayed
the promise resolves after the specified duration, meaning the iteration waits for the specific amount of time.
return Future.delayed(const Duration(milliseconds: 200), () {});