Day Planning
ক্লায়েন্ট কাজের সীমাবদ্ধতার বাইরে Clean Architecture + Riverpod প্যাটার্ন যাচাই করতে বানানো একটা Flutter ডেইলি প্ল্যানার — কাস্টম gesture-driven টাইমলাইন UI আর অফলাইন-ফার্স্ট SQLite স্টোরেজসহ।
সংক্ষেপ
Day Planning একটা ডেইলি প্ল্যানার অ্যাপ, যেটা আমি বানিয়েছি ক্লায়েন্ট কাজের সীমাবদ্ধতার বাইরে Clean Architecture আর Riverpod-এর একটা reference implementation হিসেবে। লক্ষ্য ছিল প্রোডাকশনে ব্যবহার করতে চাওয়া আর্কিটেকচার প্যাটার্নগুলো যাচাই করা — কড়া layer separation, immutable state management, আর কাস্টম rendering — এমন একটা নিয়ন্ত্রিত, single-owner কোডবেজে, যেখানে প্রতিটা tradeoff-এর সিদ্ধান্ত আমার একারই।
অ্যাপটা একটা single day view-তে ফোকাস করে: time blocking, priority setting, drag-to-reschedule, আর progress tracking। কোনো backlog নেই, ক্যালেন্ডার নেই, সাবস্ক্রিপশন নেই।
ইঞ্জিনিয়ারিং লক্ষ্য
বেশিরভাগ ডেইলি প্ল্যানার অ্যাপ হয় খুব সাধারণ (flat to-do list), নয়তো খুব জটিল (পূর্ণাঙ্গ প্রজেক্ট ম্যানেজমেন্ট টুল)। আরেকটা ফিচার চেকলিস্ট বানানোর বদলে, আমি তিনটা ইঞ্জিনিয়ারিং constraint ঠিক করেছিলাম:
- কড়া Clean Architecture layering —
domain/-এ কোনো Flutter বা I/O dependency নেই। Presentation কখনো business logic-এ leak করে না। Repository contract থাকেdomain/-এ, implementation থাকেdata/-এ। - State layer হিসেবে Riverpod — টিম কনটেক্সটের বাইরে flutter_bloc-এর (তখনকার আমার ডিফল্ট) বিপরীতে Riverpod-এর compile-time safety, auto-disposal, আর testability কেমন দাঁড়ায় সেটা মূল্যায়ন করা।
- Dependency-র বদলে কাস্টম rendering — ক্যালেন্ডার লাইব্রেরি না এনে
CustomPainterদিয়ে টাইমলাইন UI বানানো, যাচাই করতে যে ~200 লাইন painting কোড একটা ভারী থার্ড-পার্টি dependency-র জায়গা নিতে পারে কিনা।
আমার ভূমিকা
একক প্রজেক্ট — architecture, implementation, আর Play Store পাবলিশিং।
কারিগরি সিদ্ধান্ত
Architecture: Clean Architecture + Riverpod
Domain layer pure Dart — কোনো Flutter import নেই, কোনো framework dependency নেই। Riverpod-এর StateNotifier domain আর presentation layer-এর মাঝখানে বসে, framework-এর concept business logic-এ leak না করেই state ম্যানেজ করে:
class TaskNotifier extends StateNotifier<List<Task>> {
final TaskRepository _repository;
TaskNotifier(this._repository) : super([]);
Future<void> loadToday() async {
state = await _repository.getTasksForDate(DateTime.now());
}
Future<void> complete(String taskId) async {
await _repository.markComplete(taskId);
state = state.map((t) =>
t.id == taskId ? t.copyWith(completed: true) : t
).toList();
}
}এই প্রজেক্টে আমি Bloc-এর বদলে Riverpod বেছে নিয়েছি, কারণ state graph সরু (একটা task-এর list, একটা selected date, একটা drag operation flag)। Bloc-এর event → state ceremony এই স্কেলে সমানুপাতিক সুবিধা ছাড়াই বাড়তি boilerplate যোগ করত। শিক্ষণীয় বিষয়টা হলো: সরু state graph-এর অ্যাপে Riverpod ভালো করে; আর ব্যাপক, event-heavy state-এর জন্য (যেমন Astute-এর 20+ ফিচার) Bloc-ই এখনো ভালো পছন্দ।
Custom Timeline Rendering
Day view একটা ভার্টিক্যাল টাইমলাইনে time block render করে। কোনো থার্ড-পার্টি calendar/scheduling লাইব্রেরি ব্যবহার না করে, আমি CustomPainter দিয়ে লেআউটটা বানিয়েছি — প্রায় 200 লাইন painting কোড। এতে drag gesture-এর গণিত, 15-মিনিট snap logic, আর ভিজ্যুয়াল ডিজাইনের উপর পুরো নিয়ন্ত্রণ পাওয়া গেছে, আর সাথে সাধারণ-উদ্দেশ্যের calendar widget-এর dependency-র ওজন আর API সীমাবদ্ধতাও এড়ানো গেছে। scroll-view space আর time axis-এর মাঝে ম্যাপ করা coordinate transformer-টা পরে একটা প্রফেশনাল প্রজেক্টেও পুনরায় ব্যবহার হয়েছে।
Offline-First by Design
সব ডেটা লোকালি SQLite-এ (drift-এর মাধ্যমে) সংরক্ষিত হয়। কোনো backend নেই — অ্যাপ পুরোপুরি অফলাইনে কাজ করে, আর ডেটা কখনো ডিভাইস ছেড়ে যায় না। এটা একটা ইচ্ছাকৃত সরলীকরণ ছিল: একটা ডেইলি প্ল্যানারের সার্ভার লাগে না, আর network layer বাদ দিলে ব্যর্থতার একটা পুরো ক্যাটাগরিই (connectivity, sync conflict, latency) দূর হয়ে যায়।
চ্যালেঞ্জ ও সমাধান
চ্যালেঞ্জ: টাইমলাইনে drag-to-reschedule-কে fluid রেখেই 15-মিনিট ইন্টারভালে snap করতে হতো। Gesture-এর গণিত — scroll position হিসাবে রেখে pixel offset-কে time slot-এ রূপান্তর করা — বেশ কয়েকবার iteration লেগেছে।
সমাধান: scroll view-র coordinate space আর time axis-এর মাঝে ম্যাপ করে এমন একটা coordinate transformer বানিয়েছি, তারপর একটা snapping function যোগ করেছি যেটা drag-এর সময় না, শুধু gesture শেষ হলে সবচেয়ে কাছের 15-মিনিট slot-এ round করে। এতে drag স্মুথ থাকে, কিন্তু নির্ভুলভাবে ল্যান্ড করে।
চ্যালেঞ্জ: টাইমলাইন ভিউতে অনেক overlapping time block থাকা সত্ত্বেও widget performance ধরে রাখা।
সমাধান: প্রতিটা time block widget-এর চারপাশে RepaintBoundary আর যেখানে সম্ভব const constructor ব্যবহার করেছি। Flutter DevTools দিয়ে profile করে নিশ্চিত করেছি যে মিড-রেঞ্জ ডিভাইসে raster thread প্রতি frame-এ 4ms-এর নিচে থাকে।
ফলাফল
- যাচাই হয়েছে যে narrow-state অ্যাপ্লিকেশনের জন্য Clean Architecture + Riverpod, Bloc-এর event boilerplate ছাড়াই testable, maintainable কোড দেয়
- নিশ্চিত হয়েছে যে specialized UI-এর জন্য
CustomPainterসাধারণ-উদ্দেশ্যের calendar লাইব্রেরির জায়গা নিতে পারে — প্রতি প্রজেক্টে ~50KB dependency size বাঁচিয়ে - coordinate transformer প্যাটার্ন আর offline-first SQLite layer পরের প্রফেশনাল প্রজেক্টগুলোতে (Surovi Agro Nexus+ সহ) বের করে পুনরায় ব্যবহার করার মতো যথেষ্ট মজবুত প্রমাণিত হয়েছে
- এই প্যাটার্নগুলোর একটা কার্যকর reference implementation হিসেবে Play Store-এ পাবলিশ হয়েছে, শুধু একটা ডিজাইন এক্সারসাইজ না