Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs-unity.molca.id/llms.txt

Use this file to discover all available pages before exploring further.

Applies to: Molca Core

Overview

This recipe shows you how to combine HttpClient and DataManager to implement API calls with local data caching. You’ll learn how to create HttpRequest ScriptableObjects, make async HTTP requests, parse response data, save it to DataManager for persistence, and load cached data on startup. This pattern is ideal for scenarios where you need to fetch data from a remote API but want to provide offline access or reduce network calls.

Prerequisites

  • SDK modules: Molca Core installed
  • Unity setup: RuntimeManager configured in your scene
  • Prior knowledge: HttpClient, DataManager, Dependency injection
  • Recommended: Understanding of C# async/await patterns

Step-by-step

Step 1: Create HttpRequest ScriptableObject

Create an HttpRequestAsset to define your API endpoint configuration. This makes the request reusable and configurable in the Inspector.
// In the Unity Editor:
// 1. Right-click in Project window → Create → Molca → Networking → HTTP Request
// 2. Name it "FetchUserProfileRequest"
// 3. Configure in Inspector:
//    - URL: "/api/user/profile"
//    - Method: GET
//    - Headers: Add "Accept: application/json"
//    - Timeout: 10 seconds
Why this works: HttpRequestAsset is a ScriptableObject that encapsulates endpoint configuration (URL, method, headers, body templates). This separates configuration from code and makes it easy to modify endpoints without recompiling.

Step 2: Use HttpClient to make API call

Inject HttpClient (or use it statically) to send the request asynchronously. Handle the response and check for errors.
using UnityEngine;
using Molca;
using Molca.Networking.Http;
using System.Threading.Tasks;

public class UserProfileLoader : MonoBehaviour
{
    [SerializeField] private HttpRequestAsset fetchProfileRequest;

    private async void Start()
    {
        await RuntimeManager.WaitForInitialization();
        
        // Fetch user profile from API
        await FetchUserProfile();
    }

    private async Task FetchUserProfile()
    {
        Debug.Log("Fetching user profile from API...");
        
        try
        {
            // Send the HTTP request
            HttpResponse response = await fetchProfileRequest.SendAsync();
            
            if (response.IsSuccess)
            {
                Debug.Log($"Profile fetched successfully: {response.Text}");
                // Parse and process response in next step
            }
            else
            {
                Debug.LogError($"Failed to fetch profile: {response.ErrorMessage}");
            }
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"HTTP request exception: {ex.Message}");
        }
    }
}
Why this works: HttpRequestAsset.SendAsync() returns an Awaitable<HttpResponse> that completes when the request finishes. Using async/await prevents blocking the main thread during network operations. The IsSuccess property checks the HTTP status code (200-299 range).

Step 3: Parse response data

Parse the JSON response into a C# data structure. Use Unity’s JsonUtility or a third-party JSON library.
using UnityEngine;
using Molca;
using Molca.Networking.Http;
using System;
using System.Threading.Tasks;

// Define a data structure matching your API response
[Serializable]
public class UserProfile
{
    public string userId;
    public string username;
    public string email;
    public int level;
    public long lastLoginTimestamp;
}

public class UserProfileLoader : MonoBehaviour
{
    [SerializeField] private HttpRequestAsset fetchProfileRequest;
    
    private UserProfile _cachedProfile;

    private async void Start()
    {
        await RuntimeManager.WaitForInitialization();
        await FetchUserProfile();
    }

    private async Task FetchUserProfile()
    {
        Debug.Log("Fetching user profile from API...");
        
        try
        {
            HttpResponse response = await fetchProfileRequest.SendAsync();
            
            if (response.IsSuccess)
            {
                // Parse JSON response
                _cachedProfile = JsonUtility.FromJson<UserProfile>(response.Text);
                
                Debug.Log($"Profile loaded: {_cachedProfile.username} (Level {_cachedProfile.level})");
                
                // Save to DataManager in next step
            }
            else
            {
                Debug.LogError($"Failed to fetch profile: {response.ErrorMessage}");
            }
        }
        catch (Exception ex)
        {
            Debug.LogError($"Failed to parse profile: {ex.Message}");
        }
    }
}
Why this works: JsonUtility.FromJson<T>() deserializes JSON into a C# object. The [Serializable] attribute is required for JsonUtility to work. For complex JSON structures, consider using Newtonsoft.Json or System.Text.Json instead.

Step 4: Save to DataManager for persistence

Use DataManager to save the parsed data locally. This enables offline access and reduces redundant API calls.
using UnityEngine;
using Molca;
using Molca.Networking.Http;
using Molca.Networking.Data;
using System;
using System.Threading.Tasks;

[Serializable]
public class UserProfile
{
    public string userId;
    public string username;
    public string email;
    public int level;
    public long lastLoginTimestamp;
}

