Day 9: Sealed Classes (The Result Pattern)
Master exhaustive type checking and bug-proof state management using sealed classes that force you to handle every possible scenario at compile-time.
မင်္ဂလာပါ.. 👋
100 days of Flutter ရဲ့ Day 8 က ကြိုဆိုပါတယ်။ ဒီနေ့မှာတော့ dart langauge နဲ့ ပတ်သက်တာတွေကို ဆက်လက်ပြီး လေ့လာသွားပါမယ်။ ဒီနေ့ဆွေးနွေးသွားမယ့် ခေါင်းစဥ်တွေကတော့
- Sealed Classes
- Result Pattern
တွေပဲ ဖြစ်ပါတယ်။
အရင်ရက်တွေမှာ Object Oriented Programming (OOP) အကြောင်း ပြောရင်း class, abstract class စတာတွေကို ဆွေးနွေးခဲ့ပါတယ်။ ပြန်ကြည့်ချင်တယ် အောက်မှာ ပေးထားတဲ့ link တွေကနေ ဝင်ကြည့်လို့ရပါတယ်။


Sealed Class
ဒီနေ့မှာတော့ Dart 3 မှာပါလာတဲ့ sealed class တွေအကြောင်း ဆက်ပြီး ကြည့်ရအောင်ပါ။ ဒါကိုတော့ code တွေ ရေးတဲ့အခါ မမျှော်လင့်ဘဲ ဖြစ်လာနိုင်တဲ့ error တွေကို ကာကွယ်ဖို့ သုံးနိုင်ပါတယ်။ Flutter app UI တွေအတွက် state တွေကို တည်ဆောက်တဲ့အခါ သူ့ကို အသုံးပြုခြင်းအားဖြင့် အပြောင်းအလဲတွေ ရှိတယ်ဆို အလွယ်တကူ သက်ရောက်မယ့် နေရာတွေကို သိနိုင်ပါတယ်။
Sealed class အကြောင်း ပြောမယ်ဆို abstract class နဲ့ ယှဥ်ပြီး ပြောပြတဲ့အခါ ပိုပြီး ရှင်းရှင်းလင်းလင်း မြင်သွားပါလိမ့်မယ်။ abstract class ဆိုတာဘာလဲ မနေ့က ရှင်းပြပြီးပြီဆိုတော့ အသေးစိတ်မရှင်းပြတော့ပါဘူး။ abstract ကိုတော့ implementation တွေ မပါဝင်ဘဲ class တွေကို abstract သဘောနဲ့ ဘာတွေ ပါဝင်သွားမှာလဲ သတ်မှတ်တဲ့အခါ အသုံးပြုလေ့ရှိကြပါတယ်။ ဒီလိုရေးတာတွေကို BLoC state management သုံးတဲ့အခါ ပိုပြီး မြင်ရလေ့ ရှိပါတယ်။
abstract class AuthState {}
class Initial extends AuthState {}
class Loading extends AuthState {}
class Success extends AuthState {
final String userName;
Success({required this.userName});
}
String getMessage(AuthState state) {
return switch (state) {
Initial() => 'Welcome',
Loading() => 'Loading...',
Success() => 'Done!',
_ => '???',
};
}
void main() {
AuthState state = Initial();
print(getMessage(state));
state = Loading();
print(getMessage(state));
state = Success(userName: 'Aung Aung');
print(getMessage(state));
}
// Output:
// Welcome
// Loading...
// Done!ဒီမှာကြည့်မယ်ဆို Initial, Loading, Success ဆိုပြီး AuthState အခွဲတွေ ရှိတာကို တွေ့နိုင်ပါတယ်။ ဒါကိုတော့ UI မှာ state တခုကို message တခုပြမှာလို့ တွေးကြည့်လို့ရပါတယ်။

