रिपॉज़िटरी पैटर्न और टाइप्ड sealed परिणामों के साथ किसी Flutter ऐप को स्पष्ट रूप से पृथक परतों में संरचित करें।
इस पाठ को Kodokon में खोलेंलंबे समय तक टिकने के लिए बनाया गया एक Flutter ऐप तीन ज़िम्मेदारियों को अलग करता है: प्रेज़ेंटेशन (विजेट, BuildContext), लॉजिक (स्टेट, यूज़ केस) और डेटा (API, स्थानीय डेटाबेस)। मूल नियम है निर्भरताओं की दिशा: प्रेज़ेंटेशन लॉजिक पर निर्भर करता है, लॉजिक डेटा के अमूर्तनों पर निर्भर करता है - कभी भी उल्टा नहीं। ठोस रूप में, प्रेज़ेंटेशन परत के नीचे न कोई BuildContext और न ही कोई material.dart इम्पोर्ट दिखाई देना चाहिए: जो कंट्रोलर खुद अपना SnackBar दिखाता है, वह एक ऐसा कंट्रोलर है जिसका परीक्षण नहीं किया जा सकता। एक और बिंदु जिसे अक्सर अनदेखा कर दिया जाता है: त्रुटियाँ परतों के बीच के अनुबंध का हिस्सा होती हैं। बिना टाइप वाली एक्सेप्शन को रिसने देने के बजाय, परिणाम को एक sealed क्लास से मॉडल करें - कम्पाइलर आपको हर स्थिति को संभालने पर विवश करेगा।
sealed class Result<T> {
const Result();
}
class Success<T> extends Result<T> {
const Success(this.value);
final T value;
}
class Failure<T> extends Result<T> {
const Failure(this.error);
final Object error;
}
class Course {
const Course({required this.id, required this.title});
final String id;
final String title;
}रिपॉज़िटरी डोमेन और बाहरी दुनिया के बीच की सीमा है। इसकी सटीक भूमिका: डोमेन इकाइयों को उजागर करना (कभी भी कच्चे API DTO नहीं), एक्सेस रणनीति (नेटवर्क, कैश, स्थानीय डेटाबेस) को छिपाना और डेटा की ताज़गी की नीति को केंद्रीकृत करना। Dart 3 में, अनुबंध को abstract interface class से घोषित करें: यह मॉडिफ़ायर लाइब्रेरी के बाहर से केवल implements की अनुमति देता है - आप गलती से इम्प्लीमेंटेशन का कोई हिस्सा इनहेरिट नहीं कर सकते, इसलिए अनुबंध शुद्ध बना रहता है। JSON-से-इकाई मैपिंग इम्प्लीमेंटेशन में रहती है: यदि कल API किसी फ़ील्ड का नाम बदल देता है, तो केवल डेटा परत बदलती है।
abstract interface class CourseApi {
Future<List<Map<String, Object?>>> fetchRaw();
}
abstract interface class CourseRepository {
Future<Result<List<Course>>> getAll();
void invalidate();
}
class ApiCourseRepository implements CourseRepository {
ApiCourseRepository(this._api);
final CourseApi _api;
List<Course>? _cache;
@override
void invalidate() => _cache = null;
@override
Future<Result<List<Course>>> getAll() async {
final cached = _cache;
if (cached != null) {
return Success(cached);
}
try {
final rows = await _api.fetchRaw();
final courses = [
for (final row in rows)
Course(
id: row['id'] as String,
title: row['title'] as String,
),
];
_cache = courses;
return Success(courses);
} on Exception catch (error) {
return Failure(error);
}
}
}प्रेज़ेंटेशन की ओर, कंस्ट्रक्टर के ज़रिए इंजेक्ट किया गया एक ChangeNotifier अक्सर बहुत काफ़ी होता है - किसी फ़्रेमवर्क का सहारा लेने की ज़रूरत नहीं। ग्लोबल सिंगलटन से बचें: वे निर्भरता ग्राफ़ को जमा देते हैं और परीक्षणों में प्रतिस्थापन को कष्टदायक बना देते हैं। ListenableBuilder केवल अपने builder को दोबारा बनाता है, और Result पर पूर्ण switch UI को सुरक्षित कर देता है: sealed टाइप में एक तीसरा स्टेट जोड़ें और जहाँ-जहाँ उसे संभाला नहीं गया है, वहाँ-वहाँ कम्पाइलेशन विफल हो जाता है। यही तो इस पैटर्न का पूरा मूल्य है - एक चूक प्रोडक्शन बग नहीं, बल्कि एक कम्पाइल त्रुटि बन जाती है।
import 'package:flutter/material.dart';
class CourseListController extends ChangeNotifier {
CourseListController(this._repository);
final CourseRepository _repository;
Result<List<Course>>? state;
Future<void> load() async {
state = null;
notifyListeners();
state = await _repository.getAll();
notifyListeners();
}
}
class CourseListScreen extends StatelessWidget {
const CourseListScreen({
super.key,
required this.controller,
});
final CourseListController controller;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: controller,
builder: (context, _) {
return switch (controller.state) {
null => const Center(
child: CircularProgressIndicator(),
),
Failure() => const Center(
child: Text('Loading error'),
),
Success(value: final courses) =>
ListView.builder(
itemCount: courses.length,
itemBuilder: (context, i) => ListTile(
title: Text(courses[i].title),
),
),
};
},
);
}
}