본문 바로가기

Flutter] Just Do it! (Basic Series 2 Unit Converter)

 Development Environment

Os : Windows 10 (64-bit)

Android Studio Version : android-studio-ide-182.5314842-windows.exe

Flutter Version : flutter_windows_v1.2.1-stable

Device : Redmi Note 5 or Virtual Device


*If u didn't install Android Studio, plz Install : https://witch-dp.tistory.com/39

*If u didn't install Flutter , plz Install : https://witch-dp.tistory.com/40

*If u didn't connect Device, plz Connect : https://witch-dp.tistory.com/41



({unit_converter}/lib/main.dart)        ({unit_converter}/pubspec.yaml)

                              


({unit_converter}/lib/category.dart)     ({unit_converter}/lib/category_route.dart) 

                              


({unit_converter}/lib/unit.dart)           ({unit_converter}/lib/converter_route.dart)

                             


({unit_converter}/assets/regular_units.json) ({unit_converter}/lib/api.dart) 

                                             





Just Do it! Step

 1: Create rectangle

 2: Create category widget

 3: Create category route

 4: Navigate to a new screen

 5: Create Stateful widgets

 6: Add input

 7: Add backdrop
 8: Add responsive
 9: Add json units
 10: Add icons fonts
 11: Make API
 12: Error

 # Example



1. Create rectangle

 -Change Code (main.dart)

 : class MyApp

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Just Do it! Unit Converter',
home: Scaffold(
appBar: AppBar(
title: Text('Just Do it! Unit Converter ft.witch-dp'),
),
body: Center(
child: Rectangle(),
),
),
theme: new ThemeData(
primaryColor: Colors.blueGrey,
),
);
}
}


 - Add Code (main.dart)

 : class Rectangle

class Rectangle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.orangeAccent,
height: 300.0,
width: 300.0,
child: Center(
child: Text('Just Do it! \n'
'Unit Converter content \n ft.witch-dp',
style: TextStyle(fontSize: 27.0),
textAlign: TextAlign.center,
),
),
),
);
}
}


Result]


2. Create category widget

- Add and Change Code (main.dart)

 : class MyApp

backgroundColor: Colors.blueAccent[100],
body: Center(
child: Category(),
),


- Add Code (main.dart)

 : import category.dart

import 'package:unit_converter/category.dart';


- Create Dart File

 :category


Result]


- Add Code (category.dart)

import 'package:flutter/material.dart';

class Category extends StatelessWidget {
const Category();

@override
Widget build(BuildContext context) {
return Container();
}
}

Result]


- Add Code (category.dart)

final _rowHeight = 130.0;
final _borderRadius = BorderRadius.circular(_rowHeight / 4);


- Add Change Code (category.dart)

 : class Category

final String name;
final ColorSwatch color;
final IconData iconLocation;

const Category({
Key key,
@required this.name,
@required this.color,
@required this.iconLocation,
}) : assert(name != null),
assert(color != null),
assert(iconLocation != null),
super(key: key);

@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Container(
height: _rowHeight,
child: InkWell(
borderRadius: _borderRadius,
highlightColor: color,
splashColor: color,
onTap: () {
print('Tapped!');
},
child: Padding(
padding: EdgeInsets.all(15.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.all(16.0),
child: Icon(
iconLocation,
size: 80.0,
),
),
Center(
child: Text(
name,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline,
),
),
],
),
),
),
),
);


- Add Code (main.dart)

const _categoryName = 'Timer';
const _categoryIcon = Icons.timer;
const _categoryColor = Colors.cyanAccent;

- Add Code (main.dart)

 : class MyApp -> home -> body -> child

child: Category(
name: _categoryName,
color: _categoryColor,
iconLocation: _categoryIcon,
),

Result]


3. Create category route

- Remove and Change code (main.dart)

 import category.dart

 : MyApp -> home -> Scaffold

home: Container(),


- Add code (main.dart)

 import category_route.dart

import 'package:unit_converter/category_route.dart';


- Create Dart File

 :category_route


- Add Code (category_route.dart)

 : class RandomWordsState 

import 'package:flutter/material.dart';

class CategoryRoute extends StatelessWidget {
const CategoryRoute();

@override
Widget build(BuildContext context) {
final listView = Container();
final appBar = AppBar();
return Scaffold(
appBar: appBar,
body: listView,
);
}
}


Result]


- Add Code (category_route.dart)

final _backgroundColor= Colors.blueGrey;


- Add Code (category_route.dart)

 : class CategoryRoute

static const _categoryNames = <String>[
'Time',
'Currency',
'Length',
'Area',
'Volume',
'Mass',
'Energy',
'Digital Storage',
];

static const _baseColors = <Color>[
Colors.grey,
Colors.purple,
Colors.indigo,
Colors.blue,
Colors.green,
Colors.yellow,
Colors.orange,
Colors.red,
];


- Add Code (category_route.dart)

 : class CategoryRoute

