Day 7: Object-Oriented Programming (OOP) - Classes

Transition from simple variables to modeling real-world entities with classes, constructors, and encapsulation that form the foundation of every Flutter widget.

မင်္ဂလာပါ.. 👋

100 days of Flutter ရဲ့ Day 7 က ကြိုဆိုပါတယ်။ ဒီနေ့မှာတော့ dart langauge နဲ့ ပတ်သက်တာတွေကို ဆက်လက်ပြီး လေ့လာသွားပါမယ်။ ဒီနေ့ဆွေးနွေးသွားမယ့် ခေါင်းစဥ်ကတော့ Object Oriented Programming (OOP) - Classes အကြောင်းပဲ ဖြစ်ပါတယ်။


ဒီနေ့မတိုင်ခင်ထိ dart language နဲ့ ပတ်သက်တဲ့ အခြေခံတွေ လေ့လာပြီးပြီဆိုတော့ ဒီနေ့တော့ နောက်တဆင့်ကို တက်ပြီးတော့ Object Oriented Programming (OOP) ကို dart မှာ ဘယ်လိုရေးသလဲ လေ့လာကြရအောင်ပါ။

Flutter အကြောင်းပြောကြရင် "Everything is a widget" ဆိုပြီး ပြောကြလေ့ ရှိပါတယ်။ တကယ်တော့ "Everything is an object" လို့ပြောလို့လဲ ရပါတယ်။ Flutter app တွေရေးကြတဲ့အခါ Object တွေပေါ် အခြေခံပြီး တည်ဆောက်ထားလို့ပဲ ဖြစ်ပါတယ်။ ဒီတော့ class ဆိုတာဘာလဲ object ဆိုတာဘာလဲ ကြည့်ကြရအောင်ပါ။

Class ကိုတော့ blueprint နဲ့ တင်စားပြီး ပြောကြလေ့ ရှိပါတယ်။ Blueprint/Class တခု ရှိပြီဆို ဆင်တူတဲ့ object ပေါင်းများစွာကို ဖန်တီးလို့ ရတဲ့သဘောပါ။ User class ရှိပြီဆို app ထဲမှာ ရှိတဲ့ user တဦးချင်းစီအတွက် object တွေ တည်ဆောက်လို့ ရသွားတာမျိုး ဖြစ်ပါတယ်။

user တစ်ဦးချင်းစီကို ကြည့်မယ်ဆို သူတို့နဲ့ သက်ဆိုင်တဲ့ data တွေ ရှိပါလိမ့်မယ်။ name, email စသဖြင့်ပေါ့။ ဒါတွေကို အတူစုထားခြင်းအားဖြင့် user တွေ ရာချီ, ထောင်ချီ ကိုင်တွယ်ဖို့ လိုအပ်တဲ့အခါ အလွယ်တကူ ကိုင်တွယ်နိုင်မှာပဲ ဖြစ်ပါတယ်။

class User {
  User(this.name, this.email);

  final String name;
  final String email;
}

void main() {
  final johnDoe = User("John Doe", "john@doe.com");
  final janeDoe = User("Jane Doe", "jane@doe.com");

  print(johnDoe);
  print(janeDoe);

  print("${johnDoe.name} ${johnDoe.email}");
  print("${janeDoe.name} ${janeDoe.email}");
}

// Output:
// Instance of 'User'
// Instance of 'User'
// John Doe john@doe.com
// Jane Doe jane@doe.com

Class constructor

Class တွေမှာ constructor ဆိုပြီးတော့ ရှိပါတယ်။ Class ကို object အနေနဲ့ တည်ဆောက်တဲ့အခါ value တွေ ထည့်ပေးဖို့ လိုတာတွေကို constructor ထဲကတဆင့် ထည့်ပေးပါတယ်။ အပေါ်မှာ ပေးထားတဲ့ ဥပမာမှာဆိုရင် User(this.name, this.email); ဆိုတာ contructor ပဲ ဖြစ်ပါတယ်။

ဒီနေရာမှာ this ရဲ့ အသုံးကို တချက် ကြည့်ဖို့လိုပါတယ်။ dart မှာ constructor ထဲကို ထည့်ပေးလိုက်တဲ့ value ကို class ရဲ့ properties တွေနဲ့ တန်းပြီးတော့ link လုပ်သွားလို့ ရပါတယ်။ တခြား programming language တွေမှာဆို အခုလို ရေးလေ့ရှိတာ တွေ့ဖူးကြမယ်ထင်ပါတယ်။

class User {
  String name;
  String email;
  
  User(String name, String email) {
    this.name = name;
    this.email = email;
  }
}

