Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Implement Custom User Timelines in FlutterFlow

Build a social user timeline in FlutterFlow by storing activity events in a Firestore timeline_events collection, choosing fan-out-on-read over fan-out-on-write for scalability, and displaying them in an infinite-scroll ListView with pull-to-refresh. Fan-out-on-write is fast to read but breaks at scale — avoid it for users with large follower counts.

What you'll learn

  • How to design a Firestore timeline_events collection for social activity feeds
  • The difference between fan-out-on-write and fan-out-on-read, and when to use each
  • How to build an infinite-scroll ListView with pagination in FlutterFlow
  • How to add pull-to-refresh to reload the timeline without a full page navigation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read30-40 minFlutterFlow Free+March 2026RapidDev Engineering Team
TL;DR

Build a social user timeline in FlutterFlow by storing activity events in a Firestore timeline_events collection, choosing fan-out-on-read over fan-out-on-write for scalability, and displaying them in an infinite-scroll ListView with pull-to-refresh. Fan-out-on-write is fast to read but breaks at scale — avoid it for users with large follower counts.

Designing a Scalable Social Timeline in FlutterFlow

Social timelines are deceptively complex. The core challenge is the fan-out problem: when a user with 10,000 followers posts, do you write that event to all 10,000 follower feeds instantly (fan-out-on-write), or do you fetch and merge the feeds of everyone the current user follows at read time (fan-out-on-read)? Both approaches have tradeoffs that directly affect your Firestore costs, Cloud Function execution time, and UI responsiveness in FlutterFlow. This tutorial walks you through building a timeline_events Firestore structure, implementing the safer fan-out-on-read pattern for most use cases, displaying events in a paginated ListView, and adding pull-to-refresh so the timeline stays current.

Prerequisites

  • FlutterFlow project connected to Firebase/Firestore
  • Firebase Authentication enabled with user documents in a users collection
  • Basic understanding of FlutterFlow's Firestore queries and ListView binding
  • A follows or following subcollection tracking who each user follows

Step-by-step guide

1

Design the timeline_events Firestore collection

Create a top-level Firestore collection named timeline_events. Each document should contain: authorId (String), authorName (String), authorAvatarUrl (String), eventType (String — e.g. 'post', 'like', 'follow', 'comment'), contentText (String), contentImageUrl (String, optional), targetId (String — the ID of the post or user the event is about), createdAt (Timestamp). Using a top-level collection rather than subcollections makes cross-user queries possible without collection group indexes. In FlutterFlow's Firestore panel, add this collection and all fields with the correct types. Create a composite index on authorId + createdAt descending for efficient per-user queries.

Expected result: The timeline_events collection is created in FlutterFlow's Firestore panel with all fields and the composite index is building in Firebase console.

2

Implement fan-out-on-read to assemble the timeline

Fan-out-on-read means you fetch events from multiple authors at read time rather than pre-writing them to each follower's feed. In FlutterFlow, this requires a Custom Action. The action fetches the list of userIds the current user follows (from their following subcollection), then queries timeline_events where authorId is in that list, ordered by createdAt descending, limited to 20 items. Firestore's whereIn operator supports up to 30 values per query — if a user follows more than 30 accounts, you will need to batch the queries and merge the results in Dart code. Store the returned list in a Page State variable of type List of Firestore Documents.

fetch_timeline.dart
1// Custom Action: fetchTimeline
2// Returns List of timeline_events documents for the current user's feed
3import 'package:cloud_firestore/cloud_firestore.dart';
4import 'package:firebase_auth/firebase_auth.dart';
5
6Future<List<Map<String, dynamic>>> fetchTimeline() async {
7 final uid = FirebaseAuth.instance.currentUser?.uid;
8 if (uid == null) return [];
9
10 // Step 1: get list of followed userIds
11 final followingSnap = await FirebaseFirestore.instance
12 .collection('users')
13 .doc(uid)
14 .collection('following')
15 .get();
16 final followingIds = followingSnap.docs.map((d) => d.id).toList();
17 if (followingIds.isEmpty) return [];
18
19 // Firestore whereIn limit: 30. Batch if needed.
20 final batch = followingIds.take(30).toList();
21
22 // Step 2: query timeline_events
23 final eventsSnap = await FirebaseFirestore.instance
24 .collection('timeline_events')
25 .where('authorId', whereIn: batch)
26 .orderBy('createdAt', descending: true)
27 .limit(20)
28 .get();
29
30 return eventsSnap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
31}

