ڈارٹ میں ایڈوانسڈ ایرر ہینڈلنگ: ریکارڈز، رزلٹ کی اقسام، مونڈز، اور فکسڈ مستثنیات۔

ہر ڈارٹ ڈویلپر نے کسی وقت کچھ اس طرح لکھا ہے:

try {
  final user = await repository.getUser(id);
  // do something with user
} catch (e) {
  // what is e? who knows.
  print(e.toString());
}

یہ کام کرتا ہے۔ یہ مرتب کرتا ہے۔ اسے پہنچا دیا جائے گا۔ پھر، چھ ماہ بعد، ایک صارف کی طرف سے ایک خرابی کی رپورٹ میرے ان باکس میں آئی جس میں غلطی کے پیغام کی بجائے خالی اسکرین دکھائی گئی، اور مجھے اسے ٹریک کرنے میں تین گھنٹے لگے۔ catch (e) ایک بلاک جو خاموشی سے ناکامی کو نگل جاتا ہے۔

یہ ڈارٹ کے استثنیٰ پر مبنی غلطی سے نمٹنے کا بنیادی مسئلہ ہے۔ فنکشن کے دستخط میں استثناء نظر نہیں آتا۔ کال سائٹ پر کوئی قسم کی معلومات نہیں ہے۔ مرتب کرنے والا مدد نہیں کرسکتا کیونکہ یہ نہیں جانتا کہ فنکشن ناکام ہوسکتا ہے۔

ہر ناکامی کا راستہ مصنف اور کال کرنے والے کے درمیان ایک سماجی معاہدہ ہوتا ہے، اور بڑی ٹیموں میں، وہ سماجی معاہدہ کسی واقعے کے دوران صبح 2 بجے ٹوٹ جاتا ہے۔

پروڈکشن ایپلی کیشنز اس سے بہتر کے مستحق ہیں۔

اس آرٹیکل میں، ہم ڈارٹ میں غلطی سے نمٹنے کے لیے ایک مکمل، جدید نقطہ نظر دیکھیں گے، بالکل اسی طرح جیسے اصلی پروڈکشن فلٹر کوڈ بیس میں استعمال ہوتا ہے۔ ہم ڈارٹ ریکارڈز کے ساتھ شروع کرتے ہیں، ایک ہلکا پھلکا نتیجہ کنٹینر، مناسب طریقے سے سیل شدہ نتائج کی اقسام بناتے ہیں، اور اسے مونڈ پیٹرن کے ساتھ بڑھاتے ہیں۔ dartz ہم دو فنکشنل اقسام کے لیے ایک پیکج بناتے ہیں، اور آخر میں ہم Freezed کا استعمال کرتے ہوئے ٹائپ شدہ مکمل استثنا کے ساتھ ختم کرتے ہیں۔

بالآخر، آپ کے کوڈ بیس میں غلطیاں ٹائپ، نشان زد، مرتب کرنے والے کے ذریعے نافذ، اور نظر انداز کرنا ناممکن ہو جاتی ہیں۔

انڈیکس

شرطیں

شروع کرنے سے پہلے، آپ کے پاس ہونا ضروری ہے:

  • فلٹر پروجیکٹ جو Dart 3.0 اور اس سے اوپر کے ساتھ کام کرتا ہے۔

  • Dart generics اور async/await کا بنیادی علم

  • ڈارٹ کی مہر بند کلاسوں کی بنیادی سمجھ

  • کہ freezed, freezed_annotationاور build_runner پیکجز دستیاب ہیں۔

  • کہ dartz پیکجز دستیاب ہیں۔

  • flutter pub run build_runner build اپنے پروجیکٹ پر کام کر رہے ہیں۔

ڈارٹ میں استثنائی مسئلہ

آئیے اس پر ایک نظر ڈالیں کہ پورے اسٹیک میں عام استثناء پر مبنی غلطی کو سنبھالنا عملی طور پر کیسا لگتا ہے۔

// Repository
Future getUser(String id) async {
  final response = await dio.get('/users/$id');
  return User.fromJson(response.data);
}

// Use case
Future execute(String id) async {
  return await repository.getUser(id);
}

// ViewModel
Future loadUser(String id) async {
  try {
    final user = await useCase.execute(id);
    state = UserState.loaded(user);
  } catch (e) {
    state = UserState.error(e.toString());
  }
}

یہ معقول لگتا ہے۔ لیکن یہاں ایک سنگین مسئلہ چھپا ہوا ہے۔

دستخط میں ناکامی نظر نہیں آتی۔ Future کال کرنے والے کو بتائیں، "آپ کو ایک صارف موصول ہوگا۔” یہ ہمیں اس بارے میں کچھ نہیں بتاتا کہ اگر نیٹ ورک ناکام ہو جاتا ہے، ٹوکن ختم ہو جاتا ہے، یا JSON خراب ہو جاتا ہے تو کیا ہوتا ہے۔ کال کرنے والے کو یہ جاننے کے لیے عمل درآمد پڑھنا چاہیے کہ یہ فنکشن ناکام ہو سکتا ہے۔

