Application Design II / Task 4 - Final Application and E-Portfolio

22/04/2026 - 27/7/2026 (Week 1 - Week 14)

Chang Wing / 0367807 

Application Design II / Bachelors of Design (Honours) in Creative Media / Taylor's University

 Task 4 - Final Application and E-Portfolio


TABLE OF CONTENTS


BUILD PROCESS

Job Posts > Job Details Navigation

Pass parameter 


Save Jobposts to History

Step 1: Decide Where to Store the Saved Jobs

The first step was determining where to store each user's saved jobs. The standard approach is to store the saved jobs within the authenticated user's document by creating a field that contains a list of references to the saved job post documents.

users/{userId}
  savedJobs: [reference to job1, reference to job2, ...]

The savedJobs field is stored as a List<DocumentReference>, where each item points directly to a document in the jobposts collection.

This approach is preferred over adding an isSaved field to each job post document because the saved status is user-specific. Different users may save different job posts, meaning the saved state varies from one user to another. If an isSaved field were stored within the job post document itself, the value would be shared globally across all users, making it impossible to determine which individual users had saved the job.


Step 2: Add the savedJobs Field to the Users Collection

After deciding on the data structure, I updated the Firestore schema by adding a new field to the Users collection.

The following steps were performed:

  1. Open Firestore → Users Collection in FlutterFlow's Schema Editor.

  2. Add a new field named savedJobs.

  3. Set the field type to List.

  4. Set the list subtype to Document Reference (or Reference, depending on the FlutterFlow version).

This field stores a list of document references that point directly to the user's saved job posts.


Step 3: Configure the Save Button

Next, I configured the bookmark icon on the Job Detail page to save a job post when tapped.

The following action was added:

  1. Select the bookmark icon.

  2. Add an On Tap action.

  3. Choose Backend Call → Update Document.

  4. Set the document to update as the currently authenticated user's document (currentUserReference).

  5. Update the savedJobs field using:

    • Update Type: Add to List

    • Value: Current Job Document Reference

Using Add to List appends the current job reference to the existing list without overwriting previously saved jobs.

Optional: Toggle Save and Unsave

To allow users to remove a saved job by tapping the bookmark icon again, I implemented a conditional action.

The application first checks whether the current job reference already exists in the user's savedJobs list.

  • If the reference already exists, the application updates the document using Remove from List, effectively removing the saved job.

  • Otherwise, the application uses Add to List to save the job.

The bookmark icon can also be updated dynamically to display a filled bookmark when the job has been saved and an outlined bookmark when it has not.


Figure 2.2 Action flow of JobDetail page's SaveBtn



Figure 2.3 JobDetail page's SaveBtn ('SaveIcon')


Step 4: Create the Custom Action

Although the saved job references are stored within the user's document, the application still needs to retrieve the complete job post information from the jobposts collection before displaying it.

To achieve this, I created a custom action named getSavedJobPosts.

The custom action was configured with the following properties:

Return Type

List<JobpostsRecord>

Argument

  • Name: savedJobRefs

  • Type: List<DocumentReference>

The argument type matches the data type of the savedJobs field stored in Firestore. Therefore, the authenticated user's savedJobs list can be passed directly into the custom action without requiring any data conversion.


Step 5: Implement the Custom Action

After creating the custom action, I pasted the provided Dart code into FlutterFlow:

import 'package:cloud_firestore/cloud_firestore.dart';
Future<List<JobpostsRecord>> getSavedJobPosts(
List<DocumentReference>? savedJobRefs,
) async {
if (savedJobRefs == null || savedJobRefs.isEmpty) {
return [];
}
List<JobpostsRecord> results = [];
const batchSize = 10; // Firestore 'whereIn' limit
for (var i = 0; i < savedJobRefs.length; i += batchSize) {
final end = (i + batchSize > savedJobRefs.length)
? savedJobRefs.length
: i + batchSize;
final batch = savedJobRefs.sublist(i, end);
final snapshot = await FirebaseFirestore.instance
.collection('jobposts')
.where(FieldPath.documentId,
whereIn: batch.map((ref) => ref.id).toList())
.get();
results.addAll(
snapshot.docs.map((doc) => JobpostsRecord.fromSnapshot(doc)),
);
}
return results;
}