Widget _buildCategoryWidgets(List<Widget> categories) {
return ListView.builder(
itemBuilder: (BuildContext context, int index) => categories[index],
itemCount: categories.length,
);
}


- Add Code (category_route.dart)

 : class CategoryRoute -> build

final categories = <Category>[];

for (var i = 0; i < _categoryNames.length; i++) {
categories.add(Category(
name: _categoryNames[i],
color: _baseColors[i],
iconLocation: Icons.timer,
));
}

final listView = Container(
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: _buildCategoryWidgets(categories),
);

final appBar = AppBar(
elevation: 0.0,
title: Text('Unit Converter ft.witch-dp',
style: TextStyle(
color: Colors.amberAccent,
fontSize: 30.0,
),
),
centerTitle: true,
backgroundColor: _backgroundColor,
);


Result]


4. Navigate to a new screen

- Add Code (category_route.dart)

 import category_route.dart

import 'package:unit_converter/unit.dart';


- Add Code (category_route.dart)

 : class CategoryRoute -> build

List<Unit> _retrieveUnitList(String categoryName) {
return List.generate(10, (int i) {
i += 1;
return Unit(
name: '$categoryName Unit $i',
conversion: i.toDouble(),
);
});
}


- Add Code (category_route.dart)

 : class CategoryRoute -> build -> Category

units: _retrieveUnitList(_categoryNames[i]),


- Add Code (category.dart)

 : class Category

final List<Unit> units;


- Add Code (category.dart)

 : class Category

  @required this.units,
}) : assert(units != null),


- Add Code (category.dart)

 : class Category

import 'package:unit_converter/unit.dart';


- Create Dart File

 : unit


- Add Code (unit.dart)

import 'package:flutter/material.dart';

class Unit {
final String name;
final double conversion;

const Unit({
@required this.name,
@required this.conversion,
}) : assert(name != null),
assert(conversion != null);

Unit.fromJson(Map jsonMap)
: assert(jsonMap['name'] != null),
assert(jsonMap['conversion'] != null),
name = jsonMap['name'],
conversion = jsonMap['conversion'].toDouble();
}


- Change Code (category.dart)

 : class Category -> Container -> InkWell

onTap: () => _navigateToConverter(context),


- Add Code (category.dart)

 : class Category

void _navigateToConverter(BuildContext context) {
Navigator.of(context).push(MaterialPageRoute<Null>(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 1.0,
title: Text(
name,
style: Theme.of(context).textTheme.display1,
),
centerTitle: true,
backgroundColor: color,
),
body: ConverterRoute(
color: color,
name: name,
units: units,
),
);
},
));
}


- Add Code (category.dart)

 : class Category

import 'package:unit_converter/converter_route.dart';


- Create Dart File

 :converter_route


- Add Code (converter_route.dart)

 : class Category

import 'package:flutter/material.dart';
import 'package:unit_converter/unit.dart';

class ConverterRoute extends StatelessWidget {
final String name;
final Color color;
final List<Unit> units;

const ConverterRoute({
@required this.name,
@required this.color,
@required this.units,
}) : assert(name != null),
assert(color != null),
assert(units != null);

@override
Widget build(BuildContext context) {
final unitWidgets = units.map((Unit unit) {
return Container(
margin: EdgeInsets.all(8.0),
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
Text(
unit.name,
style: Theme.of(context).textTheme.headline,
),
Text(
'Conversion: ${unit.conversion}',
style: Theme.of(context).textTheme.subhead,
),
],
),
);
}).toList();

return ListView(
children: unitWidgets,
);
}
}


*Style Code Change (category.dart)

final _rowHeight = 95.0;
onTap: () => _navigateToConverter(context),
child: Padding(
padding: EdgeInsets.all(5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.all(16.0),
child: Icon(
iconLocation,
size: 60.0,
),
),


Result]


5. Create Stateful widgets

- Change Code (converter_route.dart)

 : extends StatefulWidget

class ConverterRoute extends StatefulWidget {


- Add Code (converter_route.dart)

 : class ConverterRoute

@override
_ConverterRouteState createState() => _ConverterRouteState();


- Remove and Add Code (converter_route.dart)

 : ConverterRoute -> build

class _ConverterRouteState extends State<ConverterRoute> {
@override
Widget build(BuildContext context) {
final unitWidgets = widget.units.map((Unit unit) {
return Container(
margin: EdgeInsets.all(8.0),
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
Text(
unit.name,
style: Theme.of(context).textTheme.headline,
),
Text(
'Conversion: ${unit.conversion}',
style: Theme.of(context).textTheme.subhead,
),
],
),
);
}).toList();

return ListView(
children: unitWidgets,
);
}
}


6. Add input

- Change Code (converter_route.dart)

 : class CategoryRoute -> _baseColors => Change event color