مرتب کرنے والا درج ذیل صورتوں میں مدد نہیں کرسکتا۔ اگر تم بھول جاؤ try/catch ایپ ViewModel میں مناسب طریقے سے مرتب کرتی ہے۔ تنازعات رن ٹائم، پیداوار میں، حقیقی صارفین کے سامنے ہوتے ہیں۔

catch (e) ہر چیز پر قبضہ کریں: متغیر ناموں میں ٹائپوز، null dereferences، اصل نیٹ ورک کی غلطیاں، وغیرہ سب ایک ہی کیچ بلاک میں آتے ہیں۔ نازک خرابی کے تاروں کی جانچ کیے بغیر، ان میں فرق کرنا ناممکن ہے۔

غلطیاں متعدد پرتوں میں اپنی ٹھوس کھو دیتی ہیں۔ تب تک UnauthorizedException جب آپ API پرت میں ViewModel تک پہنچ جاتے ہیں، Object. تمام ساختی معلومات ختم ہو گئی ہیں۔

حل یہ ہے کہ ناکامیوں کو فنکشن دستخط، ٹائپ سسٹم، اور کمپائلر چیکنگ کا اولین ترجیحی حصہ بنایا جائے۔ یہ بالکل وہی ہے جو اس مضمون کا نمونہ کرتا ہے۔

حصہ 1: ہلکے وزن والے رزلٹ کنٹینرز کی طرح ریکارڈ کریں۔

ڈارٹ ریکارڈز کیا ہے؟

ڈارٹ 3.0 نے ریکارڈ متعارف کرایا، ایک گمنام غیر متغیر قدر کی قسم جو مکمل کلاس تعریف کے بغیر متعدد فیلڈز کو ایک ساتھ گروپ کرتی ہے۔

// A record with two named fields
({String name, int age}) person = (name: 'Seyi', age: 28);

print(person.name); // Seyi
print(person.age);  // 28

ریکارڈ ساختی طور پر ٹائپ کیے جاتے ہیں۔ ایک ہی فیلڈ کے نام اور قسم کے دو ریکارڈ ایک ہی قسم کے ہیں، قطع نظر اس کے کہ ان کی تعریف کہاں کی گئی ہے۔ وہ ناقابل تغیر بھی ہیں اور حوالہ کے بجائے قدر کے لحاظ سے موازنہ کرتے ہیں۔

نتیجہ کی قسم کے ساتھ ریکارڈ کریں۔

غلطی سے نمٹنے میں ریکارڈ کا سب سے آسان اطلاق یہ ہے کہ کامیابی اور ناکامی کو nullable فیلڈز کے ساتھ ایک ہی ریٹرن ٹائپ میں انکوڈ کیا جائے۔

typedef Result = ({E? e, T? data});

یہ ایک ریکارڈ کی قسم کی وضاحت کرتا ہے جس میں دو ناکارہ فیلڈز ہیں۔ e غلطی اور data کامیابی کی قدر کے لیے۔ معاہدہ آسان ہے۔ بالکل ان میں سے ایک بھی کالعدم نہیں ہے۔

// On success — data is present, e is null
Result result = (e: null, data: user);

// On failure — e is present, data is null
Result result = (e: 'User not found', data: null);

یہ استثناء کے مقابلے میں پہلے سے ہی ایک نمایاں بہتری ہے۔ واپسی کی قسم اب کالر کو بتاتی ہے کہ یہ فنکشن ڈیٹا یا خرابی پیدا کر سکتا ہے۔ ناکامی دستخط کا حصہ ہے۔

آپ اپنی درخواست کی مختلف پرتوں کے لیے مزید مخصوص قسم کی تعریفیں بیان کر سکتے ہیں۔

typedef ApiResult      = ({T? data, E? exception});
typedef SecurityResponse     = ({bool? isSecured, String? error});
typedef Repository        = ApiResult;

ہر ٹائپ ڈیف ریکارڈ کی شکل کے لیے ایک معنی خیز نام فراہم کرتا ہے، جس سے تمام کال سائٹس پر ارادہ واضح ہوتا ہے۔

نام کی جگہ کنسٹرکٹرز کے ساتھ سیل بند کلاسز

دستی طور پر ہر بار نتائج کا ریکارڈ بنانا بار بار اور غلطی کا شکار ہے۔ سب سے صاف حل یہ ہے کہ سیل شدہ کلاس کو صرف جامد فیکٹری طریقوں کے لیے نام کی جگہ کے طور پر استعمال کیا جائے۔

sealed class Res {
  static Result success(T data) => (e: null, data: data);
  static Result failure(E e) => (e: e, data: null);
}

کس چیز پر توجہ دیں۔ sealed یہ یہاں پولیمورفزم کے لیے استعمال نہیں ہوا ہے۔ انسٹیٹیوٹ نہیں کیا جا سکتا۔ یہ خالصتاً ایک بامعنی، غیر قابل توسیع نام کے تحت دو متعلقہ جامد طریقوں کو گروپ کرنے کے لیے موجود ہے۔

