1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-11 07:18:15 +00:00

Adding Rally App to Flutter Samples (#135)

This commit is contained in:
lisa-liao
2019-09-05 15:36:20 -04:00
committed by Andrew Brogdon
parent 3348c2f2dd
commit c056b754a2
31 changed files with 2348 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/colors.dart';
import 'package:rally/home.dart';
import 'package:rally/login.dart';
/// The RallyApp is a MaterialApp with a theme and 2 routes.
///
/// The home route is the main page with tabs for sub pages.
/// The login route is the initial route.
class RallyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rally',
theme: _buildRallyTheme(),
home: HomePage(),
initialRoute: '/login',
routes: {'/login': (context) => LoginPage()},
);
}
ThemeData _buildRallyTheme() {
final ThemeData base = ThemeData.dark();
return ThemeData(
scaffoldBackgroundColor: RallyColors.primaryBackground,
primaryColor: RallyColors.primaryBackground,
textTheme: _buildRallyTextTheme(base.textTheme),
inputDecorationTheme: InputDecorationTheme(
labelStyle:
TextStyle(color: RallyColors.gray, fontWeight: FontWeight.w500),
filled: true,
fillColor: RallyColors.inputBackground,
focusedBorder: InputBorder.none,
),
);
}
TextTheme _buildRallyTextTheme(TextTheme base) {
return base
.copyWith(
body1: base.body1.copyWith(
fontFamily: 'Roboto Condensed',
fontSize: 14,
fontWeight: FontWeight.w400,
),
body2: base.body2.copyWith(
fontFamily: 'Eczar',
fontSize: 40,
fontWeight: FontWeight.w400,
letterSpacing: 1.4,
),
button: base.button.copyWith(
fontFamily: 'Roboto Condensed',
fontWeight: FontWeight.w700,
letterSpacing: 2.8,
),
headline: base.body2.copyWith(
fontFamily: 'Eczar',
fontSize: 40,
fontWeight: FontWeight.w600,
letterSpacing: 1.4,
),
)
.apply(
displayColor: Colors.white,
bodyColor: Colors.white,
);
}
}

View File

