PoLi

A Flutter + Firebase mobile app covering the full loop of a pickleball player: find people to play with, run the match, score it as a referee, and track your rank. Ứng dụng di động Flutter + Firebase bao trọn vòng đời của một người chơi pickleball: tìm người chơi, tổ chức trận, làm trọng tài tính điểm trực tiếp và theo dõi thứ hạng.

  • Flutter
  • Dart
  • Firebase
  • Cloud Functions
  • Google Maps
  • Android / iOS
Explore the productKhám phá sản phẩm
RoleVai trò
Solo developer — product, UI, client, backendSolo developer — product, UI, client, backend
PlatformsNền tảng
Android & iOS, one codebaseAndroid & iOS, một codebase
ScaleQuy mô
~54k lines of Dart · 87 screens~54k dòng Dart · 87 màn hình
LocalizationĐa ngữ
Vietnamese + English, 614 stringsTiếng Việt + Tiếng Anh, 614 chuỗi
PoLi home screen: 2v2 leaderboard preview, 1v1 and 2v2 game modes, room join field and the player's ranking points

The problemBài toán

A casual pickleball group in Vietnam juggles four separate tools: a chat group to find players, a note app for scores, a spreadsheet for the bracket, and word of mouth for which courts are free. Nothing talks to anything else, so a player's actual record disappears the moment the game ends. Một nhóm pickleball phong trào ở Việt Nam thường phải dùng bốn công cụ rời rạc: nhóm chat để tìm người, app ghi chú để ghi điểm, bảng tính để dựng nhánh giải, và hỏi miệng nhau xem sân nào còn trống. Không cái nào liên thông với cái nào, nên thành tích thật của người chơi biến mất ngay khi trận đấu kết thúc.

PoLi's premise is that all of it is one workflow. A match you play feeds your ranking. A tournament you organise pulls from your friends list. A score you enter as a referee writes straight to both players' histories. Tiền đề của PoLi: tất cả những việc đó vốn là một luồng công việc duy nhất. Trận bạn đánh cộng vào điểm xếp hạng. Giải bạn tổ chức lấy người từ danh sách bạn bè. Điểm bạn nhập với vai trò trọng tài ghi thẳng vào lịch sử của cả hai bên.

TournamentsGiải đấu

Knockout (group stage → bracket) and round-robin formats, with free pairs or enforced mixed pairs. Brackets render as a chart, group standings compute from entered scores, and results export to Excel. To speed up sign-up, participant names can be imported from a photo of a paper registration sheet — a Cloud Function uses Gemini to read it and split doubles lines into individual players. Thể thức loại trực tiếp (vòng bảng → nhánh) và vòng tròn tính điểm, hỗ trợ cặp tự do hoặc bắt buộc đôi nam nữ. Nhánh đấu hiển thị dạng sơ đồ, bảng xếp hạng vòng bảng tự tính từ điểm đã nhập, kết quả xuất ra Excel. Để rút ngắn khâu đăng ký, danh sách vận động viên nhập được từ ảnh chụp tờ đăng ký giấy — Cloud Function dùng Gemini đọc ảnh và tách dòng đôi thành từng người riêng.

Tournament creation screen offering knockout and round robin formats
Format selectionChọn thể thức
Pair type screen: free pair, mixed pair, or referee mode
Free, mixed, or referee modeCặp tự do, đôi nam nữ, hoặc trọng tài
Referee match queue grouped by status, each card showing both pairs and their current score
Match queue by statusDanh sách trận theo trạng thái

Matchmaking & rankingGhép trận & xếp hạng

1v1 and 2v2 rooms joined by room ID or QR code. Every completed match feeds a points-based ranking, split by format and filterable to global, friends-only, or by gender — so the leaderboard reflects games actually played rather than self-reported skill. Phòng 1v1 và 2v2, vào bằng mã phòng hoặc quét QR. Mỗi trận hoàn tất đều cộng vào hệ thống điểm xếp hạng, tách theo thể thức và lọc được theo toàn cầu, chỉ bạn bè hoặc theo giới tính — nên bảng xếp hạng phản ánh trận đã đánh thật, không phải trình độ tự khai.

Global leaderboard ranked by points with 1v1 and 2v2 tabs and gender filters
Global leaderboardBảng xếp hạng
Match history list with win and lose badges, scores, opponents and timestamps
Match historyLịch sử trận đấu
Profile screen with basic information, a runtime English/Vietnamese language switch, and rank and score per format
Profile — with the runtime EN/VN switchTrang cá nhân — có nút đổi ngôn ngữ tại chỗ

Community & courtsCộng đồng & sân bãi

Friends, friend requests, groups, and 1:1 / group chat. A Social Hub for finding players and coaches. Court discovery combines the device's location with Google Maps and Places to list nearby courts with distance, rating, and hourly price. Bạn bè, lời mời kết bạn, nhóm và chat 1:1 / chat nhóm. Social Hub để tìm người chơi và huấn luyện viên. Tính năng tìm sân kết hợp vị trí thiết bị với Google Maps và Places để liệt kê sân gần đó kèm khoảng cách, đánh giá và giá theo giờ.

Social hub with quick actions for booking a court, finding a coach, finding players and events, plus nearby court cards
Social hubSocial Hub
Court search results listing five nearby pickleball courts with address, rating and distance
Nearby courts via Maps & PlacesTìm sân qua Maps & Places
Friends tab with My Friends, Requests and Groups, each row offering chat, invite and remove actions
Friends, requests, groupsBạn bè, lời mời, nhóm

Technical highlightsĐiểm nhấn kỹ thuật