آپ کی کال سائٹ صاف اور بامقصد ہو جاتی ہے۔

// In a repository
Future> getUser(String id) async {
  try {
    final user = await _api.fetchUser(id);
    return Res.success(user);
  } on NetworkException catch (e) {
    return Res.failure(iException.internet(message: e.message));
  }
}

یہی پیٹرن ڈیو سے متعلقہ جوابات پر لاگو ہوتا ہے۔

sealed class DioResult {
  static ApiResult success(T data) => (data: data, exception: null);
  static ApiResult failure(E exception) => (data: null, exception: exception);
}

آسان قسم کے عرفی ناموں کا استعمال کرتے ہوئے ذخیرہ سطح کے نتائج کے لیے:

// GET is just ({E? e, T? res})
typedef New = GET;

sealed class R {
  static New success(T data) => (e: null, res: data);
  static New failed(iException error) => (e: error, res: null);
}

ہر مہر بند کلاس نام کی جگہ کی ایک ذمہ داری ہوتی ہے اور درخواست میں ایک ہی پرت کے نقشے ہوتے ہیں۔

ڈومین کے ساتھ مخصوص ریکارڈ کی اقسام

ریکارڈز ڈومین کے لیے مخصوص نتائج کی شکلوں کے لیے بھی اچھی طرح کام کرتے ہیں جو عام کامیابی/ناکامی کے نمونوں میں فٹ نہیں ہوتے ہیں۔

typedef SecurityResponse = ({bool? isSecured, String? error});

sealed class Check {
  static SecurityResponse isSecured() => (isSecured: true, error: null);
  static SecurityResponse isInsecured(String error) => (isSecured: false, error: error);
}

اس کا استعمال:

final check = Check.isSecured();
if (check.isSecured == true) {
  // proceed
}

final check = Check.isInsecured('Certificate validation failed');
print(check.error); // Certificate validation failed

یہ صاف، پڑھنے میں آسان اور خود دستاویزی ہے۔ ریکارڈ کی شکل آپ کو بتاتی ہے کہ فنکشن کیا واپس کر سکتا ہے۔

ذہن میں رکھنے کی حدود ہیں: ریکارڈ پر مبنی نتائج کی اقسام کے لیے آپ کو دستی طور پر چیک کرنے کی ضرورت ہوتی ہے کہ کون سے فیلڈز کالعدم نہیں ہیں۔ کسی بھی معاملے کو سنبھالنے کے لیے کوئی کمپائلر انفورسمنٹ نہیں ہے، اور نتائج کو دستی طور پر پیک کیے بغیر تبدیل کرنے کا کوئی بلٹ ان طریقہ نہیں ہے۔ یہ وہ جگہ ہے جہاں ایک مناسب مہر بند نتیجہ کی قسم کھیل میں آتی ہے۔

حصہ 2: مناسب مہر بند نتائج کی اقسام بنانا

AppResult مہربند کلاس

سیل شدہ نتائج کی قسمیں صرف ریکارڈ سے زیادہ کام کرتی ہیں۔ یہ دو ممکنہ حالتوں کے درمیان ساختی طور پر فرق کرنے کے لیے ڈارٹ کے قسم کے نظام کا استعمال کرتا ہے۔ when() ایک طریقہ جو کال کرنے والے کو کمپائل کے وقت دونوں معاملات کو سنبھالنے پر مجبور کرتا ہے۔

import 'app_failure.dart';

sealed class AppResult {
  const AppResult();

  R when({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  });
}

class AppSuccess extends AppResult {
  const AppSuccess(this.value);

  final T value;

  @override
  R when({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  }) {
    return success(value);
  }
}

class AppFailureResult extends AppResult {
  const AppFailureResult(this.error);

  final AppFailure error;

  @override
  R when({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  }) {
    return failure(error);
  }
}

آئیے ڈیزائن کے فیصلوں کو بغور دیکھتے ہیں۔

sealed class AppResult: sealed اس کا مطلب ہے کہ تمام ذیلی قسمیں ایک ہی فائل میں ہونی چاہئیں اور مرتب کرنے والا تمام ممکنہ ذیلی قسموں کو جانتا ہے۔ یہ وہی ہے جو مکمل پیٹرن کے ملاپ کو ممکن بناتا ہے۔ ڈیٹا کی قسم جو آپ کامیابی پر حاصل کرتے ہیں۔

AppSuccess: اصلی ڈیٹا رکھتا ہے۔ جب when() پر بلایا جاتا ہے AppSuccessیہ ہمیشہ کال کرتا ہے: success واپس کال کریں اور قیمت پاس کریں۔