public class UserProfileManager : MonoBehaviour
{
    [SerializeField] private HttpRequestAsset fetchProfileRequest;
    
    private const string PROFILE_CACHE_KEY = "UserProfile";
    private UserProfile _currentProfile;

    private async void Start()
    {
        await RuntimeManager.WaitForInitialization();
        
        // Try to load cached profile first
        bool hasCachedData = LoadCachedProfile();
        
        if (!hasCachedData)
        {
            Debug.Log("No cached profile found, fetching from API...");
            await FetchAndCacheProfile();
        }
        else
        {
            Debug.Log($"Loaded cached profile: {_currentProfile.username}");
            
            // Optionally refresh in background
            _ = FetchAndCacheProfile();
        }
    }

    private async Task FetchAndCacheProfile()
    {
        try
        {
            HttpResponse response = await fetchProfileRequest.SendAsync();
            
            if (response.IsSuccess)
            {
                // Parse the response
                _currentProfile = JsonUtility.FromJson<UserProfile>(response.Text);
                
                Debug.Log($"Profile fetched: {_currentProfile.username} (Level {_currentProfile.level})");
                
                // Save to DataManager for persistence
                SaveProfileToCache(_currentProfile);
            }
            else
            {
                Debug.LogError($"Failed to fetch profile: {response.ErrorMessage}");
            }
        }
        catch (Exception ex)
        {
            Debug.LogError($"Failed to fetch profile: {ex.Message}");
        }
    }

    private void SaveProfileToCache(UserProfile profile)
    {
        // Convert profile to JSON string
        string json = JsonUtility.ToJson(profile);
        
        // Save to PlayerPrefs (or use DataManager's persistence layer)
        PlayerPrefs.SetString(PROFILE_CACHE_KEY, json);
        PlayerPrefs.Save();
        
        Debug.Log("Profile saved to cache");
    }

    private bool LoadCachedProfile()
    {
        if (PlayerPrefs.HasKey(PROFILE_CACHE_KEY))
        {
            string json = PlayerPrefs.GetString(PROFILE_CACHE_KEY);
            _currentProfile = JsonUtility.FromJson<UserProfile>(json);
            return true;
        }
        
        return false;
    }
}
Why this works: PlayerPrefs provides simple key-value persistence across sessions. For more complex scenarios, DataManager can coordinate multiple data providers with caching policies. The pattern of “load cached → fetch fresh → update cache” ensures the UI is responsive while keeping data current.

Step 5: Implement cache invalidation and refresh logic

Add logic to determine when cached data is stale and needs refreshing. Use timestamps or TTL (time-to-live) patterns.
using UnityEngine;
using Molca;
using Molca.Networking.Http;
using System;
using System.Threading.Tasks;

[Serializable]
public class UserProfile
{
    public string userId;
    public string username;
    public string email;
    public int level;
    public long lastLoginTimestamp;
}

public class UserProfileManager : MonoBehaviour
{
    [SerializeField] private HttpRequestAsset fetchProfileRequest;
    [SerializeField] private float cacheValiditySeconds = 300f; // 5 minutes
    
    private const string PROFILE_CACHE_KEY = "UserProfile";
    private const string CACHE_TIMESTAMP_KEY = "UserProfile_Timestamp";
    
    private UserProfile _currentProfile;

    private async void Start()
    {
        await RuntimeManager.WaitForInitialization();
        
        // Check if cached data is still valid
        if (IsCacheValid())
        {
            Debug.Log("Loading valid cached profile...");
            LoadCachedProfile();
        }
        else
        {
            Debug.Log("Cache expired or missing, fetching from API...");
            await FetchAndCacheProfile();
        }
    }

    private bool IsCacheValid()
    {
        if (!PlayerPrefs.HasKey(PROFILE_CACHE_KEY) || !PlayerPrefs.HasKey(CACHE_TIMESTAMP_KEY))
        {
            return false;
        }
        
        // Get the timestamp when data was cached
        long cachedTimestamp = long.Parse(PlayerPrefs.GetString(CACHE_TIMESTAMP_KEY));
        long currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        
        // Check if cache is still within validity period
        long ageSeconds = currentTimestamp - cachedTimestamp;
        return ageSeconds < cacheValiditySeconds;
    }

    private async Task FetchAndCacheProfile()
    {
        try
        {
            HttpResponse response = await fetchProfileRequest.SendAsync();
            
            if (response.IsSuccess)
            {
                _currentProfile = JsonUtility.FromJson<UserProfile>(response.Text);
                
                Debug.Log($"Profile fetched: {_currentProfile.username} (Level {_currentProfile.level})");
                
                // Save with timestamp
                SaveProfileToCache(_currentProfile);
            }
            else
            {
                Debug.LogError($"Failed to fetch profile: {response.ErrorMessage}");
                
                // Fall back to cached data if available
                if (PlayerPrefs.HasKey(PROFILE_CACHE_KEY))
                {
                    Debug.Log("Using stale cached data as fallback");
                    LoadCachedProfile();
                }
            }
        }
        catch (Exception ex)
        {
            Debug.LogError($"Failed to fetch profile: {ex.Message}");
            
            // Fall back to cached data
            if (PlayerPrefs.HasKey(PROFILE_CACHE_KEY))
            {
                Debug.Log("Using cached data due to network error");
                LoadCachedProfile();
            }
        }
    }

