building Flutter UI, routes, data model, validations, spinners, error messages

Amit Shukla
6 min readJul 13, 2024

--

Service Delivery Management App

welcome back friends,

In our previous blog, we presented an expedited demonstration of the Ride or Service Delivery Management application, which utilizes the Flutter platform coupled with the Parse back-end service by Back4App.

You can access the complete source code here.

In case you prefer video blogs, here is the YouTube video tutorial.

If you are finding this blog for the first time, please access previous blog for a quick demo, so that you know exactly what to expect in this and following blogs.

Today’s session, we’ll begin creating our project by organizing the initial structure and crafting simple interface templates for user interaction with our app. By now, it’s helpful if you have already some UI/UX basic design ideas. Minimally, I expect you to visualize detailing how users will navigate through your app and what its appearance will be like.

To set up Flutter, I recommend using Visual Studio Code with the Flutter plugin, which automatically installs DART SDK. If you already have a successful installation, proceed to create a new app by running following command.

flutter create app_name

For a summary of your setup and available devices, run

flutter doctor

Finally, start your app in Chrome browser window by running following command from within your app directory.

flutter run -d chrome

At this point, it doesn’t need to be an UBER like UI and UX application, we will start first building a simple UI and once our application is ready, we can still adding more complex functionality as we progress.

Let’s add these files into our app.

├── src
│ ├── lib
│ │ ├── main.dart
| │ ├── views
| │ │ ├── app.dart
| │ │ ├── bid.dart
| │ │ ├── bids.dart
| │ │ ├── dashboard.dart
| │ │ ├── inbox.dart
| │ │ ├── login.dart
| │ │ ├── message.dart
| │ │ ├── providers.dart
| │ │ ├── ride.dart
| │ │ ├── rides.dart
| │ │ ├── settings.dart
| │ │ ├── signup.dart
| │ ├── models
| │ │ ├── datamodel.dart
| │ │ ├── validators.dart
| │ ├── bloc
| │ │ ├── auth.bloc.dart
| │ │ ├── backend.bloc.dart
| │ ├── shared
├── build
├── pubspec.yaml
├── web
│ ├── index.html
└── .gitignore

Now, let’s start with adding basic codes, please refer back to GitHub repository for complete code, Here I will only refer to important steps needed for now.

// Add the Parse SDK to your project dependencies running:
flutter pub add parse_server_sdk_flutter

Let’s first address two important files, `app.dart` and `main.dart`.

As shown below in `main.dart` file, it simply calls `app.dart` to build UI and initialize back4app back-end services.

// main.dart
import 'package:flutter/material.dart';
import './views/app.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk_flutter.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
const keyApplicationId = '<<copy_backaApp_appid_here>>';
const keyClientKey = '<<copy_back4app_client_key_here>>';
const keyParseServerUrl = 'https://parseapi.back4app.com';
await Parse().initialize(keyApplicationId, keyParseServerUrl,
clientKey: keyClientKey, autoSendSessionId: true);
runApp(const App());
}

Below `app.dart` creates main material app, which is primarily responsible for implementing UI, Themes, Color, language defaults etc. and also a place where initial routes are setup. Let’s first setup a basic material app and add routes to `app.dart`.

UI/UX

// app.dart
void main() async {
runApp(const App());
}

class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
// ThemeMode themeMode = ThemeMode.system; // setup Theme here
// Locale _locale = const Locale('en'); // default language

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: "TestApp",
supportedLocales: AppLocalizations.supportedLocales,
// locale: _locale,
// themeMode: themeMode,
theme: ThemeData.dark(),
darkTheme: ThemeData.light(),
home: LogIn(), // default opening page = LogIn page
routes: {
// '/': (context) => const LogIn()
SignUp.routeName: (context) => SignUp(),
},
);
}
}

routes

// include routeName as static const in your files
// example: signup.dart
class SignUp extends StatefulWidget {
static const routeName = '/signup'; // route name will serve as <<url>>/routeName
SignUp({super.key});
@override
SignUpState createState() => SignUpState();
}

data models

defining appropriate data models to handle data in your app may first seems a hassle and unnecessary step, but it pays off big later. Setting up appropriate class data models with JSON serialization and de-serialization methods, does wonder to your application. It not only adds, easy code reading, code predictability, code typing, usability, object oriented / duck typing paradigm which further makes app reusable components, it helps developers code and debug errors, predictable and standardize which later further improve app upgrade and maintenance activities.

Here is how to define a simple data model.

class LoginDataModel {
String email;
String password;
LoginDataModel({required this.email, required this.password});
}

// a typical complete class data model looks like this
class RideModel {
String objectId;
String uid;
String dttm;
String from;
...
RideModel({required this.objectId, required this.uid,
required this.dttm, required this.from, ...});
factory RideModel.fromJson(Map<String, dynamic> json) {
return RideModel(
objectId: json['objectId'],
uid: json['uid'],
dttm: json['dttm'],
from: json['from'],
...)
}
Map<String, dynamic> toJson() {
final _data = <String, dynamic>{};
_data['objectId'] = objectId;
_data['uid'] = uid;
_data['dttm'] = dttm;
_data['from'] = from;
...
return _data;
}
}