AppFailureResult: قبضہ AppFailure (آپ کی غلطی کا ماڈل)۔ جب when() پر بلایا جاتا ہے AppFailureResultیہ ہمیشہ کال کرتا ہے: failure کال بیک یہ اب بھی فراہم کرتا ہے۔ یہاں تک کہ اگر کوئی اقدار نہیں ہیں – یہ دونوں ذیلی قسموں کو یکساں طور پر ہم آہنگ بناتا ہے۔ AppResult زمرہ

کہ when() طریقہ: یہ بنیادی طریقہ کار ہے۔ دونوں کال بیکس ہیں۔ required. کمپائلر کال کی اجازت نہیں دیتا when() دونوں معاملات کو سنبھالے بغیر۔ غلطی کا راستہ مت بھولنا۔ آپ کامیابی کے راستے کو نہیں بھول سکتے۔ آبجیکٹ خود اس بات کا تعین کرتا ہے کہ کون سی برانچ پر عمل درآمد کیا جاتا ہے، نہ کہ کالنگ کوڈ کا if/else۔

// Repository returning AppResult
Future> login(String email, String password) async {
  try {
    final user = await _api.login(email, password);
    return AppSuccess(user);
  } on UnauthorizedException {
    return AppFailureResult(AppFailure.unauthorized());
  } on NetworkException {
    return AppFailureResult(AppFailure.network());
  } catch (e) {
    return AppFailureResult(AppFailure.unknown(e.toString()));
  }
}

نتیجہ کی کھپت when()

final result = await _repository.login(email, password);

result.when(
  success: (user) => emit(AuthState.authenticated(user)),
  failure: (error) => emit(AuthState.error(error.message)),
);

آپ اسے قدر واپس کرنے کے لیے بھی استعمال کر سکتے ہیں۔

// Returning a Widget
final widget = result.when(
  success: (user) => UserProfileCard(user: user),
  failure: (error) => ErrorView(message: error.message),
);

// Returning a String
final message = result.when(
  success: (data) => 'Welcome back, ${data.name}',
  failure: (error) => 'Something went wrong: ${error.message}',
);

واپسی کی قسم R اس کا اندازہ لگایا جاتا ہے۔ جو بھی دونوں کال بیکس واپس آتے ہیں۔ when() رپورٹ وہ Widgetتم ہو Widget. وہ Stringتم ہو String.

یہ کیوں بہتر ہے۔

استثناء ایپ کے نتائج
دستخط میں نقص ظاہر ہوا۔
کمپائلر پروسیسنگ پر مجبور کرتا ہے۔
کال سائٹ کو دونوں راستوں کی ضرورت ہے۔
تمام تہوں میں محفوظ طریقے سے داخل ہوں۔
پڑھنے کے قابل اور خود دستاویزی

حصہ 3: مونڈ پیٹرن کو بڑھانا

مونڈ کیا بناتا ہے؟

مونڈز فنکشنل پروگرامنگ میں ایک نمونہ ہیں۔ عملی طور پر، ایک قسم ایک مونڈ ہے اگر یہ تین چیزوں کو پورا کرتی ہے:

پیک – آپ اقدار کو سیاق و سباق میں رکھ سکتے ہیں۔

AppSuccess(user) // wrapping a User into AppResult

تبدیلی (نقشہ) – آپ لپیٹے ہوئے اقدار پر فنکشنز کو دستی طور پر کھولے بغیر لاگو کر سکتے ہیں۔ اگر نتیجہ ایک ناکامی ہے تو، تبدیلی کو چھوڑ دیا جاتا ہے اور ناکامی کی تشہیر کی جاتی ہے۔

سلسلہ (فلیٹ نقشہ) – آپ متعدد کارروائیوں کو ترتیب دے سکتے ہیں، ہر ایک گھوںسلا کیے بغیر، ایک ہی ریپر کی قسم واپس کرتا ہے۔ پہلی غلطی پوری چین کو شارٹ سرکٹ کر دیتی ہے۔

AppResult جیسا کہ اوپر بیان کیا گیا ہے، پہلا اصول اور روح دوسرے سے when(). لیکن بغیر map اور flatMapیہ میکانکی طور پر موناڈیک نہیں ہے۔ آئیے مسئلہ حل کریں۔

شامل کرنا map اور flatMap

sealed class AppResult {
  const AppResult();

  /// Transform the success value, propagate failure untouched
  AppResult map(R Function(T value) transform) {
    return when(
      success: (value) => AppSuccess(transform(value)),
      failure: (error) => AppFailureResult(error),
    );
  }

  /// Chain an operation that itself returns an AppResult
  AppResult flatMap(AppResult Function(T value) transform) {
    return when(
      success: (value) => transform(value),
      failure: (error) => AppFailureResult(error),
    );
  }

  R when({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  });
}

map ایک باقاعدہ فنکشن کا استعمال کرتے ہوئے کامیابی کی قدر کو تبدیل کریں۔ اگر نتیجہ پہلے ہی ناکام ہے۔ map تبدیلی کو مکمل طور پر چھوڑ دیتا ہے اور ناکامیوں کو بغیر تبدیلی کے گزرنے دیتا ہے۔ اسے "ناکامی پروپیگنڈہ” کہا جاتا ہے۔ خرابیاں خود بخود سلسلہ سے گزر جاتی ہیں۔