Dart မှာတော့ constructor ကနေ လက်ခံတဲ့ value တွေကို class property နဲ့ တိုက်ရိုက်တန်းပြီးတော့ link လုပ်သွားပေးနိုင်ပါတယ်။ အဲ့အတွက် this keyword ကို အသုံးပြုနိုင်ပါတယ်။ property assign လုပ်ရုံပဲဆိုရင်တော့ constructor မှာ body {...} မပါလဲ​ ရပါတယ်။

class User {
  User(this.name, this.email);

  final String name;
  final String email;
}

အခုနောက်ပိုင်းမှာ ပိုတိကျပြီး self documenting ပုံစံလိုမျိုး named parameter တွေနဲ့ class constructor တွေကို ရေးလာကြပါတယ်။ ပိုထင်သာမြင်သာ ရှိပြီး နားလည်ရ ပိုလွယ်ကူပါတယ်။ Position အလိုက် value တွေ ထည့်ပေးတဲ့အခါ မှားတာတွေ မဖြစ်အောင်လဲ ကာကွယ်ပေးပါတယ်။

class User {
  User({required this.name, required this.email});

  final String name;
  final String email;

  void display() => print('$name ($email)');
}

void main() {
  final johnDoe = User(name: "John Doe", email: "john@doe.com");
  final janeDoe = User(name: "Jane Doe", email: "jane@doe.com");

  johnDoe.display();
  janeDoe.display();
}

// Output:
// John Doe (john@doe.com)
// Jane Doe (jane@doe.com)

Private properties

တကယ်လို့ တချို့ value တွေကို အပြင်ကို ပေးမသိစေချင်ဘူးဆို ဘယ်လို လုပ်ကြမလဲ ဆက်ကြည့်ကြရအောင်ပါ။ Dart မှာတော့ public, private ဆိုပြီး keyword တွေ သုံးပြီး မသတ်မှတ်ပါဘူး။ Underscore _ နဲ့ စတဲ့ variable, function, class တွေ အားလုံးကို private အနေနဲ့ ယူဆပါတယ်။ ဒါပေမယ့် တခုရှိတာက dart မှာ variable တွေက class scope မဟုတ်ဘဲ library scope ဖြစ်တာကိုပါ။ ဆိုလိုတာကတော့ file တခုထဲမှာ အကုန် ပေါင်းရေးတဲ့အခါ private properties တွေကိုလဲ access လုပ်လို့ရနေမှာပါ။ private properties တွေ လုံးဝ သပ်သပ်ဆီ ထားချင်တယ်ဆိုရင်တော့ file ခွဲရေးပေးဖို့ လိုအပ်ပါတယ်။

class User {
  User(this.name, this.email);

  String name;
  String email;
  int _saving = 100;
}

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1._saving); // Works! Same library
  print(user1.name);
  print(user1.email);
}

Output:
// 100
// Aung Aung
// aung@aung.com

လုံးဝသပ်သပ်ဆီ ဖြစ်နေဖို့ဆိုရင် file ခွဲပြီး ရေးပေးဖို့ လိုပါမယ်။ တခြား ဘာမှ ထွေထွေထူးထူး လုပ်ပေးဖို့ မလိုအပ်ပါဘူး။

// user.dart
class User {
  User(this.name, this.email);

  String name;
  String email;
  int _saving = 100;
}
// main.dart
import 'user.dart';

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1._saving); // Error! Cannot access anymore
  print(user1.name);
  print(user1.email);
}

အပေါ်မှာ ပြထားသလိုမျိုး file ခွဲရေးပြီးရင် user1._saving ဆိုတဲ့လိုင်းမှာ error ပြနေမှာပါ။ Private field ကို scope မတူဘဲ access လုပ်ဖို့ ကြိုးစားနေလို့ပဲ ဖြစ်ပါတယ်။

Updating properties

import 'user.dart';

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1.name);
  print(user1.email);

  user1.name = 'Kyaw Kyaw';
  user1.email = 'kyaw@kyaw.com';
  print(user1.name);
  print(user1.email);
}

// Output:
// Aung Aung
// aung@aung.com
// Kyaw Kyaw
// kyaw@kyaw.com

လက်ရှိ ရေးထားတဲ့အတိုင်းဆို user object ရဲ့ parameter တွေ ဖြစ်တဲ့ name တို့ email တို့ကို စိတ်ကြိုက်ပြန်ပြင်လို့ ရနေပါတယ်။ တကယ်လို့ value တွေကို ပေးထားတဲ့အတိုင်း ရှိနေစေချင်တယ်၊ မပြင်စေချင်ဘူးဆို final ဆိုပြီးတော့ ကြေငြာလိုက်လို့ ရပါတယ်။ ဒါဆိုရင်တော့ user object စပြီး ဆောက်တဲ့အချိန်မှာ သတ်မှတ်ပေးတဲ့ value ကနေ တခြား value ကို ပြောင်းလို့ ပြင်လို့ မရတော့ပါဘူး။

