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