The original experiment rendered a sign-up screen in Chrome and Android, then changed the browser width to observe its layout. Its web path combined kIsWeb with LayoutBuilder and a width threshold.
The earlier setup and builds
flutter channel beta
flutter upgrade
flutter config --enable-web
flutter build apk
flutter build web
The beta-channel and web-enablement steps describe the earlier toolchain, not a requirement for a current Flutter installation.
The responsive screen
Read the historical sign-up example
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
void main() => runApp(SignUpApp());
class SignUpApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Color(0xff003c7e),
accentColor: Color(0xff003c7e),
fontFamily: 'Roboto',
textTheme: TextTheme(
headline1: TextStyle(fontSize: 30),
)),
routes: {
'/': (context) => SignUpScreen(),
'/welcome': (context) => WelcomeScreen(),
},
);
}
}
class SignUpScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[200],
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (kIsWeb) {
if (constraints.maxWidth > 700) {
return Center(
child: Container(
width: 400,
child: Container(
color: Colors.transparent,
child: Row(
children: <Widget>[
Container(
height: 300,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.yellow,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(40.0),
bottomLeft: const Radius.circular(40.0),
)),
child: Container(
child: Center(child: Text("nhancv.com")),
),
),
Expanded(
child: Container(
height: 300,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
topRight: const Radius.circular(40.0),
bottomRight: const Radius.circular(40.0),
)),
child: SignUpForm(),
),
)
],
),
),
),
);
} else {
return Center(
child: SizedBox(
width: 400,
child: Card(
child: SignUpForm(),
),
),
);
}
} else {
return Center(
child: SizedBox(
width: 400,
child: Container(
color: Colors.amber,
child: SignUpForm(),
),
),
);
}
},
),
);
}
}
class WelcomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Welcome!', style: Theme.of(context).textTheme.headline1),
),
);
}
}
class SignUpForm extends StatefulWidget {
@override
_SignUpFormState createState() => _SignUpFormState();
}
class _SignUpFormState extends State<SignUpForm> {
final _firstNameTextController = TextEditingController();
final _lastNameTextController = TextEditingController();
final _usernameTextController = TextEditingController();
double _formProgress = 0;
void _updateFormProgress() {
var progress = 0.0;
var controllers = [
_firstNameTextController,
_lastNameTextController,
_usernameTextController
];
for (var controller in controllers) {
if (controller.value.text.isNotEmpty) {
progress += 1 / controllers.length;
}
}
setState(() {
_formProgress = progress;
});
}
void _showWelcomeScreen() {
Navigator.of(context).pushNamed('/welcome');
}
@override
Widget build(BuildContext context) {
return Form(
onChanged: () => _updateFormProgress(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Sign Up', style: Theme.of(context).textTheme.headline1),
AnimatedProgressIndicator(value: _formProgress),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
controller: _firstNameTextController,
decoration: InputDecoration(hintText: 'First name'),
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
controller: _lastNameTextController,
decoration: InputDecoration(hintText: 'Last name'),
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
controller: _usernameTextController,
decoration: InputDecoration(hintText: 'Username'),
),
),
FlatButton(
color: Colors.blue,
textColor: Colors.white,
onPressed: _formProgress == 1 ? _showWelcomeScreen : null,
child: Text('Sign up'),
),
],
),
);
}
}
class AnimatedProgressIndicator extends StatefulWidget {
final double value;
AnimatedProgressIndicator({
@required this.value,
});
@override
State<StatefulWidget> createState() {
return _AnimatedProgressIndicatorState();
}
}
class _AnimatedProgressIndicatorState extends State<AnimatedProgressIndicator>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Color> _colorAnimation;
Animation<double> _curveAnimation;
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1200), vsync: this);
var colorTween = TweenSequence([
TweenSequenceItem(
tween: ColorTween(begin: Colors.red, end: Colors.orange),
weight: 1,
),
TweenSequenceItem(
tween: ColorTween(begin: Colors.orange, end: Colors.yellow),
weight: 1,
),
TweenSequenceItem(
tween: ColorTween(begin: Colors.yellow, end: Colors.green),
weight: 1,
),
]);
_colorAnimation = _controller.drive(colorTween);
_curveAnimation = _controller.drive(CurveTween(curve: Curves.easeIn));
}
void didUpdateWidget(oldWidget) {
super.didUpdateWidget(oldWidget);
_controller.animateTo(widget.value);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) => LinearProgressIndicator(
value: _curveAnimation.value,
valueColor: _colorAnimation,
backgroundColor: _colorAnimation.value.withOpacity(0.4),
),
);
}
}
The layout used a wider two-part presentation above 700 pixels and a compact form below that threshold. Platform and available width are distinct inputs; a web target is not always a wide screen.
Compiled output is still client output
The source inspected an APK and main.dart.js and observed compiled/minified files. That does not make embedded secrets private or establish application security. No release build or reverse-engineering check was run for this revision.
Source: Gist revision.