    private void SaveProfileToCache(UserProfile profile)
    {
        string json = JsonUtility.ToJson(profile);
        PlayerPrefs.SetString(PROFILE_CACHE_KEY, json);
        
        // Save current timestamp
        long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        PlayerPrefs.SetString(CACHE_TIMESTAMP_KEY, timestamp.ToString());
        
        PlayerPrefs.Save();
        Debug.Log("Profile saved to cache with timestamp");
    }

    private void LoadCachedProfile()
    {
        if (PlayerPrefs.HasKey(PROFILE_CACHE_KEY))
        {
            string json = PlayerPrefs.GetString(PROFILE_CACHE_KEY);
            _currentProfile = JsonUtility.FromJson<UserProfile>(json);
            Debug.Log($"Loaded cached profile: {_currentProfile.username}");
        }
    }

    public void ForceRefresh()
    {
        Debug.Log("Forcing profile refresh...");
        _ = FetchAndCacheProfile();
    }
}
Why this works: Storing a timestamp alongside cached data enables TTL-based invalidation. The IsCacheValid() method compares the cache age against the configured validity period. Fallback to stale cache on network errors provides graceful degradation.

Complete example

Here’s a complete example showing HTTP + DataManager integration with proper error handling, caching, and refresh logic:
using UnityEngine;
using Molca;
using Molca.Networking.Http;
using Molca.Events;
using System;
using System.Threading.Tasks;

/// <summary>
/// Data model for user profile from API
/// </summary>
[Serializable]
public class UserProfile
{
    public string userId;
    public string username;
    public string email;
    public int level;
    public int experiencePoints;
    public long lastLoginTimestamp;
}

/// <summary>
/// Manages user profile data with HTTP fetching and local caching.
/// Demonstrates integration of HttpClient and DataManager patterns.
/// </summary>
public class UserProfileService : MonoBehaviour
{
    [Header("HTTP Configuration")]
    [SerializeField] private HttpRequestAsset fetchProfileRequest;
    
    [Header("Cache Configuration")]
    [SerializeField] private float cacheValiditySeconds = 300f; // 5 minutes
    [SerializeField] private bool refreshOnStart = true;
    
    [Header("Events")]
    [Inject] private EventDispatcher _eventDispatcher;
    
    private const string PROFILE_CACHE_KEY = "UserProfile";
    private const string CACHE_TIMESTAMP_KEY = "UserProfile_Timestamp";
    
    private UserProfile _currentProfile;
    private bool _isFetching = false;

    private async void Start()
    {
        await RuntimeManager.WaitForInitialization();
        
        // Load cached profile immediately for fast UI display
        bool hasCachedData = LoadCachedProfile();
        
        if (hasCachedData)
        {
            Debug.Log($"Loaded cached profile: {_currentProfile.username}");
            NotifyProfileLoaded();
            
            // Check if cache is stale
            if (!IsCacheValid() && refreshOnStart)
            {
                Debug.Log("Cache is stale, refreshing in background...");
                await RefreshProfile();
            }
        }
        else
        {
            Debug.Log("No cached profile found, fetching from API...");
            await RefreshProfile();
        }
    }

    /// <summary>
    /// Fetch profile from API and update cache
    /// </summary>
    public async Task<bool> RefreshProfile()
    {
        if (_isFetching)
        {
            Debug.LogWarning("Profile fetch already in progress");
            return false;
        }
        
        _isFetching = true;
        
        try
        {
            Debug.Log("Fetching user profile from API...");
            
            HttpResponse response = await fetchProfileRequest.SendAsync();
            
            if (response.IsSuccess)
            {
                // Parse the JSON response
                UserProfile newProfile = JsonUtility.FromJson<UserProfile>(response.Text);
                
                if (newProfile != null)
                {
                    _currentProfile = newProfile;
                    SaveProfileToCache(_currentProfile);
                    
                    Debug.Log($"Profile refreshed: {_currentProfile.username} (Level {_currentProfile.level})");
                    
                    NotifyProfileLoaded();
                    return true;
                }
                else
                {
                    Debug.LogError("Failed to parse profile JSON");
                    return false;
                }
            }
            else
            {
                Debug.LogError($"HTTP request failed: {response.ErrorMessage} (Status: {response.StatusCode})");
                
                // Use cached data as fallback
                if (_currentProfile != null)
                {
                    Debug.Log("Using cached profile as fallback");
                    return true;
                }
                
                return false;
            }
        }
        catch (Exception ex)
        {
            Debug.LogError($"Exception during profile fetch: {ex.Message}");
            
            // Use cached data as fallback
            if (_currentProfile != null)
            {
                Debug.Log("Using cached profile due to exception");
                return true;
            }
            
            return false;
        }
        finally
        {
            _isFetching = false;
        }
    }