ColorSwatch(0xFFFF009F, {
'highlight': Color(0xFF7F004F),
'splash': Color(0xFFFF009F),
}),
ColorSwatch(0xFF9100FF, {
'highlight': Color(0xFF2A007F),
'splash': Color(0xFF9100FF),
}),
ColorSwatch(0xFF2F72FF, {
'highlight': Color(0xFF00007F),
'splash': Color(0xFF2F72FF),
}),
ColorSwatch(0xFF007BFF, {
'highlight': Color(0xFF000EFF),
'splash': Color(0xFF007BFF),
}),
ColorSwatch(0xFF7CC159, {
'highlight': Color(0xFF007F12),
'splash': Color(0xFF7CC159),
}),
ColorSwatch(0xFFFFF8A9, {
'highlight': Color(0xFFFFEF00),
'splash': Color(0xFFFFF8A9),
}),
ColorSwatch(0xFFFF9F00, {
'highlight': Color(0xFFE76C28),
'splash': Color(0xFFFF9F00),
}),
ColorSwatch(0xFFFF877E, {
'highlight': Color(0xFFFF0000),
'splash': Color(0xFFFF877E),
'error': Color(0xFF4E0A00),
}),


- Change Code (category.dart)

 : Category -> build -> InkWell

highlightColor: color['highlight'],
splashColor: color['splash'],


- Add Code (converter_route.dart)

const _padding = EdgeInsets.all(16.0);


- Add Code (converter_route.dart)

 : Class _ConverterRouteState

Unit _fromValue;
Unit _toValue;
double _inputValue;
String _convertedValue = '';
List<DropdownMenuItem> _unitMenuItems;
bool _showValidationError = false;

@override
void initState() {
super.initState();
_createDropdownMenuItems();
_setDefaults();
}

void _createDropdownMenuItems() {
var newItems = <DropdownMenuItem>[];
for (var unit in widget.units) {
newItems.add(DropdownMenuItem(
value: unit.name,
child: Container(
child: Text(
unit.name,
softWrap: true,
),
),
));
}
setState(() {
_unitMenuItems = newItems;
});
}

void _setDefaults() {
setState(() {
_fromValue = widget.units[0];
_toValue = widget.units[1];
});
}

String _format(double conversion) {
var outputNum = conversion.toStringAsPrecision(7);
if (outputNum.contains('.') && outputNum.endsWith('0')) {
var i = outputNum.length - 1;
while (outputNum[i] == '0') {
i -= 1;
}
outputNum = outputNum.substring(0, i + 1);
}
if (outputNum.endsWith('.')) {
return outputNum.substring(0, outputNum.length - 1);
}
return outputNum;
}

void _updateConversion() {
setState(() {
_convertedValue =
_format(_inputValue * (_toValue.conversion / _fromValue.conversion));
});
}

void _updateInputValue(String input) {
setState(() {
if (input == null || input.isEmpty) {
_convertedValue = '';
} else {
try {
final inputDouble = double.parse(input);
_showValidationError = false;
_inputValue = inputDouble;
_updateConversion();
} on Exception catch (e) {
print('Error: $e');
_showValidationError = true;
}
}
});
}

Unit _getUnit(String unitName) {
return widget.units.firstWhere(
(Unit unit) {
return unit.name == unitName;
},
orElse: null,
);
}

void _updateFromConversion(dynamic unitName) {
setState(() {
_fromValue = _getUnit(unitName);
});
if (_inputValue != null) {
_updateConversion();
}
}

void _updateToConversion(dynamic unitName) {
setState(() {
_toValue = _getUnit(unitName);
});
if (_inputValue != null) {
_updateConversion();
}
}

Widget _createDropdown(String currentValue, ValueChanged<dynamic> onChanged) {
return Container(
margin: EdgeInsets.only(top: 16.0),
decoration: BoxDecoration(
color: Colors.grey[50],
border: Border.all(
color: Colors.grey[400],
width: 1.0,
),
),
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Theme(
data: Theme.of(context).copyWith(
canvasColor: Colors.grey[50],
),
child: DropdownButtonHideUnderline(
child: ButtonTheme(
alignedDropdown: true,
child: DropdownButton(
value: currentValue,
items: _unitMenuItems,
onChanged: onChanged,
style: Theme.of(context).textTheme.title,
),
),
),
),
);
}


- Change Code (converter_route.dart)

 : Category -> build

  final input = Padding(
padding: _padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
style: Theme.of(context).textTheme.display1,
decoration: InputDecoration(
labelStyle: Theme.of(context).textTheme.display1,
errorText: _showValidationError ? 'Invalid number entered' : null,
labelText: 'Input',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(0.0),
),
),
keyboardType: TextInputType.number,
onChanged: _updateInputValue,
),
_createDropdown(_fromValue.name, _updateFromConversion),
],
),
);

final arrows = RotatedBox(
quarterTurns: 1,
child: Icon(
Icons.compare_arrows,
size: 40.0,
),
);

