← Back

Virtual Subject Injection: Seamless Integration with Existing UI

·content-duo

Virtual Subject Injection: Seamless Integration with Existing UI

Adding a new adaptive curriculum to an existing mobile app typically requires months of UI development: new screens, navigation patterns, progress indicators, and lesson displays. This frontend work can delay feature launches and introduce platform-specific bugs (iOS vs. Android inconsistencies).

We bypassed all of that by creating a virtual subject—an entry in the subject tree that doesn't correspond to static content but instead triggers adaptive lesson generation. The mobile app treats it like any other subject, using existing lesson endpoints and UI components. Result: zero mobile app changes, seamless "Practice" subject integration, and instant deployment.

The Traditional Approach: Rebuild the UI

Adding adaptive curriculum typically follows this path:

  1. Backend: Build adaptive lesson generation API
  2. iOS: Create new "Practice" tab with custom UI
  3. Android: Replicate iOS UI with Android components
  4. Testing: Test both platforms independently
  5. Release: Coordinate mobile app releases with backend deployment

This process takes 2-3 months and creates tight coupling between backend features and frontend capabilities.

Before: New UI Required

Static Subjects              Mobile UI
┌──────────────────┐        ┌──────────────────┐
 Subject ID: 1    │───────>│ Play Tab         
 Subject ID: 2             - Subject List   
 Subject ID: 3             - Lesson Display 
└──────────────────┘        └──────────────────┘
                            Requires new UI for
                            adaptive content

The Virtual Subject Approach: Reuse Existing UI

Instead of building new UI, we inject a virtual subject (ID: -6, name: "Practice") into the subject tree API. The mobile app renders it like any other subject, but when the user taps it, the backend dynamically generates an adaptive lesson instead of serving static content.

After: Zero UI Changes

Static + Virtual            Mobile UI (Unchanged)
┌──────────────────┐        ┌──────────────────┐
 Subject ID: 1    │───────>│ Play Tab         
 Subject ID: 2             - Subject List   
 Subject ID: 3               + "Practice"   
 Subject ID: -6   │◄───────│ - Same UI        
 (Virtual/Adapt)           - Zero changes   
└──────────────────┘        └──────────────────┘

The mobile app knows nothing about adaptive curriculum. It sees "Practice" as just another subject with chapters and lessons.

Technical Implementation

Subject Tree API Modification

The subject tree endpoint (/api/v1/subjects/tree) returns a hierarchical list of subjects, chapters, and lessons. We modified it to inject the virtual subject:

def get_subject_tree(user_id, app_name):
    # Fetch static subjects from database
    subjects = Subject.query.filter_by(app_name=app_name).all()

    # Check if adaptive curriculum is enabled for this app
    if is_adaptive_enabled(app_name):
        # Inject virtual subject
        virtual_subject = {
            "id": -6,
            "name": "Practice",
            "description": "Personalized lessons based on your progress",
            "icon_url": "https://cdn.alphazed.app/icons/practice.png",
            "chapters": [
                {
                    "id": -100,
                    "name": "Adaptive Lessons",
                    "lessons": []  # Populated dynamically
                }
            ],
            "order": 0  # Appears first in list
        }
        subjects.insert(0, virtual_subject)

    return subjects

The negative ID (-6) distinguishes virtual subjects from real ones (positive IDs). This convention prevents ID collisions and signals special handling.

Virtual Chapter IDs

Each adaptive lesson also gets a virtual chapter ID (-100) to maintain the three-level hierarchy (subject → chapter → lesson) that the mobile app expects:

{
  "subject_id": -6,
  "chapters": [
    {
      "id": -100,
      "name": "Adaptive Lessons",
      "lessons": [
        {
          "id": 12345,
          "title": "Lesson 1",
          "items": [...]
        }
      ]
    }
  ]
}

The mobile app renders this identically to static subjects, but the backend knows that subject_id: -6 triggers adaptive generation.

Lesson Endpoint Routing

When the user taps a lesson in the "Practice" subject, the mobile app calls the standard lesson endpoint:

GET /api/v1/lessons/next?subject_id=-6&chapter_id=-100

The backend detects the virtual subject ID and routes to the adaptive lesson generator instead of the static lesson repository:

@app.route('/api/v1/lessons/next')
def get_next_lesson():
    subject_id = request.args.get('subject_id', type=int)
    chapter_id = request.args.get('chapter_id', type=int)
    user_id = get_current_user_id()

    # Check if this is a virtual subject
    if subject_id == -6:
        # Generate adaptive lesson
        lesson = content_duo_service.generate_lesson(user_id)
        return jsonify(lesson)
    else:
        # Fetch static lesson
        lesson = static_lesson_service.get_next_lesson(subject_id, chapter_id)
        return jsonify(lesson)

The mobile app receives a lesson in the same format as static lessons, so existing rendering code works without modification.

Advantages of Virtual Subjects

1. Zero Mobile App Changes

The biggest win: no iOS or Android development required. The backend controls feature rollout entirely through API changes.

2. Instant Deployment

Backend deploys are faster and less risky than mobile releases. We can deploy adaptive curriculum, test it, and iterate multiple times per day—impossible with mobile app stores.

3. Platform Consistency

