Mutual Fund Explorer & Portfolio Tracker
React Native · Expo SDK 54 · TypeScript
Growth is a cross-platform mobile application for discovering mutual funds, viewing NAV performance history through interactive charts, and organizing funds into custom watchlist portfolios. All fund data is sourced from the public MFAPI endpoint.
Submitted for the Groww Mobile SDE Intern Assignment.
| iOS | Android |
|---|---|
growth_ios.mp4 |
growth_android.mp4 |
| Resource | Link |
|---|---|
| APK | Download APK |
| Video Demo | Google Drive |
- Features
- Architecture
- Tech Stack
- Project Structure
- State Management
- Data Fetching & Caching
- Persistence
- UI State Handling
- Testing
- Getting Started
- Design Decisions
- Evaluation Criteria
- Four curated categories: Index Funds, Bluechip Funds, Tax Saver (ELSS), Large Cap Funds.
- Each category renders a 2×2 grid of fund cards (Fund Name, AMC logo, latest NAV) with a View All CTA.
- Pull-to-refresh support.
- Since MFAPI lacks a category endpoint, the Search API is queried with relevant keywords (
index,bluechip,tax saver,large cap) to populate each section.
- Dedicated tab for user-created portfolio folders (e.g. "Retirement", "Tax Savers").
- Summary strip showing total portfolios and tracked funds.
- Create and delete portfolios via bottom sheet and confirmation dialog.
- Skia-drawn empty state illustration with descriptive text and CTA when no portfolios exist.
- AMC Name, Scheme Type (Growth / IDCW), Category, and latest NAV with daily change.
- Interactive NAV line chart (Victory Native + Skia) with 6M / 1Y / ALL range selector.
- Raw NAV data downsampled to 180 points via bucket-averaging for smooth rendering.
- Bookmark toggle reflecting whether the fund exists in any watchlist folder.
- Toast notification on successful portfolio addition.
- Triggered from the Fund Details screen.
- Checkbox list of existing portfolios with inline create-new input.
- Keyboard-aware sheet with auto-expand on input focus.
- View All: Infinite-scroll list powered by
@shopify/flash-listwith pagination skeleton loaders. - Search: Debounced input (300ms), recent suggestions, animated transitions across recent / loading / results / empty / error states.
┌──────────────────────────────────────────────┐
│ Screens │
│ app/ (Expo Router) │
├──────────────────────────────────────────────┤
│ Components │
│ primitives/ explore/ fund/ watchlist/ │
├──────────────────────────────────────────────┤
│ Hooks Layer │
│ use-category-funds use-fund-details │
│ use-search-funds use-folder-funds │
├───────────────────┬──────────────────────────┤
│ Services │ Stores │
│ (Axios + MMKV) │ (Zustand + MMKV) │
├───────────────────┴──────────────────────────┤
│ Types & Constants │
└──────────────────────────────────────────────┘
| Layer | Responsibility |
|---|---|
| Screens | File-based routing via Expo Router. Custom animated tab bar. Stack navigation for detail screens. |
| Components | Design-system primitives (Screen, Text, Card, Button, Chip, Skeleton, StateView) and feature-specific components. Zero hardcoded colors — all primitives consume theme tokens. |
| Hooks | React Query hooks encapsulating data-fetching logic. Each returns typed data with isLoading, isError, and derived states. |
| Services | Axios HTTP client configured for MFAPI.in. MMKV storage adapters for Zustand and React Query persistence. |
| Stores | Zustand store with MMKV middleware for watchlist state. Reactive — mutations propagate instantly across all screens. |
| Category | Library | Version |
|---|---|---|
| Framework | React Native | 0.81 |
| Platform | Expo SDK | 54 |
| Language | TypeScript | 5.9 |
| Routing | Expo Router | 6 |
| State Management | Zustand | 5 |
| Server State | TanStack React Query | 5 |
| HTTP | Axios | 1.15 |
| Storage | react-native-mmkv | 3 |
| Charts | Victory Native (Skia) | 41 |
| Graphics | @shopify/react-native-skia | 2.2 |
| Lists | @shopify/flash-list | 2.3 |
| Animation | react-native-reanimated | 4.1 |
| Bottom Sheet | @gorhom/bottom-sheet | 5.2 |
| Haptics | expo-haptics | 15 |
| Images | expo-image | 3 |
| Fonts | expo-font | 14 |
| Testing | Jest + jest-expo | 29 / 55 |
growth/
├── app/ # Screens (Expo Router)
│ ├── _layout.tsx # Root layout — providers, splash, font loading
│ ├── search.tsx # Search with debounced input
│ ├── (tabs)/
│ │ ├── _layout.tsx # Animated tab bar (Ionicons + Reanimated)
│ │ ├── index.tsx # Explore — category grids
│ │ └── watchlist.tsx # Portfolio folders + create sheet
│ ├── fund/[code].tsx # Fund detail — chart, info, bookmark
│ ├── category/[key].tsx # Category listing — infinite scroll
│ └── folder/[id].tsx # Portfolio detail — fund list
│
├── components/
│ ├── primitives/ # Design-system atoms (15 components)
│ ├── explore/ # Category grid, fund cards
│ ├── fund/ # Chart, range selector, info row, bookmark
│ ├── watchlist/ # Folder card, add-to-watchlist sheet
│ └── illustrations/ # Skia empty-state illustrations
│
├── hooks/
│ ├── use-theme.ts # System color scheme + MMKV persistence
│ ├── use-haptic.ts # expo-haptics wrapper
│ ├── use-debounced-value.ts # Generic debounce (300ms default)
│ └── queries/ # React Query hooks (4 hooks)
│
├── services/
│ ├── api.ts # Axios instance (api.mfapi.in)
│ ├── mmkv.ts # MMKV adapters for Zustand + React Query
│ └── mfapi.ts # searchFunds, getFundLatest, getFundDetails
│
├── stores/
│ └── watchlist-store.ts # Zustand + MMKV persistence
│
├── constants/
│ ├── theme.ts # Colors, Spacing, Radius, Typography
│ ├── categories.ts # Category definitions + search keywords
│ └── amc-logos.ts # Fund house → CDN logo URL mapping
│
├── types/ # TypeScript type definitions
├── utils/ # format, downsample, color-from-string
├── __tests__/ # 7 test suites (69 tests)
└── assets/ # Fonts (Clash Display), icons, splash
interface WatchlistState {
folders: Folder[];
createFolder: (name: string) => string;
deleteFolder: (folderId: string) => void;
toggleFundInFolder: (schemeCode: number, folderId: string) => void;
isFundInAnyFolder: (schemeCode: number) => boolean;
getFoldersContainingFund: (schemeCode: number) => string[];
}Persisted to MMKV via zustand/middleware. Toggling a fund on the detail screen instantly updates the bookmark icon and all watchlist views.
| Hook | Purpose | Stale Time |
|---|---|---|
useCategoryFunds |
Explore grid — search + top 4 latest NAVs | 3h |
useFundDetails |
Fund detail — full NAV history with date range | 3h |
useSearchFunds |
Search — debounced with keepPreviousData |
6h |
useFolderFunds |
Portfolio detail — parallel latest fetches | 3h |
| Endpoint | Usage |
|---|---|
GET /mf/search?q={query} |
Explore categories, Search screen |
GET /mf/{code}/latest |
Fund cards, portfolio fund lists |
GET /mf/{code} |
Fund detail (full NAV history + metadata) |
- React Query handles request deduplication, automatic retry (2 attempts, exponential backoff), and stale-while-revalidate.
- MMKV-backed persister saves the query cache to disk with a 7-day max age — the app serves cached data when offline.
- Stale times are tuned per query type to balance freshness with API load.
Since MFAPI has no category endpoint, each category is configured with a searchKeyword. useCategoryFunds queries the Search API, ranks results by relevance, then fetches the latest NAV for the top 4. Results are cached for instant subsequent loads.
| Data | Storage | Mechanism |
|---|---|---|
| Watchlist folders & fund associations | MMKV | Zustand persist middleware |
| API response cache | MMKV | React Query PersistQueryClientProvider |
| Theme preference | MMKV | Zustand store (system default) |
All persistence uses react-native-mmkv — a C++ JSI-based key-value store, significantly faster than AsyncStorage.
Every screen implements explicit Loading, Error, and Empty states:
| Screen | Loading | Error | Empty |
|---|---|---|---|
| Explore | Staggered skeleton sections | StateView with retry |
Detected when all categories return 0 funds |
| Search | 5 skeleton rows | Network error icon with retry | Illustration with query text |
| Fund Detail | Full-page skeleton | Error fallback message | N/A |
| Category | 5 skeleton rows | StateView with retry |
StateView empty state |
| Watchlist | N/A (local) | N/A (local) | Illustration with CTA |
| Portfolio Detail | 5 skeleton rows | Alert icon with retry | Illustration with "Explore Funds" CTA |
Skeleton loaders use Reanimated opacity pulses with staggered delays for a cascading effect. The reusable StateView component manages state transitions:
type UiState = 'loading' | 'success' | 'error' | 'empty';Framework: Jest 29 with jest-expo preset
Libraries: @testing-library/react-native, @testing-library/jest-native
PASS __tests__/watchlist-store.test.ts (15 tests)
PASS __tests__/format.test.ts (14 tests)
PASS __tests__/amc-logos.test.ts (10 tests)
PASS __tests__/mfapi.test.ts (8 tests)
PASS __tests__/downsample.test.ts (7 tests)
PASS __tests__/categories.test.ts (5 tests)
PASS __tests__/color-from-string.test.ts (5 tests)
Test Suites: 7 passed, 7 total
Tests: 69 passed, 69 total
| Suite | Coverage |
|---|---|
watchlist-store |
Create/delete folders, toggle funds, query fund membership |
format |
INR formatting, NAV delta, date parsing, scheme type detection |
amc-logos |
URL resolution, whitespace normalization, keyword fallback |
mfapi |
API request params, error propagation, edge cases (mocked Axios) |
downsample |
Bucket-averaging accuracy, edge cases, large datasets |
categories |
Category keys, labels, search keywords |
color-from-string |
Deterministic generation, consistency, uniqueness |
npm test- Node.js >= 18
- Xcode 16+ (iOS) or Android Studio (Android)
# Clone
git clone https://github.com/tkshsbcue/Growth.git
cd Growth/growth
# Install
npm install
# Generate native projects
npx expo prebuild
# iOS
npx expo run:ios
# Android
npx expo run:androidnpx expo run:ios --device "iPhone Air"
npx expo run:android --device "Pixel_7_API_34"npx expo start --clearTypography — Clash Display (Semibold) by Indian Type Foundry is used for the "Growth" brand title. Loaded via Font.loadAsync with a graceful system-font fallback. All other text uses platform defaults (SF Pro / Roboto).
Splash Screen — Animated brand reveal: fade in (400ms) → hold (1s) → fade out (400ms) before the main app renders.
Theming — Follows the system color scheme automatically. 19 semantic color tokens with full light and dark implementations. Status bar adapts dynamically.
Tab Bar — Animated Ionicons (compass / bookmark) with spring-based bounce, color crossfade, and underline indicator.
AMC Logos — 50+ fund house names mapped to Groww CDN URLs. Whitespace normalization handles API inconsistencies. Keyword-based fuzzy matching as fallback. Unknown AMCs render a deterministic colored monogram.
Chart Performance — NAV data is downsampled to 180 points via bucket-averaging. Range selection scopes data server-side via API params.
Haptic Feedback — Applied throughout: card presses, button taps, tab switches, bookmark toggles, search actions, chip selections, and portfolio operations.
Offline Support — React Query cache persisted to MMKV (7-day max age). Watchlist data is also MMKV-persisted. The app remains functional with cached data when offline.
| Criteria | Status | Details |
|---|---|---|
| Architecture | ☑ | Layered architecture with Zustand, clean folder structure, separation of concerns |
| State Management | ☑ | Zustand 5 for client state, React Query 5 for server state |
| Persistence | ☑ | MMKV for watchlist folders and fund associations |
| Local Caching | ☑ | MMKV-backed React Query cache, works offline with stale data |
| State Handling | ☑ | Explicit Loading, Error, and Empty states on all screens |
| Brownie Points | ||
| Modern Declarative UI | ☑ | 100% functional components with Hooks |
| Search Debouncing | ☑ | 300ms debounce via custom hook, 2-char minimum |
| Theme Support | ☑ | System-driven light/dark with 19 semantic tokens |
| Unit Tests | ☑ | 7 suites, 69 tests — stores, utilities, API, constants |
| Animations | ☑ | Reanimated 4 — springs, skeleton pulses, tab animations, toast, transitions |
All data from MFAPI:
| Endpoint | Description |
|---|---|
GET /mf/search?q={query} |
Search funds by name |
GET /mf/{scheme_code}/latest |
Latest NAV for a fund |
GET /mf/{scheme_code} |
Full NAV history + metadata |