Skip to content

Repository files navigation

AAIR Todo — React Native Developer Exercise

A To-Do List app built with Expo, TypeScript, and expo-router, featuring voice-powered task creation via the Deepgram speech-to-text API.

Built for the AAIR Labs React Native Developer Exercise (Stage 1).


Table of Contents


Features

Core (per the brief)

  • Add, view, complete/incomplete, and delete tasks
  • Task list with clear visual distinction between completed and incomplete tasks
  • Data persists locally via AsyncStorage — tasks survive app restarts and force-quits
  • Navigation between a Task List screen and an Add/Edit Task screen
  • Voice input via a floating action button: tap, speak one or several tasks naturally, review an editable transcript, confirm to add
  • Handles edge cases: empty task title is blocked, empty task list shows a proper empty state

Bonus

  • Due dates, with dynamic sectioning by due-date proximity (Overdue / Today / This Week / Later / No Due Date)
  • Search/filter functionality, plus All / Pending / Completed filter tabs with live counts
  • Light/dark theme, toggleable, persisted
  • TypeScript throughout, no any in application code
  • Unit tests across the app's core logic (see Testing)
  • Animations and transitions: swipe-to-complete/delete on task cards, animated theme toggle, animated empty states, reactive waveform during voice recording, entrance animations on the onboarding screen

Beyond the brief

  • Full Task Detail screen with a created → due → completed timeline
  • Editing existing tasks (separate from creating new ones, same form component, same validation)
  • Zod + React Hook Form validation on the task form
  • A splitting heuristic that distinguishes "log one task with a longer description" from "log several short tasks in one breath" — see Architecture Notes

Tech Stack

Concern Choice
Framework Expo (managed workflow)
Routing expo-router (file-based)
Language TypeScript
State React Context + useReducer
Forms & validation React Hook Form + Zod
Persistence @react-native-async-storage/async-storage
Animation React Native Reanimated
Gestures React Native Gesture Handler
Voice recording expo-av
Speech-to-text Deepgram API (nova-2 model)
Testing Jest (jest-expo preset) + @testing-library/react-native

Setup & Running Locally

  1. Install dependencies

    npm install
    # or
    yarn install
  2. Environment variables — create a .env file in the project root:

    EXPO_PUBLIC_DEEPGRAM_API_KEY=your_deepgram_api_key_here
    

    Required for voice input. Get a free key at console.deepgram.com — includes $200 in free credits, no card required to start. .env is gitignored; it is not included in this submission.

  3. Start the dev server

    npx expo start
  4. Run it

    • i for iOS simulator, a for Android emulator, or scan the QR code with Expo Go on a physical device.
    • Voice input requires a real device, or a simulator/emulator with working microphone passthrough — many default simulators don't support live mic input, and the recording step will silently fail or error out on those.

Project Structure

app/                        # expo-router routes (thin — logic lives in src/screens)
  _layout.tsx                # Root layout: providers (Theme, Task), Stack navigator
  index.tsx                  # Redirects to /onboarding
  onboarding.tsx
  (tabs)/
    _layout.tsx               # Tab navigator
    index.tsx                 # → DashboardScreen
  task/[id].tsx               # → TaskDetailScreen
  create-task.tsx             # → TaskFormScreen (mode="create")
  edit-task/[id].tsx          # Resolves task, → TaskFormScreen (mode="edit")

src/
  components/
    core/                     # Reusable primitives (Input, HeaderTitle, EmptyState, GoBackBtn...)
    dashboard/                # FilterTabs, SectionContainer, TodoItemCard, VoiceCaptureFAB
    forms/                    # FormInput, FormDateField, TaskFormFields, DisplayStatus
    voice/                    # VoiceRecordingModal, Waveform
    layout/                   # FormContainer
  context/                    # ThemeContext, TaskContext (state + AsyncStorage persistence)
  hooks/                      # useZodForm, useTaskForm, useGroupedTasks, useVoiceRecorder
  services/                   # storage.ts (AsyncStorage), speechToText.ts (Deepgram)
  utils/                      # taskSplitter, date helpers, scaling, id generator
  validations/                # Zod schemas
  theme/                      # Light/dark color tokens
  constants/                  # Metrics, typography scale
  types/                      # Shared TypeScript types
  screenshots/                # Submission screenshots (see below)

__tests__ folders live alongside the code they test (e.g. src/utils/__tests__/).

Architecture Notes

State management — React Context + useReducer, not a state library like Zustand. The brief's own evaluation criteria calls out "components, hooks, state, navigation" as what's being assessed, so leaning on raw fundamentals felt like the more honest signal for a skills exercise, even though Zustand is my usual default on other projects.

