-
follow the instraction - https://flutter.dev/docs/get-started/install/macos (you can skip iOS setup).
-
For IDE I recommand to use VSCode - https://code.visualstudio.com/
- Download flutter extention: in VSCode go to view -> Extainsions -> search for flutter -> install it or shift command x -> search for flutter and install it.
- Also install mateirial Icon theme - shift command x - search for flutter and install it.
-
For phone emolator we will use android studio :
- Install android studio(so you can use their emolator) - https://developer.android.com/studio/?gclid=EAIaIQobChMIpo7-tIH75QIVGODtCh127QL1EAAYASAAEgLqi_D_BwE
-
now try to create new project -
flutter create --androidx [pick a name for your app]
By now ,you should be able to run flutter doctor
on this project and succeed.
You won't need the project you created , you will use this code base as start point
More Info and examples for flutter: A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter, view online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.
option shift F -> format your code
control shift R -> helps you when you need to create/change widgets
All along doing this workshop you'll have to use emulator - I recomand to use
Pixel XL API 28 with Android 9.0(Google APIs)
(all this project has been tested with it)
- Prerequisites
- Introduction
- Step One - Login And Register Page
- Step Two - Home Screen
- Step Three - Add Button and Add Item Screen
- Step Four - App Drawer
- Step Five - Mangeing Itemsr
- Bonus Part: Step Six - Like Button
if you still didn't create new flutter project then run flutter create --androidx [pick a name for your app]
after it finish, try to run your new app, in vscode you can click on debug
-> start without debuging
(you can start it from the terminal by runing the command flutter run
).
First we will have to do some setup , so our project will be ready to develop.
Our app will be using google firebase
for authantication, file storage and database. We won't be learning firebase
during this workshop, you'll get code snippets for already done integration.
Usually when you start to develop new app (ios or android), you get register as apple developer and register for a Google Play Developer account. Evantually you will register your app in apple store and google play store. During this workshop you wont do it , its already been enabled for you 😏
so lets start 💪
First step will be to edit pubspec.yaml
file.
(Every pub package needs some metadata so it can specify its dependencies.)
this file is like package.json in node.
You about to add some pub packges in the near future so get ready.
add this piece of code to pubspec.yaml
file, under this lines :
dependencies:
flutter:
sdk: flutter
The indention is realy importent here!
keep all the packges in same space line as flutter
packege is.
provider: ^3.1.0
intl: ^0.16.0
http: ^0.12.0+2
shared_preferences: ^0.5.4+3
image_picker: ^0.6.2+1
path_provider: ^1.4.4
firebase_storage: ^3.1.0
Lets create some folders, to make our future app arrageable and accsesible.
our main workplace folder will be lib
.
we could create all of our dart
files under it, but we will prefer to make some sub folders, to make it easier and cleaner.
So lets make screens
, widgets
, models
and providers
folders under lib
folder.
any dart file we will create in our project will be in snake-case
now under models
folder we need to create http_exception.dart
file and copy this code inside :
class HttpException implements Exception{
final String message;
HttpException(this.message);
@override
String toString() {
return message;
}
}
Also under providers
folder , we need to create auth.dart
which will have our Login
, Signin
and Logout
logic:
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/http_exception.dart';
class Auth with ChangeNotifier {
String _token;
DateTime _expiryDate;
String _userId;
Timer _authTimer;
bool get isAuth {
return token != null;
}
String get token {
if (_expiryDate != null &&
_expiryDate.isAfter(DateTime.now()) &&
_token != null) {
return _token;
} else {
return null;
}
}
String get userId {
return _userId;
}
Future<void> _authenticate(
String email, String password, String urlSegment) async {
final url =
'https://identitytoolkit.googleapis.com/v1/accounts:$urlSegment?key=AIzaSyCngoLTNTKO-D8eX3D_-9lrTNNzbPr5Gvk';
try {
final response = await http.post(
url,
body: json.encode(
{
'email': email,
'password': password,
'returnSecureToken': true,
},
),
);
final responseData = json.decode(response.body);
if (responseData['error'] != null) {
throw HttpException(responseData['error']['message']);
}
_token = responseData['idToken'];
_userId = responseData['localId'];
_expiryDate = DateTime.now().add(
Duration(
seconds: int.parse(
responseData['expiresIn'],
),
),
);
_autoLogout();
notifyListeners();
final prefs = await SharedPreferences.getInstance();
final userData = json.encode(
{
'token': _token,
'userId': _userId,
'expiryDate': _expiryDate.toIso8601String()
},
);
prefs.setString('userData', userData);
} catch (error) {
throw error;
}
}
Future<void> signup(String email, String password) async {
return _authenticate(email, password, 'signUp');
}
Future<void> login(String email, String password) async {
return _authenticate(email, password, 'signInWithPassword');
}
Future<bool> tryAutoLogin() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey('userData')) {
return false;
} else {
final extractedUserData =
json.decode(prefs.getString('userData')) as Map<String, Object>;
final expiryDate = DateTime.parse(extractedUserData['expiryDate']);
if (expiryDate.isBefore(DateTime.now())) {
return false;
} else {
_token = extractedUserData['token'];
_userId = extractedUserData['userId'];
_expiryDate = expiryDate;
notifyListeners();
_autoLogout();
return true;
}
}
}
Future<void> logout() async{
_token = null;
_userId = null;
_expiryDate = null;
if (_authTimer != null) {
_authTimer.cancel();
_authTimer = null;
}
notifyListeners();
final prefs = await SharedPreferences.getInstance();
prefs.remove('userData');
prefs.clear();
}
void _autoLogout() {
if (_authTimer != null) {
_authTimer.cancel();
} else {
final timeToExpiry = _expiryDate.difference(DateTime.now()).inSeconds;
_authTimer = Timer(Duration(seconds: timeToExpiry), logout);
}
}
}
Now we will change our main.dart
file, this file controls our app theme
, app font
, page lending and our app routs
. Its basiclly controlls our entire app.
Until now we had deafult config from what flutter create
made for us,it's time to change it.
we will change MyApp
class
we can delete MyHomePage
and _MyHomePageState
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Auth(),
),
],
child: Consumer<Auth>(
builder: (ctx, auth, _) => MaterialApp(
title: 'Wix Flutter Workshop',
theme: ThemeData(
primarySwatch: Colors.blue,
accentColor: Colors.orange,
),
home: auth.isAuth
? ItemsOverviewScreen()
: FutureBuilder(
future: auth.tryAutoLogin(),
builder: (context, authResultSnapshot) =>
authResultSnapshot.connectionState ==
ConnectionState.waiting
? SplashScreen()
: AuthScreen(),
),
),
),
);
}
}
Please make sure you have both of this lines in top of the page:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
You'll gonna see this import in almost every file in our app
import 'package:flutter/material.dart';
Its a Flutter widgets implementing Material Design package. material.dart package gives us accsses to lots of ready to use widgets (both for ios and android)
Now we have some missing widgets and screens -> lets create them.
Under screens
folder, create items_overview_screen.dart
- it will be our first
StatefulWidget
f you decided to use vsCode, start writing st
and it will suggest you whether to create statefull
widget or stateless
widget, pick statefull
.
name the class ItemsOverviewScreen
.
any dart class we will create in our project will be in camel-case
make sure to import material.dart
, we will need to use it's widgets.
instade of returning container
widget , we will return [Scaffold
] widget (https://api.flutter.dev/flutter/material/Scaffold-class.html)
(This widget provides APIs for showing drawers
, snack bars
, and bottom sheets
)
for now we will return scaffold with appBar
and body
appBar: AppBar(
title: Text('Flutter Workshop'),
),
body
property will be for nowCircularProgressIndicator
, ui widget which we can use for loader for now. (A material design circular progress indicator, which spins to indicate that the application is busy). we will also want ourbody
to be center , therfore we will return theCircularProgressIndicator
insideCenter
widget
```dart
body: Center(
child: CircularProgressIndicator(),
),
```
it suppose to look like this :
return Scaffold(
appBar: AppBar(
title: Text('Flutter Workshop'),
),
body: Center(
child: CircularProgressIndicator(),
),
);
In main.dart
file we can use our new widget. we just need to import it at the top of the page :
import './screens/Items_overview_screen.dart';
Lets create the splash_screen
widget
In screens
folder we need to create splash_screen.dart
It will be stateless
widget.
we will return Scaffold
widget
body
property will beCenter
widget andText
widget inside of it , with ''Loading...'' as text back inmain.dart
we will import it at the top of the page :
import './screens/splash_screen.dart';
splash_screen.dart
import 'package:flutter/material.dart';
class SplashScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Loading...'),
),
);
}
}
Lets create the auth_screen
widget
In screens
folder we need to create auth_screen.dart
It will be StatelessWidget
Don't forget to import 'package:flutter/material.dart'
It will return Scaffold
As you can see , each screen widget returns Scaffold
widget
-
we will use
Stack
widget (This widget is useful if you want to overlap several children in a simple way, for example having some text and an image, overlaid with a gradient and a button attached to the bottom) Inside theStack
widget:- we will have a
Continer
SingleChildScrollView
(is used when we want to enable scrolling over a widget (A box in which a single widget can be scrolled))-
inside
SingleChildScrollView
we will useContiner
as child , and this time we will have to give itheight
andwidth
properties beacuse its inside aStack
. we don't want to give it a fixed size likehight :50, width: 50
it maybe good for our spesific simulator , but we got tons of diffrent phones sizes.we will use
context
to get th simulator size. insideBuild
function we will add this code :dart final deviceSize = MediaQuery.of(context).size;
now we can use `deviceSize` as height and width ```dart height: deviceSize.height, width: deviceSize.width, ```
-
child
propery will be aColumn
widget (A widget that displays its children in a vertical array). -
we will want to center our login/sing in widget, therefoe we will add
mainAxisAlignment
andcrossAxisAlignment
properties to ourColumn
widgewtmainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
-
now lets use
Flexible
widget which evantually will be title for our auth screen (Flexible is a widget that controls how a child of a Row, Column, or Flex flexes.Using a Flexible widget gives a child of a Row, Column, or Flex the flexibility to expand to fill the available space)-
It will have
Container
as a child -
Lets add some style properties to the
Container
widgetmargin: EdgeInsets.only(bottom: 20.0), padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 94.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: Colors.white70, boxShadow: [ BoxShadow( blurRadius: 8, color: Colors.black26, offset: Offset(0, 2), ) ], ),
-
The child widget of
Container
will beText
, with 'Workshop' as text,- Lets add the style property , with
TextStyle
widget inside. - we will use color from our
theme
(which can be found inmain.dart
file), we can get it fromcontext
, like this:color: Theme.of(context).accentTextTheme.title.color
- we also want to add a bit of font style
fontSize: 42, fontFamily: 'Anton', fontWeight: FontWeight.normal,
- Lets add the style property , with
-
-
- we will have a
-
now back to
Column
widget -
children
property is array , lets add anotherFlexible
widget to it. it will have two properties:flex: deviceSize.width > 600 ? 2 : 1, child: AuthCard(),
auth_screen
import 'package:flutter/material.dart';
enum AuthMode { Signup, Login }
class AuthScreen extends StatelessWidget {
static const routeName = '/auth';
@override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.9),
),
),
Container(
child: Container(
height: deviceSize.height,
width: deviceSize.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
child: Container(
margin: EdgeInsets.only(bottom: 20.0),
padding:
EdgeInsets.symmetric(vertical: 8.0, horizontal: 94.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.white70,
boxShadow: [
BoxShadow(
blurRadius: 8,
color: Colors.black26,
offset: Offset(0, 2),
)
],
),
child: Text(
'Workshop',
style: TextStyle(
color: Theme.of(context).accentTextTheme.title.color,
fontSize: 42,
fontFamily: 'Anton',
fontWeight: FontWeight.normal,
),
),
),
),
Flexible(
flex: deviceSize.width > 600 ? 2 : 1,
child: AuthCard(),
),
],
),
),
),
],
),
);
}
}
now we can import auth_screen
in top of main.dart
file
import './screens/auth_screen.dart';
Lets create the AuthButton
widget
In widgets
folder we need to create autn_button.dart
It will be StatelessWidget
we will be passing parameters to the widget :
isLoading
,authMode
and onSubmit
(just like props in react).
for that we will need to create properties in AuthButton
class , and they will be final
(it's a StatelessWidget
, once it renders, his properties won't change under any circumstances).
class AuthButton extends StatelessWidget {
final bool isLoading;
authMode
will be in type ofAuthMode
onSubmit
will be type ofFunction
we will create Constructor function with named parameters
AuthButton(
{@required this.isLoading,
@required this.authMode,
@required this.onSubmit}
);
it can also be written like this
AuthButton(
@required this.isLoading,
@required this.authMode,
@required this.onSubmit);
but then the order of the parameters will be importent, it less convenient while we can enjoy the benefits of named parameters in dart.
AuthButton(false, authMode, () =>{}))
VS
AuthButton(isLoading: false, authMode: authMode, onSubmit: () =>{}))
Inside build method we will return RaisedButton
(A material design "raised button").
-
child
propety will haveText
, it will have to two modes :login
andsign up
Text(authMode == AuthMode.Login ? 'LOGIN' : 'SIGN UP'),
-
onPress
property will beonSubmit
, the function we got in our parameters
time for a bit of styling
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 8.0),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context) .primaryTextTheme.button.color,
We also need to handle the loading stage
Therefore we will need to wrapp RaisedButton
with if else
segment
- if
isLoading
true then returnCircularProgressIndicator
else returnRaisedButton
auth button
import 'package:flutter/material.dart';
import '../screens/auth_screen.dart';
class AuthButton extends StatelessWidget {
final bool isLoading;
final AuthMode authMode;
final Function onSubmit;
AuthButton(
{@required this.isLoading,
@required this.authMode,
@required this.onSubmit});
@override
Widget build(BuildContext context) {
if (isLoading)
return CircularProgressIndicator();
else
return RaisedButton(
child: Text(authMode == AuthMode.Login ? 'LOGIN' : 'SIGN UP'),
onPressed: onSubmit,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 8.0),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.button.color,
);
}
}
Lets create the EmailInput
widget
In inputs
folder we need to create email_input.dart
It will be StatelessWidget
which gets onSaved
function
It will return TextFormField
widget (This is a convenience widget that wraps a TextField widget in a FormField).
decoretion
property we will useInputDecoration
labelText
will be of 'E-Mail'
keyboardType
propetry will beTextInputType.emailAddress
we also want to validate the input we get
validator
property will have this code :validator: (value) { if (value.isEmpty || !value.contains('@')) { return 'Invalid email!'; } },
onSaved
will pass the value toonSave
function we gotonSaved: (value) => onSaved(value));
email_input
import 'package:flutter/material.dart';
class EmailInput extends StatelessWidget {
final Function onSaved;
const EmailInput({@required this.onSaved});
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(labelText: 'E-Mail'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value.isEmpty || !value.contains('@')) {
return 'Invalid email!';
}
},
onSaved: (value) => onSaved(value));
}
}
Lets create the PasswordInput
widget
In inputs
folder we need to create password_input.dart
It will be StatelessWidget
which gets onSaved
function and controller
type of TextEditingController
We will return TextFormField
widget.
decoretion
property we will useInputDecoration
labelText
will be of 'Password'
obscureText
will be truecontroller
will handle the updatingobscureText: true, controller: controller,
we also want to validate the input we get
validator
property will have this code :validator: (value) { if (value.isEmpty || value.length < 5) { return 'Password is too short!'; } },
onSaved
will trigger theonSave
function we got
password_input.dart
import 'package:flutter/material.dart';
class PasswordInput extends StatelessWidget {
final Function onSaved;
final TextEditingController controller;
const PasswordInput({@required this.onSaved,@required this.controller});
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
controller: controller,
validator: (value) {
if (value.isEmpty || value.length < 5) {
return 'Password is too short!';
}
},
onSaved: onSaved,
);
}
}
now we can finnaly build our AuthCard
widget 🙌
Lets create the AuthCard
widget
In widgets
folder we need to create auth_card.dart
It will be StatefulWidget
- make sure to import
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart';
dart:io is a library that allows you to work with files, directories, sockets, processes, HTTP servers and clients, and more. provider (A mixture between dependency injection (DI) and state management, built with widgets for widgets) we will use it as our state managment in our project
Under class AuthCard
add this line of code:
const AuthCard({
Key key,
}) : super(key: key);
we need to add with SingleTickerProviderStateMixin
to _AuthCardState
so we could to enable useage of animationController
in state
part
- lets add some properties for our class to enable useage of our widgets:
final GlobalKey<FormState> _formKey = GlobalKey(); AuthMode _authMode = AuthMode.Login; Map<String, String> _authData = { 'email': '', 'password': '', }; var _isLoading = false; final _passwordController = TextEditingController(); AnimationController _controller; Animation<Offset> _slideAnimation; Animation<double> _opacityAnimation;
- copy this snippet of code , look for the right place where to add
authButton
,emailInput
andpasswordInput
, you will have to pass the right parameters
snippet
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(
milliseconds: 300,
),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, -1.5),
end: Offset(0, 0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
),
);
_opacityAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(
'An error occured',
),
content: Text(message),
actions: <Widget>[
FlatButton(
child: Text('Okay'),
onPressed: () {
Navigator.of(ctx).pop();
},
)
],
),
);
}
Future<void> _submit() async {
if (!_formKey.currentState.validate()) {
// Invalid!
return;
}
_formKey.currentState.save();
setState(() {
_isLoading = true;
});
try {
if (_authMode == AuthMode.Login) {
// Log user in
await Provider.of<Auth>(context, listen: false).login(
_authData['email'],
_authData['password'],
);
} else {
// Sign user up
await Provider.of<Auth>(context, listen: false).signup(
_authData['email'],
_authData['password'],
);
}
} on HttpException catch (error) {
var errorMessage = 'Authenticate failed';
if (error.toString().contains('EMAIL_EXISTS')) {
errorMessage = 'This email address already in use';
} else if (error.toString().contains('INVALID_EMAIL')) {
errorMessage = 'This is not a valid email address';
} else if (error.toString().contains('WEAK_PASSWORD')) {
errorMessage = 'This password is too weak';
} else if (error.toString().contains('EMAIL_NOT_FOUND')) {
errorMessage = 'Could not find a user with that email';
} else if (error.toString().contains('INVALID_PASSWORD')) {
errorMessage = 'This is not a valid password address';
}
_showErrorDialog(errorMessage);
} catch (error) {
const errorMessage = 'Could not authenticate you. Please try again later';
_showErrorDialog(errorMessage);
}
setState(() {
_isLoading = false;
});
}
void _switchAuthMode() {
if (_authMode == AuthMode.Login) {
setState(() {
_authMode = AuthMode.Signup;
});
_controller.forward();
} else {
setState(() {
_authMode = AuthMode.Login;
});
_controller.reverse();
}
}
void _onSaveField(String key, String value) {
_authData[key] = value;
}
@override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
elevation: 8.0,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeIn,
height: _authMode == AuthMode.Signup ? 320 : 260,
constraints:
BoxConstraints(minHeight: _authMode == AuthMode.Signup ? 320 : 260),
width: deviceSize.width * 0.75,
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
##### add EmailInput ######,
##### add PasswordInput ######,
AnimatedContainer(
constraints: BoxConstraints(
minHeight: _authMode == AuthMode.Signup ? 60 : 0,
maxHeight: _authMode == AuthMode.Signup ? 120 : 0,
),
duration: Duration(milliseconds: 300),
curve: Curves.easeIn,
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: TextFormField(
enabled: _authMode == AuthMode.Signup,
decoration:
InputDecoration(labelText: 'Confirm Password'),
obscureText: true,
validator: _authMode == AuthMode.Signup
? (value) {
if (value != _passwordController.text) {
return 'Passwords do not match!';
}
}
: null,
),
),
),
),
SizedBox(
height: 20,
),
##### add AuthButton ######,
FlatButton(
child: Text(
'${_authMode == AuthMode.Login ? 'SIGNUP' : 'LOGIN'} INSTEAD'),
onPressed: _switchAuthMode,
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 4),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
textColor: Theme.of(context).primaryColor,
),
],
),
),
),
),
);
}
- for the
onSave
function you will have to pass value and the right key , here is an example :
(value) => _onSaveField('password', value)
auth_card.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wix_flutter_workshop/providers/auth.dart';
import 'package:wix_flutter_workshop/screens/auth_screen.dart';
import 'package:wix_flutter_workshop/widgets/auth_button.dart';
import 'package:wix_flutter_workshop/widgets/inputs/email_input.dart';
import 'package:wix_flutter_workshop/widgets/inputs/password_input.dart';
import '../models/http_exception.dart';
class AuthCard extends StatefulWidget {
const AuthCard({
Key key,
}) : super(key: key);
@override
_AuthCardState createState() => _AuthCardState();
}
class _AuthCardState extends State<AuthCard>
with SingleTickerProviderStateMixin {
final GlobalKey<FormState> _formKey = GlobalKey();
AuthMode _authMode = AuthMode.Login;
Map<String, String> _authData = {
'email': '',
'password': '',
};
var _isLoading = false;
final _passwordController = TextEditingController();
AnimationController _controller;
Animation<Offset> _slideAnimation;
Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(
milliseconds: 300,
),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, -1.5),
end: Offset(0, 0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
),
);
_opacityAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(
'An error occured',
),
content: Text(message),
actions: <Widget>[
FlatButton(
child: Text('Okay'),
onPressed: () {
Navigator.of(ctx).pop();
},
)
],
),
);
}
Future<void> _submit() async {
if (!_formKey.currentState.validate()) {
// Invalid!
return;
}
_formKey.currentState.save();
setState(() {
_isLoading = true;
});
try {
if (_authMode == AuthMode.Login) {
// Log user in
await Provider.of<Auth>(context, listen: false).login(
_authData['email'],
_authData['password'],
);
} else {
// Sign user up
await Provider.of<Auth>(context, listen: false).signup(
_authData['email'],
_authData['password'],
);
}
} on HttpException catch (error) {
var errorMessage = 'Authenticate failed';
if (error.toString().contains('EMAIL_EXISTS')) {
errorMessage = 'This email address already in use';
} else if (error.toString().contains('INVALID_EMAIL')) {
errorMessage = 'This is not a valid email address';
} else if (error.toString().contains('WEAK_PASSWORD')) {
errorMessage = 'This password is too weak';
} else if (error.toString().contains('EMAIL_NOT_FOUND')) {
errorMessage = 'Could not find a user with that email';
} else if (error.toString().contains('INVALID_PASSWORD')) {
errorMessage = 'This is not a valid password address';
}
_showErrorDialog(errorMessage);
} catch (error) {
const errorMessage = 'Could not authenticate you. Please try again later';
_showErrorDialog(errorMessage);
}
setState(() {
_isLoading = false;
});
}
void _switchAuthMode() {
if (_authMode == AuthMode.Login) {
setState(() {
_authMode = AuthMode.Signup;
});
_controller.forward();
} else {
setState(() {
_authMode = AuthMode.Login;
});
_controller.reverse();
}
}
void _onSaveField(String key, String value) {
_authData[key] = value;
}
@override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
elevation: 8.0,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeIn,
height: _authMode == AuthMode.Signup ? 320 : 260,
constraints:
BoxConstraints(minHeight: _authMode == AuthMode.Signup ? 320 : 260),
width: deviceSize.width * 0.75,
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
EmailInput(
onSaved: (value) => _onSaveField('email', value),
),
PasswordInput(
onSaved: (value) => _onSaveField('password', value),
controller: _passwordController),
AnimatedContainer(
constraints: BoxConstraints(
minHeight: _authMode == AuthMode.Signup ? 60 : 0,
maxHeight: _authMode == AuthMode.Signup ? 120 : 0,
),
duration: Duration(milliseconds: 300),
curve: Curves.easeIn,
child: SlideTransition(
position: _slideAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: TextFormField(
enabled: _authMode == AuthMode.Signup,
decoration:
InputDecoration(labelText: 'Confirm Password'),
obscureText: true,
validator: _authMode == AuthMode.Signup
? (value) {
if (value != _passwordController.text) {
return 'Passwords do not match!';
}
}
: null,
),
),
),
),
SizedBox(
height: 20,
),
AuthButton(
isLoading: _isLoading,
authMode: _authMode,
onSubmit: _submit,
),
FlatButton(
child: Text(
'${_authMode == AuthMode.Login ? 'SIGNUP' : 'LOGIN'} INSTEAD'),
onPressed: _switchAuthMode,
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 4),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
textColor: Theme.of(context).primaryColor,
),
],
),
),
),
),
);
}
}
make sure to import AuthCard
in auth_screen
.
now you have everything ready for try login or sign in! 🙌
lets start creating our home screen
- we need to create 2 new providers :
item
anditems
- we need to refactor our
ItemsOverviewScreen
- we need to create new widget
ItemsGrid
Lets create the Items
and Item
providers
In providers
folder we need to create items.dart
and item.dart
they will be Providers
item
provider will represent our item data, whom will be save in our server
it will be class
that uses ChangeNotifier
mixin in dart you do it with with
keyword
class Item with ChangeNotifier
it will have those properties:
final String id;
final String title;
final String description;
final double price;
final File image;
bool isFavorite;
make sure to import foundation.dart
and import material.dart
from flutter
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:io';
we will need to create a constractor function with named paramters
Item({
@required this.id,
@required this.title,
@required this.description,
@required this.price,
this.imagePath,
this.isFavorite = false,
});
item.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Item with ChangeNotifier {
final String id;
final String title;
final String description;
final double price;
final File image;
bool isFavorite;
Item({
@required this.id,
@required this.title,
@required this.description,
@required this.price,
this.image,
this.isFavorite = false,
});
}
items
provider will hold all of our CRUD
(create, read, update, delete) logic against our server
we will be useing auth
and user
that we got when we loged in, that way we could have permissions over the each item
copy the code from here ⬇️
items.dart
import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import '../providers/item.dart';
class Items with ChangeNotifier {
final String baseUrl = 'https://flutter-workshop-eef86.firebaseio.com';
List<Item> _items = [];
final String authToken;
final String userId;
Items(this.authToken, this.userId, this._items);
List<Item> get items {
return [..._items];
}
Item findById(String id) {
return _items.firstWhere((item) => item.id == id);
}
Future<void> fetchAndSetItems([bool filterByUser = false]) async {
final filterUrl =
filterByUser ? 'orderBy="creatorId"&equalTo="$userId"' : '';
var url = '$baseUrl/items.json?auth=$authToken&$filterUrl';
print(authToken);
try {
final response = await http.get(url);
final extractedData = json.decode(response.body) as Map<String, dynamic>;
if (extractedData == null) {
return;
} else {
final List<Item> loadedItems = [];
extractedData.forEach((itemId, itemData) {
loadedItems.add(Item(
id: itemId,
title: itemData['title'],
description: itemData['description'],
price: itemData['price'],
isFavorite: false,
image: itemData['image'],
));
});
_items = loadedItems;
notifyListeners();
}
} catch (error) {
throw error;
}
}
}
now add this lines of code to main.dart
file inside providers
array
ChangeNotifierProxyProvider<Auth, Items>(
builder: (ctx, auth, prevpItems) => Items(
auth.token,
auth.userId,
prevpItems == null ? [] : prevpItems.items,
),
),
Don't forget to import import items.dart
!
until now it was just a screen widget that renders loader, now we will make it show our items
- becasue we now going to work against the server
- we will need to handle
Future
(async code) , lets start by handling theinit
andload
stage inItemsOverviewScreen
create _isInit
and _isLoading
vars in _ProductsOverviewScreenState
class , both should be in initial as false
- add
didChangeDependencies
function (Called when a dependency of this State object changes) It will be handling updates of screen when we will get the items from the server We will callfetchAndSetItems
(function we have fromitems
provider) to get the products from the server and when it will finish , we will update the state
var _isInit = false;
var _isLoading = false;
@override
void didChangeDependencies() {
if (!_isInit) {
_isLoading = true;
Provider.of<Items>(context).fetchAndSetItems().then((_) {
setState(() {
_isLoading = false;
});
});
}
_isInit = true;
super.didChangeDependencies();
}
Lets refactor body
in Scaffold
widget :
body: _isLoading
? Center(
child: CircularProgressIndicator(),
)
: ItemsGrid(),
Now we are geting error, it beacuse ItemsGrid
widget is not exist, we need to create it
items_overview_screen.dart
class ItemsOverviewScreen extends StatefulWidget {
@override
_ProductsOverviewScreenState createState() => _ProductsOverviewScreenState();
}
class _ProductsOverviewScreenState extends State<ItemsOverviewScreen> {
var _isInit = false;
var _isLoading = false;
@override
void didChangeDependencies() {
if (!_isInit) {
_isLoading = true;
Provider.of<Items>(context).fetchAndSetItems().then((_) {
setState(() {
_isLoading = false;
});
});
}
_isInit = true;
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Workshop'),
),
body: _isLoading
? Center(
child: CircularProgressIndicator(),
)
: ItemsGrid(),
);
}
}
Lets create ItemsGrid
widget
In widgets
folder we need to create items_grid.dart
It will be StatelessWidget
Inside build function , we need to get our items from the Items
provider context
final items = Provider.of<Items>(context).items;
This time we will use GridView
(A scrollable, 2D array of widgets), it will take care of the layout for us.
we will use it in it's builder way :
-
itemCount
property will have how many items there are -
gridDelegate
property (delegate that controls the layout of the children within the GridView) we will passSliverGridDelegateWithFixedCrossAxisCount
-
itemBuilder
property we will need to loop over items and returnItemWidget
-
padding
property will get EdgeInsets.all(10.0)GridView.builder( padding: const EdgeInsets.all(10.0), itemCount: items.length, itemBuilder: (ctx, i) => ChangeNotifierProvider.value( value: items[i], child: Container( child: ItemWidget(), ), ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 3 / 2, crossAxisSpacing: 10, mainAxisSpacing: 10, ), );
-
we are now geting error beacuse
ItemWidget
is not exist -> so we need to create it
items_grid.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../widgets/item_widget.dart';
import '../providers/items.dart';
class ItemsGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
final items = Provider.of<Items>(context).items;
return GridView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: items.length,
itemBuilder: (ctx, i) => ChangeNotifierProvider.value(
value: items[i],
child: Container(
child: ItemWidget(),
),
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3 / 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
);
}
}
Lets create ItemWidget
widget
In widgets
folder we need to create item_widget.dart
It will be StatelessWidget
we will need to use Assets
so we could use Image as placeholder -> lets add import in pubspec.yaml
file
assets:
- assets/images/wix-logo.jpg
In build function ,we will get our item from Item
provider
final item = Provider.of<Item>(context, listen: false);
we will use now ClipRRect
(clips its child using a rounded rectangle, similar to ClipOval
and ClipPath
)
-
borderRadius
property will be 10 -
child
property will beGridTile
which is part ofGridView
list, (we are usingGridView
in ourItemsWidget
as you know) it will be covered withHero
widget, so we will have a nice hero animation (A widget that marks its child as being a candidate for hero animations)tag
propety will beitem.id
, so it will know which widget should get the hero animation, and we will need to add identical tag for the secondHero
widget the sametag
(widget where the animation trrigers and the widget where is should haapen)
-
child
property will beFadeInImage
widget (An image that shows a placeholder image while the target image is loading, then fades in the new image when it loads) -
placeholder
proprty will be image from our assets images
AssetImage('assets/images/wix-logo.jpg')
-
image
propetry will useFileImage
widget ,it will load the image asFile
widget (fromdart:io
) -
fit
propetry will beBoxFit.cover
-
fotter
will beGridTileBar
GridTileBar(backgroundColor: Colors.black87,
title: Text(
item.title,
textAlign: TextAlign.center,
),
)
now we can import ItemWidget
in ItemsGrid
item_widget.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/item.dart';
class ItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final item = Provider.of<Item>(context, listen: false);
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: GridTile(
child: GestureDetector(
onTap: () {
},
child: Hero(
tag: item.id,
child: FadeInImage(
placeholder: AssetImage('assets/images/wix-logo.jpg'),
image: FileImage(item.image),
fit: BoxFit.cover,
),
),
),
footer: GridTileBar(
backgroundColor: Colors.black87,
title: Text(
item.title,
textAlign: TextAlign.center,
),
),
),
);
}
}
Lets create ItemDetailScreen
widget
In screens
folder we need to create item_detail_screen.dart
It will be StatelessWidget
It will show more dutails about the item
- bigger image
- details about the item
add route to the file , that way we could approach it
static const routeName = '/item-detail';
we will need to get the item id somehow so we could be able to show the right item data -> we will use context
for it
when you navigate from ItemOverviewScreen
(acutally ItemWidget
inside of it) by clicking on the item , you can pass arguments
to ItemDetailScreen
that way we will be able to use them and get the right item data
we will pass the item Id
lets add to onTap
function in ItemWidget
which will pass argumants
- item.id
inside:
Navigator.of(context).pushNamed(
ItemDetailScreen.routeName,
arguments: item.id,
);
Now, we will add to ItemDetailScreen
a call to the ModalRoute
, so it would be able to get the itemId
from the arguments, then we'll use it to get the right item from Items Provider
final itemId = ModalRoute.of(context).settings.arguments as String;
final loadedItem = Provider.of<Items>(
context,
listen: false,
).findById(itemId);
now that we got the infrastructure ready, lets add our ui for screen detail widget
It will return Scaffold
It will contain our second part of the Hero
animation
we will have a new widget - CustomScrollView
(A ScrollView that creates custom scroll effects using slivers) for a cool scroll
slivers
property will contain-
SliverAppBar
(A material design app bar that integrates with a CustomScrollView).expandedHeight
will be 300pinned
will be trueflexibleSpace
will beFlexibleSpaceBar
title
will be title of the itembackground
will be containing ourHero
widgettag
will be with the sametag
as inItemWidget
(item id)child
will beImage.file
- we will get the path to file from
loadedItem.image.path
and withfit
property asBoxFit.cover
- we will get the path to file from
-
SliverList
(A sliver that places multiple box children in a linear array along the main axis) will contain more details about the itemdelegate
property will be SilverChildListDelegate (A delegate that supplies children for slivers using an explicit list)
-
UI
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: 300,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(loadedItem.title),
background: Hero(
tag: loadedItem.id,
child: Image.file(
File(
loadedItem.image.path,
),
fit: BoxFit.cover,
),
),
),
),
SliverList(
delegate: SliverChildListDelegate(
[
SizedBox(
height: 10,
),
Text(
'\$${loadedItem.price}',
style: TextStyle(
color: Colors.grey,
fontSize: 20,
),
textAlign: TextAlign.center,
),
SizedBox(
height: 10,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10),
width: double.infinity,
child: Text(
loadedItem.description,
textAlign: TextAlign.center,
softWrap: true,
),
),
SizedBox(
height: 800,
),
],
),
),
],
),
);
thats it , now it suppose to work - try by clicking on the item!
item_detail_screen.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/items.dart';
class ItemDetailScreen extends StatelessWidget {
static const routeName = '/item-detail';
@override
Widget build(BuildContext context) {
final itemId = ModalRoute.of(context).settings.arguments as String;
final loadedItem = Provider.of<Items>(
context,
listen: false,
).findById(itemId);
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: 300,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(loadedItem.title),
background: Hero(
tag: loadedItem.id,
child: Image.file(
File(
loadedItem.image.path,
),
fit: BoxFit.cover,
),
),
),
),
SliverList(
delegate: SliverChildListDelegate(
[
SizedBox(
height: 10,
),
Text(
'\$${loadedItem.price}',
style: TextStyle(
color: Colors.grey,
fontSize: 20,
),
textAlign: TextAlign.center,
),
SizedBox(
height: 10,
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10),
width: double.infinity,
child: Text(
loadedItem.description,
textAlign: TextAlign.center,
softWrap: true,
),
),
SizedBox(
height: 800,
),
],
),
),
],
),
);
}
}
Lets add add button to our main screen ItemsOverviewScreen
In Scaffold
we need to add another property floatingActionButton
, inside of it we will use FloatingActionButton
widget
- we will have in
onPressed
property function which will have navigation to our new widgetaddItemScreen
widget
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed(AddItemScreen.routeName);
},
tooltip: 'Add Item',
child: const Icon(Icons.add),
),
Lets create AddItemScreen
widget
In screens
folder we need to create add_item_screen.dart
It will be StatefulWidget
It will show more dutails about the item
- bigger image
- details about the item
first lets create route to addItemScreen
screen
static const routeName = '/add-item';
and we need to add this rout to the routs in main.dart
file
AddItemScreen.routeName: (ctx) => AddItemScreen(),
Now go to items.dart
and add the code below , it would be usefull later
import 'package:path/path.dart' as path;
...
...
...
Future<void> addItem(Item item) async {
final url = '$baseUrl/items.json?auth=$authToken';
try {
print(path.basename(item.image.path));
print(path.extension(item.image.path));
final response = await http.post(url,
body: json.encode({
'title': item.title,
'description': item.description,
'price': item.price,
'creatorId': userId,
'image': path.basename(item.image.path)
}));
await uploadPic(item.image);
final newItem = Item(
title: item.title,
description: item.description,
price: item.price,
id: json.decode(response.body)['name'],
image: item.image);
_items.add(newItem);
} catch (error) {
throw error;
}
}
Future<void> uploadPic(File image) async {
String fileName = path.basename(image.path);
StorageReference firebaseStorageRef =
FirebaseStorage.instance.ref().child(fileName);
StorageUploadTask uploadTask = firebaseStorageRef.putFile(image);
uploadTask.onComplete;
}
Now lets start working on AddItemScreen
logic and ui
We will have a Form
widget for inputs which will be handling the input and its validations
Inside _AddItemScreenState
:
we need to create GlobalKey
for the form state (Global keys uniquely identify elements. Global keys provide access to other objects that are associated with those elements, such as BuildContext. For StatefulWidgets, global keys also provide access to State)
we will have init values for the inputs and also empty Item
variable
we will use TextFormField
widget for the simple text inputs
for easy navigation between the inputs will use focusNode
properties so now we need to initate them
Also lets add isLoading
and isInit
variables
final _form = GlobalKey<FormState>();
final _descriptionFocusNode = FocusNode();
final _priceFocusNode = FocusNode();
File _pickedImage;
var _addItem = Item(
id: null,
title: '',
price: 0,
description: '',
image: null,
);
var _initValues = {
'title': '',
'description': '',
'price': '',
'image': '',
};
var _isInit = true;
var _isLoading = false;
Now we can start working on the ui, we will return Scaffold
widget (it will be similar to our other Scaffold
widgets)
return Scaffold(
appBar: AppBar(
title: Text('Add New Item'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.save),
onPressed: _saveForm,
)
],
),
body: _isLoading
? Center(
child: CircularProgressIndicator(),
)
: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _form,
child: ListView(
children: <Widget>[
....
....
....
Now we can start adding the inputs widgets
Form
lets us use onSubmit
function for all the inputs inside of it, therefore we will have in each TextFieldInput
widget onSave
function to handle it self when it got submitted
- each input will get its own
initValue
property from_initValues
variable - each input will have
decoration
property with his title onFieldSubmitted
property will be moving focus between inputs after submiting each onevalidator
property will get input value and check it with our custome validation rules
TextFormField(
initialValue: _initValues['title'],
decoration: InputDecoration(labelText: 'Title'),
textInputAction: TextInputAction.next,
onSaved: (value) => _addItem = Item(
title: value,
id: _addItem.id,
isFavorite: _addItem.isFavorite,
price: _addItem.price,
description: _addItem.description,
image:
_pickedImage != null ? _pickedImage : _addItem,
),
validator: (value) {
if (value.isEmpty) {
return 'Please provide a value';
}
return null;
},
onFieldSubmitted: (_) =>
FocusScope.of(context).requestFocus(_priceFocusNode),
),
Now create also price
and description
textFields by yourself
inputs
TextFormField(
initialValue: _initValues['price'],
decoration: InputDecoration(labelText: 'Price'),
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
focusNode: _priceFocusNode,
onSaved: (value) => _addItem = Item(
title: _addItem.title,
id: _addItem.id,
isFavorite: _addItem.isFavorite,
price: double.parse(value),
description: _addItem.description,
image:
_pickedImage != null ? _pickedImage : _addItem,
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter a price';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number';
}
if (double.parse(value) <= 0) {
return 'Please enter a number greter then zero';
}
return null;
},
onFieldSubmitted: (_) => FocusScope.of(context)
.requestFocus(_descriptionFocusNode),
),
TextFormField(
initialValue: _initValues['description'],
decoration: InputDecoration(labelText: 'Description'),
maxLines: 3,
onSaved: (value) => _addItem = Item(
title: _addItem.title,
id: _addItem.id,
isFavorite: _addItem.isFavorite,
price: _addItem.price,
description: value,
image:
_pickedImage != null ? _pickedImage : _addItem,
),
validator: (value) {
if (value.isEmpty) {
return 'Please provide a description';
}
if (value.length < 10) {
return 'Should be at least 10 charectersling';
}
return null;
},
keyboardType: TextInputType.multiline,
focusNode: _descriptionFocusNode,
),
Lets create ImageInput
widget
In inputs
folder we need to create image_input.dart
It will be Stateful
it will get onSelectImage
function from AddItemScreen
so it would be avialable in the form and will pass back image file
we will use image_picker
package
Lets create takePicture
function , which will take picture with phone camera , and storedImage
variable
File _storedImage;
_takePicture() async {
final imageFile = await ImagePicker.pickImage(
source: ImageSource.camera,
maxWidth: 600,
);
if (imageFile == null) {
return;
}
setState(() {
_storedImage = imageFile;
});
widget.onSelectImage(imageFile);
}
Now add this code for the input ui
onPressed
will trriger _takePicture
function , and then will update AddItemScreen
form
return Row(
children: <Widget>[
Container(
width: 150,
height: 100,
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.grey),
),
child: _storedImage != null
? Image.file(
_storedImage,
fit: BoxFit.cover,
width: double.infinity,
)
: Text(
'No Image Taken',
textAlign: TextAlign.center,
),
alignment: Alignment.center,
),
SizedBox(width: 10),
Expanded(
child: FlatButton.icon(
icon: Icon(Icons.camera),
label: Text('Take picture'),
textColor: Theme.of(context).primaryColor,
onPressed: _takePicture,
),
),
],
);
image_input.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class ImageInput extends StatefulWidget {
final Function onSelectImage;
ImageInput(this.onSelectImage);
@override
_ImageInputState createState() => _ImageInputState();
}
class _ImageInputState extends State<ImageInput> {
File _storedImage;
_takePicture() async {
final imageFile = await ImagePicker.pickImage(
source: ImageSource.camera,
maxWidth: 600,
);
if (imageFile == null) {
return;
}
setState(() {
_storedImage = imageFile;
});
widget.onSelectImage(imageFile);
}
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Container(
width: 150,
height: 100,
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.grey),
),
child: _storedImage != null
? Image.file(
_storedImage,
fit: BoxFit.cover,
width: double.infinity,
)
: Text(
'No Image Taken',
textAlign: TextAlign.center,
),
alignment: Alignment.center,
),
SizedBox(width: 10),
Expanded(
child: FlatButton.icon(
icon: Icon(Icons.camera),
label: Text('Take picture'),
textColor: Theme.of(context).primaryColor,
onPressed: _takePicture,
),
),
],
);
}
}
back to the form , lets add ImageInput
under the others inputs in ListView
children list
Container(
width: 100,
height: 100,
margin: EdgeInsets.only(top: 8, right: 10),
child: ImageInput(_selectImage),
),
- we will pass as argumanet
_selectImage
function
void _selectImage(File pickedImage) {
_pickedImage = pickedImage;
}
now we will create _saveForm
function , it will validate our inputs and then will add new item
- it will show loader while we are wating for response
- in case of error it will show
AlertDialog
- after it will finish all steps, it will close
AddItemScreen
- it will use
Navigator.of(context).pop())
to close screen after add new item will finish
- it will use
_saveForm
Future<void> _saveForm() async {
final isValid = _form.currentState.validate();
if (!isValid) {
return;
}
_form.currentState.save();
setState(() {
_isLoading = true;
});
try {
await Provider.of<Items>(context, listen: false).addItem(_addItem);
} catch (error) {
await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('An error occurred'),
content: Text(error.toString()), //some thing went wrong
actions: <Widget>[
FlatButton(
child: Text('Okay'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
),
);
}
setState(() {
_isLoading = false;
});
Navigator.of(context).pop();
}
Now all left to do is to dispose
all inputs elements with focusNode
in dispose
time (Called when this object is removed from the tree permanently)
@override
void dispose() {
_priceFocusNode.dispose();
_descriptionFocusNode.dispose();
super.dispose();
}
add_item_screen.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_proj/providers/item.dart';
import '../providers/items.dart';
class AddItemScreen extends StatefulWidget {
static const routeName = '/add-item';
@override
_AddItemScreenState createState() => _AddItemScreenState();
}
class _AddItemScreenState extends State<AddItemScreen> {
final _form = GlobalKey<FormState>();
final _descriptionFocusNode = FocusNode();
final _priceFocusNode = FocusNode();
File _pickedImage;
void _selectImage(File pickedImage) {
_pickedImage = pickedImage;
}
var _addItem = Item(
id: null,
title: '',
price: 0,
description: '',
image: null,
);
var _initValues = {
'title': '',
'description': '',
'price': '',
'image': '',
};
var _isInit = true;
var _isLoading = false;
@override
void dispose() {
_priceFocusNode.dispose();
_descriptionFocusNode.dispose();
super.dispose();
}
Future<void> _saveForm() async {
final isValid = _form.currentState.validate();
if (!isValid) {
return;
}
_form.currentState.save();
setState(() {
_isLoading = true;
});
try {
await Provider.of<Items>(context, listen: false).addItem(_addItem);
} catch (error) {
await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('An error occurred'),
content: Text(error.toString()), //some thing went wrong
actions: <Widget>[
FlatButton(
child: Text('Okay'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
),
);
}
setState(() {
_isLoading = false;
});
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add New Item'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.save),
onPressed: _saveForm,
)
],
),
body: _isLoading
? Center(
child: CircularProgressIndicator(),
)
: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _form,
child: ListView(
children: <Widget>[
TextFormField(
initialValue: _initValues['title'],
decoration: InputDecoration(labelText: 'Title'),
textInputAction: TextInputAction.next,
onSaved: (value) => _addItem = Item(
title: value,
id: _addItem.id,
isFavorite: _addItem.isFavorite,
price: _addItem.price,
description: _addItem.description,
image:
_pickedImage != null ? _pickedImage : _addItem,
),
validator: (value) {
if (value.isEmpty) {
return 'Please provide a value';
}
return null;
},
onFieldSubmitted: (_) =>
FocusScope.of(context).requestFocus(_priceFocusNode),
),
TextFormField(
initialValue: _initValues['price'],
decoration: InputDecoration(labelText: 'Price'),
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
focusNode: _priceFocusNode,
onSaved: (value) => _addItem = Item(
title: _addItem.title,
id: _addItem.id,
isFavorite: _addItem.isFavorite,
price: double.parse(value),
description: _addItem.description,
image:
_pickedImage != null ? _pickedImage : _addItem,
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter a price';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number';
}
if (double.parse(value) <= 0) {
return 'Please enter a number greter then zero';
}
return null;
},
onFieldSubmitted: (_) => FocusScope.of(context)
.requestFocus(_descriptionFocusNode),
),
TextFormField(
initialValue: _initValues['description'],
decoration: InputDecoration(labelText: 'Description'),
maxLines: 3,
onSaved: (value) => _addItem = Item(
title: _addItem.title,
id: _addItem.id,
isFavorite: _addItem.isFavorite,
price: _addItem.price,
description: value,
image:
_pickedImage != null ? _pickedImage : _addItem,
),
validator: (value) {
if (value.isEmpty) {
return 'Please provide a description';
}
if (value.length < 10) {
return 'Should be at least 10 charectersling';
}
return null;
},
keyboardType: TextInputType.multiline,
focusNode: _descriptionFocusNode,
),
Container(
width: 100,
height: 100,
margin: EdgeInsets.only(top: 8, right: 10),
child: ImageInput(_selectImage),
),
],
),
),
),
);
}
}
We are starting to have lots of screens, we should make navigation between them easy
Lets make navigation menu which would be App Drawer
For now it will have buttons for Logout
and Home
navigation
Lets create AppDrawer
widget
In widgets
folder we need to create app_drawer.dart
It will be StatelessWidget
It will return Drawer
widget (A material design panel that slides in horizontally from the edge of a Scaffold to show navigation links in an application)
inside Drawer
we will return Column
widget with some widgets:
-
AppBar
withtitle
and inside it (An app bar consists of a toolbar and potentially other widgets, such as a TabBar and a FlexibleSpaceBar. App bars typically expose one or more common actions with IconButtons which are optionally followed by a PopupMenuButton for less common operations (sometimes called the "overflow menu"))AppBar( title: Text('Flutter Wix Workshop'), automaticallyImplyLeading: false, ),
-
Divider
(A thin horizontal line, with padding on either side) -
ListTile
(A single fixed-height row that typically contains some text as well as a leading or trailing icon) it will contain theIcon
,Text
andonTap
propertiesDivider(), ListTile( leading: Icon(Icons.exit_to_app), title: Text('Logout'), onTap: () { Navigator.of(context).pop(); Navigator.of(context).pushReplacementNamed('/'); Provider.of<Auth>(context, listen: false).logout(); }, ),
we will make two sets of Divider
+ ListTile
for Logout
and Home
try to create one for Home
with home icon. onTap
will navigate to ItemsOvwerviewScreen
(it will pushReplacementNamed
and not just push
the rout)
app_drawer.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth.dart';
class AppDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
AppBar(
title: Text('Flutter Wix Workshop'),
automaticallyImplyLeading: false,
),
Divider(),
ListTile(
leading: Icon(Icons.home),
title: Text('Home'),
onTap: () {
Navigator.of(context).pushReplacementNamed('/');
},
),
Divider(),
ListTile(
leading: Icon(Icons.exit_to_app),
title: Text('Logout'),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushReplacementNamed('/');
Provider.of<Auth>(context, listen: false).logout();
},
),
],
),
);
}
}
Now we can start using our new AppDrawer
- In
itemsOverViewScreen
widget add ourappDrawer
widget insidedrawer
property ofScafolled
drawer: AppDrawer(),
so we can add new Item already , but what about delete it ? or maybe edit it we need a place to mange our items
Lets create MangeItemsScreen
widget
In screens
folder we need to create mange_items_screen.dart
It will be StatelessWidget
In this page we will show only items which the logged in user created
we will pass filterByUser
true to fetchAndSetItems
function in the items provider
First lets add route to it just like before
static const routeName = '/manage-items';
We need also to add the rout to main.dart
file
ManageItemsScreen.routeName: (ctx) => ManageItemsScreen(),
We will use FutureBuilder
widget (Widget that builds itself based on the latest snapshot of interaction with a Future)
Lets create _refreshItems
function and pass it to FutureBuilder
future
property ,it will be triggered eachwe refresh the screen
Future<void> _refreshItems(BuildContext context) async {
await Provider.of<Items>(context, listen: false).fetchAndSetItems(true);
}
We will return Scaffold
widget
Inside it will have AppBar
actions
property will containIconButton
withadd
icon andonPressed
function which will navigate us toAddItemScreen
drawer
property will be with ourappDrawer
widget we created before
appBar: AppBar(
title: Text('Your Items'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
Navigator.of(context).pushNamed(AddItemScreen.routeName);
},
)
],
),
drawer: AppDrawer(),
body
property will haveFutureBuilder
widget which will contain two properties :future
andbuilder
future
property will contain_refreshItems(context)
builder
property will get called first when the widget is renderd (we will showCircularProgressIndicator
widget for it) , then when the future will finish and we will get response,FutureBuilder
will handle it, and we will get to show the items.
body: FutureBuilder(
future: _refreshItems(context),
builder: (context, snapshot) =>
snapshot.connectionState == ConnectionState.waiting
? Center(
child: CircularProgressIndicator(),
)
: RefreshIndicator(
onRefresh: () => _refreshItems(context),
child: Consumer<Items>(
builder: (context, itemsData, _) => Padding(
padding: EdgeInsets.all(8),
child: ListView.builder(
itemCount: itemsData.items.length,
itemBuilder: (_, i) => Column(
children: <Widget>[
.
.
.
.
.
Divider(),
],
),
),
),
),
),
),
We are missing a widget for the represent items
data we have , so lets create one
Lets create ManageItemView
widget
In widgets
folder we need to create manage_item_view.dart
It will be StatelessWidget
It will get id
, title
, and image
as class properties
final String id;
final String title;
final File image;
ManageItemView(this.id, this.title, this.image);
It will return ListTile
widget (A single fixed-height row that typically contains some text as well as a leading or trailing icon)
it will contain title
, leading
and trailing
properties
title
property will beText
widget of the title we gotleading
property will beCircleAvatar
widget withbackgroundImage
of the Image we gottrailing
property will returnContainer
widget , for now it will return emptyRow
widget
return ListTile(
title: Text(title),
leading: CircleAvatar(
backgroundImage: FileImage((image)),
),
trailing: Container(
width: 100,
child: Row(
children: <Widget>[
],
),
),
);
mange_item_view.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:workshop1/screens/edit_item_screen.dart';
import '../providers/items.dart';
class ManageItemView extends StatelessWidget {
final String id;
final String title;
final File image;
ManageItemView(this.id, this.title, this.image);
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
leading: CircleAvatar(
backgroundImage: FileImage((image)),
),
trailing: Container(
width: 100,
child: Row(
children: <Widget>[
],
),
),
);
}
}
Lets fill up the code that missing in MangeItemsScreen
in the itemBuilder childern
ManageItemView(
itemsData.items[i].id,
itemsData.items[i].title,
itemsData.items[i].image,
),
Lets add button for our manage page in the AppDrawer
, under home
button we will add Divider
and ListTile
with 'Mange Items' text
Divider(),
ListTile(
leading: Icon(Icons.edit),
title: Text('Mange Items'),
onTap: () {
Navigator.of(context)
.pushReplacementNamed(ManageItemsScreen.routeName);
},
),
mange_items_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:workshop1/screens/edit_item_screen.dart';
import 'package:workshop1/widgets/user_item_view.dart';
import '../widgets/app_drawer.dart';
import '../providers/items.dart';
class UserItemsScreen extends StatelessWidget {
static const routeName = '/user-items';
Future<void> _refreshItems(BuildContext context) async {
await Provider.of<Items>(context, listen: false).fetchAndSetItems(true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Your Items'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
Navigator.of(context).pushNamed(AddItemScreen.routeName);
},
)
],
),
drawer: AppDrawer(),
body: FutureBuilder(
future: _refreshItems(context),
builder: (context, snapshot) =>
snapshot.connectionState == ConnectionState.waiting
? Center(
child: CircularProgressIndicator(),
)
: RefreshIndicator(
onRefresh: () => _refreshItems(context),
child: Consumer<Items>(
builder: (context, itemsData, _) => Padding(
padding: EdgeInsets.all(8),
child: ListView.builder(
itemCount: itemsData.items.length,
itemBuilder: (_, i) => Column(
children: <Widget>[
ManageItemView(
itemsData.items[i].id,
itemsData.items[i].title,
itemsData.items[i].image,
),
Divider(),
],
),
),
),
),
),
),
);
}
}
Lets start with adding this code to items.dart
which will remove items from our server
Future<void> deleteItem(String id) async {
final url = '$baseUrl/items/$id.json?auth=$authToken';
final existingItemIndex = _items.indexWhere((item) => item.id == id);
var existingItem = _items[existingItemIndex];
_items.removeAt(existingItemIndex);
notifyListeners();
final response = await http.delete(url);
if (response.statusCode >= 400) {
_items.insert(existingItemIndex, existingItem);
notifyListeners();
throw HttpException('Could not delete item');
} else {
existingItem = null;
}
}
Now lets add button in ManageItemView
widget, which will remove the item when it got clicked
- Under
trailing -> children widgets
, we will addIconButton
widget who will be with icondelete
onPressed
will call our new function inItems
provider , it will passid
of item that will be removedlisten: false
will get value once and ignore updates (we don't need more)- in case of error we will show
snackBar
-
we need to get Scaffold from the context for that (under widget
build
function we will add it)final scaffold = Scaffold.of(context);
-
IconButton
IconButton(
icon: Icon(Icons.delete),
onPressed: () async {
try {
Provider.of<Items>(context, listen: false).deleteItem(id);
} catch (error) {
scaffold.showSnackBar(SnackBar(
content: Text(
'Deleteing faild!',
textAlign: TextAlign.center,
),
));
}
},
color: Theme.of(context).errorColor,
)
we want to have new action - like an item It will be spread in couple of widgets
Lets add like button on the item in our home screen
first lets add the function we need in our Item
provider (this time not Items
provider)
It will update item isFavorite
staus in server for us
final String baseUrl = 'https://flutter-workshop-eef86.firebaseio.com';
void _setFavoriteValue(bool newValue) {
isFavorite = newValue;
notifyListeners();
}
Future<void> toggleFavoriteStatus(String token, String userId) async {
final oldStatus = isFavorite;
isFavorite = !isFavorite;
notifyListeners();
final url = '$baseUrl/userFavorites/$userId/$id.json?auth=$token';
try {
final response = await http.put(url,
body: json.encode(
isFavorite,
));
if (response.statusCode >= 400) {
_setFavoriteValue(oldStatus);
}
} catch (error) {
_setFavoriteValue(oldStatus);
throw error;
}
}
Lets add new button for it in ItemWidget
we need first to get authData
from AuthProvider
- we need to add this line of code under
build
functionfinal authData = Provider.of<Auth>(context, listen: false);
Lets add leading
property inside our footer GridTileBar
widget
It will be Consumer
widget , and it will listen to Item
provider
builder
property will hold insideIconButton
which will showfavorite
icaon orfavorite_border
(depens if its liked or not)onPressed
property will trrigertoggleFavoriteStatus
function- we need to pass
authData.token
andauthData.userId
like button
leading: Consumer<Item>(
builder: (ctx, item, child) => IconButton(
icon: Icon(
item.isFavorite ? Icons.favorite : Icons.favorite_border),
color: Theme.of(context).accentColor,
onPressed: () {
item.toggleFavoriteStatus(
authData.token,
authData.userId,
);
},
),
),
Lets add popup menu to select if we want to show only favorites items
In ItemsOverviewScreen
in AppBar
widget, we will add actions
property
-
we will use
PopupMenuButton
widget (Displays a menu when pressed and calls onSelected when the menu is dismissed because an item was selected. The value passed to onSelected is the value of the selected menu item)onSelect
property will get selected value and will check if_showOnlyFavorites
should be true or falseicon
property will beIcons.more_vert
-
we need to add
enum FilterOptions
,lets add it on top of our classenum FilterOptions { Favorites, All }
itemBuilder
property will be array ofPopupMenuItem
widget- one will be
Only Favorites
with value ofFilterOptions.Favorites
- another one will be
Show All
with value ofFilterOptions.All
- one will be
-
we need to add to call to
Items
provider
List<Item> get favoritesItems {
return _items.where((productItem) => productItem.isFavorite).toList();
}
Now lets pass _showFavorites
to ItemsGrid
Inside ItemsGrid
we will add showOnlyFavorites
property to the class
final bool showOnlyFavorites;
ItemsGrid(this.showOnlyFavorites);
We just need to get the right product according to the showFavorites
property
final productsData = Provider.of<Products>(context);
final prodcuts =
showOnlyFavorites ? productsData.favoritesItems : productsData.items;
now lets just to update fetchAndSetItems
in Items
provider
Future<void> fetchAndSetItems([bool filterByUser = false]) async {
final filterUrl =
filterByUser ? 'orderBy="creatorId"&equalTo="$userId"' : '';
var url = '$baseUrl/items.json?auth=$authToken&$filterUrl';
print(authToken);
try {
final response = await http.get(url);
final extractedData = json.decode(response.body) as Map<String, dynamic>;
if (extractedData == null) {
return;
} else {
final url =
'https://flutter-course-9a6bf.firebaseio.com/userFavorites/$userId.json?auth=$authToken';
final favoriteResponse = await http.get(url);
final favoriteData = json.decode(favoriteResponse.body);
final List<Item> loadedItems = [];
for (var entry in extractedData.entries) {
var file = await downloadImage(entry.value['image']);
Future.delayed(const Duration(milliseconds: 20), () => "20");
loadedItems.add(Item(
id: entry.key,
title: entry.value['title'],
description: entry.value['description'],
price: entry.value['price'],
isFavorite: favoriteData == null ? false : favoriteData[entry.key] ?? false,
image: file,
));
}
_items = loadedItems;
notifyListeners();
}
} catch (error) {
throw error;
}
}
PopupMenuButton
actions: <Widget>[
PopupMenuButton(
onSelected: (FilterOptions selectedValue) {
setState(() {
if (selectedValue == FilterOptions.Favorites) {
_showOnlyFavorites = true;
} else {
_showOnlyFavorites = false;
}
});
},
icon: Icon(
Icons.more_vert,
),
itemBuilder: (_) => [
PopupMenuItem(
child: Text('Only Favorites'),
value: FilterOptions.Favorites),
PopupMenuItem(child: Text('Show All'), value: FilterOptions.All),
],
)
],