Skip to content

Production-grade Unity C# mobile app for interactive, personalized reading — featuring custom serialization, adaptive storytelling, REST API integration, and async analytics. youtu.be/yHaH8IDxgMc

License

Notifications You must be signed in to change notification settings

The-Reading-Club/ReadingApp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

By José A. Alvarez Cabrera (@josealvarez97)

ReadingApp – Interactive & Personalized Reading Platform

Project Overview

ReadingApp is a Unity-based C# mobile application that delivers interactive, personalized reading experiences for children. It showcases a wide range of my technical skills: from advanced C# programming and asynchronous programming to clever use of Unity features for UI, persistence, and personalization. The project was originally built with Unity’s legacy UI Engine and gradually transitioned to Unity UIElements, reflecting adaptability to new frameworks and capacity to undertake gradual migrations in production. It also integrates various cloud services (via custom REST APIs, Firebase, sophisticated cloud functions) to enrich functionality. The codebase is structured for clarity and maintainability, using modern C# features and design patterns to keep the app modular, efficient, and robust.

Reading Club: Children's Books – Demo (Mobile App)

Unity UI – Legacy to UIElements Migration

Early versions of ReadingApp used Unity’s older Canvas UI (with UnityEngine.UI components), but as Unity introduced the new UIElements system, the project began a gradual migration—it turned out to be a good decision. This means the codebase handles both systems side-by-side – an indicator of proficiency in both traditional and modern Unity UI paradigms. For example, some screens are still built with GameObjects, Canvas, and MonoBehaviour event handlers, while newer interfaces use UIDocument with UXML/USS layouts and VisualElement logic. In one portion of the code, a TabbedMenu class obtains a UIDocument’s root and queries for specific elements to wire up events at runtime (see here). This dual approach required understanding differences in event handling, layout, and performance between the two UI systems. The migration was done in stages, demonstrating flexibility in adopting new tech without rewriting the entire app at once. The result is a snappy UI that combines the familiarity of Unity’s classic UI with the responsiveness and superior modularity of UIElements.

Data Persistence and Saving System

Persistence of user data is handled by a custom serialization engine that I implemented from scratch. At its core is a generic SavingSystem class that can save and load any data type by using C# generics and a common base class. For example, the SaveData<TData> method (where TData extends a base Data class) serializes an object to a binary .dat file using .NET’s BinaryFormatter (See here). The generic design means one method handles saving of different data types – whether it’s user profiles, settings, or progress – simply by specifying the type. Internally, it constructs a filename based on the type name or an ID, so each data class has its own file (See here). On load, the system attempts to deserialize the file; if it’s missing or corrupted, the code gracefully catches exceptions and initializes a fresh data object with default values (see here and here). This robust error-handling means no more “save file corruption” woes – the app will auto-recover by wiping bad data and starting anew. Overall, this custom saving framework highlights modern C# proficiency (generics, file I/O, try-catch for specific exceptions) and ensures persistent data is reliable.

// Saving any Data-derived object to persistent storage (binary serialization)
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Create(Application.persistentDataPath + dataPath);
bf.Serialize(file, objectToSave);
file.Close();
Debug.Log(string.Format("File saved successfully ({0})", typeof(TData).ToString()));

See source file of the above snippet here.

Several game subsystems leverage this saving system. For example, user credentials are saved securely (encrypted) after login, and a Word Bank of learned vocabulary is persisted so children can build a personal dictionary. The elegance of the solution is that adding new saved data (e.g., a new settings class or achievement log) requires minimal code – just define a class inheriting Data with an initializer, and the generic save/load covers it. This reflects clean, DRY design and forward-thinking extensibility.

Personalization & User Profiles

Personalization is at the heart of ReadingApp. The application supports multiple child profiles under a parent account, each with their own preferences and progress. I created data structures like AccountResource (for parent login info) and UsersResource (for child profiles list) to manage this. Profiles are stored locally via the saving system and synced with a cloud database when online. This multi-user setup means a family can have several readers, and the app cleanly separates each user’s data (name, age, achievements, word bank, etc.).

