Kodokon kodokon.com

किसी ऐप की आर्किटेक्चर: UI, लॉजिक, डेटा

रिपॉज़िटरी पैटर्न और टाइप्ड sealed परिणामों के साथ किसी Flutter ऐप को स्पष्ट रूप से पृथक परतों में संरचित करें।

11 मिनट · 3 प्रश्न

इस पाठ को Kodokon में खोलें

लंबे समय तक टिकने के लिए बनाया गया एक Flutter ऐप तीन ज़िम्मेदारियों को अलग करता है: प्रेज़ेंटेशन (विजेट, BuildContext), लॉजिक (स्टेट, यूज़ केस) और डेटा (API, स्थानीय डेटाबेस)। मूल नियम है निर्भरताओं की दिशा: प्रेज़ेंटेशन लॉजिक पर निर्भर करता है, लॉजिक डेटा के अमूर्तनों पर निर्भर करता है - कभी भी उल्टा नहीं। ठोस रूप में, प्रेज़ेंटेशन परत के नीचे न कोई BuildContext और न ही कोई material.dart इम्पोर्ट दिखाई देना चाहिए: जो कंट्रोलर खुद अपना SnackBar दिखाता है, वह एक ऐसा कंट्रोलर है जिसका परीक्षण नहीं किया जा सकता। एक और बिंदु जिसे अक्सर अनदेखा कर दिया जाता है: त्रुटियाँ परतों के बीच के अनुबंध का हिस्सा होती हैं। बिना टाइप वाली एक्सेप्शन को रिसने देने के बजाय, परिणाम को एक sealed क्लास से मॉडल करें - कम्पाइलर आपको हर स्थिति को संभालने पर विवश करेगा।

DART
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;
}
एक sealed टाइप विफलता को सिग्नेचर में स्पष्ट कर देता है।

रिपॉज़िटरी डोमेन और बाहरी दुनिया के बीच की सीमा है। इसकी सटीक भूमिका: डोमेन इकाइयों को उजागर करना (कभी भी कच्चे API DTO नहीं), एक्सेस रणनीति (नेटवर्क, कैश, स्थानीय डेटाबेस) को छिपाना और डेटा की ताज़गी की नीति को केंद्रीकृत करना। Dart 3 में, अनुबंध को abstract interface class से घोषित करें: यह मॉडिफ़ायर लाइब्रेरी के बाहर से केवल implements की अनुमति देता है - आप गलती से इम्प्लीमेंटेशन का कोई हिस्सा इनहेरिट नहीं कर सकते, इसलिए अनुबंध शुद्ध बना रहता है। JSON-से-इकाई मैपिंग इम्प्लीमेंटेशन में रहती है: यदि कल API किसी फ़ील्ड का नाम बदल देता है, तो केवल डेटा परत बदलती है।

DART
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);
    }
  }
}
रिपॉज़िटरी मैपिंग करता है, कैश करता है और त्रुटियों को एक Result में बदल देता है।

प्रेज़ेंटेशन की ओर, कंस्ट्रक्टर के ज़रिए इंजेक्ट किया गया एक ChangeNotifier अक्सर बहुत काफ़ी होता है - किसी फ़्रेमवर्क का सहारा लेने की ज़रूरत नहीं। ग्लोबल सिंगलटन से बचें: वे निर्भरता ग्राफ़ को जमा देते हैं और परीक्षणों में प्रतिस्थापन को कष्टदायक बना देते हैं। ListenableBuilder केवल अपने builder को दोबारा बनाता है, और Result पर पूर्ण switch UI को सुरक्षित कर देता है: sealed टाइप में एक तीसरा स्टेट जोड़ें और जहाँ-जहाँ उसे संभाला नहीं गया है, वहाँ-वहाँ कम्पाइलेशन विफल हो जाता है। यही तो इस पैटर्न का पूरा मूल्य है - एक चूक प्रोडक्शन बग नहीं, बल्कि एक कम्पाइल त्रुटि बन जाती है।

DART
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),
              ),
            ),
        };
      },
    );
  }
}
null = लोड हो रहा है, और switch sealed टाइप की हर स्थिति को कवर करता है।

ज्ञान जांच

सुनिश्चित करें कि आपको इस पाठ के मुख्य बिंदु याद हैं।

  1. UI, ApiCourseRepository के बजाय CourseRepository इंटरफ़ेस पर क्यों निर्भर करता है?
    • परीक्षणों में एक फ़ेक इम्प्लीमेंटेशन डालने और विजेट को छुए बिना डेटा स्रोत बदलने के लिए
    • क्योंकि अमूर्त क्लास को इंस्टैंशिएट करना तेज़ होता है
    • क्योंकि Dart किसी विजेट से ठोस क्लास को इंस्टैंशिएट करने से मना करता है
  2. Result जैसा sealed टाइप किसी एक्सेप्शन की तुलना में क्या लाभ देता है?
    • कम्पाइलर जाँचता है कि switch पूर्ण है: आप त्रुटि वाली स्थिति को भूल नहीं सकते
    • यह एसिंक्रोनस कोड को तेज़ बना देता है
    • यह हीप पर किसी भी ऑब्जेक्ट आवंटन से बचाता है
    • यह आइसोलेट से आने वाली त्रुटियों को भी पकड़ लेता है
  3. JSON-से-Course-इकाई मैपिंग कहाँ रहनी चाहिए?
    • डेटा परत में, ताकि डोमेन API के फ़ॉर्मैट से अनजान रहे
    • विजेट में, प्रदर्शन के जितना संभव हो उतना करीब
    • main() में, runApp से पहले