final output = Padding(
padding: _padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
InputDecorator(
child: Text(
_convertedValue,
style: Theme.of(context).textTheme.display1,
),
decoration: InputDecoration(
labelText: 'Output',
labelStyle: Theme.of(context).textTheme.display1,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(0.0),
),
),
),
_createDropdown(_toValue.name, _updateToConversion),
],
),
);

final converter = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
input,
arrows,
output,
],
);

return Padding(
padding: _padding,
child: converter,
);


Result]


7. Add backdrop

- Change Code (main.dart)

 : class MyApp -> build

  textTheme: Theme.of(context).textTheme.apply(
bodyColor: Colors.black,
displayColor: Colors.grey[650],
),
primaryColor: Colors.amberAccent,
textSelectionHandleColor: Colors.indigoAccent[100],
),


- Remove and Change Code (converter_route.dart)

 : class ConverterRoute -> final

 : class ConverterRoute

final Category category;

const ConverterRoute({
@required this.category,
}) : assert(category != null);


-Add Code (converter_route.dart)

 : class _ConverterRouteState

@override
void didUpdateWidget(ConverterRoute old) {
super.didUpdateWidget(old);
if (old.category != widget.category) {
_createDropdownMenuItems();
_setDefaults();
}
}


-Change Code (converter_route.dart)

  : class _ConverterRouteState -> _createDropdownMenuItems

for (var unit in widget.category.units) {


-Change Code (converter_route.dart)

  : class _ConverterRouteState -> _setDefaults

_fromValue = widget.category.units[0];
_toValue = widget.category.units[1];


-Change Code (converter_route.dart)

  : class _ConverterRouteState -> _getUnit

return widget.category.units.firstWhere(


-Remove Code (category.dart)

 : final

 : class Category -> build, _navigateToConverter

 : import converter_route.dart


-Change Code (category.dart)

  : class Category -> Category

const Category({
@required this.name,
@required this.color,
@required this.iconLocation,
@required this.units,
}) : assert(name != null),
assert(color != null),
assert(iconLocation != null),
assert(units != null);


-Remove Code (category_route.dart)

 : final

-Divide and Add Code (category_route.dart)
 : CategoryRoute into CategoryRoute and _CategoryRouteState

class CategoryRoute extends StatefulWidget {
const CategoryRoute();

@override
_CategoryRouteState createState() => _CategoryRouteState();
}

class _CategoryRouteState extends State<CategoryRoute> {
Category _defaultCategory;
Category _currentCategory;
final _categories = <Category>[];


-Add Code (category_route.dart)

  : class _CategoryRouteState

@override
void initState() {
super.initState();
for (var i = 0; i < _categoryNames.length; i++) {
var category = Category(
name: _categoryNames[i],
color: _baseColors[i],
iconLocation: Icons.Timer,
units: _retrieveUnitList(_categoryNames[i]),
);
if (i == 0) {
_defaultCategory = category;
}
_categories.add(category);
}
}

void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}


-Change Code (category_route.dart)

 : _CategoryRouteState -> build


final listView = Padding(
padding: EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: 48.0,
),
child: _buildCategoryWidgets(),
);

return Backdrop(
currentCategory:
_currentCategory == null ? _defaultCategory : _currentCategory,
frontPanel: _currentCategory == null
? ConverterRoute (category: _defaultCategory)
: ConverterRoute (category: _currentCategory),
backPanel: listView,
frontTitle: Text('Unit Converter'),
backTitle: Text('Select a Category'),
);
}


- Add Code (category_route.dart)

import converter_route.dart

import 'converter_route.dart';


- Create Dart File

 : backdrop


- Add Code (backdrop.dart)

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'category.dart';

const double _kFlingVelocity = 2.0;

class _BackdropPanel extends StatelessWidget {
const _BackdropPanel({
Key key,
this.onTap,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.title,
this.child,
}) : super(key: key);

final VoidCallback onTap;
final GestureDragUpdateCallback onVerticalDragUpdate;
final GestureDragEndCallback onVerticalDragEnd;
final Widget title;
final Widget child;

@override
Widget build(BuildContext context) {
return Material(
elevation: 2.0,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onVerticalDragUpdate: onVerticalDragUpdate,
onVerticalDragEnd: onVerticalDragEnd,
onTap: onTap,
child: Container(
height: 48.0,
padding: EdgeInsetsDirectional.only(start: 16.0),
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: Theme.of(context).textTheme.subhead,
child: title,
),
),
),
Divider(
height: 1.0,
),
Expanded(
child: child,
),
],
),
);
}
}

class _BackdropTitle extends AnimatedWidget {
final Widget frontTitle;
final Widget backTitle;

const _BackdropTitle({
Key key,
Listenable listenable,
this.frontTitle,
this.backTitle,
}) : super(key: key, listenable: listenable);

@override
Widget build(BuildContext context) {
final Animation<double> animation = this.listenable;
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: Interval(0.5, 1.0),
).value,
child: backTitle,
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: Interval(0.5, 1.0),
).value,
child: frontTitle,
),
],
),
);
}
}