Story content is customized for each child. Stories can dynamically insert the child’s name and use appropriate pronouns – achieved by a combination of placeholders in story text and a PronounsBank of he/she/they word variants. During story loading, after downloading a story’s JSON data, the app populates a StoryFile object with personalization fields like specialWordTypes, promptsHelperInfo, and inputPronouns (see here). This means if a story has blanks like “[Name] went to [his/her] room”, the engine knows how to fill in the child’s name and choose the correct pronoun set. (Disclaimer: I make not political/philosophical statement regarding this feature; the purpose was to support the personalization needs of our users as imagined by my non-technical co-founder—the CEO—who takes the credit for originally conceiving the idea for the project.)

Another aspect of personalization is reading level adjustment. Each story exists in multiple difficulty levels (e.g. Junior, Intermediate, Advanced versions of text). The loader code demonstrates how it reads separate JSON files for each level and attaches them to the StoryFile object (see here). At runtime, the app can switch the content based on the child’s reading level, making sure the experience is neither too easy nor too hard. This adaptive content feature shows my ability to handle complex data (multiple files, arrays of chapters per level, etc.) and integrate it seamlessly into the storytelling engine.

Personal vocabulary building is also implemented. When a reader encounters a new or difficult word, they can tap it to bring up an interactive Word Selector popup. The selected word appears with options to hear it pronounced or save it to their Word Bank. The Word Bank is essentially a dictionary of words the child finds interesting or challenging, which is stored per user. The code for this feature is straightforward yet effective: tapping a word triggers OnSelectWord() which activates a popup UI and shows the word (see here). A quick check determines if the word is already saved in the dictionary, toggling the “Save” button or a “Saved” label accordingly. If not saved, clicking the save button calls WordBank.SaveToWordBank(...) to persist it, and updates the UI state. This interactivity encourages personalized learning, as each child’s app experience grows unique to their vocabulary progress.

(Illustration of Word selection logic and saving to personal Word Bank, with dynamic UI feedback:)

public void OnSelectWord() {
    popUpPanel.SetActive(true);
    wordText.text = selectedWord.Word.word;
    UpdateWordBankStatus();   // Show Save button or "Saved" badge
}
...
public void AddToWordBank() {
    wordBank.SaveToWordBank(selectedWord.Word);
    UpdateWordBankStatus();   // Refresh UI to reflect new saved word
}

See snippets in source file here and here, respectively.

Additionally, the app tracks achievements and rewards for each user – for example, finishing a first book might unlock a “Junior Reader” badge. The code uses an Achievements system (with classes like AchievementProfile and AchievementsGranter) to evaluate conditions and award these badges. Achievements are stored and displayed in the UI (there’s an AwardsScreen UIElements panel), giving positive reinforcement to the reader. This shows how the project integrates game-like progression elements to personalize motivation.

Interactive Reading Features

The reading experience in the app is highly interactive, leveraging multimedia and responsive design. Some key interactive features include:

  • Tap-to-Pronounce: For any word in a story, the child can tap it and hear the pronunciation via Text-to-Speech. Under the hood, this triggers the TextToSpeechModule which checks if the audio for that word is already cached. If not, it calls a cloud function to retrieve synthesized speech. The module uses UnityWebRequest to POST the word to a cloud endpoint and gets back audio bytes, which are then converted to an AudioClip and played (see here and here). The audio bytes are also saved locally (and in memory) for offline reuse, showing efficient design. This feature required integrating with an external Google Cloud Function (for TTS) and handling binary data in Unity, demonstrating both network and audio processing skills.

  • Word Bank & Review: Saved words in the Word Bank aren’t just stored; the app provides a review interface where kids can revisit those words, hear them again, and perhaps see definitions or use them in sentences. The data structures behind this use dictionaries for quick lookup of words (see here) and custom ScriptableObjects to hold lists of words. This design allows the Word Bank to be easily expanded or reset per user, and it persists through app sessions via the saving system.

  • Interactive Personalization Prompts in Stories: Stories include personalization prompts or questions (like “Where does the story take place?”) to engage the reader. The code has support for these through structures like modifyByTypePrompts and promptsHelperInfo in the StoryFile (see here). The reading interface checks these at the start of the story and shows a custom prompt UI. Implementing this meant the story JSON needed to capture interactive elements, and the app needed logic to render and handle them – a complex integration of content and code that highlights my ability to build rich interactive experiences, not just static text pages.

  • Parental Gates & Restricted Actions: Certain actions, such as accessing the in-app Shop (for purchasing books or subscriptions), are protected by a parental verification gate. The ShopUIHandler demonstrates that when the Shop button is invoked, it first launches a ParentalGateController quiz instead of showing the store immediately (see here). Only upon a correct answer does it proceed to open the Shop. This security feature showcases consideration for child safety and the use of event-driven flows (the PGateShop_OnRightAnswerSelected event triggers the actual shop display).