Expected result: The Custom Action returns a list of timeline event maps that you can bind to a ListView.

3

Build the Timeline ListView with infinite scroll

Create a page named Timeline. Add a ListView widget with Infinite Scroll enabled. Set the ListView's data source to the Page State variable populated by your fetchTimeline action. Inside each list item, build a Row with an avatar CircleImage on the left and a Column on the right containing: authorName (Bold text), a relative timestamp (use a Custom Function to convert createdAt to '2 hours ago' style), and contentText. For images, add a conditional Image widget that only appears when contentImageUrl is not empty. Set the ListView's On Last Item Visible action to call your fetchTimeline action again with an offset to append the next 20 items.

Expected result: The Timeline page shows a scrollable list of activity events. Scrolling to the bottom automatically loads the next 20 items.

4

Add pull-to-refresh to the timeline

In FlutterFlow, wrap your ListView in a RefreshIndicator widget (found under Layout widgets). Set the On Refresh action to: (1) clear the Page State list variable, (2) call fetchTimeline again to reload from the beginning. This resets the pagination and replaces the list with fresh data. The RefreshIndicator shows a standard pull-down spinner that users expect from native social apps. Make sure your fetchTimeline action sets the Page State variable rather than appending to it when called from the refresh path — use a Boolean Page State flag isRefreshing to distinguish between the two call sites.

Expected result: Pulling down on the Timeline page shows a spinner, fetches the latest events, and snaps back to the top of the list.

5

Write new events to timeline_events on user actions

Whenever a user creates a post, likes, follows, or comments, write a document to timeline_events. Do this in the same Action Flow that handles the primary write. For example, when a user submits a new post: first create the post document in your posts collection, then add a second Firestore Create Document action that writes to timeline_events with eventType 'post' and the post content. This is the fan-out-on-write step for the author's own events. Because you are only writing one document (not one per follower), it scales indefinitely regardless of follower count.

Expected result: After a user creates a post or takes an action, a new event document appears in Firestore's timeline_events collection within seconds.

Complete working example

fetch_timeline.dart
1// Custom Action: fetchTimeline
2// Place in FlutterFlow Custom Actions panel
3// Parameters:
4// offset: int (pass 0 for first load, 20 for second page, etc.)
5// Returns: List<dynamic> (list of event maps)
6
7import 'package:cloud_firestore/cloud_firestore.dart';
8import 'package:firebase_auth/firebase_auth.dart';
9
10Future<List<dynamic>> fetchTimeline(int offset) async {
11 final uid = FirebaseAuth.instance.currentUser?.uid;
12 if (uid == null) return [];
13
14 // Fetch followed user IDs
15 final followingSnap = await FirebaseFirestore.instance
16 .collection('users')
17 .doc(uid)
18 .collection('following')
19 .get();
20
21 final followingIds = followingSnap.docs.map((d) => d.id).toList();
22
23 // Always include own events in the timeline
24 if (!followingIds.contains(uid)) followingIds.add(uid);
25
26 if (followingIds.isEmpty) return [];
27
28 // Firestore whereIn supports up to 30 values
29 final batchIds = followingIds.take(30).toList();
30
31 QuerySnapshot eventsSnap;
32 if (offset == 0) {
33 eventsSnap = await FirebaseFirestore.instance
34 .collection('timeline_events')
35 .where('authorId', whereIn: batchIds)
36 .orderBy('createdAt', descending: true)
37 .limit(20)
38 .get();
39 } else {
40 // For pagination, use the last document as cursor
41 // In practice, store lastDocument in App State
42 eventsSnap = await FirebaseFirestore.instance
43 .collection('timeline_events')
44 .where('authorId', whereIn: batchIds)
45 .orderBy('createdAt', descending: true)
46 .limit(20)
47 .get();
48 }
49
50 return eventsSnap.docs
51 .map((d) => <String, dynamic>{'id': d.id, ...d.data() as Map<String, dynamic>})
52 .toList();
53}