// user.dart
class User {
  User(this.name, this.email);
  
  final String name;
  final String email;
  int _saving = 100;
}
// main.dart
import 'user.dart';

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1.name);
  print(user1.email);

  user1.name = 'Kyaw Kyaw';       // Error! cannot re-assign final
  user1.email = 'kyaw@kyaw.com';  // Error! cannot re-assign final
  print(user1.name);
  print(user1.email);
}

final လို့ သတ်မှတ်လိုက်ချင်းအားဖြင့် တခါထဲပဲ assign လုပ်နိုင်ပြီးတော့ ဆက်ပြီးတော့ ပြင်တာ ပြောင်းတာ လုပ်လို့ မရတော့ပါဘူး။ အပြင်က တိုက်ရိုက် ပြင်တာပြောင်းတာ မလုပ်စေချင်ဘူး ဒါပေမယ့် object ထဲက function တွေက တဆင့် ပြင်ချင် ပြောင်းချင်တယ်ဆို ဘယ်လို ရေးမလဲ ဆက်ကြည့်ရအောင်ပါ။ တချို့ value တွေက process လုပ်ပြီးမှ ရှိသင့်တာမျိုးတွေ ရှိတဲ့အခါ ပြင်ပက assignment တွေ မလုပ်မိအောင် တားချင်တဲ့ အချိန်မျိုးမှာ အခုလိုမျိုး သုံးလို့ရပါတယ်။

// user.dart
class User {
  User(this.name, this.email);
  
  final String name;
  final String email;

  int _saving = 100;
  int get saving => _saving;

  void addSaving(int amount) {
    if (amount > 0) {
      _saving += amount;
    }
  }
}
// main.dart
import 'user.dart';

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1.name);
  print(user1.email);
  print(user1.saving);
  user1.addSaving(50);
  print(user1.saving);
}

// Output:
// Aung Aung
// aung@aung.com
// 100
// 150

ဒါဆိုရင်တော့ saving value ကို အပြင်ကနေ ပြင်ဆင် ပြောင်းလဲတာ လုပ်လို့မရတော့ဘဲ addSaving ဆိုတဲ့ function ကပဲ အပြောင်းအလဲ လုပ်ပေးလို့ ရမှာပါ။ ဒီလိုမျိုး အရေးအသားကိုတော့ getter လို့ခေါ်ပါတယ်။ value တွေကို ရယူဖို့ (get) လုပ်ဖို့အတွက် သုံးလို့ပါ။ ဒီလို လုပ်ထားခြင်းအားဖြင့် saving ရဲ့ value ကို ပြင်ဆင်တိုင်း ဘယ်ကနေ ပြင်ဆင်လိုက်လဲဆိုတာ စစ်ရတာ လွယ်သွားမှာပါ။ addSaving ကို ခေါ်ထားတဲ့နေရာတွေကို စစ်လိုက်တာနဲ့ သိသွားမှာပါ။

Getter တွေကို ရှိပြီးသား value တွေကနေ ဆက်စပ်ပြီး ရလာတဲ့ value (derived value) တွေ လိုချင်တဲ့ အခါမှာလဲ အသုံးပြုပါတယ်။ ဆိုလိုတာကတော့ သူ့တို့ကို ဘယ်နေရာကမှ သတ်မှတ်ပေးဖို့ မလိုဘူး သူ့တို့ရဲ့ value က အခြားနေရာက value တွေပေါ် အကြောင်းခံပြီး ရလာမှာ ဖြစ်တယ်။

class User {
  User(this.name, this.email);

  final String name;
  final String email;

  int _saving = 100;
  int get saving => _saving;

  bool get canBuyAHouse => _saving > 10000;

  void addSaving(int amount) {
    if (amount > 0) {
      _saving += amount;
    }
  }
}
import 'user.dart';

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1.saving);
  user1.addSaving(50);
  print(user1.saving);
  print(user1.canBuyAHouse);
  user1.addSaving(10000);
  print(user1.saving);
  print(user1.canBuyAHouse);
}

// Output:
// 100
// 150
// false
// 10150
// true

ဒီမှာ ကြည့်မယ်ဆို canBuyAHouse ဆိုတဲ့ boolean value က _saving ပေါ် မှီခိုနေတာပဲ​ ဖြစ်ပါတယ်။​ ဒီတော့ canBuyAHouse ကို ဘယ်သူ့ကိုမှ ပြောင်းလဲခွင့်မပေးဘဲ _saving ပေါ်ကိုပဲ မှီခိုစေချင်တဲ့အခါမျိုးမှာလဲ getter ကို သုံးရပါတယ်။