Overall, these interactive features are implemented cleanly, often using Unity events and ScriptableObject-based event systems to decouple the UI from the underlying logic. For instance, a custom GameEvent ScriptableObject broadcasts events to GameEventListener components (see here and here). This decoupling allows various UI elements to listen for things like “login failed” or “story loaded” and respond without tight coupling to the triggering code. Such an architecture is a mark of elegant design, making the app easier to extend and maintain.

Cloud Integration & Backend Services

ReadingApp was not just an offline app – before being open sourced, for years it was fully integrated with cloud services and a backend, demonstrating my skills in networking and backend integration. Key integrations include:

  • RESTful API Client: The app communicates with a custom backend (e.g., for user authentication, profile syncing, and analytics) via a DatabaseAPIClient. I implemented reusable methods for common calls – for example, POST_Login(email, password, onSuccess, onError) handles sending credentials and processes the JSON response containing an auth token (see here and here). The code uses Unity’s UnityWebRequest under the hood for these calls, enabling asynchronous, non-blocking HTTP requests within Unity’s coroutine system. On a successful login, the app stores the returned token for authenticated subsequent requests and immediately uses it to fetch the user profiles from the server (see here and here). By abstracting these details in DatabaseAPIClient, the login UI code remains clean – simply providing callbacks for success or failure – showcasing a well-architected separation of concerns.

  • Cloud Content Delivery: Books and stories can be updated or added server-side without requiring an app update. To facilitate this, the app downloads story content bundles from cloud storage on demand. The StoryBundleLoader uses an HttpClient to fetch a zipped story archive from a Google Cloud Storage URL asynchronously (see here). Thanks to Unity’s move to .NET 4.x, I was able to use async/await and HttpClient for efficient background downloading (replacing older blocking WebClient approaches). Once downloaded, the zip is extracted at runtime (using .NET’s ZipFile.ExtractToDirectory API) and the story data is loaded into the app’s structures. This mechanism allows the app to pull in new content or updates (“lastUpdated” timestamps are checked to decide if a redownload is needed) seamlessly. It highlights proficiency in file I/O, use of tasks in Unity, and working with compressed data – all while keeping the user experience smooth (the UI isn’t locked during downloads).

// Asynchronously download updated story content from cloud storage
HttpClient client = ReadingApp.Analytics.TheReadingClubAnalytics.Client;
HttpResponseMessage response = await client.GetAsync(zippedStoryUrl);
byte[] responseBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync(zipPath, responseBytes);
// ... then extract the ZIP and load story JSON into the app ...

See source file of the above snippet here.

  • Text-to-Speech Cloud Function: As mentioned, the app uses a cloud function for TTS. The endpoint (hosted on Google Cloud Functions) accepts a word and returns a WAV audio. The integration required crafting an HTTP POST with JSON body from Unity and handling the byte response. Unity’s UnityWebRequest is used within a Coroutine to send the request and await the result (see here and here). Upon success, the raw audio bytes are obtained and converted to sound. By offloading TTS to the cloud, the app stays lightweight, and by caching results in the local AudioBank, it avoids repeat network calls for the same word. This part of the project demonstrates skill in connecting Unity with cloud services securely and efficiently (including setting proper request headers, handling errors, etc.).

  • Firebase Analytics & Notifications: The ReadingApp also integrates Firebase services. For analytics, important events (like completing a story, or a successful login) are logged not only to our backend but also to Firebase Analytics. The code can conditionally send events to Firebase by constructing Firebase.Analytics.Parameter arrays and calling FirebaseAnalytics.LogEvent(...) (see here). This dual-logging ensures we leverage Firebase’s dashboard and insights in addition to our own system. Moreover, the app includes push notification support using Firebase Cloud Messaging. I extended the default Unity Android activity with a custom subclass (MessagingUnityPlayerActivity) to handle notification data payloads (see here and here). This was needed to fix an known issue where tapping a notification with data wouldn’t wake the app correctly – my override ensures the message intent is forwarded to the Firebase Messaging service so the app can respond when launched. Setting this up required knowledge of Android SDK, Unity plugin architecture (the class resides in Assets/Plugins/Android), and Firebase’s workflow – an integration that goes beyond typical Unity development. The result is the ability to send targeted reminders or new content alerts to users even when the app is closed, enhancing re-engagement.

  • In-App Purchases: The project also integrated Unity’s In-App Purchasing (IAP) system to allow purchasing of premium content. Evidence of this is in the repository’s generated security files (e.g. AppleTangle.cs, GooglePlayTangle.cs for receipt validation). While much of the heavy lifting is handled by Unity IAP, I implemented the logic to prompt the parental gate before purchases (to ensure kids don’t buy inadvertently) and to handle the transaction callbacks (unlocking content upon successful purchase, etc.). This demonstrates the ability to work with commerce components and platform-specific compliance (e.g., receipt verification, App Store guidelines).