The custom action performs the following tasks:

  • Accepts a list of saved job document references.

  • Checks whether the list is empty or null. If no saved jobs exist, it immediately returns an empty list.

  • Processes the saved job references in batches to avoid exceeding Firestore query limits and to remain compatible with whereIn query constraints.

  • Extracts the document ID from each document reference using ref.id.

  • Queries the jobposts collection using:

.where(
  FieldPath.documentId,
  whereIn: batch.map((ref) => ref.id).toList(),
)

Instead of searching for a custom ID field, the query retrieves documents directly by their Firestore document IDs.

Each retrieved document is converted into a JobpostsRecord using:

JobpostsRecord.fromSnapshot(doc)

The retrieved records are then appended to a results list.

After all batches have been processed, the function returns the complete list of JobpostsRecord objects.

This implementation allows the application to retrieve every saved job post efficiently while accommodating Firestore's query limitations when handling large numbers of saved jobs.


Figure 2.4 Creating getSavedJobPosts Custom Code


Step 6: Connect the Custom Action to the History Page

Once the custom action had been created, I connected it to the History Page.

I added an On Page Load action and selected the getSavedJobPosts custom action.

The savedJobRefs parameter was bound to:

Authenticated User → savedJobs

This passes the authenticated user's list of saved job references into the custom action.

The returned value was stored in a page state variable named:

savedJobPostsList

This page state variable contains the complete list of retrieved JobpostsRecord objects and serves as the data source for the History Page.


Step 7: Display the Saved Job Posts

Finally, I configured the GridView to display the saved job posts.

Instead of using a Backend Query, I changed the GridView's Generate Dynamic Children data source to the page state variable:

savedJobPostsList


Figure 2.5 Generating Dynamic Children for GridView that displays saved jobs


Within the GridView item template, each widget was bound to the corresponding property of the current job post.

For example:

  • ImagecurrentItem.imageURL

  • Job TitlecurrentItem.title

  • Job DescriptioncurrentItem.description

  • Company NamecurrentItem.companyName

  • SalarycurrentItem.salary

Any additional UI components were also bound to their respective fields from currentItem.

As a result, whenever the History Page loads, the application retrieves the authenticated user's saved job references, fetches the corresponding job post documents from the jobposts collection, stores the retrieved records in savedJobPostsList, and dynamically generates the GridView using those records. This implementation ensures that only the authenticated user's saved job posts are displayed while always presenting the most up-to-date information stored in the jobposts collection.



Navigation Flow Across The App


Figure 2.6 Storyboard (Full view)



Figure 2.7 Storyboard (Close up)



Figure 2.8 Storyboard (Close up)




PRESENTATION SLIDE
Figure 3.1 Presentation Slide: App Design I - Problem Statement & App Concept





PRESENTATION VIDEO





REFLECTION

Reflection

As I progressed to finish up all the core features of the app, I encountered more challenges, especially when connecting the UI design with the backend logic. Some interactions that seemed simple when designing in Figma became more complicated when implementing them in FlutterFlow, as I needed to consider database structure, data flow, conditional logic, and the limitations of the platform. There were also times where I had to adjust my original UI design to fit the backend logic.

Throughout the development process, I learned that building an application is not only about creating a visually appealing UI, but also about understanding how every interaction works behind the scenes. I became more familiar with debugging, testing different approaches, and solving problems when certain features did not work as expected. This process also helped me understand the importance of designing with technical feasibility in mind from the beginning.

Looking back, I feel that the biggest lesson I learned was not just how to use FlutterFlow, but also how important it is to align UX decisions with backend logic and technical limitations. As a UI/UX designer, I learned that a good design should not only focus on how the interface looks, but also consider how users interact with the system and how the data moves behind. By understanding both sides, I wish I will be able to create UIs that are not only visually engaging, but also realistic and functional to implement.



Comments