Flutter and Firebase are a powerful combination for building cross-platform applications quickly and efficiently. Flutter provides a rich set of UI tools and widgets for creating beautiful and responsive user interfaces, while Firebase offers a comprehensive suite of backend services, including authentication, database management, and cloud functions. In this article, we’ll explore how to build a job portal application using Flutter for the frontend and Firebase for the backend.
Why Flutter and Firebase?
- Rapid Development: Flutter’s hot reload and expressive UI toolkit allow for faster development cycles.
- Cross-Platform: Build and deploy applications for iOS, Android, and the web from a single codebase.
- Real-time Data: Firebase’s real-time database ensures instant data synchronization across devices.
- Scalability: Firebase services scale effortlessly with your application’s growth.
- Cost-Effective: Firebase provides generous free tiers for many of its services, making it ideal for startups and small projects.
Project Overview
Our job portal application will include the following features:
- User Authentication: Registration and login for job seekers and employers.
- Job Listings: A list of available jobs with detailed descriptions.
- Job Posting: Employers can post new job openings.
- Job Application: Job seekers can apply for jobs.
- Search and Filters: Users can search for jobs based on keywords, location, and category.
Setting Up Firebase
First, let’s set up our Firebase project:
- Go to the Firebase Console and create a new project.
- Register your Flutter app with Firebase by following the setup instructions for both Android and iOS.
- Enable the Authentication, Cloud Firestore, and Cloud Storage services in the Firebase Console.
Setting Up Flutter
Next, let’s create a new Flutter project and add the necessary Firebase packages:
flutter create job_portal_app
cd job_portal_app
flutter pub add firebase_core firebase_auth cloud_firestore firebase_storage provider
Now, let’s break down the key components and features of our application.
Authentication
We’ll use Firebase Authentication to handle user registration and login. Here’s a simplified implementation using Provider for state management:
// auth_service.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
class AuthService extends ChangeNotifier {
final FirebaseAuth _auth = FirebaseAuth.instance;
User? _user;
User? get user => _user;
AuthService() {
_auth.authStateChanges().listen((user) {
_user = user;
notifyListeners();
});
}
Future<void> signUp(String email, String password) async {
try {
await _auth.createUserWithEmailAndPassword(email: email, password: password);
} catch (e) {
print(e);
rethrow;
}
}
Future<void> signIn(String email, String password) async {
try {
await _auth.signInWithEmailAndPassword(email: email, password: password);
} catch (e) {
print(e);
rethrow;
}
}
Future<void> signOut() async {
await _auth.signOut();
}
}
Here’s the Registration and Login UI components:
// registration_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_service.dart';
class RegistrationScreen extends StatefulWidget {
@override
_RegistrationScreenState createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
final _formKey = GlobalKey<FormState>();
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Register')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(labelText: 'Password'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
try {
await Provider.of<AuthService>(context, listen: false)
.signUp(emailController.text, passwordController.text);
Navigator.pop(context); // Go back to login screen
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to register: $e')),
);
}
}
},
child: Text('Register'),
),
],
),
),
),
);
}
}
// login_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'auth_service.dart';
import 'registration_screen.dart';
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(labelText: 'Password'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
try {
await Provider.of<AuthService>(context, listen: false)
.signIn(emailController.text, passwordController.text);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to sign in: $e')),
);
}
}
},
child: Text('Login'),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => RegistrationScreen()),
);
},
child: Text('Register'),
),
],
),
),
),
);
}
}
Job Listings
Let’s fetch job listings from Firebase Cloud Firestore and display them using a Flutter ListView.
// job_listing_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class JobListingService extends ChangeNotifier {
final FirebaseFirestore _db = FirebaseFirestore.instance;
Stream<List<Job>> getJobStream() {
return _db.collection('jobs').snapshots().map((snapshot) {
return snapshot.docs.map((doc) => Job.fromFirestore(doc)).toList();
});
}
}
class Job {
final String title;
final String description;
final String company;
Job({
required this.title,
required this.description,
required this.company,
});
factory Job.fromFirestore(DocumentSnapshot<Map<String, dynamic>> doc) {
final data = doc.data();
return Job(
title: data?['title'] ?? '',
description: data?['description'] ?? '',
company: data?['company'] ?? '',
);
}
}
// job_listings_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'job_listing_service.dart';
class JobListingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Job Listings')),
body: StreamBuilder<List<Job>>(
stream: Provider.of<JobListingService>(context, listen: false).getJobStream(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final jobs = snapshot.data;
if (jobs == null || jobs.isEmpty) {
return Center(child: Text('No jobs available.'));
}
return ListView.builder(
itemCount: jobs.length,
itemBuilder: (context, index) {
final job = jobs[index];
return Card(
margin: EdgeInsets.all(8.0),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(job.title, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('Company: ${job.company}'),
SizedBox(height: 8),
Text(job.description),
],
),
),
);
},
);
},
),
);
}
}
Job Posting
Employers can post new jobs to the Firebase Cloud Firestore using a form.
// job_posting_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class JobPostingScreen extends StatefulWidget {
@override
_JobPostingScreenState createState() => _JobPostingScreenState();
}
class _JobPostingScreenState extends State<JobPostingScreen> {
final _formKey = GlobalKey<FormState>();
final titleController = TextEditingController();
final descriptionController = TextEditingController();
final companyController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Post a Job')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: titleController,
decoration: InputDecoration(labelText: 'Job Title'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a job title';
}
return null;
},
),
TextFormField(
controller: companyController,
decoration: InputDecoration(labelText: 'Company Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter the company name';
}
return null;
},
),
TextFormField(
controller: descriptionController,
maxLines: 5,
decoration: InputDecoration(labelText: 'Job Description'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a job description';
}
return null;
},
),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
try {
await FirebaseFirestore.instance.collection('jobs').add({
'title': titleController.text,
'company': companyController.text,
'description': descriptionController.text,
});
Navigator.pop(context); // Go back to job listings
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to post job: $e')),
);
}
}
},
child: Text('Post Job'),
),
],
),
),
),
);
}
}
Job Application
Users can apply for a job and store applications in Firebase.
// job_application_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class JobApplicationService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<void> applyForJob(String jobId) async {
final user = _auth.currentUser;
if (user != null) {
await _db.collection('applications').add({
'jobId': jobId,
'userId': user.uid,
'appliedAt': Timestamp.now(),
});
} else {
throw Exception('User not logged in');
}
}
}
// job_detail_screen.dart
import 'package:flutter/material.dart';
import 'job_application_service.dart';
class JobDetailScreen extends StatelessWidget {
final Job job;
JobDetailScreen({required this.job});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(job.title)),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(job.title, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('Company: ${job.company}'),
SizedBox(height: 16),
Text(job.description),
SizedBox(height: 24),
ElevatedButton(
onPressed: () async {
try {
final jobApplicationService = JobApplicationService();
await jobApplicationService.applyForJob(job.title); // Use Job Title as Job ID for demonstration
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Applied for job!')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to apply: $e')),
);
}
},
child: Text('Apply Now'),
),
],
),
),
);
}
}
Search and Filters
Implement a search functionality using Firebase queries to filter job listings.
// job_listing_service.dart (Enhanced for Search)
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class JobListingService extends ChangeNotifier {
final FirebaseFirestore _db = FirebaseFirestore.instance;
Stream<List<Job>> getJobStream({String? searchTerm}) {
Query<Map<String, dynamic>> query = _db.collection('jobs');
if (searchTerm != null && searchTerm.isNotEmpty) {
query = query.where('title', isGreaterThanOrEqualTo: searchTerm).where('title', isLessThan: searchTerm + 'z');
}
return query.snapshots().map((snapshot) {
return snapshot.docs.map((doc) => Job.fromFirestore(doc)).toList();
});
}
}
// search_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'job_listing_service.dart';
import 'job_listings_screen.dart'; // Reusing job list
class SearchScreen extends StatefulWidget {
@override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final _searchController = TextEditingController();
String _searchTerm = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search Jobs')),
body: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Search by Job Title',
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchTerm = '';
});
},
),
),
onChanged: (value) {
setState(() {
_searchTerm = value;
});
},
),
),
Expanded(
child: Provider.of<JobListingService>(context, listen: false)
.getJobStream(searchTerm: _searchTerm)
.map((jobs) {
if (jobs.isEmpty) {
return Center(child: Text('No jobs found.'));
}
return JobListingsScreen(); // Reuse the Job Listing Screen
}).buildStreamWidget(initialData: SizedBox.shrink()), // Use initialData,
)
],
),
);
}
}
Conclusion
Using Flutter and Firebase, we can create a fully functional job portal application. Flutter simplifies UI development, while Firebase provides a scalable and cost-effective backend solution. This approach enables developers to focus on delivering value to their users without the complexities of managing infrastructure.