In summary, the app’s cloud and backend integrations show a full-stack mindset: I wrote client-side code that talks to servers, handles JSON APIs, downloads files, and integrates third-party services. All network operations are done asynchronously so the UI remains responsive – whether it’s awaiting an HTTP response or running a coroutine – exemplifying modern best practices in app development.

Analytics and Monitoring

To continuously improve the app and understand user engagement, I built a comprehensive analytics system into ReadingApp. The TheReadingClubAnalytics static class is responsible for collecting event data and sending it to our server (and Firebase). This system showcases advanced C# usage, especially around asynchronous programming and performance:

  • The class maintains a static HttpClient instance as a singleton for all network operations (see here). This reuse of HttpClient follows .NET recommended practice to avoid socket exhaustion and improve performance. It’s a subtle detail that indicates knowledge of how HttpClient works (a novice might naively create a new client for each request).

  • Publishing an analytics event is as simple as calling TheReadingClubAnalytics.PublishAnalytics(eventID, params...). Internally, this builds a JSON string of the event data (combining constant info like timestamp, user ID, etc., with the specific event parameters) and then calls an async method to send it off (see here and here). Notably, PublishAnalytics is an async void style fire-and-forget call – it deliberately does not await the sending task, so as not to stall the main thread (see here). Instead, the sending happens in the background. I used ConfigureAwait(false) when kicking off the task to prevent Unity’s synchronization context from trying to handle continuation on the main thread. This shows an understanding of how async/await interacts with Unity’s single-threaded nature.

  • The SendAnalyticsAsync method itself is an async Task that performs the HTTP POST to our analytics API endpoint with the event JSON (see here). It yields control immediately (await Task.Yield()) to ensure the actual send happens asynchronously (see here). Inside a try/catch, it uses HttpClient.PostAsync to transmit the data and then reads the response. All exceptions (like HttpRequestException for network issues) are caught and logged so they don’t crash the app (see here). This robust error handling and use of try/catch around async calls is critical in an environment where an unhandled exception could terminate the app. By catching and logging, I ensure that even if analytics fails, the app keeps running and we have a record of what went wrong.