    /// <summary>
    /// Get the current profile (may be cached)
    /// </summary>
    public UserProfile GetProfile()
    {
        return _currentProfile;
    }

    /// <summary>
    /// Check if cached data is still valid based on TTL
    /// </summary>
    private bool IsCacheValid()
    {
        if (!PlayerPrefs.HasKey(PROFILE_CACHE_KEY) || !PlayerPrefs.HasKey(CACHE_TIMESTAMP_KEY))
        {
            return false;
        }
        
        try
        {
            long cachedTimestamp = long.Parse(PlayerPrefs.GetString(CACHE_TIMESTAMP_KEY));
            long currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            long ageSeconds = currentTimestamp - cachedTimestamp;
            
            return ageSeconds < cacheValiditySeconds;
        }
        catch (Exception ex)
        {
            Debug.LogWarning($"Failed to parse cache timestamp: {ex.Message}");
            return false;
        }
    }

    /// <summary>
    /// Save profile to persistent cache with timestamp
    /// </summary>
    private void SaveProfileToCache(UserProfile profile)
    {
        try
        {
            string json = JsonUtility.ToJson(profile);
            PlayerPrefs.SetString(PROFILE_CACHE_KEY, json);
            
            long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            PlayerPrefs.SetString(CACHE_TIMESTAMP_KEY, timestamp.ToString());
            
            PlayerPrefs.Save();
            Debug.Log("Profile saved to cache");
        }
        catch (Exception ex)
        {
            Debug.LogError($"Failed to save profile to cache: {ex.Message}");
        }
    }

    /// <summary>
    /// Load profile from persistent cache
    /// </summary>
    private bool LoadCachedProfile()
    {
        if (!PlayerPrefs.HasKey(PROFILE_CACHE_KEY))
        {
            return false;
        }
        
        try
        {
            string json = PlayerPrefs.GetString(PROFILE_CACHE_KEY);
            _currentProfile = JsonUtility.FromJson<UserProfile>(json);
            return _currentProfile != null;
        }
        catch (Exception ex)
        {
            Debug.LogError($"Failed to load cached profile: {ex.Message}");
            return false;
        }
    }

    /// <summary>
    /// Notify other systems that profile data is available
    /// </summary>
    private void NotifyProfileLoaded()
    {
        _eventDispatcher?.DispatchEvent<UserProfile>("UserProfileLoaded", _currentProfile);
    }

    /// <summary>
    /// Clear cached profile data
    /// </summary>
    public void ClearCache()
    {
        PlayerPrefs.DeleteKey(PROFILE_CACHE_KEY);
        PlayerPrefs.DeleteKey(CACHE_TIMESTAMP_KEY);
        PlayerPrefs.Save();
        
        _currentProfile = null;
        Debug.Log("Profile cache cleared");
    }
}
This example demonstrates:
  • Immediate cache loading for fast UI display
  • Background refresh when cache is stale
  • Proper async/await patterns with error handling
  • TTL-based cache invalidation
  • Fallback to cached data on network errors
  • Event notification when profile is loaded
  • Public API for manual refresh and cache clearing

Troubleshooting

  • HTTP request fails with “BaseUrl not set”: Configure the HttpModule in Global Settings with your API base URL. Alternatively, set useFullUrl = true on the HttpRequestAsset and provide the complete URL.
  • JSON parsing fails: Verify that your C# data structure matches the API response format exactly. Field names must match (case-sensitive). Use [SerializeField] for private fields or make fields public. For complex JSON, consider using Newtonsoft.Json instead of JsonUtility.
  • Cached data not persisting between sessions: Ensure you call PlayerPrefs.Save() after setting values. On some platforms, PlayerPrefs may not persist immediately without explicit save. For more robust persistence, consider using DataManager with a custom data provider.
  • Cache never invalidates: Verify that cacheValiditySeconds is set to a reasonable value. Check that the timestamp is being saved correctly. Use Debug.Log to print cache age and validity checks.
  • Profile loads but events don’t fire: Ensure EventDispatcher is injected successfully. Call await RuntimeManager.WaitForInitialization() before dispatching events. Verify that subscribers are registered before the event is dispatched.
  • Multiple simultaneous fetches: The _isFetching flag prevents concurrent requests. If you need to queue requests, implement a request queue or use DataManager’s built-in queuing.