App တွေ သုံးတဲ့အခါ data source တွေကနေ fetch တဲ့အချိန်မှာ loading ဆိုပြီး ပြလေ့ ရှိကြတာ ပုံမှန်ပါ။ ဒီတော့ ဒီလို အခြေအနေတွေကို ကိုင်တယ်ဖို့ဆို အပေါ်မှာ ပြထားသလိုမျိုး အခြေအနေတခုအတွက် class တခု သတ်မှတ်ပြီး ရေးလေ့ရှိတာမျိုးပါ။
ပြထားတဲ့ code က ရေးတာတွေလဲ မှန်ပြီးတော့ အလုပ်လဲသေချာ လုပ်နေပါတယ်။ ဒီလိုနဲ့ တစ်ပတ်၊ တစ်လလောက်ကြတာတော့ တစ်ယောက်ယောက်က Failure အခြေအနေကိုလဲ ကိုင်တွယ်ဖို့လိုတယ်ဆိုပြီး အခုလို ထည့်လိုက်တယ်ဆိုပါစို့။
abstract class AuthState {}
class Initial extends AuthState {}
class Loading extends AuthState {}
class Failure extends AuthState {}
class Success extends AuthState {
final String userName;
Success({required this.userName});
}ထည့်လိုက်တဲ့အခါ အကုန်လုံးဆက်ပြီး အလုပ်လုပ်နေမှာပါ။ ဒါပေမယ့် တစ်ယောက်ယောက်က အောက်မှာ ပြထားသလိုမျိုး getMessage ထဲကို Failure object ထည့်ပေးလိုက်တယ်ဆိုပါစို့။ ဒါဆိုရင်တော့ Failure အခြေအနေကို getMessage ထဲမှာ handle လုပ်မထားတဲ့အတွက် အခုလိုမျိုး မြင်နေရမှာပဲ ဖြစ်ပါတယ်။
void main() {
AuthState state = Failure();
print(getMessage(state));
}
// Output:
// ???Codebase တွေ ကြီးလာတဲ့အခါ အခုလိုမျိုး မထည့်မိလိုက်တာကနေ အခြားသော error ကြီးကြီးမားမားတွေ ဖြစ်လာနိုင်ပါတယ်။ switch expression နဲ့မဟုတ်ဘဲ switch statement နဲ့ဖြစ်စေ၊ if-else ဖြစ်စေ ရေးလဲ အခုလိုမျိုး ကြုံလာနိုင်ပါသေးတယ်။ ကိုယ်က အခုမှ codebase ကို ကိုင်တွယ်ဖူးပြီးတော့ အခုလိုမျိုး ပြင်ပေးရမယ့် နေရာတွေကို သိမနေဘူးဆို error တွေ တက်ဖို့ ပိုလွယ်ပါတယ်။
ဒီတော့ ဘယ်လိုဖြေရှင်းမလဲဆိုတော့ sealed class တွေ သုံးလို့ရပါတယ်။ အပေါ်က code ကို sealed class နဲ့ရေးပြီး ဘာတွေ ကွာသွားမလဲ ဆက်ကြည့်လိုက်ရအောင်ပါ။
sealed class AuthState {}
class Initial extends AuthState {}
class Loading extends AuthState {}
class Failure extends AuthState {}
class Success extends AuthState {
final String userName;
Success({required this.userName});
}
String getMessage(AuthState state) {
return switch (state) {
Initial() => 'Welcome',
Loading() => 'Loading...',
Success() => 'Done!',
Failure() => 'Failed!',
};
}
void main() {
AuthState state = Initial();
print(getMessage(state));
state = Loading();
print(getMessage(state));
// state = Success(userName: 'Aung Aung');
// print(getMessage(state));
state = Failure();
print(getMessage(state));
}
// Output:
// Welcome
// Loading...
// Failed!ဒီမှာကြည့်မယ်ဆို switch အတွက် default အနေနဲ့ သတ်မှတ်ပေးဖို့ မလိုအပ်တော့ပါဘူး။ ရှေ့မှာတုန်းက _ => '???' ဆိုပြီး ရေးထားတာက sealed class သတ်မှတ်ပြီးတဲ့အခါမှာတော့ ဖယ်လိုက်လို့ရပါပြီ။ sealed နဲ့ ရေးလိုက်ခြင်းအားဖြင့် သူ့မှာ ဖြစ်နိုင်တဲ့ state တွေအားလုံးကို စုစည်းပေးလိုက်သလိုမျိုး ဖြစ်သွားပါတယ်။ ဒီလိုဖြစ်ဖို့အတွက် သူ့မှာ သတ်မှတ်ချက်တော့ ရှိပါတယ်။ sealed class ကို extends လုပ်တာတွေက သူနဲ့ file တခုထဲ, library တခုထဲအောက်မှာ ရှိနေဖို့လိုအပ်ပါတယ်။ ဒီတော့မှ compiler က ဖြစ်နိုင်တာတွေ အားလုံးကို သိပြီးတော့ တခုခုလွဲနေတယ်ဆိုရင် error အနေနဲ့ ထုတ်ပြနိုင်မှာပါ။

