Summoning builders…Compiling dreams…Polishing pixels…Aligning the stars to your roadmap…Brewing the backlog…Forging gold from ideas…Charting the constellations…Waking the render spirits…

    Insights/Software Engineering/How To Code & Develop A 5 Screen Application Using Our Flutter Framework

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

    Marco Lopes
    September 25, 2019
    1.5k views
    Software Engineering
    app
    code
    development
    flutter
    framework
    bounty
    freelance
    jobs
    opportunities
    partnerships
    community
    denver
    colorado
    tutorials
    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:

    1. Home: introduces the application.
    2. Opportunities: displays a short list of sample listings.
    3. Opportunity details: shows the selected listing.
    4. Interest form: collects and validates a name and email.
    5. 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

    1. Open the list from Home.
    2. Select each listing and check its detail title.
    3. Submit an empty form and confirm that errors appear.
    4. Try an obviously malformed email address.
    5. Enter valid-looking values and verify the confirmation name.
    6. Return home and confirm that Back does not reopen the form.
    7. 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, not Class; 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.