flatMap زنجیروں میں ایک ایسا آپریشن جو خود ہی واپس آجاتا ہے۔ AppResult. یہ وہی ہے جو ترتیب کی اجازت دیتا ہے، جہاں عمل میں ہر قدم آزادانہ طور پر کامیاب یا ناکام ہوسکتا ہے. flatMap جڑیں تاکہ پہلی ناکامی سے سلسلہ رک جائے۔

سلسلہ کا کام

مندرجہ ذیل ترتیب وار آپریشنز ہر ایک مونیڈک چین کے بغیر ناکام ہو سکتے ہیں:

final loginResult = await login(email, password);

loginResult.when(
  success: (user) async {
    final profileResult = await getProfile(user.id);
    profileResult.when(
      success: (profile) async {
        final settingsResult = await loadSettings(profile.settingsId);
        settingsResult.when(
          success: (settings) => emit(AppState.ready(settings)),
          failure: (error) => emit(AppState.error(error)),
        );
      },
      failure: (error) => emit(AppState.error(error)),
    );
  },
  failure: (error) => emit(AppState.error(error)),
);

ہر ایک قدم پر گہرے اندر کی اور بار بار غلطی سے نمٹنے کے لیے۔ کے ساتھ flatMap:

final result = (await login(email, password))
    .flatMap((user) => getProfile(user.id))
    .flatMap((profile) => loadSettings(profile.settingsId))
    .map((settings) => settings.theme);

result.when(
  success: (theme) => emit(AppState.ready(theme)),
  failure: (error) => emit(AppState.error(error)),
);

ہر قدم کو صرف اس صورت میں انجام دیا جاتا ہے جب پچھلا مرحلہ کامیاب ہو۔ پہلی غلطی پوری چین کو شارٹ سرکٹ کر دیتی ہے۔ خرابی کو سنبھالنا ہر قدم کے بجائے آخر میں ایک بار ہوتا ہے۔ یہ اصلی ایپلیکیشن کوڈ پر لاگو ہونے والے مونڈ پیٹرن کی پوری طاقت ہے۔

حصہ 4: ڈارٹز کے ساتھ

کون سا؟

Either فنکشنل پروگرامنگ کی ایک قسم جو دو ممکنہ اقدار میں سے ایک کی نمائندگی کرتی ہے۔ Left یا Right. کنونشن کے مطابق:

کہ dartz یہ پیکیج ڈارٹ کو یہ اور بہت سے دوسرے فنکشنل پروگرامنگ پرائمیٹوز فراہم کرتا ہے۔ اپنے پروجیکٹ میں شامل کریں:

dependencies:
  dartz: ^0.10.1

کوڈ بیس میں جو ہم بنا رہے ہیں، Either ارادے کو واضح کرنے کے لیے ایک قسم کے عرف کے ساتھ استعمال کیا جاتا ہے۔

import 'package:dartz/dartz.dart';

typedef API = Either;

یہاں قواعد دیکھیں۔ Left کامیابی کی قدر ہے Tاور Right ناکامی پر مشتمل ہے iException. یہ فنکشنل پروگرامنگ کے معیار سے جان بوجھ کر الٹ جانا ہے۔ دونوں اصول اصل کوڈ بیس میں موجود ہیں۔ اہم بات مستقل مزاجی ہے۔

اصل میں اس کا استعمال کرتے ہوئے

دو میں سے ایک قدر پیدا کرتا ہے۔

// Success — Left holds the data
Either result = Left(user);

// Failure — Right holds the exception
Either result = Right(iException.internet(message: 'No connection'));

معلوم کریں کہ آپ کس طرف ہیں:

if (result.isLeft()) {
  final user = result.fold((user) => user, (_) => null);
}

پلنگ ریکارڈز اور یا تو

اصل طاقت ہے۔ API typedef سے آتا ہے: ApiRes – ایک یوٹیلیٹی کلاس جو ڈیٹا لیئر کی ریکارڈ پر مبنی دنیا اور ڈومین لیئر کی یا تو پر مبنی دنیا کے درمیان تبدیل ہوتی ہے:

class ApiRes {
  static Future> deserialize(ApiResult res) async {
    return (res.data != null)
        ? Left(res.data as T)
        : Right(res.exception!);
  }

  static Future deserializeDynamic(
    ApiResult res,
  ) async {
    return (res.data != null) ? Left(res.data) : Right(res.exception!);
  }
}

ApiResult ڈیٹا ریجن کی ریکارڈ کی قسم۔ ایک ڈیو جواب جو کہ کالعدم فیلڈ میں لپٹا ہوا ہے۔ ApiRes.deserialize وہ ریکارڈ لیں اور اسے کسی مناسب چیز میں تبدیل کریں۔ Eitherآپ کی ڈومین پرت میں استعمال کے لیے تیار ہے۔