class Backdrop extends StatefulWidget {
final Category currentCategory;
final Widget frontPanel;
final Widget backPanel;
final Widget frontTitle;
final Widget backTitle;

const Backdrop({
@required this.currentCategory,
@required this.frontPanel,
@required this.backPanel,
@required this.frontTitle,
@required this.backTitle,
}) : assert(currentCategory != null),
assert(frontPanel != null),
assert(backPanel != null),
assert(frontTitle != null),
assert(backTitle != null);

@override
_BackdropState createState() => _BackdropState();
}

class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}

@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
setState(() {
_controller.fling(
velocity:
_backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity);
});
} else if (!_backdropPanelVisible) {
setState(() {
_controller.fling(velocity: _kFlingVelocity);
});
}
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

bool get _backdropPanelVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}

void _toggleBackdropPanelVisibility() {
FocusScope.of(context).requestFocus(FocusNode());
_controller.fling(
velocity: _backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity);
}

double get _backdropHeight {
final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
return renderBox.size.height;
}

void _handleDragUpdate(DragUpdateDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;

_controller.value -= details.primaryDelta / _backdropHeight;
}

void _handleDragEnd(DragEndDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;

final double flingVelocity =
details.velocity.pixelsPerSecond.dy / _backdropHeight;
if (flingVelocity < 0.0)
_controller.fling(velocity: math.max(_kFlingVelocity, -flingVelocity));
else if (flingVelocity > 0.0)
_controller.fling(velocity: math.min(-_kFlingVelocity, -flingVelocity));
else
_controller.fling(
velocity:
_controller.value < 0.5 ? -_kFlingVelocity : _kFlingVelocity);
}

Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double panelTitleHeight = 48.0;
final Size panelSize = constraints.biggest;
final double panelTop = panelSize.height - panelTitleHeight;

Animation<RelativeRect> panelAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, panelTop, 0.0, panelTop - panelSize.height),
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);

return Container(
key: _backdropKey,
color: widget.currentCategory.color,
child: Stack(
children: <Widget>[
widget.backPanel,
PositionedTransition(
rect: panelAnimation,
child: _BackdropPanel(
onTap: _toggleBackdropPanelVisibility,
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
title: Text(widget.currentCategory.name),
child: widget.frontPanel,
),
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: widget.currentCategory.color,
elevation: 0.0,
leading: IconButton(
onPressed: _toggleBackdropPanelVisibility,
icon: AnimatedIcon(
icon: AnimatedIcons.close_menu,
progress: _controller.view,
),
),
title: _BackdropTitle(
listenable: _controller.view,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
),
body: LayoutBuilder(
builder: _buildStack,
),
resizeToAvoidBottomPadding: false,
);
}
}


- Change Code  (category_route.dart)

 : class _CategoryRouteState -> _buildCategoryWidgets

Widget _buildCategoryWidgets() {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return CategoryTile(
category: _categories[index],
onTap: _onCategoryTap,
);
},
itemCount: _categories.length,
);
}


- Create Dart File

 : category_tile


- Add Code (category_tile.dart)

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'category.dart';

const double _kFlingVelocity = 2.0;

class _BackdropPanel extends StatelessWidget {
const _BackdropPanel({
Key key,
this.onTap,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.title,
this.child,
}) : super(key: key);

final VoidCallback onTap;
final GestureDragUpdateCallback onVerticalDragUpdate;
final GestureDragEndCallback onVerticalDragEnd;
final Widget title;
final Widget child;

@override
Widget build(BuildContext context) {
return Material(
elevation: 2.0,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onVerticalDragUpdate: onVerticalDragUpdate,
onVerticalDragEnd: onVerticalDragEnd,
onTap: onTap,
child: Container(
height: 48.0,
padding: EdgeInsetsDirectional.only(start: 16.0),
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: Theme.of(context).textTheme.subhead,
child: title,
),
),
),
Divider(
height: 1.0,
),
Expanded(
child: child,
),
],
),
);
}
}

class _BackdropTitle extends AnimatedWidget {
final Widget frontTitle;
final Widget backTitle;

const _BackdropTitle({
Key key,
Listenable listenable,
this.frontTitle,
this.backTitle,
}) : super(key: key, listenable: listenable);

@override
Widget build(BuildContext context) {
final Animation<double> animation = this.listenable;
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: Interval(0.5, 1.0),
).value,
child: backTitle,
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: Interval(0.5, 1.0),
).value,
child: frontTitle,
),
],
),
);
}
}

class Backdrop extends StatefulWidget {
final Category currentCategory;
final Widget frontPanel;
final Widget backPanel;
final Widget frontTitle;
final Widget backTitle;

const Backdrop({
@required this.currentCategory,
@required this.frontPanel,
@required this.backPanel,
@required this.frontTitle,
@required this.backTitle,
}) : assert(currentCategory != null),
assert(frontPanel != null),
assert(backPanel != null),
assert(frontTitle != null),
assert(backTitle != null);

@override
_BackdropState createState() => _BackdropState();
}