// Asynchronously send analytics event without blocking the Unity main thread
try {
    var reqBody = new PublishAnalyticsReqBody(eventJSON);
    string strBody = JsonUtility.ToJson(reqBody);
    var content = new StringContent(strBody, Encoding.UTF8, "application/json");
    HttpResponseMessage response = await Client.PostAsync(API_PUBSUB_ANALYTICS_PUBLISH_MESSAGE, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Debug.Log("Sent analytics async successfully");
} catch (HttpRequestException e) {
    Debug.LogError("Exception Caught in SendAnalyticsAsync! Message: " + e.Message);
}

See code snippets in source file here and here, respectively.

  • Another neat aspect is that the analytics system attaches a unique session ID on startup and includes it with every event, along with the user’s ID and app version, etc. This happens automatically so that on the server side we can group events by session or user. My code notes formatting issues encountered (e.g., there’s a comment about timestamp formats and BigQuery schema—see here), reflecting that I troubleshooted integration with analytics storage (like Google BigQuery via Pub/Sub).

  • As mentioned, events can also be forwarded to Firebase Analytics conditionally. By structuring it as an optional step, I decoupled our internal analytics from Firebase – we can turn it on/off or log selectively to Firebase without disrupting the core flow. This kind of flexibility in design is important in production apps (for example, to limit logging verbose events to Firebase if not needed).

In essence, the analytics subsystem demonstrates expertise in concurrency and asynchronous operations in Unity, safe network programming, and integration with external analytics platforms. It was built to have minimal performance impact on the app (no noticeable lag when an event is logged) and maximal usefulness for monitoring user behavior and app health.

Code Elegance and Modern C# Practices

Throughout the codebase, I aimed for clean, modern, and elegant code. Here are a few highlights that showcase my C# prowess and software engineering practices:

  • Modern Language Features: The project makes extensive use of C# features like async/await for concurrency, LINQ and lambdas, and tuples. For instance, the analytics builder uses a tuple array (string key, string value)[] to assemble event parameters, which is iterated in a clear, functional style (see here and here). The code also uses extension methods and dictionary initializers; e.g., using Dictionary.GetValueOrDefault(key, default) when checking cached translations (see here), indicating the project targets a recent .NET runtime. Nullable awareness and the ?. operator appear in places to safely handle objects, and string interpolation ($"string {variable}") is used for clean logging. All these contribute to concise, readable code.

  • ScriptableObject Architecture: I leveraged Unity’s ScriptableObjects not just for data, but for architecture. The GameEvent system is one example, decoupling event senders from listeners. Another example is the use of “Variable” ScriptableObjects (e.g., WordVariable, StoryFileVariable) to hold global state in a manageable way. Instead of using singletons or static variables, a ScriptableObject asset holds the current story or selected word, and any object that needs to know it can reference that asset. This pattern (inspired by Unity’s best practices) leads to an inspector-driven, designer-friendly architecture where, for example, UI elements can bind to a SelectedWordVariable to always show the latest tapped word. It’s an elegant solution for managing state across scenes and objects without tight coupling.

  • Clean Project Organization: The repository is organized into clear folders by feature: Scripts/ReadingInterface, Scripts/StoriesLoadingAndListing, Scripts/WordBank, UI/Uxml & UI/Uss for layouts and styles, etc. Such organization speaks to an eye for maintainability. Editor scripts (like CreateAssetBundles.cs for building content bundles) are separated under an Editor folder. Even deprecated code has its own folder, showing that I kept old experiments but isolated them safely. Consistent naming conventions and abundant comments (including links to documentation and StackOverflow for tricky parts—see here and here) make the codebase easier for any collaborator to understand. This level of cleanliness and documentation is part of my coding style.

  • Defensive Programming and Debugging Aids: The code includes many Debug.Assert and Debug.LogError calls to catch issues early in development (for example, ensuring critical objects aren’t null, as seen when loading story files and verifying the usersResource is assigned—see here). There are also TODO/FIXME comments and version notes (like a marker of an update on Sep 27 2023 in the code—funny, but see here) which show iterative improvement and awareness of potential pitfalls. By peppering the code with these, I ensured easier debugging and higher code reliability. It shows I write code with testing and troubleshooting in mind, not just the “happy path”.

  • Performance Considerations: Wherever possible, I avoid unnecessary allocations and blocking calls. The use of asynchronous file and web operations has been discussed, but even on the gameplay side, I use techniques like object pooling for certain UI elements (e.g., an ObjectPool for word buttons might exist) and efficient lookups (caching dictionaries for quick membership tests in the Word Bank—see here). Heavy computations or loops are kept outside of per-frame Update methods; the app logic often reacts to events instead of continuously polling, which is a smarter design for performance on mobile devices.

In conclusion, the ReadingApp project is a comprehensive demonstration of my ability to build a complex, feature-rich application in C# and Unity. It combines modern C# techniques (async, generics, etc.) with game development specifics (Unity engine knowledge, ScriptableObjects, asset bundles) and full-stack considerations (networking, cloud services, security). The code not only accomplishes the functional goals (engaging interactive reading for kids) but does so in a way that is elegant, scalable, and maintainable. This project highlights that I can deliver high-quality software with a broad range of technologies, writing code that I’m proud to say is clean, powerful, and effective.

About

Production-grade Unity C# mobile app for interactive, personalized reading — featuring custom serialization, adaptive storytelling, REST API integration, and async analytics. youtu.be/yHaH8IDxgMc

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages