How To Code & Develop A 5 Screen Application Using Our Flutter Framework

A useful five-screen app needs more than five empty pages. It needs a clear user journey, navigation that behaves predictably, data passed between screens, and a form that does something when someone taps Submit.
In this tutorial, we’ll build a small opportunity-board prototype: a developer opens a welcome screen, browses opportunities, reads a listing, fills out an interest form, and sees a confirmation. It’s a practical foundation for a freelance, community, or partnership app.
This tutorial uses Flutter conventions available in 2019; tools, interfaces, and some APIs may have changed since then.
Flutter is Google’s UI toolkit, not a proprietary Kordspace framework. Here, “our framework” means the small application structure we’ll build with Flutter. In 2019, Android and iOS are its established production targets; web and desktop support are still developing, so we’ll focus on mobile.
What we’re building
Our app has five screens:
- Home: introduces the application.
- Opportunities: displays a short list of sample listings.
- Opportunity details: shows the selected listing.
- Interest form: collects and validates a name and email.
- Confirmation: acknowledges the local demo submission and returns home.
The listings are illustrative, not live jobs. There is no backend, authentication, or persistent storage. The form will validate input and navigate, but it will not send an application anywhere.
That deliberately limited scope lets us concentrate on the fundamentals: widgets, routes, state, and a complete interaction flow.
1. Install Flutter and verify your environment
Install the Flutter SDK using the instructions for your operating system, add its bin directory to your PATH, and install an editor such as Android Studio or Visual Studio Code with Flutter and Dart support.
For Android development, install the Android SDK and create an emulator, or connect a device with USB debugging enabled. Building and running an iOS app requires macOS and Xcode.
Check your setup:
flutter doctor
flutter devices
Resolve the issues relevant to your target platform before writing application code. A missing Android SDK component or an unconfigured iOS toolchain is an environment problem, not a Dart problem.
Practical tip: Get the generated starter app running first. This separates installation troubleshooting from mistakes introduced while building the tutorial.
2. Create the project and organize its files
Create a project using a lowercase, underscore-separated name:
flutter create opportunity_board
cd opportunity_board
flutter run
Once the starter app launches, stop it and organize lib as follows:
lib/
main.dart
screens/
home_screen.dart
opportunities_screen.dart
detail_screen.dart
interest_screen.dart
confirmation_screen.dart
Each screen gets its own file. For an app this small, constructor arguments and local form state are sufficient; adding a state-management package would create more concepts than we need.
We’ll use Material widgets included with Flutter. No additional packages or changes to pubspec.yaml are necessary.
Set up the application entry point
Replace lib/main.dart with:
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
void main() => runApp(OpportunityApp());
class OpportunityApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Opportunity Board',
theme: ThemeData(primarySwatch: Colors.indigo),
home: HomeScreen(),
);
}
}
main() starts the program. runApp() installs the root widget, while MaterialApp provides the theme and navigation infrastructure.
The following snippets are complete file contents. Create all five files before running the finished app, since each screen imports the next one.
3. Build the home screen
Add this to lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import 'opportunities_screen.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Opportunity Board')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Find your next project',
style: Theme.of(context).textTheme.headline,
textAlign: TextAlign.center,
),
SizedBox(height: 16.0),
Text('Explore sample development opportunities.'),
SizedBox(height: 24.0),
RaisedButton(
child: Text('Browse opportunities'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OpportunitiesScreen(),
),
);
},
),
],
),
),
),
);
}
}
Scaffold supplies the basic page layout. Column arranges its children vertically, and Padding keeps content away from the edges.
This screen is a StatelessWidget because it does not own changing data. Navigation changes the route stack rather than a field inside this widget.
4. Add the opportunity list and detail screens
These two screens demonstrate a common pattern: select a record, then pass the selected value to another screen.
Create the list
Add lib/screens/opportunities_screen.dart:
import 'package:flutter/material.dart';
import 'detail_screen.dart';
class OpportunitiesScreen extends StatelessWidget {
final List<String> opportunities = <String>[
'Flutter prototype project',
'Community app collaboration',
'Mobile interface review',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Opportunities')),
body: ListView.builder(
itemCount: opportunities.length,
itemBuilder: (context, index) {
final title = opportunities[index];
return ListTile(
title: Text(title),
subtitle: Text('Sample listing'),
trailing: Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailScreen(title: title),
),
);
},
);
},
),
);
}
}
ListView.builder creates rows as needed rather than requiring a manually written widget for every listing. That makes the same layout useful when a longer list eventually comes from an API.
Create the detail page
Add lib/screens/detail_screen.dart:
import 'package:flutter/material.dart';
import 'interest_screen.dart';
class DetailScreen extends StatelessWidget {
final String title;
DetailScreen({Key key, @required this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Opportunity details')),
body: ListView(
padding: const EdgeInsets.all(24.0),
children: <Widget>[
Text(title, style: Theme.of(context).textTheme.title),
SizedBox(height: 16.0),
Text(
'This sample opportunity explores a small mobile '
'project. Discuss scope, availability, and deliverables '
'before agreeing to any work.',
),
SizedBox(height: 24.0),
RaisedButton(
child: Text('Express interest'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => InterestScreen(title: title),
),
);
},
),
],
),
);
}
}
The constructor makes the selected title an explicit dependency. This is easier to follow than storing the selection in a global variable.
For a production board, replace the string with an Opportunity model containing an ID, description, and other fields. An ID is especially important when submitting data to a server; titles are not reliable unique identifiers.
5. Build a validated interest form
Add lib/screens/interest_screen.dart:
import 'package:flutter/material.dart';
import 'confirmation_screen.dart';
class InterestScreen extends StatefulWidget {
final String title;
InterestScreen({Key key, @required this.title}) : super(key: key);
@override
_InterestScreenState createState() => _InterestScreenState();
}
class _InterestScreenState extends State<InterestScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
void _submit() {
if (!_formKey.currentState.validate()) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ConfirmationScreen(
title: widget.title,
name: _nameController.text.trim(),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Express interest')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(24.0),
children: <Widget>[
Text(widget.title),
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
textCapitalization: TextCapitalization.words,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Enter your name.';
}
return null;
},
),
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
autocorrect: false,
validator: (value) {
final email = (value ?? '').trim();
if (!RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
.hasMatch(email)) {
return 'Enter a valid-looking email address.';
}
return null;
},
),
SizedBox(height: 24.0),
Text('Demo only: your information is not sent or saved.'),
RaisedButton(
child: Text('Submit demo interest'),
onPressed: _submit,
),
],
),
),
);
}
}
This screen uses a StatefulWidget to own the form key and text controllers across rebuilds. Disposing the controllers releases their resources when the screen is removed.
A validator returns an error message for invalid input and null for acceptable input. The email check is intentionally basic: it catches obvious mistakes but does not prove that an address exists or belongs to the user.
Using a scrollable body also helps keep fields and buttons reachable when the keyboard reduces the available space.
6. Add confirmation and return home correctly
Add lib/screens/confirmation_screen.dart:
import 'package:flutter/material.dart';
class ConfirmationScreen extends StatelessWidget {
final String title;
final String name;
ConfirmationScreen({
Key key,
@required this.title,
@required this.name,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Demo confirmation')),
body: ListView(
padding: const EdgeInsets.all(24.0),
children: <Widget>[
Icon(Icons.check_circle, size: 64.0, color: Colors.green),
SizedBox(height: 16.0),
Text('Thanks, $name!'),
Text('You completed the demo form for "$title".'),
Text('No application has been sent.'),
SizedBox(height: 24.0),
RaisedButton(
child: Text('Return home'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
);
}
}
Understand the route stack
Every Navigator.push adds a route. The app bar’s automatic back button removes the current route and reveals the previous one.
A single pop from confirmation would return to the form, not home. popUntil removes routes until the original home route remains. Pushing another home screen instead would create duplicate home pages and confusing back-button behavior.
This prototype deliberately allows going back to edit the form. A real submission workflow may need different navigation to prevent accidental resubmission.
7. Run, test, and troubleshoot
Run the completed app and inspect the analyzer output:
flutter analyze
flutter run
Hot reload is useful for layout changes and text edits. Use a hot restart when you need to recreate application state or rerun initialization.
Verify the whole journey
- Open the list from Home.
- Select each listing and check its detail title.
- Submit an empty form and confirm that errors appear.
- Try an obviously malformed email address.
- Enter valid-looking values and verify the confirmation name.
- Return home and confirm that Back does not reopen the form.
- Repeat on a small screen with the keyboard visible.
The generated project also includes a counter-app widget test. Replace that stale test before running flutter test. A minimal smoke test in test/widget_test.dart is:
import 'package:flutter_test/flutter_test.dart';
import 'package:opportunity_board/main.dart';
void main() {
testWidgets('opens the opportunity list', (tester) async {
await tester.pumpWidget(OpportunityApp());
await tester.tap(find.text('Browse opportunities'));
await tester.pumpAndSettle();
expect(find.text('Flutter prototype project'), findsOneWidget);
});
}
Common pitfalls
- Incorrect Dart capitalization: use
class, notClass; language keywords are case-sensitive. - Wrong navigation context: use a context below
MaterialApp, as these screen widgets do. - Unreachable content: prefer scrolling over rigid heights for forms and detail pages.
- Confusing validation with submission: a valid form is not evidence of a successful server request.
- Mixing API generations: these examples intentionally use 2019-era, pre-null-safety Dart and widgets such as
RaisedButton.
Takeaway
You now have five connected screens with list selection, constructor-based data passing, form validation, and predictable return navigation. Before turning this prototype into a real opportunity board, add a backend, server-side validation, loading and failure states, and appropriate privacy handling. Keep the same principle: complete one understandable user journey before expanding the architecture.
Conversation (0)
No comments yet. Start the conversation.
Join the conversation
Create a free member account to comment.