# How to Implement Custom User Timelines in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-40 min
- Compatibility: FlutterFlow Free+
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
// Custom Action: fetchTimeline
// Returns List of timeline_events documents for the current user's feed
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

Future<List<Map<String, dynamic>>> fetchTimeline() async {
  final uid = FirebaseAuth.instance.currentUser?.uid;
  if (uid == null) return [];

  // Step 1: get list of followed userIds
  final followingSnap = await FirebaseFirestore.instance
      .collection('users')
      .doc(uid)
      .collection('following')
      .get();
  final followingIds = followingSnap.docs.map((d) => d.id).toList();
  if (followingIds.isEmpty) return [];

  // Firestore whereIn limit: 30. Batch if needed.
  final batch = followingIds.take(30).toList();

  // Step 2: query timeline_events
  final eventsSnap = await FirebaseFirestore.instance
      .collection('timeline_events')
      .where('authorId', whereIn: batch)
      .orderBy('createdAt', descending: true)
      .limit(20)
      .get();

  return eventsSnap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
}
```

**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 code example

File: `fetch_timeline.dart`

```dart
// Custom Action: fetchTimeline
// Place in FlutterFlow Custom Actions panel
// Parameters:
//   offset: int (pass 0 for first load, 20 for second page, etc.)
// Returns: List<dynamic> (list of event maps)

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

Future<List<dynamic>> fetchTimeline(int offset) async {
  final uid = FirebaseAuth.instance.currentUser?.uid;
  if (uid == null) return [];

  // Fetch followed user IDs
  final followingSnap = await FirebaseFirestore.instance
      .collection('users')
      .doc(uid)
      .collection('following')
      .get();

  final followingIds = followingSnap.docs.map((d) => d.id).toList();

  // Always include own events in the timeline
  if (!followingIds.contains(uid)) followingIds.add(uid);

  if (followingIds.isEmpty) return [];

  // Firestore whereIn supports up to 30 values
  final batchIds = followingIds.take(30).toList();

  QuerySnapshot eventsSnap;
  if (offset == 0) {
    eventsSnap = await FirebaseFirestore.instance
        .collection('timeline_events')
        .where('authorId', whereIn: batchIds)
        .orderBy('createdAt', descending: true)
        .limit(20)
        .get();
  } else {
    // For pagination, use the last document as cursor
    // In practice, store lastDocument in App State
    eventsSnap = await FirebaseFirestore.instance
        .collection('timeline_events')
        .where('authorId', whereIn: batchIds)
        .orderBy('createdAt', descending: true)
        .limit(20)
        .get();
  }

  return eventsSnap.docs
      .map((d) => <String, dynamic>{'id': d.id, ...d.data() as Map<String, dynamic>})
      .toList();
}
```

## Common mistakes

- **Using fan-out-on-write for users with 100K+ followers** — Fan-out-on-write means writing one Firestore document to every follower's feed when a popular user posts. A Cloud Function doing 100,000 writes will hit the 9-minute execution timeout, leave many followers without the event, and generate a massive Firestore bill. Fix: 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.
- **Querying timeline_events without a createdAt index** — Firestore requires composite indexes for queries that filter on one field and order by another. Without an index on authorId + createdAt, your query will fail in production with a Firestore index error, even if it works in the emulator. Fix: 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.
- **Loading all timeline events into memory without pagination** — A Firestore query without a limit() clause fetches every document that matches the filter. A user who has followed 20 active accounts for a year could have thousands of events — loading all of them causes slow page loads and high memory usage on the device. Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-custom-user-timelines-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-custom-user-timelines-in-flutterflow