اصل میں ذخیرہ کرنے کا طریقہ یہ ہے:

Future> getUser(String id) async {
  // Data layer returns a record
  final res = await _dataSource.fetchUser(id);

  // Convert to Either at the boundary
  return ApiRes.deserialize(res);
}

تہوں کے درمیان حدود منتقلی پوائنٹس ہیں۔ ڈیٹا ریجن میں ریکارڈ کے ساتھ کام کریں۔ بارڈر پر آپ تبدیل کرتے ہیں۔ ڈومین پرت پر، آپ ایک یا دوسرے کے ساتھ کام کرتے ہیں۔ ہر پرت کی ایک قسم ہوتی ہے جو اس کے لیے بہترین ہے۔

ان میں سے ایک کو گنا

dartz پیشکش fold ایک طریقہ جو اس سے ملتا جلتا کام کرتا ہے: when() کو AppResult:

final result = await repository.getUser(id);

result.fold(
  (user) => emit(UserState.loaded(user)),       // Left — success
  (exception) => emit(UserState.error(exception.message)), // Right — failure
);

dartz یہ آؤٹ آف دی باکس مونیڈک آپریشنز بھی فراہم کرتا ہے۔

// map — transform the Left value
final nameResult = result.map((user) => user.name);

// flatMap / bind — chain Either-returning operations
final profileResult = result.flatMap(
  (user) => getProfile(user.id),
);

یہ ایک مکمل خصوصیات والی ٹول کٹ ہے جسے آپ خود اسے بنائے بغیر فوراً استعمال کر سکتے ہیں۔

حصہ 5: منجمد کے ساتھ ٹائپ شدہ مستثنیات

یہ ایک استثناء کے ساتھ کیوں جم جاتا ہے؟

معیاری ڈارٹ مستثنیات میں بہت کم مفید معلومات ہوتی ہیں۔

throw Exception('Something went wrong');
// At the catch site: what went wrong? what type? what code? who knows.

یہاں تک کہ کسٹم استثنائی کلاسوں کو بھی صحیح طریقے سے لاگو کرنے کے لئے تھوڑا سا بوائلر پلیٹ کی ضرورت ہوتی ہے۔ ==، hashCode، toStringتغیر پذیری، کاپی کے ساتھ۔ فریزڈ خود بخود یہ سب تخلیق کرتا ہے اور اس کے اوپر مکمل پیٹرن ملاپ کا اضافہ کرتا ہے۔

مطلوبہ پیکجز شامل کریں۔

dependencies:
  freezed_annotation: ^2.4.1

dev_dependencies:
  freezed: ^2.4.5
  build_runner: ^2.4.6

iException تعمیر

import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'exception.freezed.dart';

@freezed
class iException with _$iException {
  const factory iException.internet({
    required String message,
    int? code,
  }) = InternetException;

  const factory iException.mapper({
    required String message,
    int? code,
  }) = MapperException;

  const factory iException.validation({
    required String message,
    int? code,
  }) = ValidationException;

  const factory iException.unauthorized({
    required String message,
    int? code,
  }) = UnauthorizedException;

  const factory iException.unknown({
    required String message,
    int? code,
  }) = UnknownException;

  const iException._();
}

کوڈ جنریشن چلائیں۔

flutter pub run build_runner build --delete-conflicting-outputs

منجمد اس سے کیا تخلیق کرتا ہے:

iException (sealed base)
├── InternetException    — network failures, no connectivity
├── MapperException      — JSON parsing and deserialization failures
├── ValidationException  — input validation failures
├── UnauthorizedException — auth failures, expired tokens
└── UnknownException     — catch-all for unexpected errors

ہر ذیلی طبقہ مکمل طور پر ناقابل تغیر ہے۔ == اور hashCode فیلڈ کے مطابق مناسب toString. استثناء کی تخلیق واضح اور واضح ہے۔

iException.internet(message: 'No internet connection')
iException.unauthorized(message: 'Session expired', code: 401)
iException.validation(message: 'Email format is invalid')
iException.mapper(message: 'Failed to parse UserResponse', code: 500)
iException.unknown(message: e.toString())

نجی تعمیر کنندہ const iException._() بیس کلاس میں مثال کے طریقہ یا گیٹر کو شامل کرتے وقت فریز کی ضرورت۔ یہ فریزڈ سے تیار کردہ ذیلی طبقات کو اسے کال کرنے کی اجازت دیتا ہے۔ super._() بیس میں عوامی تعمیر کنندہ کو بے نقاب کیے بغیر۔

استثناء کی اقسام کے لیے پیٹرن کی مماثلت

کیونکہ iException یہ ایک منجمد مہر کلاس ہے۔ when, maybeWhen، mapاور maybeMap کوڈ جنریشن مفت ہے۔

exception.when(
  internet: (message, code) => 'No internet: $message',
  mapper: (message, code) => 'Parse error: $message',
  validation: (message, code) => 'Invalid input: $message',
  unauthorized: (message, code) => 'Unauthorised — please log in again',
  unknown: (message, code) => 'Unexpected error: $message',
);