error validations

define error validations logic

// datamodels/validators.dart

class Validators {
String? evalEmail(String value) {
String pattern = r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regex = RegExp(pattern);
if (value.isEmpty) {
return "email is required";
}
else if (!regex.hasMatch(value)) {
return 'please enter valid email';
}
return null;
}
}
String? evalPassword(String value) {
String pattern = r'^(?=.*?[a-z])(?=.*?[0-9]).{8,}$';
RegExp regex = RegExp(pattern);
if (value.isEmpty) {
return "password is Required";
}
else if (!regex.hasMatch(value)) {
return 'please enter 8 chars alphanumeric password';
}
return null;
}
final validatorBloc = Validators();

call error validations in your text input fields.

// example login.dart
import 'package:flutter/material.dart';
import '../models/datamodel.dart';
import '../models/validators.dart';

class LogIn extends StatefulWidget {
static const routeName = '/login';
LogIn({super.key});

@override
LogInState createState() => LogInState();
}

class LogInState extends State<LogIn> {
final _formKey = GlobalKey<FormState>();
var model = LoginDataModel(email: '-',password: 'na');
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();

@override
void initState() {
model.password = "";
_passwordController.clear();
_btnEnabled = false;
super.initState();
}

@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: createNavLogInBar(context, widget),
body: Material(
child: Container(
child: userForm(context));
}

Widget userForm(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: () =>
setState(() => _btnEnabled = _formKey.currentState!.validate()),
child: SingleChildScrollView(
child: Center(
child: Column(
children: <Widget>[
SizedBox(
width: 300.0,
// margin: const EdgeInsets.only(top: 25.0),
child: TextFormField(
controller: _emailController,
cursorColor: Colors.blueAccent,
keyboardType: TextInputType.emailAddress,
maxLength: 50,
obscureText: false,
onChanged: (value) => model.email = value,
validator: (value) {
return Validators().evalEmail(value!);
},
decoration: InputDecoration(
icon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0)),
hintText: "hint text",
labelText: "label text",
// errorText: snapshot.error,
),
)),
Container(
margin: const EdgeInsets.only(top: 5.0),
),
Container(
width: 300.0,
margin: const EdgeInsets.only(top: 25.0),
child: TextFormField(
controller: _passwordController,
cursorColor: Colors.blueAccent,
keyboardType: TextInputType.visiblePassword,
maxLength: 50,
obscureText: true,
onChanged: (value) => model.password = value,
validator: (value) {
return Validators().evalPassword(value!);
},
decoration: InputDecoration(
icon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0)),
hintText: "hint password text",
labelText: "label password text",
),
)),
Container(
margin: const EdgeInsets.only(top: 25.0),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
),
signinSubmitBtn(context),
Container(
margin: const EdgeInsets.only(top: 15.0),
),
]
);
}

Widget signinSubmitBtn(context) {
return ElevatedButton(
onPressed: _btnEnabled == true ? () => login("email") : null,
child: Text("login"));
}
}

spinners

bool spinnerVisible = false;
toggleSpinner() {
setState(() => spinnerVisible = !spinnerVisible);
}

// calling spinner in API fetch
void login() async {
toggleSpinner();
await callBackendAPI();
toggleSpinner();
}

// another approach is to define a custom spinner class
// which simply shows a circular progress indicator
class CustomSpinner extends StatelessWidget {
final bool toggleSpinner;
const CustomSpinner({super.key, required this.toggleSpinner});

@override
Widget build(BuildContext context) {
return Center(child: toggleSpinner ? const CircularProgressIndicator() : null);
}
}

custom messages

enum cMessageType { error, success }

// calling spinner in API fetch
bool messageVisible = false;
showMessage(bool msgVisible, msgType, message) {
messageVisible = msgVisible;
setState(() {
messageType = msgType == "error"
? cMessageType.error.toString()
: cMessageType.success.toString();
messageTxt = message;
});
}
// another approach is to define a custom message class
// which simply shows text in red/green colors
class CustomMessage extends StatelessWidget {
final bool toggleMessage;
final toggleMessageType;
final String toggleMessageTxt;
const CustomMessage(
{super.key,
required this.toggleMessage,
this.toggleMessageType,
required this.toggleMessageTxt})
: super();
@override
Widget build(BuildContext context) {
return Center(
child: toggleMessage
? Text(toggleMessageTxt,
style: toggleMessageType == cMessageType.error.toString()
? "RED"
: "GREEN")
: null);
}
}

Please do not forget to Like and bookmark this article and subscribe to my YouTube and X accounts for more content.

--

--

Amit Shukla
Amit Shukla

Written by Amit Shukla

Build and Share Quality code for Desktop and Mobile App Software development using Web, AI, Machine Learning, Deep Learning algorithms. Learn, Share & Grow.

No responses yet