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.
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
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.
Flagship featureTính năng chủ lực
Live referee scoringTrọng tài tính điểm trực tiếp
Pickleball doubles uses side-out scoring, which is genuinely tricky: only the serving team can score, each team gets two server turns before the serve passes over, partners swap boxes after every point they win, and the team serving first in a set gets only one server turn to offset the advantage. Referees get this wrong constantly.Pickleball đôi dùng luật side-out, và luật này thực sự rối: chỉ đội giao bóng mới được ghi điểm, mỗi đội có hai lượt giao trước khi mất quyền giao, hai đồng đội đổi ô sau mỗi điểm thắng, và đội giao đầu set chỉ được một lượt giao để bù lợi thế. Trọng tài nghiệp dư sai chỗ này liên tục.
PoLi models the whole ruleset as a pure state machine — awardRally(match, winningTeam) → newMatch. No I/O, no widgets, fully unit tested. The UI is a live court diagram: player chips physically move between boxes as the serve rotates, so the referee always sees who serves next without doing the mental arithmetic. On top of that sit configurable rules (points to win, win-by-two, number of sets, auto change-ends at a chosen score), a coin toss, direct score correction, and full set history.PoLi mô hình hóa toàn bộ luật thành một máy trạng thái thuần — awardRally(match, winningTeam) → newMatch. Không I/O, không widget, unit test đầy đủ. Giao diện là sơ đồ sân trực quan: thẻ tên người chơi di chuyển giữa các ô đúng theo vòng xoay giao bóng, nên trọng tài luôn thấy ai giao tiếp theo mà không phải tự nhẩm. Bên trên là bộ luật cấu hình được (điểm thắng, hơn 2 điểm, số set, tự đổi bên tại mốc điểm), tung đồng xu, sửa điểm trực tiếp và lưu lịch sử từng set.
The live court. Player chips move between boxes as the serve rotates — the referee reads position, never arithmetic.Sơ đồ sân trực tiếp. Thẻ người chơi di chuyển giữa các ô theo vòng giao bóng — trọng tài nhìn vị trí, không phải tự nhẩm.
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.
Format selectionChọn thể thứcFree, mixed, or referee modeCặp tự do, đôi nam nữ, hoặc trọng tàiMatch 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 leaderboardBảng xếp hạngMatch historyLịch sử trận đấuProfile — 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 hubSocial HubNearby courts via Maps & PlacesTìm sân qua Maps & PlacesFriends, 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.
flutter_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.