class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}

@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
setState(() {
_controller.fling(
velocity:
_backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity);
});
} else if (!_backdropPanelVisible) {
setState(() {
_controller.fling(velocity: _kFlingVelocity);
});
}
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

bool get _backdropPanelVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}

void _toggleBackdropPanelVisibility() {
FocusScope.of(context).requestFocus(FocusNode());
_controller.fling(
velocity: _backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity);
}

double get _backdropHeight {
final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
return renderBox.size.height;
}

void _handleDragUpdate(DragUpdateDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;

_controller.value -= details.primaryDelta / _backdropHeight;
}

void _handleDragEnd(DragEndDetails details) {
if (_controller.isAnimating ||
_controller.status == AnimationStatus.completed) return;

final double flingVelocity =
details.velocity.pixelsPerSecond.dy / _backdropHeight;
if (flingVelocity < 0.0)
_controller.fling(velocity: math.max(_kFlingVelocity, -flingVelocity));
else if (flingVelocity > 0.0)
_controller.fling(velocity: math.min(-_kFlingVelocity, -flingVelocity));
else
_controller.fling(
velocity:
_controller.value < 0.5 ? -_kFlingVelocity : _kFlingVelocity);
}

Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double panelTitleHeight = 48.0;
final Size panelSize = constraints.biggest;
final double panelTop = panelSize.height - panelTitleHeight;

Animation<RelativeRect> panelAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, panelTop, 0.0, panelTop - panelSize.height),
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);

return Container(
key: _backdropKey,
color: widget.currentCategory.color,
child: Stack(
children: <Widget>[
widget.backPanel,
PositionedTransition(
rect: panelAnimation,
child: _BackdropPanel(
onTap: _toggleBackdropPanelVisibility,
onVerticalDragUpdate: _handleDragUpdate,
onVerticalDragEnd: _handleDragEnd,
title: Text(widget.currentCategory.name),
child: widget.frontPanel,
),
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: widget.currentCategory.color,
elevation: 0.0,
leading: IconButton(
onPressed: _toggleBackdropPanelVisibility,
icon: AnimatedIcon(
icon: AnimatedIcons.close_menu,
progress: _controller.view,
),
),
title: _BackdropTitle(
listenable: _controller.view,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
),
body: LayoutBuilder(
builder: _buildStack,
),
resizeToAvoidBottomPadding: false,
);
}
}


Result]


8. Add responsive

- Add Code (category_route.dart)

 : class _CategoryRouteState -> build

assert(debugCheckHasMediaQuery(context));


- Change Code (category_route.dart)

 : class _CategoryRouteState -> build 

child: _buildCategoryWidgets(MediaQuery.of(context).orientation),


- Change Code (category_route.dart)

 : class _CategoryRouteState -> _buildCategoryWidgets

Widget _buildCategoryWidgets(Orientation deviceOrientation) {
if (deviceOrientation == Orientation.portrait) {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return CategoryTile(
category: _categories[index],
onTap: _onCategoryTap,
);
},
itemCount: _categories.length,
);
} else {
return GridView.count(
crossAxisCount: 2,
childAspectRatio: 3.0,
children: _categories.map((Category c) {
return CategoryTile(
category: c,
onTap: _onCategoryTap,
);
}).toList(),
);
}
}


- Change Code (converter_route.dart)

 : _ConverterRouteState

final _inputKey = GlobalKey(debugLabel: 'inputText');

- Add Code (converter_route.dart)

 : _ConverterRouteState -> build -> Padding -> Column -> TextField

key: _inputKey,


- Change Code (converter_route.dart)

 : _ConverterRouteState -> converter

final converter = ListView(
children: [
input,
arrows,
output,
],
);


- Change Code (converter_route.dart)

 : _ConverterRouteState -> build -> return Padding -> child

OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
if (orientation == Orientation.portrait) {
return converter;
} else {
return Center(
child: Container(
width: 450.0,
child: converter,
),
);
}
},
),


Result]


9. Add json units

- Add Code (pubspec.yaml)

 : flutter

assets:
- assets/regular_units.json


- Create Directory

 : assets



- Create json File

 : regular_units


- Add Code (regular_units.json)

{
"Time": [
{
"name": "Second",
"conversion": 1.0,
"base_unit": true
},
{
"name": "Millisecond",
"conversion": 1000.0
},
{
"name": "Minute",
"conversion": 0.0166666666666667
},
{
"name": "Hour",
"conversion": 0.0002777777777778
},
{
"name": "Day",
"conversion": 0.00001157407407
},
{
"name": "Week",
"conversion": 0.000001653439153
},
{
"name": "Fortnight",
"conversion": 0.000000826719576
}
],
"Length": [
{
"name": "Meter",
"conversion": 1.0,
"base_unit": true
},
{
"name": "Millimeter",
"conversion": 1000.0
},
{
"name": "Centimeter",
"conversion": 100.0
},
{
"name": "Kilometer",
"conversion": 0.001
},
{
"name": "Inch",
"conversion": 39.3701
},
{
"name": "Foot",
"conversion": 3.28084
},
{
"name": "Yard",
"conversion": 1.09361
},
{
"name": "Mile",
"conversion": 0.000621371
}
],
"Area": [
{
"name": "Meter squared",
"conversion": 1.0,
"base_unit": true
},
{
"name": "Kilometer squared",
"conversion": 0.000001
},
{
"name": "Square inch",
"conversion": 1550.0
},
{
"name": "Square foot",
"conversion": 10.7639
},
{
"name": "Square yard",
"conversion": 1.19599
},
{
"name": "Acre",
"conversion": 0.000247105
},
{
"name": "Hectare",
"conversion": 0.0001
}
],
"Volume": [
{
"name": "Cubic meter",
"conversion": 1.0,
"base_unit": true
},
{
"name": "US legal cup",
"conversion": 4166.67
},
{
"name": "US fluid ounce",
"conversion": 33814.0
},
{
"name": "US tablespoon",
"conversion": 67628.0
},
{
"name": "US teaspoon",
"conversion": 202884.0
},
{
"name": "Imperial cup",
"conversion": 3519.51
},
{
"name": "Imperial fluid ounce",
"conversion": 35195.1
},
{
"name": "Imperial tablespoon",
"conversion": 56312.13673
},
{
"name": "Imperial teaspoon",
"conversion": 168936.41019
}
],
"Mass": [
{
"name": "Kilogram",
"conversion": 1.0,
"base_unit": true
},
{
"name": "Gram",
"conversion": 1000.0
},
{
"name": "Ounce",
"conversion": 35.274
},
{
"name": "Pound",
"conversion": 2.20462
},
{
"name": "Stone",
"conversion": 0.157473
},
{
"name": "US ton",
"conversion": 0.00110231
},
{
"name": "Metric ton",
"conversion": 0.001
}
],
"Energy": [
{
"name": "Joule",
"conversion": 1.0,
"base_unit": true
},
{
"name": "Kilojoule",
"conversion": 0.001
},
{
"name": "Gram calorie",
"conversion": 0.239006
},
{
"name": "Kilocalorie (Calorie)",
"conversion": 0.000239006
},
{
"name": "Watt hour",
"conversion": 0.000277778
},
{
"name": "British thermal unit",
"conversion": 0.000947817
},
{
"name": "US therm",
"conversion": 0.0000000094804
}
],
"Digital Storage": [
{
"name": "Megabyte",
"conversion": 1.0,
"base_unit": true
},
{
"name": "Megabit",
"conversion": 8.0
},
{
"name": "Mebibyte",
"conversion": 0.953674
},
{
"name": "Byte",
"conversion": 1000000.0
},
{
"name": "Kilobyte",
"conversion": 1000.0
},
{
"name": "Gigabyte",
"conversion": 0.001
},
{
"name": "Terabyte",
"conversion": 0.000001
},
{
"name": "Petabyte",
"conversion": 0.000000001
}
]
}


- Add Code (category_route.dart)

 : import dart:async

 : import dart:convert

import 'dart:async';
import 'dart:convert';


- Remove Code (category_route.dart)

 : class _CategoryRouteState -> _categoryNamesinitState_retrieveUnitList


- Add Code (category_route.dart)

 : class _CategoryRouteState -> _retrieveLocalCategories

Future<void> _retrieveLocalCategories() async {
final json = DefaultAssetBundle
.of(context)
.loadString('assets/regular_units.json');
final data = JsonDecoder().convert(await json);
if (data is! Map) {
throw ('Data retrieved from API is not a Map');
}
var categoryIndex = 0;
data.keys.forEach((key) {
final List<Unit> units =
data[key].map<Unit>((dynamic data) => Unit.fromJson(data)).toList();

var category = Category(
name: key,
units: units,
color: _baseColors[categoryIndex],
iconLocation: Icons.timer,
);
setState(() {
if (categoryIndex == 0) {
_defaultCategory = category;
}
_categories.add(category);
});
categoryIndex += 1;
});
}


- Add Code (category_route.dart)

 : class _CategoryRouteState -> build


if (_categories.isEmpty) {
return Center(
child: Container(
height: 180.0,
width: 180.0,
child: CircularProgressIndicator(),
),
);
}


Result]


10. Add icons fonts

- Create Directory

 : assets -> icons


- Add image file (icons)

image_file.zip


- Add Code (pubspec.yaml)

 : flutter -> assets

- assets/regular_units.json
- assets/icons/area.png
- assets/icons/digital_storage.png
- assets/icons/length.png
- assets/icons/mass.png
- assets/icons/power.png
- assets/icons/time.png
- assets/icons/volume.png
- assets/icons/currency.png


- Change Code (category.dart)

 : class Category

final String iconLocation;


- Add Code (category_route.dart)

 : class _CategoryRouteState

