Building Job Portal Applications with Flutter and Firebase

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:

  1. Go to the Firebase Console and create a new project.
  2. Register your Flutter app with Firebase by following the setup instructions for both Android and iOS.
  3. 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.