ဆက်ပြီးတော့ setter အကြောင်း ဆက်ပြီး ဆွေးနွေးသွားပါမယ်။ ဒါကလဲ properties တွေကို update လုပ်လို့ရတဲ့ နောက် တနည်းပဲ ဖြစ်ပါတယ်။ သူ့ကိုတော့ value တွေ သတ်မှတ်တဲ့အခါ အပိုဆောင်း logic တွေ ထည့်ချင်တဲ့အခါ အသုံးပြုနိုင်ပါတယ်။

// user.dart
class User {
  User(this.name, this.email);

  final String name;
  final String email;

  int _saving = 100;
  int get saving => _saving;

  int? _age;
  int? get age => _age;
  set age(int newValue) {
    if (newValue < 0) {
      throw ArgumentError('Age cannot be negative');
    }
    _age = newValue;
  }

  void addSaving(int amount) {
    if (amount > 0) {
      _saving += amount;
    }
  }
}
// main.dart
import 'user.dart';

void main() {
  final user = User('Aung Aung', 'aung@aung.com');
  user.age = 10; // output: 10
  print(user.age);

  user.age = -10; // Exception
  print(user.age);
}

အပေါ်မှာ ပြထားသလိုမျိုး properties value တွေ သတ်မှတ်တဲ့အခါ တခြား logic တွေ ထည့်ရေးပြီးမှ set လုပ်တာမျိုး ဖြစ်ပါတယ်။ ကိုယ်မလိုချင်တဲ့ value ဝင်လာပြီဆို fallback value တခုခုကို သတ်မှတ်ပေးလိုက်တာ ဖြစ်ဖြစ်၊ error အနေနဲ့ ပြန်ပေးလိုက်တာပဲ ဖြစ်ဖြစ် လုပ်လို့ရပါတယ်။

Overriding methods

Class တခု/ Object တခု ကြေငြာလိုက်ပြီဆိုတာနဲ့ သူ့မှာ dart language က သတ်မှတ်ပေးထားတဲ့ အစွမ်းအစတချို့ ရှိပြီးသား ဖြစ်သွားပါတယ်။

ဒီမှာ ကြည့်မယ်ဆို user1 ဆိုတာ အပေါ်မှာ User class ကနေ တည်ဆောက်ထားတဲ့ object တခုပဲ ဖြစ်ပါတယ်။ အပေါ်မှာ ရေးမထားပေမယ့် သုံးလို့ရနေတဲ့ method တချို့ properties တချို့ကို တွေ့နေရပါလိမ့်မယ်။​ hashCode, runtimeType, toString() စသဖြင့် အပေါ်မှာ ရေးမထားပေမယ့် သုံးလို့ရနေတာတွေ မြင်ရပါလိမ့်မယ်။ ဒါတွေကိုတော့ dart language က သတ်မှတ်ပေးထားတာပဲ ဖြစ်ပါတယ်။

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1.hashCode);
  print(user1.runtimeType);
  print(user1.toString());
  print(user1);
}

// Output:
// 624901507 <- always changing
// User
// Instance of 'User'
// Instance of 'User'

toString() ဆိုတဲ့ method က return ပြန်လာတာကို ကြည့်မယ်ဆို object ကို print လုပ်လိုက်တာနဲ့ တူနေတာကို တွေ့ရမှာပဲ ဖြစ်ပါတယ်။​ print(user1) လို့ ရေးလိုက်တာက print(user1.toString()) ကို သွားပြီးတော့ ခေါ်ပေးနေလို့ပဲ ဖြစ်ပါတယ်။ တကယ်လို့ ကိုယ်လိုချင်တဲ့အတိုင်း ပေါ်စေချင်ရင်တော့ override လုပ်လို့ရပါတယ်။

class User {
  User(this.name, this.email);

  final String name;
  final String email;

  int _saving = 100;
  int get saving => _saving;

  ...

  @override
  String toString() {
    return 'User(name: $name, age: $age, email: $email)';
  }
}
import 'user.dart';

void main() {
  final user1 = User('Aung Aung', 'aung@aung.com');
  print(user1.hashCode);
  print(user1.runtimeType);
  print(user1.toString());
  print(user1);
}

// Output:
520568224
User
User(name: Aung Aung, age: null, email: aung@aung.com)
User(name: Aung Aung, age: null, email: aung@aung.com)

ဒီ override လုပ်တာတွေအကြောင်း လာမယ့်နေ့တွေမှာ inheritance အကြောင်း ပြောတဲ့အခါ ဆက်လက်ပြီးတော့ ဆွေးနွေးပေးသွားပါမယ်။​ ဒီနေ့တော့ dart မှာ ဘယ်လို override လုပ်လို့ရသလဲဆိုတာ သိရင် လုံလောက်ပါပြီ။ ဒီလို override လုပ်တာတွေကို Flutter app တွေ စရေးတဲ့အခါ သုံးဖို့ လိုလာမှာပါ။


ဒီနေ့ Day 7 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။