scientific_input 1.0.0
scientific_input: ^1.0.0 copied to clipboard
Flutter widgets and utilities for parsing, validating, formatting, and displaying scientific notation such as 1.2e-3 and 1.2 × 10⁻³.
import 'package:flutter/material.dart';
import 'package:scientific_input/scientific_input.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scientific Input Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
final ScientificNumberController _inputController =
ScientificNumberController(text: '1.2e-3');
@override
void dispose() {
_inputController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Scientific Input'),
bottom: const TabBar(
tabAlignment: TabAlignment.center,
tabs: [
Tab(text: 'Input'),
Tab(text: 'Text'),
Tab(text: 'Tooltip'),
],
),
),
body: SafeArea(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: TabBarView(
children: [
_InputShowcase(inputController: _inputController),
const _TextShowcase(),
const _TooltipShowcase(),
],
),
),
),
),
),
);
}
}
class _InputShowcase extends StatelessWidget {
const _InputShowcase({required this.inputController});
final ScientificNumberController inputController;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: inputController,
builder: (context, _) {
return _ShowcaseScrollView(
children: [
const _IntroCard(
title: 'Covers the common entry flow',
description: 'The example focuses on the default typed-input experience first, since that is the most common way apps use the package.',
),
_ShowcaseCard(
title: 'Keep written input',
description: 'Users can type standard notation like 1.2e-3 or paste readable text like 1.2 x 10^-3. The field keeps their written form and shows a readable preview underneath.',
child: ScientificNumberInputField(
controller: inputController,
labelText: 'Value',
hintText: 'e.g. -3.5e6',
previewTooltipBuilder: (data) => data.value.toString(),
),
),
_ControllerStateCard(
title: 'Controller state',
controller: inputController,
rows: [
_StateRowData(
label: 'Parsed double',
value: inputController.doubleValue?.toString() ?? 'null',
),
_StateRowData(
label: 'Raw scientific',
value: inputController.rawScientificText ?? 'null',
),
_StateRowData(
label: 'Readable text',
value: inputController.readableText ?? 'null',
),
],
),
],
);
},
);
}
}
class _TextShowcase extends StatelessWidget {
const _TextShowcase();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textStyle = theme.textTheme.titleMedium?.copyWith(
fontFamily: 'monospace',
);
return _ShowcaseScrollView(
children: [
const _IntroCard(
title: 'Display-only formatting',
description: 'Use ScientificNumberText when you already have stored values and only need consistent readable output.',
),
_ShowcaseCard(
title: 'Static scientific values',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoRow(
label: 'Selectable text',
child: ScientificNumberText(
'1.2e-3',
selectable: true,
style: textStyle,
),
),
const SizedBox(height: 12),
_InfoRow(
label: 'Plain number stays plain',
child: ScientificNumberText('12', style: textStyle),
),
const SizedBox(height: 12),
_InfoRow(
label: 'Invalid fallback',
child: ScientificNumberText(
'abc',
invalidText: 'Invalid scientific value',
style: textStyle,
),
),
],
),
),
],
);
}
}
class _TooltipShowcase extends StatelessWidget {
const _TooltipShowcase();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textStyle = theme.textTheme.titleMedium?.copyWith(
fontFamily: 'monospace',
);
return _ShowcaseScrollView(
children: [
const _IntroCard(
title: 'Tooltips and full-value display',
description: 'Both the input preview and the display-only widget can expose the parsed decimal value for hover and long-press interactions.',
),
_ShowcaseCard(
title: 'Preview tooltip',
child: ScientificNumberInputField(
labelText: 'Tooltip preview',
hintText: 'Try 5e6',
showFormattedPreview: true,
previewTooltipBuilder: (data) => data.value.toString(),
),
),
_ShowcaseCard(
title: 'Text tooltip',
description: 'Hover or long-press the formatted text to reveal the full parsed value. This demo keeps the text non-selectable so the tooltip gesture stays available.',
child: Align(
alignment: Alignment.centerLeft,
child: ScientificNumberText(
'5e6',
style: textStyle,
tooltipBuilder: (data) => data.value.toString(),
),
),
),
],
);
}
}
class _ShowcaseScrollView extends StatelessWidget {
const _ShowcaseScrollView({required this.children});
final List<Widget> children;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (var i = 0; i < children.length; i++) ...[
children[i],
if (i != children.length - 1) const SizedBox(height: 12),
],
],
),
);
}
}
class _IntroCard extends StatelessWidget {
const _IntroCard({required this.title, required this.description});
final String title;
final String description;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
elevation: 0,
color: theme.colorScheme.surfaceContainerHighest,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleLarge),
const SizedBox(height: 8),
Text(
description,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
}
class _ShowcaseCard extends StatelessWidget {
const _ShowcaseCard({
required this.title,
this.description,
required this.child,
});
final String title;
final String? description;
final Widget child;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleMedium),
if (description != null) ...[
const SizedBox(height: 6),
Text(
description!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(height: 16),
child,
],
),
),
);
}
}
class _ControllerStateCard extends StatelessWidget {
const _ControllerStateCard({
required this.title,
required this.controller,
required this.rows,
});
final String title;
final ScientificNumberController controller;
final List<_StateRowData> rows;
@override
Widget build(BuildContext context) {
final errorText = controller.errorText;
return _ShowcaseCard(
title: title,
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < rows.length; i++) ...[
_InfoRow(label: rows[i].label, value: rows[i].value),
if (i != rows.length - 1) const SizedBox(height: 10),
],
if (errorText != null) ...[
const SizedBox(height: 10),
_InfoRow(label: 'Error', value: errorText),
],
],
),
),
);
}
}
class _InfoRow extends StatelessWidget {
const _InfoRow({required this.label, this.value, this.child})
: assert(
value != null || child != null,
'Either value or child must be provided.',
);
final String label;
final String? value;
final Widget? child;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 132,
child: Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 12),
Expanded(
child:
child ??
SelectableText(
value!,
style: theme.textTheme.bodyMedium?.copyWith(
fontFamily: 'monospace',
),
),
),
],
);
}
}
class _StateRowData {
const _StateRowData({required this.label, required this.value});
final String label;
final String value;
}