အခြား တနေရာကနေပြီး extend လုပ်ဖို့ ကြိုးစားရင်တော့ အပေါ်မှာပြထားသလိုမျိုး error တွေ့နေရမှာပါ။
The Result Pattern
Result pattern ကတော့ အပေါ်မှာ ပြောခဲ့တဲ့ sealed ပေါ်အခြေခံပြီး ပြုလုပ်ထားတာ ဖြစ်ပါတယ်။ Flutter မှာ ဒီ pattern ကို try/catch တွေအစား အသုံးပြုလာကြပါတယ်။ try/catch ဆိုတာကတော့ program/app မှာ error/exception ဖြစ်တဲ့အခါ လုံးဝကြီး crush သွားတာမျိုး ဖြစ်မသွားအောင် သုံးတာပဲ ဖြစ်ပါတယ်။
void main() {
try {
int result = 10 ~/ 0;
print(result);
} catch (e) {
print('Error: $e'); // Output: Error: IntegerDivisionByZeroException
}
try {
int number = int.parse('abc');
print(number);
} catch (e) {
print('Error: $e'); // Output: Error: FormatException: Invalid radix-10 number
}
try {
String? name = null;
print(name!.length);
} catch (e) {
print('Error: $e'); // Output: Error: Null check operator used on a null value
}
try {
List<int> numbers = [1, 2, 3];
print(numbers[10]);
} catch (e) {
print('Error: $e'); // Output: Error: RangeError (index)
}
}ဒီမှာ ပြထားတဲ့ code ကို run လိုက်တဲ့အခါ တခုချင်းက exception ဖြစ်စေမှာပဲ ဖြစ်ပါတယ်။ ဒါပေမယ့် syntax အရ ဘာမှ လွဲမှားနေတာမျိုး မရှိတဲ့အတွက် code ရေးတဲ့အခါ error ပြနေမှာ မဟုတ်ပါဘူး။ ဒီလိုမျိုး အခြေအနေတွေကို ပုံမှန်ဆိုရင် try/catch နဲ့ ကိုင်တွယ်ပါတယ်။ try ထဲမှာ error တခုခုဖြစ်ပြီး exception အနေနဲ့ ထွက်လာတဲ့အခါ catch ကနေပြီးတော့ ဆက်လက်ပြီး ကိုင်တွယ်ပေးသွားမှာဖြစ်ပါတယ်။ တကယ်လို့ အခုလိုမျိုး error ဖြစ်တာတွေကို ကိုင်တယ်မထားဘူးဆို app crush တဲ့အထိ ဖြစ်နိုင်ပါတယ်။
ဒါပေမယ့် နေရာတိုင်းမှာ try/catch တွေနဲ့ ရေးတဲ့အခါ ဘာ error က ဘယ်နေရာက ဖြစ်လဲဆိုတော လိုက်ကြည့်ရခက်ပါတယ်။ ဒီတော့ sealed class သုံးလို့ရတာကို အခြေခံပြီး Result pattern ဆိုတာကို အသုံးပြုလာကြပါတယ်။ ဒီတော့ နေရာတိုင်းမှာ try/catch နဲ့ လိုက်ရေးနေစရာမလိုတော့ပါဘူး။
sealed class Result<T> {}
class Success<T> extends Result<T> {
final T data;
Success(this.data);
}
class Failure<T> extends Result<T> {
final String error;
Failure(this.error);
}Result<int> divide(int a, int b) {
if (b == 0) {
return Failure('Cannot divide by zero');
}
return Success(a ~/ b);
}
Result<int> parseNumber(String text) {
try {
final number = int.parse(text);
return Success(number);
} catch (e) {
return Failure('Invalid number: $text');
}
}
void main() {
final result1 = divide(10, 2);
final message1 = switch (result1) {
Success(data: var value) => 'Result: $value',
Failure(error: var msg) => 'Error: $msg',
};
print(message1);
final result2 = divide(10, 0);
final message2 = switch (result2) {
Success(data: var value) => 'Result: $value',
Failure(error: var msg) => 'Error: $msg',
};
print(message2);
final result3 = parseNumber('42');
print(switch (result3) {
Success(data: var n) => 'Parsed: $n',
Failure(error: var e) => 'Failed: $e',
});
final result4 = parseNumber('abc');
print(switch (result4) {
Success(data: var n) => 'Parsed: $n',
Failure(error: var e) => 'Failed: $e',
});
}
// Output:
// Result: 5
// Error: Cannot divide by zero
// Parsed: 42
// Failed: Invalid number: abcဒီမှာကြည့်မယ်ဆို Result pattern နဲ့ ရေးထားတာကို တွေ့ရမှာပါ။ result pattern ကနေ ပြန်ရလာတဲ့ value တွေကို ဘယ်လို ပြန်သုံးနိုင်မလဲဆိုတာပါ တွေ့နိုင်ပါတယ်။ ဒါကိုတော့ pattern matching နဲ့ တွဲပြီးတော့ ကြည့်ကြည့်လို့ရပါတယ်။ ရှေ့ရက်တွေမှာ ပြောထားခဲ့တဲ့ pattern matching ဆိုတာကို return ပြန်လာတဲ့ object တွေအတွက်ပါ de-structure လုပ်ပြီးတော့ သုံးနိုင်တာကို တွေ့ရမှာပါ။
class Success<T> extends Result<T> {
final T data; // <-- This property
Success(this.data);
}
// Pattern matching extracts it:
Success(data: var value)
^^^^ ^^^^^
| |
| +-- New variable name (you choose this)
+-- Property name (must match class property)
// Another way Named Pattern
Success(:var data)
^^^^ ^^^^
| |
| +-- Property name from the class
+-- Shorthand: "extract property with same variable name"အပေါ်မှာတော့ ရေးပုံရေးနည်း ၂မျိုးကို ပြထားပေးပါတယ်။ တခုကတော့ variable ကို နောက် variable name တခုနဲ့ ကြေငြာသွားတာဖြစ်ပြီး နောက်တခုကတော့ ရလာမယ့် variable ကို တခါထဲ named pattern နဲ့ match လုပ်သွားတာပဲ ဖြစ်ပါတယ်။ အဆင်ပြေတာ သုံးသွားလို့ရပါတယ်။
Base class ဖြစ်တဲ့ Result class ကို sealed class အဖြစ်နဲ့ ကြေငြာထားတာ ဖြစ်တဲ့အတွက် Success နဲ့ Failure ၂ခုကိုပဲ match လုပ်ပေးဖို့လိုပါတော့တယ်။ _ နဲ့ default case အတွက်ဆိုပြီး ကြေငြာဖို့ မလိုတော့ပါဘူး။
ရှေ့ရက်တွေက မပြောရသေးတာ တခုကတော့ အပေါ်က ဥပမာမှာ အသုံးပြုထားတဲ့ T ဆိုတာပဲ ဖြစ်ပါတယ်။ ဒါကိုတော့ generic type လို့ခေါ်ပါတယ်။ T မှ မဟုတ်ပါဘူး တခြား alphabet letter တွေကိုလဲ အသုံးပြုလို့ရပါတယ်။ သူ့ကိုတော့ Type Reference လုပ်ချင်တာမျိုးမှာ အသုံးပြုပါတယ်။ အပေါ်မှာ ပေးထားတဲ့ ဥပမာမှာ ကြည့်မယ်ဆို Result အနေနဲ့ ပြန်တာက Result<int> ဆိုပြီး int value ကို ပြန်ပေးမယ်လို့ ရေးထားတာကို တွေ့ရမှာပါ။ ဒီလိုမျိုး Result<int> မှာပါတဲ့ int type က T ကနေတဆင့် Success class ထဲကို ထည့်ပေးလို့ရမယ့် value ရဲ့ type ဖြစ်သွားပါတယ်။ class Success<T> extends Result<T> ဒီမှာ ကြေငြာထားတာကို ကြည့်မယ်ဆို Success<T> ဆိုတာရဲ့ T type က Result<T> ဆိုတဲ့ Result ထဲမှာ ပါလာတဲ့ type ကို သွားပြီး တော့ reference လုပ်လိုက်တာပဲ ဖြစ်ပါတယ်။ ဒီမှာ တခု သတိထားရမှာကတော့ ကိုယ်ထည့်ပေးတဲ့ alphabet က type တခုအတွက်ဆို အတူတူပဲ သုံးရပါတယ်။ class Success<P> extends Result<T> လိုမျိုး သွားရေးရင်တော့ P ရဲ့ type ကို reference လုပ်စရာ မရှိတဲ့အတွက် အလုပ်လုပ်မှာ မဟုတ်ပါဘူး။
ဒီတော့ sealed class ကို သုံးခြင်းအားဖြင့် အောက်မှာ ဖော်ပြထားတဲ့ အခြေအနေမျိုးတွေ ကိုင်တယ်ဖို့ လိုအပ်တဲ့အခါ အလွယ်တကူပဲ ကိုင်တွယ်သွားနိုင်မှာ ဖြစ်ပါတယ်။
Common Use Cases
- Network requests (Loading -> Success or Failure)
- Form Validation (Valid or Invalid -> Processing -> Submitted)
- Media Player (Stopped -> Playing -> Paused -> Buffering -> Error)
- Connection Status (Disconnected -> Connecting -> Disconnected -> Error)
လာမယ့် Flutter ပိုင်းရောက်တဲ့အခါ Riverpod အကြောင်းတို့ AI Integration တို့မှာ sealed class ရဲ့ အဓိက အသေးစိတ် လက်တွေ့အသုံးချပုံတွေကို တွေ့ရမှာပါ။
ဒီနေ့ Day 9 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။
- Youtube: https://www.youtube.com/@arkarmintun
- Newsletter: https://arkar.dev/
- Discord: https://discord.gg/3xUJ6k6dkH

