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');
: _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 -> _categoryNames, initState, _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)
- 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 :
'Study > Flutter' 카테고리의 다른 글
Flutter] Just Do it! (Basic Series 4 Login) #yet (0) | 2019.04.21 |
---|---|
Flutter] Just Do it! (Basic Series 3 Whatsapp) (0) | 2019.04.17 |
Flutter] Just Do it! (Basic Series 1-2 Navigates The Favorited) (0) | 2019.03.16 |
Flutter] Just Do it! (Basic Series 1-1 Infinite Scrolling ListView) (0) | 2019.03.14 |
Flutter Install (ft.Android Studio) (0) | 2019.03.09 |