تمام کیسز مطلوب ہیں۔ کمپائلر نامکمل میچوں کو مسترد کرتا ہے۔ آپ اتفاقی طور پر صرف کچھ استثنائی اقسام کو ہینڈل نہیں کر سکتے ہیں اور خود بخود دوسروں کو یاد نہیں کر سکتے ہیں۔

اگر آپ صرف ایک مخصوص قسم میں دلچسپی رکھتے ہیں:

exception.maybeWhen(
  unauthorized: (message, code) => _redirectToLogin(),
  orElse: () => _showGenericError(exception),
);

ایک صاف ستھرا بنیادی حاصل کرنے والا پیٹرن

بنیاد پر بہتری کے قابل ایک چیز iException ہم ایک محفوظ فراہم کرتے ہیں۔ message گیٹر جو بغیر پھینکے تمام ذیلی قسموں پر کام کرتا ہے۔ UnimplementedError:

const iException._();

String get displayMessage => when(
  internet: (message, _) => message,
  mapper: (message, _) => message,
  validation: (message, _) => message,
  unauthorized: (message, _) => message,
  unknown: (message, _) => message,
);

اب iException – بلایا جا سکتا ہے قطع نظر اس کے کہ یہ کس قسم کا ہے۔ .displayMessage محفوظ طریقے سے:

// In a ViewModel or BLoC — no need to pattern match just for the message
emit(ErrorState(message: exception.displayMessage));

یہ آپ جس ڈیفالٹ گیٹر کو پھینک رہے ہیں اس سے کہیں زیادہ صاف ہے۔ UnimplementedError رن ٹائم پر۔

حصہ 6: جامع خلاصہ

مجموعی فن تعمیر

یہاں یہ ہے کہ ایک حقیقی صاف فن تعمیر فلٹر ایپلی کیشن میں چاروں پیٹرن کیسے جڑے ہوئے ہیں:

Data Layer
  Dio/HTTP call returns raw response
    └── Wrapped in ApiResult (record type)
          │
          ▼
Repository Layer
  ApiRes.deserialize() converts record → Either
    └── Returns API = Either
          │
          ▼
Domain / Use Case Layer
  AppResult is the standard return type
    └── Sealed class with AppSuccess and AppFailureResult
          │
          ▼
Presentation Layer
  result.when() handles both paths
    └── exception.when() handles all failure types

ہر پرت کا نتیجہ اس کی ذمہ داری کے لیے موزوں ہے۔ منتقلی سرحدوں پر ہوتی ہے۔ پریزنٹیشن پرت ہمیشہ ہینڈل کرتی ہے: AppResult – یا آپ کو ریکارڈ کے بارے میں کچھ جاننے کی ضرورت نہیں ہے۔

اسٹوریج کی سطح

class AuthRepository {
  final AuthDataSource _dataSource;

  AuthRepository(this._dataSource);

  Future> login(String email, String password) async {
    // Data source returns a record
    final res = await _dataSource.login(email, password);

    // Convert to Either at the data/domain boundary
    final either = await ApiRes.deserialize(res);

    // Convert Either to AppResult for the domain layer
    return either.fold(
      (user) => AppSuccess(user),
      (exception) => AppFailureResult(exception),
    );
  }

  Future>> getUsers() async {
    final res = await _dataSource.fetchUsers();
    final either = await ApiRes.deserialize>(res);

    return either.fold(
      (users) => AppSuccess(users),
      (exception) => AppFailureResult(exception),
    );
  }
}

ڈومین پرت

class LoginUseCase {
  final AuthRepository _repository;

  LoginUseCase(this._repository);

  Future> execute(String email, String password) async {
    if (email.isEmpty || password.isEmpty) {
      return AppFailureResult(
        iException.validation(message: 'Email and password are required'),
      );
    }

    return _repository.login(email, password);
  }
}

استعمال کے معاملات میں توثیق کی اپنی پرت شامل کریں۔ ValidationException ذخیرہ تک پہنچنے سے پہلے۔ تمام ناکامیاں ایک ہی عمل سے گزرتی ہیں۔ AppResult براہ کرم قسم درج کریں چاہے وہ کہاں سے آیا ہو۔

پریزنٹیشن پرت

class AuthViewModel extends ChangeNotifier {
  final LoginUseCase _loginUseCase;

  AuthViewModel(this._loginUseCase);

  AuthState _state = const AuthState.idle();
  AuthState get state => _state;

  Future login(String email, String password) async {
    _state = const AuthState.loading();
    notifyListeners();

    final result = await _loginUseCase.execute(email, password);

    result.when(
      success: (user) {
        _state = AuthState.authenticated(user);
      },
      failure: (exception) {
        // Pattern match on the exception type for specific handling
        final message = exception.when(
          internet: (msg, _) => 'No internet connection. Please check your network.',
          unauthorized: (msg, _) => 'Your session has expired. Please log in again.',
          validation: (msg, _) => msg,
          mapper: (msg, _) => 'Something went wrong. Please try again.',
          unknown: (msg, _) => 'An unexpected error occurred.',
        );

        _state = AuthState.error(message);
      },
    );

    notifyListeners();
  }
}

مکمل پیٹرن کی مماثلت کی دو سطحیں – ایک نتیجہ پر اور ایک استثناء کی قسم پر۔ ہر ممکنہ ناکامی کا ایک مخصوص، صارف دوست پیغام ہوتا ہے۔ کمپائلر اس بات کی ضمانت دیتا ہے کہ کچھ بھی غائب نہیں ہے۔

اور ہم کثیر مرحلہ بہاؤ کے لیے حصہ 3 سے موناڈک چین کا استعمال کرتے ہیں:

Future loadDashboard(String userId) async {
  _state = const DashboardState.loading();
  notifyListeners();

  final result = (await _userRepo.getUser(userId))
      .flatMap((user) => _profileRepo.getProfile(user.profileId))
      .flatMap((profile) => _settingsRepo.loadSettings(profile.settingsId))
      .map((settings) => DashboardData(settings: settings));

  result.when(
    success: (data) => _state = DashboardState.loaded(data),
    failure: (exception) => _state = DashboardState.error(
      exception.displayMessage,
    ),
  );

  notifyListeners();
}

تین ترتیب وار غیر مطابقت پذیر آپریشنز، جن میں سے ہر ایک آزادانہ طور پر ناکام ہو سکتا ہے، آخر میں ایک ہی ایرر ہینڈلر کے ساتھ کلین چین میں ہینڈل کیا جاتا ہے۔ پروڈکشن گریڈ ایرر ہینڈلنگ ایسا ہی لگتا ہے۔

نتیجہ

ایرر ہینڈلنگ ان خصوصیات میں سے ایک ہے جو ہر کوڈ بیس میں ہوتی ہے، لیکن بہت کم لوگ اسے اچھی طرح سے انجام دیتے ہیں۔ پھینکنے اور پکڑنے کی مستثنیات کا ڈارٹ کا ڈیفالٹ چھوٹے منصوبوں کے لیے آسان ہے، لیکن پیمانے پر مسئلہ بن جاتا ہے۔ ناکامیاں پوشیدہ ہو جاتی ہیں، قسم کی معلومات تہوں میں ختم ہو جاتی ہیں، اور اگر کچھ غلط ہو جاتا ہے تو کمپائلر آپ کی مدد نہیں کر سکتا۔

اس مضمون کا پیٹرن اس کو مکمل طور پر بدل دیتا ہے۔

ریکارڈز ایک ہلکا پھلکا رزلٹ کنٹینر فراہم کرتا ہے جس میں کوئی بوائلر پلیٹ نہیں ہے، جو اسے پرت کے لیے مخصوص نتائج کی اقسام اور ڈومین کے لیے مخصوص جوابات کے لیے مثالی بناتا ہے۔ سیل شدہ نتائج کی قسم کو کمپائلر انفورسمنٹ ملتا ہے۔ دونوں راستے درکار ہیں اور غلطیوں کو خاموشی سے نظر انداز نہیں کیا جا سکتا۔ مونڈ پیٹرن چین کے ذریعے خودکار ناکامی کے پھیلاؤ کے ذریعے ترتیب وار آپریشنز کو صاف ستھرا کرنے کی صلاحیت کا اضافہ کرتا ہے۔ یا dartz یہ ایک مکمل خصوصیات والی ٹول کٹ اور ڈیٹا اور ڈومین لیئرز کے درمیان واضح قسم کی باؤنڈری فراہم کرتا ہے۔ اور فکسڈ مستثنیات خرابی کی حالت کی ساخت، ناقابل تبدیلی، اور مکمل پیٹرن کی مماثلت فراہم کرتے ہیں تاکہ غلطی کی تمام اقسام کو واضح طور پر سنبھالا جائے اور کچھ بھی نہ بچ سکے۔

اگر آپ ان کے حل کردہ مسائل کو سمجھتے ہیں تو ان میں سے کوئی بھی نمونہ پیچیدہ نہیں ہے۔ اور جو مسئلہ وہ حل کرتے ہیں (پوشیدہ، ناقابل نفاذ، اور ٹائپ غیر محفوظ ایرر ہینڈلنگ) فلٹر ایپلی کیشنز میں پروڈکشن بگز کے سب سے عام ذرائع میں سے ایک ہے۔

اگلا مرحلہ ان نمونوں میں سے کسی ایک کو حقیقی پروجیکٹ پر لاگو کرنا ہے۔ اسے استعمال کرنے سے آپ کے پورے کوڈ بیس کی غلطی سے نمٹنے کی کہانی اور عمل کو مکمل طور پر تبدیل کر دیا جاتا ہے۔

اوپر تک سکرول کریں۔