Voice input pipelineexpo-av records audio locally; the finished clip is sent to Deepgram's pre-recorded transcription endpoint (nova-2 model). This is a batch, not streaming, API — so there is no live word-by-word transcript while the user is still talking. Instead:

  • During recording: a waveform reacts to live mic metering (via onRecordingStatusUpdate) for real-time confidence that you're being heard.
  • The moment recording stops: the clip is transcribed, and a fully editable transcript field appears before anything is saved — this is the actual "visible feedback of what you said," just positioned after processing completes rather than live, since a batch API has no other honest way to offer it.

Stop behavior — hybrid by design: automatic silence detection (stops ~1.5s after the user goes quiet, mirroring how voice assistants behave) plus a manual Stop button as an always-available override. Silence detection only arms after a minimum recording duration and only after real speech has already been detected, so it can't fire before the user starts talking.

Task splitting (src/utils/taskSplitter.ts) — two behaviors depending on what was actually said:

  1. Multiple short items joined by "and"/"then" ("buy provisions and call mom") → split into separate, title-only tasks.
  2. A single longer dictation with no connector words → split into a title (first clause/sentence) + a description (the remainder, hard-capped at 500 words).

Why the OpenAI API key was swapped for Deepgram — the brief names "the OpenAI API or another speech-to-text API." The available OpenAI key didn't have Whisper model access enabled at the project-permission level (not something fixable client-side, and dashboard access to fix it wasn't available). Deepgram was substituted, using the exact same "single isolated service file" architecture (src/services/speechToText.ts) — swapping providers touched only that one file, nothing else in the voice pipeline changed.

Security — the Deepgram API key ships in the client bundle, since EXPO_PUBLIC_ env vars are inlined at build time. Acceptable for a take-home exercise; in a production app this call would be proxied through a backend so the key never reaches the client. Noted explicitly here rather than left unstated.


Known Limitations

Being upfront about tradeoffs rather than leaving them for a reviewer to discover:

  • The task-splitting heuristic isn't perfect. It treats "and"/"then" as task boundaries, so a single dictated task that happens to use "and" as an ordinary conjunction (e.g. "submit the report and make sure it includes the Q3 numbers") will get split into two tasks rather than one task with a longer description. A more robust version would use an LLM call to classify intent rather than a connector-word regex — noted as the natural next iteration, not implemented here to keep the voice pipeline's cost and latency predictable for a take-home.
  • No live partial transcript during recording — an inherent constraint of using a batch (not streaming) transcription API, explained above.
  • Voice input doesn't work in most simulators without explicit microphone passthrough configuration — test on a real device for the most reliable experience.

Testing

npm test

Weighted deliberately toward pure-logic and isolated-component tests over full screen integration tests — in React Native, screen-level tests require heavy mocking of native modules (Reanimated, gesture-handler, expo-router) and tend to end up testing the mocks more than the app. Covered:

File What it covers
src/utils/__tests__/taskSplitter.test.ts Multi-task vs. title/description splitting, truncation, 500-word cap, punctuation cleanup, connector-word edge cases
src/utils/__tests__/date.test.ts Relative date formatting, overdue detection, calendar-day boundary handling
src/services/__tests__/storage.test.ts AsyncStorage read/write round-trip, corrupted-data fallback, write/read failure handling
src/context/__tests__/taskReducer.test.ts Every TaskContext reducer action in isolation, including no-op/unknown-action cases
src/hooks/__tests__/useGroupedTasks.test.ts Dynamic sectioning across all three filter tabs, search filtering, recomputation on prop changes
src/components/core/__tests__/EmptyState.test.tsx Component-level render + interaction test

Note on renderHook: the current version of @testing-library/react-native made renderHook (and its rerender) async, so hook tests await renderHook(...) and await rerender(...) throughout — this is intentional, not a leftover bug.


Screenshots

Required by the brief, plus a few extra to show off theming and the detail view. Save each capture with the exact filename below so these embeds resolve correctly — PNG or JPG, full device screen, no mockup frames.

Required

Onboarding Screen

Onboarding Screen - Dark
Dark
Onboarding Screen - Light
Light

Task List

Task List - Empty
Empty State
Task List - Mixed
Mixed Completed/Incomplete

Add Task Screen

Add Task Form

Voice Input

Voice Input - Listening
Listening / FAB Active
Voice Input - Review
Review & Confirm

Bonus Features

Theming

Dark Mode
Dark Mode
Light Mode
Light Mode

Search & Due Dates

Search In Use
Search In Use
Due Dates
Due Dates & Sectioning

Extra

Task Detail Screen

Task Detail

Voice Input Flow — Screen Recording

A short screen recording of the full voice flow (tap → listen → process → review → confirm) is included at ./src/screenshots/voice-input-demo.mov.


Built by Emmanuel Chika.

About

A todo application

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages