mirror of https://github.com/flutter/samples.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
7.9 KiB
320 lines
7.9 KiB
// Copyright 2022 The Flutter team. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_layout_grid/flutter_layout_grid.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:math_expressions/math_expressions.dart';
|
|
import 'package:window_size/window_size.dart';
|
|
|
|
void main() {
|
|
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
setWindowTitle('Simplistic Calculator');
|
|
}
|
|
|
|
runApp(
|
|
const ProviderScope(
|
|
child: CalculatorApp(),
|
|
),
|
|
);
|
|
}
|
|
|
|
@immutable
|
|
class CalculatorState {
|
|
const CalculatorState({
|
|
required this.buffer,
|
|
required this.mode,
|
|
required this.error,
|
|
});
|
|
|
|
final String buffer;
|
|
final CalculatorEngineMode mode;
|
|
final String error;
|
|
|
|
CalculatorState copyWith({
|
|
String? buffer,
|
|
CalculatorEngineMode? mode,
|
|
String? error,
|
|
}) =>
|
|
CalculatorState(
|
|
buffer: buffer ?? this.buffer,
|
|
mode: mode ?? this.mode,
|
|
error: error ?? this.error,
|
|
);
|
|
}
|
|
|
|
enum CalculatorEngineMode { input, result }
|
|
|
|
class CalculatorEngine extends StateNotifier<CalculatorState> {
|
|
CalculatorEngine()
|
|
: super(
|
|
const CalculatorState(
|
|
buffer: '0',
|
|
mode: CalculatorEngineMode.result,
|
|
error: '',
|
|
),
|
|
);
|
|
|
|
void addToBuffer(String str) {
|
|
if (state.mode == CalculatorEngineMode.result) {
|
|
state = state.copyWith(
|
|
buffer: str,
|
|
mode: CalculatorEngineMode.input,
|
|
error: '',
|
|
);
|
|
} else {
|
|
state = state.copyWith(
|
|
buffer: state.buffer + str,
|
|
error: '',
|
|
);
|
|
}
|
|
}
|
|
|
|
void evaluate() {
|
|
try {
|
|
final parser = Parser();
|
|
final cm = ContextModel();
|
|
final exp = parser.parse(state.buffer);
|
|
final result = exp.evaluate(EvaluationType.REAL, cm) as double;
|
|
|
|
if (result.isInfinite) {
|
|
state = state.copyWith(
|
|
error: 'Result is Infinite',
|
|
buffer: '',
|
|
mode: CalculatorEngineMode.result,
|
|
);
|
|
} else if (result.isNaN) {
|
|
state = state.copyWith(
|
|
error: 'Result is Not a Number',
|
|
buffer: '',
|
|
mode: CalculatorEngineMode.result,
|
|
);
|
|
} else {
|
|
final resultStr = result.ceil() == result
|
|
? result.toInt().toString()
|
|
: result.toString();
|
|
state = state.copyWith(
|
|
buffer: resultStr,
|
|
mode: CalculatorEngineMode.result,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
state = state.copyWith(
|
|
error: err.toString(),
|
|
buffer: '',
|
|
mode: CalculatorEngineMode.result,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
final calculatorStateProvider =
|
|
StateNotifierProvider<CalculatorEngine, CalculatorState>(
|
|
(_) => CalculatorEngine());
|
|
|
|
class ButtonDefinition {
|
|
const ButtonDefinition({
|
|
required this.areaName,
|
|
required this.label,
|
|
required this.op,
|
|
this.type = CalcButtonType.outlined,
|
|
});
|
|
|
|
final String areaName;
|
|
final String label;
|
|
final CalculatorEngineCallback op;
|
|
final CalcButtonType type;
|
|
}
|
|
|
|
final buttonDefinitions = <ButtonDefinition>[
|
|
ButtonDefinition(
|
|
areaName: 'seven',
|
|
op: (engine) => engine.addToBuffer('7'),
|
|
label: '7',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'eight',
|
|
op: (engine) => engine.addToBuffer('8'),
|
|
label: '8',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'nine',
|
|
op: (engine) => engine.addToBuffer('9'),
|
|
label: '9',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'four',
|
|
op: (engine) => engine.addToBuffer('4'),
|
|
label: '4',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'five',
|
|
op: (engine) => engine.addToBuffer('5'),
|
|
label: '5',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'six',
|
|
op: (engine) => engine.addToBuffer('6'),
|
|
label: '6',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'one',
|
|
op: (engine) => engine.addToBuffer('1'),
|
|
label: '1',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'two',
|
|
op: (engine) => engine.addToBuffer('2'),
|
|
label: '2',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'three',
|
|
op: (engine) => engine.addToBuffer('3'),
|
|
label: '3',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'zero',
|
|
op: (engine) => engine.addToBuffer('0'),
|
|
label: '0',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'point',
|
|
op: (engine) => engine.addToBuffer('.'),
|
|
label: '.',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'equals',
|
|
op: (engine) => engine.evaluate(),
|
|
label: '=',
|
|
type: CalcButtonType.elevated,
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'plus',
|
|
op: (engine) => engine.addToBuffer('+'),
|
|
label: '+',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'minus',
|
|
op: (engine) => engine.addToBuffer('-'),
|
|
label: '-',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'multiply',
|
|
op: (engine) => engine.addToBuffer('*'),
|
|
label: '*',
|
|
),
|
|
ButtonDefinition(
|
|
areaName: 'divide',
|
|
op: (engine) => engine.addToBuffer('/'),
|
|
label: '/',
|
|
),
|
|
];
|
|
|
|
class CalculatorApp extends ConsumerWidget {
|
|
const CalculatorApp({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final state = ref.watch(calculatorStateProvider);
|
|
|
|
return MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
home: Scaffold(
|
|
body: Container(
|
|
color: Colors.white,
|
|
child: LayoutGrid(
|
|
areas: '''
|
|
display display display display
|
|
seven eight nine divide
|
|
four five six multiply
|
|
one two three minus
|
|
zero point equals plus
|
|
''',
|
|
columnSizes: [1.fr, 1.fr, 1.fr, 1.fr],
|
|
rowSizes: [
|
|
1.fr,
|
|
1.fr,
|
|
1.fr,
|
|
1.fr,
|
|
1.fr,
|
|
],
|
|
children: [
|
|
NamedAreaGridPlacement(
|
|
areaName: 'display',
|
|
child: SizedBox.expand(
|
|
child: state.error.isEmpty
|
|
? Text(
|
|
state.buffer,
|
|
textAlign: TextAlign.end,
|
|
style: const TextStyle(fontSize: 80),
|
|
)
|
|
: Text(
|
|
state.error,
|
|
textAlign: TextAlign.start,
|
|
style: const TextStyle(
|
|
fontSize: 40,
|
|
color: Colors.red,
|
|
),
|
|
maxLines: 2,
|
|
),
|
|
),
|
|
),
|
|
...buttonDefinitions.map(
|
|
(definition) => NamedAreaGridPlacement(
|
|
areaName: definition.areaName,
|
|
child: CalcButton(
|
|
label: definition.label,
|
|
op: definition.op,
|
|
type: definition.type,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
typedef CalculatorEngineCallback = void Function(CalculatorEngine engine);
|
|
enum CalcButtonType { outlined, elevated }
|
|
|
|
class CalcButton extends ConsumerWidget {
|
|
const CalcButton({
|
|
Key? key,
|
|
required this.op,
|
|
required this.label,
|
|
required this.type,
|
|
}) : super(key: key);
|
|
|
|
final CalculatorEngineCallback op;
|
|
final String label;
|
|
final CalcButtonType type;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final buttonConstructor = type == CalcButtonType.elevated
|
|
? ElevatedButton.new
|
|
: OutlinedButton.new;
|
|
|
|
return SizedBox.expand(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8),
|
|
child: buttonConstructor(
|
|
onPressed: () => op(ref.read(calculatorStateProvider.notifier)),
|
|
child: Text(
|
|
label,
|
|
style: const TextStyle(fontSize: 40, color: Colors.black54),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|