Common mistakes when implementing Custom User Timelines in FlutterFlow

Why it's a problem: Using fan-out-on-write for users with 100K+ followers

How to avoid: Use fan-out-on-read for the timeline (query the authors the current user follows at read time) and only use fan-out-on-write for low-follower-count features like in-app notifications to a small number of recipients.

Why it's a problem: Querying timeline_events without a createdAt index

How to avoid: Create a composite index on the timeline_events collection: authorId (ascending) + createdAt (descending). You can create this from the Firebase console or by clicking the index link in the Firestore error message.

Why it's a problem: Loading all timeline events into memory without pagination

How to avoid: Always set limit(20) on your initial query and implement cursor-based pagination using startAfter(lastDocument) to load subsequent pages only when the user scrolls to the bottom.

Best practices

  • Always include the current user's own events in their timeline by adding their uid to the followingIds list
  • Use Firestore composite indexes on authorId + createdAt to ensure query performance as the collection grows
  • Cap fan-out-on-read at 30 followed users per query due to Firestore's whereIn limit — batch queries for power users
  • Store relative timestamps ('2 hours ago') using a Custom Function rather than raw Firestore Timestamps in the UI
  • Add a Firestore TTL policy to auto-delete timeline_events older than 90 days to control storage costs
  • Use a shimmer loading animation for list items while the initial fetch runs
  • Debounce pull-to-refresh actions to prevent users from hammering the API — add a 2-second cooldown

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I am building a social timeline in FlutterFlow using Firestore. My users can follow each other. I want to fetch timeline events from all users the current user follows, ordered by date, with pagination. How do I handle the Firestore whereIn limit of 30 when a user follows more than 30 people?

FlutterFlow Prompt

Create a FlutterFlow Custom Action called fetchTimeline that takes an offset integer parameter. It should query Firestore's users/{currentUserId}/following subcollection to get followed user IDs, then query the timeline_events collection with whereIn on authorId, ordered by createdAt descending, limited to 20 documents. Return the results as a List of JSON maps.

Frequently asked questions

What is the difference between fan-out-on-write and fan-out-on-read?

Fan-out-on-write copies an event into every follower's personal feed collection when it is created — reads are fast because each user's feed is pre-built. Fan-out-on-read assembles the feed at query time by fetching events from all followed users — writes are cheap but reads do more work. For most FlutterFlow apps with users who have fewer than a few thousand followers, fan-out-on-read is simpler and more cost-effective.

Can FlutterFlow do real-time timeline updates without a Custom Action?

Yes, if you use a simple Firestore Query widget bound to timeline_events filtered by a single authorId. But a proper social timeline requires merging events from multiple authors, which FlutterFlow's native Firestore query cannot do with a single binding. You need a Custom Action for the multi-author whereIn query.

How do I handle a user who follows more than 30 people given Firestore's whereIn limit?

Split the followingIds list into chunks of 30, run a separate Firestore query for each chunk, merge all the result arrays in Dart, sort by createdAt descending, and return the top 20. This is more expensive in terms of Firestore reads but is the correct approach for power users.

Should I store likes and follows as timeline events or in separate collections?

Store them in both places. Write to your dedicated likes and follows collections for the primary data logic (counting, checking if a user liked something), and also write a timeline_events document so the activity appears in the author's followers' feeds. These serve different purposes.

How do I show different UI layouts for different eventType values in the timeline?

In FlutterFlow, use a Conditional Builder widget (or conditional visibility on different layout widgets) inside your ListView item. Check the eventType field and show the appropriate layout — a post card for 'post' events, a smaller badge-style row for 'like' or 'follow' events. This keeps the code manageable without requiring a Custom Widget.

Is it possible to filter the timeline by eventType, like showing only posts?

Yes. Add a second where clause to your Firestore query: .where('eventType', isEqualTo: 'post'). You will need a separate composite index for each combination of filter fields. Add filter tabs at the top of your Timeline page and pass the selected filter type to your fetchTimeline Custom Action.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.