@@ -0,0 +1,193 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/colors.dart';
import 'package:rally/data.dart';
class RallyLineChart extends StatelessWidget {
RallyLineChart({this.events = const []}) : assert(events != null);
final List<DetailedEventData> events;
@override
Widget build(BuildContext context) {
return CustomPaint(painter: RallyLineChartPainter(context, events));
}
}
class RallyLineChartPainter extends CustomPainter {
RallyLineChartPainter(this.context, this.events);
final BuildContext context;
// Events to plot on the line as points.
final List<DetailedEventData> events;
// Number of days to plot.
// This is hardcoded to reflect the dummy data, but would be dynamic in a real
// app.
final int numDays = 52;
// Beginning of window. The end is this plus numDays.
// This is hardcoded to reflect the dummy data, but would be dynamic in a real
// app.
final DateTime startDate = DateTime.utc(2018, 12, 1);
// Ranges uses to lerp the pixel points.
// This is hardcoded to reflect the dummy data, but would be dynamic in a real
// app.
final double maxAmount = 3000.0; // minAmount is assumed to be 0.0
// The number of milliseconds in a day. This is the inherit period fot the
// points in this line.
static const int millisInDay = 24 * 60 * 60 * 1000;
// Amount to shift the tick drawing by so that the sunday ticks do not start
// on the edge.
final int tickShift = 3;
// Arbitrary unit of space for absolute positioned painting.
final double space = 16.0;
@override
void paint(Canvas canvas, Size size) {
double ticksTop = size.height - space * 5;
double labelsTop = size.height - space * 2;
_drawLine(
canvas,
Rect.fromLTWH(0.0, 0.0, size.width, ticksTop),
);
_drawXAxisTicks(
canvas,
Rect.fromLTWH(0.0, ticksTop, size.width, labelsTop - ticksTop),
);
_drawXAxisLabels(
canvas,
Rect.fromLTWH(0.0, labelsTop, size.width, size.height - labelsTop),
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
void _drawLine(Canvas canvas, Rect rect) {
final linePaint = Paint()
..color = RallyColors.accountColor(2)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
// Arbitrary value for the first point. In a real app, a wider range of
// points would be used that go beyond the boundaries of the screen.
double lastAmount = 800.0;
// Try changing this value between 1, 7, 15, etc.
int smoothing = 7;
// Align the points with equal deltas (1 day) as a cumulative sum.
int startMillis = startDate.millisecondsSinceEpoch;
final points = [
Offset(0.0, (maxAmount - lastAmount) / maxAmount * rect.height)
];
for (int i = 0; i < numDays + smoothing; i++) {
int endMillis = startMillis + millisInDay * 1;
final filteredEvents = events.where((e) {
return startMillis <= e.date.millisecondsSinceEpoch &&
e.date.millisecondsSinceEpoch <= endMillis;
}).toList();
lastAmount += filteredEvents.fold<num>(0.0, (sum, e) => sum + e.amount);
double x = i / numDays * rect.width;
double y = (maxAmount - lastAmount) / maxAmount * rect.height;
points.add(Offset(x, y));
startMillis = endMillis;
}
final Path path = Path();
path.moveTo(points[0].dx, points[0].dy);
for (int i = 1; i < points.length - smoothing; i += smoothing) {
double x1 = points[i].dx;
double y1 = points[i].dy;
double x2 = (x1 + points[i + smoothing].dx) / 2;
double y2 = (y1 + points[i + smoothing].dy) / 2;
path.quadraticBezierTo(x1, y1, x2, y2);
}
canvas.drawPath(path, linePaint);
}
/// Draw the X-axis increment markers at constant width intervals.
void _drawXAxisTicks(Canvas canvas, Rect rect) {
double dayTop = (rect.top + rect.bottom) / 2;
for (int i = 0; i < numDays; i++) {
double x = rect.width / numDays * i;
canvas.drawRect(
Rect.fromPoints(
Offset(x, i % 7 == tickShift ? rect.top : dayTop),
Offset(x, rect.bottom),
),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = RallyColors.gray25,
);
}
}
/// Set X-axis labels under the X-axis increment markers.
void _drawXAxisLabels(Canvas canvas, Rect rect) {
final selectedLabelStyle = Theme.of(context).textTheme.body1.copyWith(
fontWeight: FontWeight.w700,
);
final unselectedLabelStyle = Theme.of(context).textTheme.body1.copyWith(
fontWeight: FontWeight.w700,
color: RallyColors.gray25,
);
final leftLabel = TextPainter(
text: TextSpan(
text: 'AUGUST 2019',
style: unselectedLabelStyle,
),
textDirection: TextDirection.ltr,
);
leftLabel.layout();
leftLabel.paint(canvas, Offset(rect.left + space / 2, rect.center.dy));
final centerLabel = TextPainter(
text: TextSpan(text: 'SEPTEMBER 2019', style: selectedLabelStyle),
textDirection: TextDirection.ltr,
);
centerLabel.layout();
final double x = (rect.width - centerLabel.width) / 2;
final double y = rect.center.dy;
centerLabel.paint(canvas, Offset(x, y));
final rightLabel = TextPainter(
text: TextSpan(
text: 'OCTOBER 2019',
style: unselectedLabelStyle,
),
textDirection: TextDirection.ltr,
);
rightLabel.layout();
rightLabel.paint(
canvas,
Offset(rect.right - centerLabel.width - space / 2, rect.center.dy),
);
}
}

View File

@@ -0,0 +1,239 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/colors.dart';
import 'package:rally/data.dart';
import 'package:rally/formatters.dart';
/// A colored piece of the [RallyPieChart].
class RallyPieChartSegment {
final Color color;
final double value;
const RallyPieChartSegment({this.color, this.value});
}
List<RallyPieChartSegment> buildSegmentsFromAccountItems(
List<AccountData> items) {
return List<RallyPieChartSegment>.generate(
items.length,
(i) => RallyPieChartSegment(
color: RallyColors.accountColor(i),
value: items[i].primaryAmount,
),
);
}
List<RallyPieChartSegment> buildSegmentsFromBillItems(List<BillData> items) {
return List<RallyPieChartSegment>.generate(
items.length,
(i) => RallyPieChartSegment(
color: RallyColors.billColor(i),
value: items[i].primaryAmount,
),
);
}
List<RallyPieChartSegment> buildSegmentsFromBudgetItems(
List<BudgetData> items) {
return List<RallyPieChartSegment>.generate(
items.length,
(i) => RallyPieChartSegment(
color: RallyColors.budgetColor(i),
value: items[i].primaryAmount - items[i].amountUsed,
),
);
}
/// An animated circular pie chart to represent pieces of a whole, which can
/// have empty space.
class RallyPieChart extends StatefulWidget {
RallyPieChart(
{this.heroLabel, this.heroAmount, this.wholeAmount, this.segments});
final String heroLabel;
final double heroAmount;
final double wholeAmount;
final List<RallyPieChartSegment> segments;
_RallyPieChartState createState() => _RallyPieChartState();
}
class _RallyPieChartState extends State<RallyPieChart>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 600), vsync: this);
animation = CurvedAnimation(
parent: TweenSequence(<TweenSequenceItem<double>>[
TweenSequenceItem(tween: Tween(begin: 0.0, end: 0.0), weight: 1.0),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.5),
]).animate(controller),
curve: Curves.decelerate);
controller.forward();
}
dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return _AnimatedRallyPieChart(
animation: animation,
centerLabel: widget.heroLabel,
centerAmount: widget.heroAmount,
total: widget.wholeAmount,
segments: widget.segments,
);
}
}
class _AnimatedRallyPieChart extends AnimatedWidget {
_AnimatedRallyPieChart({
Key key,
this.animation,
this.centerLabel,
this.centerAmount,
this.total,
this.segments,
}) : super(key: key, listenable: animation);
final Animation<double> animation;
final String centerLabel;
final double centerAmount;
final double total;
final List<RallyPieChartSegment> segments;
Widget build(BuildContext context) {
final labelTextStyle = Theme.of(context)
.textTheme
.body1
.copyWith(fontSize: 14.0, letterSpacing: 0.5);
return DecoratedBox(
decoration: _RallyPieChartOutlineDecoration(
maxFraction: animation.value, total: total, segments: segments),
child: SizedBox(
height: 300.0,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
centerLabel,
style: labelTextStyle,
),
Text(
Formatters.usdWithSign.format(centerAmount),
style: Theme.of(context).textTheme.headline,
),
],
),
),
),
);
}
}
class _RallyPieChartOutlineDecoration extends Decoration {
_RallyPieChartOutlineDecoration(
{this.maxFraction, this.total, this.segments});
final double maxFraction;
final double total;
final List<RallyPieChartSegment> segments;
@override
BoxPainter createBoxPainter([onChanged]) {
return _RallyPieChartOutlineBoxPainter(
maxFraction: maxFraction,
wholeAmount: total,
segments: segments,
);
}
}
class _RallyPieChartOutlineBoxPainter extends BoxPainter {
_RallyPieChartOutlineBoxPainter(
{this.maxFraction, this.wholeAmount, this.segments});
final double maxFraction;
final double wholeAmount;
final List<RallyPieChartSegment> segments;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
// Create two padded rects to draw arcs in: one for colored arcs and one for
// inner bg arc.
const double strokeWidth = 4.0;
final outerRadius =
min(configuration.size.width, configuration.size.height) / 2;
final outerRect = Rect.fromCircle(
center: configuration.size.center(Offset.zero),
radius: outerRadius - strokeWidth * 3.0);
final innerRect = Rect.fromCircle(
center: configuration.size.center(Offset.zero),
radius: outerRadius - strokeWidth * 4.0);
// Paint each arc with spacing.
double cummulativeSpace = 0.0;
double cummulativeTotal = 0.0;
const double wholeRadians = (2.0 * pi);
const double spaceRadians = wholeRadians / 180.0;
final wholeMinusSpacesRadians =
wholeRadians - (segments.length * spaceRadians);
for (RallyPieChartSegment segment in segments) {
final paint = Paint()..color = segment.color;
final start = maxFraction *
((cummulativeTotal / wholeAmount * wholeMinusSpacesRadians) +
cummulativeSpace) -
pi / 2.0;
final sweep =
maxFraction * (segment.value / wholeAmount * wholeMinusSpacesRadians);
canvas.drawArc(outerRect, start, sweep, true, paint);
cummulativeTotal += segment.value;
cummulativeSpace += spaceRadians;
}
// Paint any remaining space black (e.g. budget amount remaining).
double remaining = wholeAmount - cummulativeTotal;
if (remaining > 0) {
final paint = Paint()..color = Colors.black;
final start = maxFraction *
((cummulativeTotal / wholeAmount * wholeMinusSpacesRadians) +
spaceRadians * segments.length) -
pi / 2.0;
final sweep = maxFraction *
(remaining / wholeAmount * wholeMinusSpacesRadians - spaceRadians);
canvas.drawArc(outerRect, start, sweep, true, paint);
}
// Paint a smaller inner circle to cover the painted arcs, so they are
// display as segments.
Paint bgPaint = Paint()..color = RallyColors.primaryBackground;
canvas.drawArc(innerRect, 0.0, 2.0 * pi, true, bgPaint);
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class VerticalFractionBar extends StatelessWidget {
VerticalFractionBar({this.color, this.fraction});
final Color color;
final double fraction;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32.0,
width: 4.0,
child: Column(
children: [
SizedBox(
height: (1 - fraction) * 32.0,
child: Container(
color: Colors.black,
),
),
SizedBox(
height: fraction * 32.0,
child: Container(color: color),
),
],
),
);
}
}

View File

@@ -0,0 +1,69 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
/// Most color assignments in Rally are not like the the typical color
/// assignments that are common in other apps. Instead of primarily mapping to
/// component type and part, they are assigned round robin based on layout.
class RallyColors {
static const accountColors = [
Color(0xFF005D57),
Color(0xFF04B97F),
Color(0xFF37EFBA),
Color(0xFF007D51),
];
static const billColors = [
Color(0xFFFFDC78),
Color(0xFFFF6951),
Color(0xFFFFD7D0),
Color(0xFFFFAC12),
];
static const budgetColors = [
Color(0xFFB2F2FF),
Color(0xFFB15DFF),
Color(0xFF72DEFF),
Color(0xFF0082FB),
];
static const gray = Color(0xFFD8D8D8);
static const gray60 = Color(0x99D8D8D8);
static const gray25 = Color(0x40D8D8D8);
static const white60 = Color(0x99FFFFFF);
static const primaryBackground = Color(0xFF33333D);
static const inputBackground = Color(0xFF26282F);
static const cardBackground = Color(0x03FEFEFE);
/// Convenience method to get a single account color with position i.
static Color accountColor(int i) {
return cycledColor(accountColors, i);
}
/// Convenience method to get a single bill color with position i.
static Color billColor(int i) {
return cycledColor(billColors, i);
}
/// Convenience method to get a single budget color with position i.
static Color budgetColor(int i) {
return cycledColor(budgetColors, i);
}
/// Gets a color from a list that is considered to be infinitely repeating.
static Color cycledColor(List<Color> colors, int i) {
return colors[i % colors.length];
}
}

View File

@@ -0,0 +1,223 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Calculates the sum of the primary amounts of a list of [AccountData].
double sumAccountDataPrimaryAmount(List<AccountData> items) {
return items.fold(
0,
(sum, next) => sum + next.primaryAmount,
);
}
/// Calculates the sum of the primary amounts of a list of [BillData].
double sumBillDataPrimaryAmount(List<BillData> items) {
return items.fold(
0,
(sum, next) => sum + next.primaryAmount,
);
}
/// Calculates the sum of the primary amounts of a list of [BudgetData].
double sumBudgetDataPrimaryAmount(List<BudgetData> items) {
return items.fold(
0,
(sum, next) => sum + next.primaryAmount,
);
}
/// Calculates the sum of the amounts used of a list of [BudgetData].
double sumBudgetDataAmountUsed(List<BudgetData> items) {
return items.fold(
0.0,
(sum, next) => sum + next.amountUsed,
);
}
/// A data model for an account.
///
/// The [primaryAmount] is the balance of the account in USD.
class AccountData {
const AccountData({this.name, this.primaryAmount, this.accountNumber});
/// The display name of this entity.
final String name;
// The primary amount or value of this entity.
final double primaryAmount;
/// The full displayable account number.
final String accountNumber;
}
/// A data model for a bill.
///
/// The [primaryAmount] is the amount due in USD.
class BillData {
const BillData({this.name, this.primaryAmount, this.dueDate});
/// The display name of this entity.
final String name;
// The primary amount or value of this entity.
final double primaryAmount;
/// The due date of this bill.
final String dueDate;
}
/// A data model for a budget.
///
/// The [primaryAmount] is the budget cap in USD.
class BudgetData {
const BudgetData({this.name, this.primaryAmount, this.amountUsed});
/// The display name of this entity.
final String name;
// The primary amount or value of this entity.
final double primaryAmount;
/// Amount of the budget that is consumed or used.
final double amountUsed;
}
class DetailedEventData {
const DetailedEventData({
this.title,
this.date,
this.amount,
});
final String title;
final DateTime date;
final double amount;
}
/// Class to return dummy data lists.
///
/// In a real app, this might be replaced with some asynchronous service.
class DummyDataService {
static List<AccountData> getAccountDataList() {
return [
AccountData(
name: 'Checking',
primaryAmount: 2215.13,
accountNumber: '1234561234',
),
AccountData(
name: 'Home Savings',
primaryAmount: 8678.88,
accountNumber: '8888885678',
),
AccountData(
name: 'Car Savings',
primaryAmount: 987.48,
accountNumber: '8888889012',
),
AccountData(
name: 'Vacation',
primaryAmount: 253.0,
accountNumber: '1231233456',
),
];
}
static List<DetailedEventData> getDetailedEventItems() {
return [
DetailedEventData(
title: 'Genoe', date: DateTime.utc(2019, 1, 24), amount: -16.54),
DetailedEventData(
title: 'Fortnightly Subscribe',
date: DateTime.utc(2019, 1, 5),
amount: -12.54),
DetailedEventData(
title: 'Circle Cash', date: DateTime.utc(2019, 1, 5), amount: 365.65),
DetailedEventData(
title: 'Crane Hospitality',
date: DateTime.utc(2019, 1, 4),
amount: -705.13),
DetailedEventData(
title: 'ABC Payroll',
date: DateTime.utc(2018, 12, 15),
amount: 1141.43),
DetailedEventData(
title: 'Shrine', date: DateTime.utc(2018, 12, 15), amount: -88.88),
DetailedEventData(
title: 'Foodmates', date: DateTime.utc(2018, 12, 4), amount: -11.69),
];
}
static List<BillData> getBillDataList() {
return [
BillData(
name: 'RedPay Credit',
primaryAmount: 45.36,
dueDate: 'Jan 29',
),
BillData(
name: 'Rent',
primaryAmount: 1200.0,
dueDate: 'Feb 9',
),
BillData(
name: 'TabFine Credit',
primaryAmount: 87.33,
dueDate: 'Feb 22',
),
BillData(
name: 'ABC Loans',
primaryAmount: 400.0,
dueDate: 'Feb 29',
),
];
}
static List<BudgetData> getBudgetDataList() {
return [
BudgetData(
name: 'Coffee Shops',
primaryAmount: 70.0,
amountUsed: 45.49,
),
BudgetData(
name: 'Groceries',
primaryAmount: 170.0,
amountUsed: 16.45,
),
BudgetData(
name: 'Restaurants',
primaryAmount: 170.0,
amountUsed: 123.25,
),
BudgetData(
name: 'Clothing',
primaryAmount: 70.0,
amountUsed: 19.45,
),
];
}
static List<String> getSettingsTitles() {
return [
'Manage Accounts',
'Tax Documents',
'Passcode and Touch ID',
'Notifications',
'Personal Information',
'Paperless Settings',
'Find ATMs',
'Help',
];
}
}

View File

@@ -0,0 +1,344 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/charts/pie_chart.dart';
import 'package:rally/charts/line_chart.dart';
import 'package:rally/charts/vertical_fraction_bar.dart';
import 'package:rally/colors.dart';
import 'package:rally/data.dart';
import 'package:rally/formatters.dart';
class FinancialEntityView extends StatelessWidget {
FinancialEntityView({
this.heroLabel,
this.heroAmount,
this.wholeAmount,
this.segments,
this.financialEntityCards,
}) : assert(segments.length == financialEntityCards.length);
/// The amounts to assign each item.
///
/// This list must have the same length as [colors].
final List<RallyPieChartSegment> segments;
final String heroLabel;
final double heroAmount;
final double wholeAmount;
final List<FinancialEntityCategoryView> financialEntityCards;
@override
Widget build(BuildContext context) {
return Column(
children: [
RallyPieChart(
heroLabel: heroLabel,
heroAmount: heroAmount,
wholeAmount: wholeAmount,
segments: segments,
),
SizedBox(
height: 1.0,
child: Container(
color: Color(0xA026282F),
),
),
ListView(shrinkWrap: true, children: financialEntityCards)
],
);
}
}
/// A reusable widget to show balance information of a single entity as a card.
class FinancialEntityCategoryView extends StatelessWidget {
const FinancialEntityCategoryView({
@required this.indicatorColor,
@required this.indicatorFraction,
@required this.title,
@required this.subtitle,
@required this.amount,
@required this.suffix,
});
final Color indicatorColor;
final double indicatorFraction;
final String title;
final String subtitle;
final double amount;
final Widget suffix;
@override
Widget build(BuildContext context) {
return FlatButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<FinancialEntityCategoryDetailsPage>(
builder: (context) => FinancialEntityCategoryDetailsPage(),
),
);
},
child: SizedBox(
height: 68,
child: Column(
children: [
Expanded(
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12, right: 12),
child: VerticalFractionBar(
color: indicatorColor,
fraction: indicatorFraction,
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context)
.textTheme
.body1
.copyWith(fontSize: 16.0),
),
Text(
subtitle,
style: Theme.of(context)
.textTheme
.body1
.copyWith(color: RallyColors.gray60),
),
],
),
Spacer(),
Text(
'\$ ' + Formatters.usd.format(amount),
style: Theme.of(context)
.textTheme
.body2
.copyWith(fontSize: 20.0, color: RallyColors.gray),
),
SizedBox(width: 32.0, child: suffix),
],
),
),
Divider(
height: 1,
indent: 16,
endIndent: 16,
color: Color(0xAA282828),
),
],
),
),
);
}
}
/// Data model for [FinancialEntityCategoryView].
class FinancialEntityCategoryModel {
final Color indicatorColor;
final double indicatorFraction;
final String title;
final String subtitle;
final double usdAmount;
final Widget suffix;
const FinancialEntityCategoryModel(
this.indicatorColor,
this.indicatorFraction,
this.title,
this.subtitle,
this.usdAmount,
this.suffix,
);
}
FinancialEntityCategoryView buildFinancialEntityFromAccountData(
AccountData model,
int i,
) {
return FinancialEntityCategoryView(
suffix: Icon(Icons.chevron_right, color: Colors.grey),
title: model.name,
subtitle: '• • • • • • ${model.accountNumber.substring(6)}',
indicatorColor: RallyColors.accountColor(i),
indicatorFraction: 1.0,
amount: model.primaryAmount,
);
}
FinancialEntityCategoryView buildFinancialEntityFromBillData(
BillData model,
int i,
) {
return FinancialEntityCategoryView(
suffix: Icon(Icons.chevron_right, color: Colors.grey),
title: model.name,
subtitle: model.dueDate,
indicatorColor: RallyColors.billColor(i),
indicatorFraction: 1.0,
amount: model.primaryAmount,
);
}
FinancialEntityCategoryView buildFinancialEntityFromBudgetData(
BudgetData item,
int i,
BuildContext context,
) {
return FinancialEntityCategoryView(
suffix: Text(' LEFT',
style: Theme.of(context)
.textTheme
.body1
.copyWith(color: RallyColors.gray60, fontSize: 10.0)),
title: item.name,
subtitle: Formatters.usdWithSign.format(item.amountUsed) +
' / ' +
Formatters.usdWithSign.format(item.primaryAmount),
indicatorColor: RallyColors.budgetColor(i),
indicatorFraction: item.amountUsed / item.primaryAmount,
amount: item.primaryAmount - item.amountUsed,
);
}
List<FinancialEntityCategoryView> buildAccountDataListViews(
List<AccountData> items) {
return List<FinancialEntityCategoryView>.generate(
items.length, (i) => buildFinancialEntityFromAccountData(items[i], i));
}
List<FinancialEntityCategoryView> buildBillDataListViews(List<BillData> items) {
return List<FinancialEntityCategoryView>.generate(
items.length, (i) => buildFinancialEntityFromBillData(items[i], i));
}
List<FinancialEntityCategoryView> buildBudgetDataListViews(
List<BudgetData> items, BuildContext context) {
return [
for (var i = 0; i < items.length; i++)
buildFinancialEntityFromBudgetData(items[i], i, context)
];
}
class FinancialEntityCategoryDetailsPage extends StatelessWidget {
final List<DetailedEventData> items =
DummyDataService.getDetailedEventItems();
@override
Widget build(BuildContext context) {
final List<_DetailedEventCard> cards = items
.map((i) => _DetailedEventCard(
title: i.title,
subtitle: Formatters.date.format(i.date),
amount: i.amount,
))
.toList();
return Scaffold(
appBar: AppBar(
elevation: 0.0,
centerTitle: true,
title: Text(
'Checking',
style: Theme.of(context).textTheme.body1.copyWith(fontSize: 18.0),
),
),
body: Column(children: [
SizedBox(
height: 200.0,
width: double.infinity,
child: RallyLineChart(events: items)),
Flexible(
child: ListView(shrinkWrap: true, children: cards),
)
]),
);
}
}
class _DetailedEventCard extends StatelessWidget {
const _DetailedEventCard({
@required this.title,
@required this.subtitle,
@required this.amount,
});
final String title;
final String subtitle;
final double amount;
@override
Widget build(BuildContext context) {
return FlatButton(
onPressed: () {},
child: SizedBox(
height: 68.0,
child: Column(
children: [
SizedBox(
height: 67.0,
child: Row(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context)
.textTheme
.body1
.copyWith(fontSize: 16.0),
),
Text(
subtitle,
style: Theme.of(context)
.textTheme
.body1
.copyWith(color: RallyColors.gray60),
)
],
),
Spacer(),
Text(
'\$${Formatters.usd.format(amount)}',
style: Theme.of(context)
.textTheme
.body2
.copyWith(fontSize: 20.0, color: RallyColors.gray),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
height: 1.0,
child: Container(
color: Color(0xAA282828),
),
),
)
],
),
),
);
}
}

View File

@@ -0,0 +1,21 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:intl/intl.dart';
class Formatters {
static final NumberFormat usd = NumberFormat.currency(name: '');
static final NumberFormat usdWithSign = NumberFormat.currency(name: '\$');
static final DateFormat date = DateFormat('MM-dd-yy');
}

View File

@@ -0,0 +1,221 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/tabs/accounts.dart';
import 'package:rally/tabs/bills.dart';
import 'package:rally/tabs/budgets.dart';
import 'package:rally/tabs/overview.dart';
import 'package:rally/tabs/settings.dart';
const int tabCount = 5;
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
TabController _tabController;
_HomePageState() {
_tabController = TabController(length: tabCount, vsync: this);
}
@override
void initState() {
super.initState();
print('_HomePageState initState');
_tabController.addListener(() {
if (_tabController.indexIsChanging &&
_tabController.previousIndex != _tabController.index) {
setState(() {});
}
});
}
@override
dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
body: SafeArea(
child: Column(
children: [
Theme(
// This theme effectively removes the default visual touch
// feedback for tapping a tab, which is replaced with a custom
// animation.
data: theme.copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: TabBar(
// Setting isScrollable to true prevents the tabs from being
// wrapped in [Expanded] widgets, which allows for more
// flexible sizes and size animations among tabs.
isScrollable: true,
labelPadding: EdgeInsets.zero,
tabs: _buildTabs(theme),
controller: _tabController,
// This removes the tab indicator.
indicatorColor: Colors.transparent,
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: _buildTabViews(),
),
)
],
),
),
);
}
List<Widget> _buildTabs(ThemeData theme) {
return <Widget>[
_buildTab(theme, Icons.pie_chart, 'OVERVIEW', 0),
_buildTab(theme, Icons.attach_money, 'ACCOUNTS', 1),
_buildTab(theme, Icons.money_off, 'BILLS', 2),
_buildTab(theme, Icons.table_chart, 'BUDGETS', 3),
_buildTab(theme, Icons.settings, 'SETTINGS', 4),
];
}
List<Widget> _buildTabViews() {
return [
OverviewView(),
AccountsView(),
BillsView(),
BudgetsView(),
SettingsView(),
];
}
Widget _buildTab(
ThemeData theme,
IconData iconData,
String title,
int index,
) {
return _RallyTab(
theme.textTheme.button,
Icon(iconData),
title,
_tabController.index == index,
);
}
}
class _RallyTab extends StatefulWidget {
final TextStyle style;
final Text titleText;
final Icon icon;
final bool isExpanded;
_RallyTab(TextStyle style, Icon icon, String title, bool isExpanded)
: this.style = style,
this.titleText = Text(title, style: style),
this.icon = icon,
this.isExpanded = isExpanded;
_RallyTabState createState() => _RallyTabState();
}
class _RallyTabState extends State<_RallyTab>
with SingleTickerProviderStateMixin {
Animation<double> _titleSizeAnimation;
Animation<double> _titleFadeAnimation;
Animation<double> _iconFadeAnimation;
AnimationController _controller;
@override
initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 200),
vsync: this,
);
_titleSizeAnimation = _controller.view;
_titleFadeAnimation = _controller.drive(CurveTween(curve: Curves.easeOut));
_iconFadeAnimation = _controller.drive(Tween(begin: 0.6, end: 1));
if (widget.isExpanded) {
_controller.value = 1.0;
}
}
@override
void didUpdateWidget(_RallyTab oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isExpanded) {
_controller.forward();
} else {
_controller.reverse();
}
}
Widget build(BuildContext context) {
// Calculate the width of each unexpanded tab by counting the number of
// units and dividing it into the screen width. Each unexpanded tab is 1
// unit, and there is always 1 expanded tab which is 1 unit + any extra
// space determined by the multiplier.
final double width = MediaQuery.of(context).size.width;
final double expandedTitleWidthMultiplier = 2;
final double unitWidth = width / (tabCount + expandedTitleWidthMultiplier);
return SizedBox(
height: 56,
child: Row(
children: <Widget>[
FadeTransition(
child: SizedBox(
width: unitWidth,
child: widget.icon,
),
opacity: _iconFadeAnimation,
),
FadeTransition(
child: SizeTransition(
child: SizedBox(
width: unitWidth * expandedTitleWidthMultiplier,
child: Center(child: widget.titleText),
),
axis: Axis.horizontal,
axisAlignment: -1,
sizeFactor: _titleSizeAnimation,
),
opacity: _titleFadeAnimation,
),
],
),
);
}
@override
dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,68 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 24),
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 64),
child: SizedBox(
height: 160,
child: Image.asset('assets/logo.png'),
),
),
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username',
),
),
SizedBox(height: 12),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
),
obscureText: true,
),
SizedBox(
height: 120,
child: Image.asset('assets/thumb.png'),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:rally/app.dart';
void main() => runApp(RallyApp());

View File

@@ -0,0 +1,37 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/data.dart';
import 'package:rally/finance.dart';
import 'package:rally/charts/pie_chart.dart';
/// A page that shows a summary of accounts.
class AccountsView extends StatelessWidget {
final List<AccountData> items = DummyDataService.getAccountDataList();
@override
Widget build(BuildContext context) {
double balanceTotal = sumAccountDataPrimaryAmount(items);
return FinancialEntityView(
heroLabel: 'Total',
heroAmount: balanceTotal,
segments: buildSegmentsFromAccountItems(items),
wholeAmount: balanceTotal,
financialEntityCards: buildAccountDataListViews(items),
);
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/data.dart';
import 'package:rally/finance.dart';
import 'package:rally/charts/pie_chart.dart';
/// A page that shows a summary of bills.
class BillsView extends StatefulWidget {
@override
_BillsViewState createState() => _BillsViewState();
}
class _BillsViewState extends State<BillsView>
with SingleTickerProviderStateMixin {
final List<BillData> items = DummyDataService.getBillDataList();
@override
Widget build(BuildContext context) {
double dueTotal = sumBillDataPrimaryAmount(items);
return FinancialEntityView(
heroLabel: 'Due',
heroAmount: dueTotal,
segments: buildSegmentsFromBillItems(items),
wholeAmount: dueTotal,
financialEntityCards: buildBillDataListViews(items),
);
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/charts/pie_chart.dart';
import 'package:rally/data.dart';
import 'package:rally/finance.dart';
class BudgetsView extends StatefulWidget {
@override
_BudgetsViewState createState() => _BudgetsViewState();
}
class _BudgetsViewState extends State<BudgetsView>
with SingleTickerProviderStateMixin {
final List<BudgetData> items = DummyDataService.getBudgetDataList();
@override
Widget build(BuildContext context) {
double capTotal = sumBudgetDataPrimaryAmount(items);
double usedTotal = sumBudgetDataAmountUsed(items);
return FinancialEntityView(
heroLabel: 'Left',
heroAmount: capTotal - usedTotal,
segments: buildSegmentsFromBudgetItems(items),
wholeAmount: capTotal,
financialEntityCards: buildBudgetDataListViews(items, context),
);
}
}

View File

@@ -0,0 +1,151 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/colors.dart';
import 'package:rally/data.dart';
import 'package:rally/finance.dart';
import 'package:rally/formatters.dart';
/// A page that shows a status overview.
class OverviewView extends StatefulWidget {
@override
_OverviewViewState createState() => _OverviewViewState();
}
class _OverviewViewState extends State<OverviewView> {
@override
Widget build(BuildContext context) {
final accountDataList = DummyDataService.getAccountDataList();
final billDataList = DummyDataService.getBillDataList();
final budgetDataList = DummyDataService.getBudgetDataList();
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListView(children: [
_AlertsView(),
SizedBox(height: 16),
_FinancialView(
title: 'Accounts',
total: sumAccountDataPrimaryAmount(accountDataList),
financialItemViews: buildAccountDataListViews(accountDataList),
),
SizedBox(height: 16),
_FinancialView(
title: 'Bills',
total: sumBillDataPrimaryAmount(billDataList),
financialItemViews: buildBillDataListViews(billDataList),
),
SizedBox(height: 16),
_FinancialView(
title: 'Budgets',
total: sumBudgetDataPrimaryAmount(budgetDataList),
financialItemViews: buildBudgetDataListViews(budgetDataList, context),
),
SizedBox(height: 16),
]),
);
}
}
class _AlertsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(left: 16, top: 4, bottom: 4),
color: RallyColors.cardBackground,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Alerts'),
FlatButton(
onPressed: () {},
child: Text('SEE ALL'),
textColor: Colors.white,
),
],
),
Container(color: RallyColors.primaryBackground, height: 1),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Heads up, youve used up 90% of your Shopping budget for '
'this month.'),
),
SizedBox(
width: 100,
child: Align(
alignment: Alignment.topRight,
child: IconButton(
onPressed: () {},
icon: Icon(Icons.sort, color: RallyColors.white60),
),
),
),
],
)
],
),
);
}
}
class _FinancialView extends StatelessWidget {
_FinancialView({this.title, this.total, this.financialItemViews});
final String title;
final double total;
final List<FinancialEntityCategoryView> financialItemViews;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
color: RallyColors.cardBackground,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text(title),
),
Padding(
padding: EdgeInsets.only(left: 16, right: 16),
child: Text(
Formatters.usdWithSign.format(total),
style: theme.textTheme.body2.copyWith(
fontSize: 44.0,
fontWeight: FontWeight.w600,
),
),
),
...financialItemViews.sublist(0, min(financialItemViews.length, 3)),
FlatButton(
child: Text('SEE ALL'),
textColor: Colors.white,
onPressed: () {},
),
],
),
);
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2019-present the Flutter authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rally/data.dart';
class SettingsView extends StatefulWidget {
@override
_SettingsViewState createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
List<Widget> items = DummyDataService.getSettingsTitles()
.map((title) => _SettingsItem(title))
.toList();
@override
Widget build(BuildContext context) {
return ListView(children: items);
}
}
class _SettingsItem extends StatelessWidget {
_SettingsItem(this.title);
final String title;
@override
Widget build(BuildContext context) {
return FlatButton(
textColor: Colors.white,
child: SizedBox(
height: 60,
child: Row(children: <Widget>[
Text(title),
]),
),
onPressed: () {},
);
}
}