Pure-function scoring engineEngine tính điểm dạng hàm thuần

Side-out rules live in a static class with no Flutter or Firebase dependency, so the trickiest logic in the app is testable in milliseconds — covering the first-serve-of-set exception, box swapping, side-out server selection, win-by-two and auto change-ends. Luật side-out nằm trong static class không phụ thuộc Flutter hay Firebase, nên phần logic khó nhất của app lại test được trong vài mili giây — phủ ngoại lệ lượt giao đầu set, đổi ô, chọn người giao khi side-out, luật hơn 2 điểm và tự đổi bên.

Offline-first refereeingTrọng tài offline-first

Courts have bad signal and a referee cannot wait on a network round-trip between rallies. Match state persists locally and syncs to Firestore when connectivity allows, so scoring never blocks on the network. Sân bãi hay mất sóng và trọng tài không thể chờ round-trip mạng giữa hai pha bóng. Trạng thái trận lưu cục bộ và đồng bộ lên Firestore khi có mạng, nên việc tính điểm không bao giờ bị chặn bởi kết nối.

Read-cost-aware cachingCaching có ý thức về chi phí đọc

Firestore bills per document read, and leaderboard and profile are the hottest paths. A dedicated caching layer with TTL invalidation sits in front of those queries, cutting repeat reads substantially. Firestore tính tiền theo document read, mà bảng xếp hạng và trang cá nhân là hai màn hình mở nhiều nhất. Một lớp cache riêng với TTL đứng trước các truy vấn đó, giảm đáng kể số lần đọc lặp.

Push notifications end to endPush notification xuyên suốt

FCM token lifecycle on the client, a Firestore onCreate trigger in TypeScript Cloud Functions that fans out messages, and a navigation service that deep-links a tapped notification to the right screen with the right arguments. Vòng đời FCM token ở client, trigger onCreate trên Firestore viết bằng TypeScript Cloud Functions để phát tán tin, và service điều hướng deep-link thông báo tới đúng màn hình với đúng tham số.

Bilingual from the startSong ngữ ngay từ đầu

614 strings across Vietnamese and English via generated AppLocalizations, switchable at runtime from the profile screen — not a bolt-on after launch. 614 chuỗi tiếng Việt và tiếng Anh qua AppLocalizations được generate, đổi ngôn ngữ ngay trong app từ trang cá nhân — không phải chắp vá sau khi ra mắt.

Two entry points, one codebaseHai entry point, một codebase

main.dart ships the player app; main_admin.dart builds an admin console with mobile and web layouts from the same models and services. main.dart build app người chơi; main_admin.dart build console quản trị với bố cục mobile và web từ cùng model và service.

ArchitectureKiến trúc

lib/
  main.dart / main_admin.dart   # Player app and admin console entry points
  models/                       # Tournament, court, chat, user, referee match, ...
  services/                     # One module per domain — Firebase access and
                                # business logic, kept out of the widget tree
  providers/                    # provider-based app state
  screens/                      # UI grouped by feature (87 screens)
  components/                   # Shared widgets
  design_system/ theme/         # Design tokens, colors, typography
  route/                        # Named routes
  l10n/                         # VI / EN localization
functions/src/                  # TypeScript Cloud Functions (FCM fan-out, Gemini OCR)

The rule I held to: screens render, services decide. Firestore queries and domain rules live in services/ — which is exactly why the scoring engine could be extracted into something unit-testable in the first place. Nguyên tắc tôi bám suốt dự án: màn hình chỉ render, service mới ra quyết định. Truy vấn Firestore và luật nghiệp vụ nằm trong services/ — cũng chính vì vậy mà engine tính điểm mới tách ra được thành thứ unit test được.

StackCông nghệ

FrameworkFlutter (Dart 3.2+), Material Design 3
Stateprovider
BackendFirebase — Auth, Cloud Firestore, Storage, Cloud Messaging
ServerlessCloud Functions (TypeScript) — push fan-out, Gemini-backed OCRCloud Functions (TypeScript) — phát tán push, OCR qua Gemini
MapsBản đồgoogle_maps_flutter, geolocator, geocoding, Places API
Local storageLưu cục bộshared_preferences — offline referee stateshared_preferences — trạng thái trọng tài offline
OtherKhácqr_flutter / mobile_scanner, excel, image_picker, intl, google_fonts
Testingflutter_test — scoring engine, tournament model, referee screenflutter_test — engine tính điểm, model giải đấu, màn hình trọng tài

From idea to operationTừ ý tưởng đến vận hành

Need a mobile product with real business logic?Bạn cần một sản phẩm mobile có nghiệp vụ thực tế?

I can help shape the workflow, build the Flutter application and connect the backend into one maintainable product.Tôi có thể cùng bạn định hình quy trình, xây dựng ứng dụng Flutter và kết nối backend thành một sản phẩm dễ vận hành, dễ phát triển.

PoLi's scoring feature is designed to apply the rules correctly instead of making the referee remember every detail. The app tracks serving turns, player positions, and each set's result automatically, reducing mistakes during a match. Keeping this logic separate from the interface also makes the product easier to test, maintain, and extend. Điểm tôi muốn nhấn mạnh ở PoLi là tính năng chấm điểm được xây dựng để xử lý đúng luật, thay cho việc trọng tài phải tự nhớ mọi quy tắc. Ứng dụng tự theo dõi lượt giao bóng, vị trí người chơi và kết quả từng set, giúp giảm sai sót trong trận đấu. Phần xử lý được tách khỏi giao diện nên cũng dễ kiểm tra, bảo trì và mở rộng về sau.