Since both iOS and Android use the same API, there's no risk of platform-specific bugs or UI inconsistencies. If it works on one platform, it works on both.

4. Gradual Rollout

Feature flags control virtual subject injection per app, per environment, or per user cohort. We can:

  • Enable for 10% of users (A/B testing)
  • Enable for staging but not production
  • Enable for Amal but not Thurayya

5. Fallback Safety

If adaptive lesson generation fails (database error, timeout, etc.), we can gracefully fall back to static content or hide the virtual subject entirely—no mobile app crash.

Implementation Details

Files Modified

  • src/resources/user/play/subject_tree.py - Subject tree API endpoint
  • src/resources/user/play/lesson.py - Lesson endpoint routing
  • src/services/content_duo/content_duo.py - Adaptive lesson generation

Commits

  • 831e7c4 - Add virtual subject injection to subject tree API
  • f0a0917 - Route lesson requests to adaptive generator
  • 22d4e61 - Add feature flag for virtual subject enablement
  • f0ac780 - Add error handling and fallback logic

Test Coverage

15 integration tests covering:

  • Virtual subject appears in tree when enabled
  • Virtual subject absent when disabled
  • Lesson routing to adaptive generator
  • Fallback to static content on error
  • Per-app feature flag behavior

Backward Compatibility

Older mobile app versions that don't recognize virtual subjects handle them gracefully:

// Mobile app code (simplified)
subjects.forEach(subject => {
    if (subject.id > 0) {  // Skip non-positive IDs
        renderSubject(subject);
    }
});

This filtering logic (present in older versions) hides virtual subjects from users on outdated apps, preventing confusion until they update.

Virtual Subject Metadata

The virtual subject includes metadata that helps the mobile app render it appropriately:

{
  "id": -6,
  "name": "Practice",
  "description": "Personalized lessons based on your progress",
  "icon_url": "https://cdn.alphazed.app/icons/practice.png",
  "is_adaptive": true,
  "estimated_time_minutes": 5,
  "difficulty_level": "adaptive",
  "completion_percentage": 0  // Adaptive subjects don't have fixed completion
}

The is_adaptive: true flag allows newer mobile apps to show special UI (e.g., animated icons, "Personalized for you" badge) while older apps simply render it as a normal subject.

Handling Edge Cases

Empty Lesson Pools

If the user has completed all available content, generate_lesson() returns null. The backend removes the virtual subject from the tree:

lesson = content_duo_service.generate_lesson(user_id)
if lesson is None:
    # User has completed all content, hide virtual subject
    return subjects  # Without virtual subject injected

First-Time Users

New users have no attempt history, so HLR can't calculate retention scores. The adaptive generator defaults to beginner-level content with standard slot distribution until 10+ attempts have been recorded.

Concurrent Lesson Requests

If a user taps "Practice" repeatedly, we prevent duplicate session creation with idempotency keys:

def generate_lesson(user_id, idempotency_key=None):
    # Check for existing session with this key
    if idempotency_key:
        existing = Session.query.filter_by(
            user_id=user_id,
            idempotency_key=idempotency_key
        ).first()
        if existing:
            return existing.lesson

    # Generate new lesson
    ...

Performance Considerations

Virtual subject injection adds minimal latency to the subject tree API (<5ms). The adaptive lesson generation (triggered on lesson request, not subject tree load) takes 50-150ms depending on user history size.

To optimize:

  • Cache user persona calculations (5-minute TTL)
  • Pre-calculate retention scores for frequently-accessed concepts
  • Index last_attempt_at and user_id for fast queries

Future Enhancements

Multiple Virtual Subjects

We could inject multiple virtual subjects for different purposes:

  • "Daily Challenge" (high-difficulty adaptive lessons)
  • "Quick Review" (review-only lessons, no new content)
  • "Vocabulary Builder" (vocabulary-focused adaptive lessons)

Each would have a different negative ID and configuration.

Dynamic Subject Names

Personalize the virtual subject name based on user persona:

  • Beginner: "Your Learning Path"
  • Intermediate: "Practice & Review"
  • Advanced: "Master Challenge"

Progress Indicators

While adaptive subjects don't have fixed completion percentages, we can show relative progress:

  • Concepts mastered: 45 / 200
  • Current mastery level: Intermediate
  • Study streak: 7 days

Results: Seamless Integration

Virtual subject injection delivered:

  • Zero mobile app changes - Both iOS and Android work without modification
  • Instant deployment - Backend-only feature rollout
  • Seamless UX - "Practice" subject appears naturally in Play tab
  • Platform consistency - Identical behavior on iOS and Android
  • Future-proof architecture - Backend experiments don't require frontend coordination

Key Technical Insight

Virtual IDs (negative integers) provide a clean separation between static and dynamic content. This pattern generalizes beyond adaptive curriculum:

  • Virtual achievements (dynamic based on behavior)
  • Virtual leaderboards (real-time ranking)
  • Virtual challenges (time-limited events)

Any dynamic content can be injected as virtual entities, allowing backend experimentation without frontend coupling.


Implementation Files: src/resources/user/play/, src/services/content_duo/ Commits: 831e7c4, f0a0917, 22d4e61, f0ac780 Strategy: Inject virtual subject ID=-6 into subject tree API