static const _icons = <String>[
'assets/icons/time.png',
'assets/icons/length.png',
'assets/icons/area.png',
'assets/icons/volume.png',
'assets/icons/mass.png',
'assets/icons/power.png',
'assets/icons/digital_storage.png',
'assets/icons/currency.png',
];


- Change Code (category_route.dart)

 : class _CategoryRouteState -> _retrieveLocalCategories -> Category

iconLocation: _icons[categoryIndex],


- Change Code (category_tile.dart)

 : class _CategoryRouteState -> build -> InkWell -> Row -> Padding

child: Image.asset(category.iconLocation),


Result]


11. Make API

- Add Code (converter_route.dart)

 : import api.dart

import 'api.dart';


- Change Code (converter_route.dart)

 : class _CategoryRouteState -> _updateConversion

Future<void> _updateConversion() async {
if (widget.category.name == apiCategory['name']) {
final api = Api();
final conversion = await api.convert(apiCategory['route'],
_inputValue.toString(), _fromValue.name, _toValue.name);
setState(() {
_convertedValue = _format(conversion);
});
} else {
setState(() {
_convertedValue = _format(
_inputValue * (_toValue.conversion / _fromValue.conversion));
});
}
}


- Create Dart File

 : api


- Add Code (api.dart)

import 'dart:async';
import 'dart:convert' show json, utf8;
import 'dart:io';

const apiCategory = {
'name': 'Currency',
'route': 'currency',
};

class Api {
final HttpClient _httpClient = HttpClient();
final String _url = 'flutter.udacity.com';

Future<List> getUnits(String category) async {
final uri = Uri.https(_url, '/$category');
final jsonResponse = await _getJson(uri);
if (jsonResponse == null || jsonResponse['units'] == null) {
print('Error retrieving units.');
return null;
}
return jsonResponse['units'];
}

Future<double> convert(
String category, String amount, String fromUnit, String toUnit) async {
final uri = Uri.https(_url, '/$category/convert',
{'amount': amount, 'from': fromUnit, 'to': toUnit});
final jsonResponse = await _getJson(uri);
if (jsonResponse == null || jsonResponse['status'] == null) {
print('Error retrieving conversion.');
return null;
} else if (jsonResponse['status'] == 'error') {
print(jsonResponse['message']);
return null;
}
return jsonResponse['conversion'].toDouble();
}

Future<Map<String, dynamic>> _getJson(Uri uri) async {
try {
final httpRequest = await _httpClient.getUrl(uri);
final httpResponse = await httpRequest.close();
if (httpResponse.statusCode != HttpStatus.OK) {
return null;
}
final responseBody = await httpResponse.transform(utf8.decoder).join();
return json.decode(responseBody);
} on Exception catch (e) {
print('$e');
return null;
}
}
}


- Add Code (category_route.dart)

 : class _CategoryRouteState -> didChangeDependencies -> if

await _retrieveApiCategory();


- Add Code (category_route.dart)

 : class _CategoryRouteState

Future<void> _retrieveApiCategory() async {
setState(() {
_categories.add(Category(
name: apiCategory['name'],
units: [],
color: _baseColors.last,
iconLocation: _icons.last,
));
});
final api = Api();
final jsonUnits = await api.getUnits(apiCategory['route']);
if (jsonUnits != null) {
final units = <Unit>[];
for (var unit in jsonUnits) {
units.add(Unit.fromJson(unit));
}
setState(() {
_categories.removeLast();
_categories.add(Category(
name: apiCategory['name'],
units: units,
color: _baseColors.last,
iconLocation: _icons.last,
));
});
}
}


Result]


12. Error

- Add Code (converter_route.dart)

 : class _ConverterRouteState -> setState

bool _showErrorUI = false;


- Change Code (category_route.dart)

 : class _CategoryRouteState -> _updateConversion

if (conversion == null) {
setState(() {
_showErrorUI = true;
});
} else {
setState(() {
_showErrorUI = false;
_convertedValue = _format(conversion);
});
}


- Add Code (category_route.dart)

 : class _CategoryRouteState -> build

if (widget.category.units == null ||
(widget.category.name == apiCategory['name'] && _showErrorUI)) {
return SingleChildScrollView(
child: Container(
margin: _padding,
padding: _padding,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: widget.category.color['error'],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 180.0,
color: Colors.white,
),
Text(
"Oh no! We can't connect right now!",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline.copyWith(
color: Colors.white,
),
),
],
),
),
);
}


- Change Code (category_tile.dart)

 : class CategoryTile-> category_tile

const CategoryTile({
Key key,
@required this.category,
this.onTap,
}) : assert(category != null),
super(key: key);


- Change Code (category_route.dart)

 : class _CategoryRouteState -> _buildCategoryWidgets ->ListView.builder -> CategoryTile

category: _category,
onTap:
_category.name == apiCategory['name'] && _category.units.isEmpty
? null
: _onCategoryTap,


Result]


# Example

Result]



refer to Link :

https://www.udacity.com/