দৃষ্টিনন্দন, ক্রস-প্ল্যাটফর্ম মোবাইল অ্যাপ্লিকেশন তৈরির জন্য ফ্লাটার (Flutter) একটি শক্তিশালী আর স্বনামধন্য ফ্রেমওয়ার্ক, যেটি ডার্ট (Dart) প্রোগ্রামিং ল্যাঙ্গুয়েজ ব্যবহার করে।

এর মানে হচ্ছে ফ্লাটার ব্যবহার করে একটা সিঙ্গেল কোডবেজের মধ্যে অ্যান্ড্রয়েড ও আইওএসের জন্য অ্যাপ বানানো সম্ভব।

ফ্লাটার নিয়ে এত উন্মাদনা দেখে বোঝাই যাচ্ছে এটা শেখা কত দামি একটা স্কিল এবং সন্তোষজনক প্রচেষ্ঠা। তবে, ফ্লাটার শেখার পথটা একটু অস্পষ্ট যেহেতু এটায় ব্যবহৃত ভাষাটি নতুন।

  • এর ভাষাটি প্রতিনিয়তই আপডেট হচ্ছে (যার কারণে কয়েক মাস আগের টিউটোরিয়ালও এখন পুরোনো হয়ে গিয়েছে)
  • পাইথন বা অন্যান্য প্রোগ্রামিং ভাষার যেমন সাজানো-গোছানো বই রয়েছে তেমন বই কিংবা রিসোর্সের যথেষ্ট অভাব এই ভাষার ক্ষেত্রে

এই গাইডটি একইসাথে টিউটোরিয়াল, উদাহরণ (স্ক্রিনশটসহ), রিসোর্স, আর এমনকি উদাহরণ হিসেবে একটা প্রজেক্টও নিয়ে এসেছে, যাতে করে আপনি দ্রুত ও সহজে ফ্লাটার শিখতে পারেন। আপনি সম্পূর্ণ বিগেনার কিংবা ইন্টারমেডিয়েট বা অ্যাডভান্স প্রোগ্রামার যেটাই হন না কেন, এই গাইডটি আপনি সহজেই কাজে লাগাতে পারবেন। আশা করি এটা আপনাদের কাজে আসবে।

শুরু করা যাক

ডার্ট এবং ফ্লাটার শুরুর আগে, অ্যাপের কোড লেখার জন্য আমাদের প্রথমে প্রোগ্রামিং এনভায়রমেন্ট তৈরি করে নিতে হবে। 

যে দুটো প্রধান IDE ডার্ট এবং ফ্লাটারের সবচেয়ে বেশি ফিচারের সুবিধা দেয় সেগুলো হল Visual Studio Code (VS Code) এবং Android Studio। এটা আপনার সিদ্ধান্ত আপনি কোনটা ব্যবহার করবেন, তবে আমার কাছে ভিএস কোডটা বেশি পছন্দ কারণ এটা দেখতেও অসাধারণ।

আপনার যদি coc বা native Isp এবং ফ্লাটারের এক্সটেনশনগুলো ইন্সটল করা থাকে, তাহলে চাইলে আপনি Vimও ব্যবহার করতে পারেন।

সঠিক IDE বেছে নেয়া খুবই গুরুত্বপূর্ণ কারণ এর মাধ্যমে আপনি ডার্ট প্রোগ্রামিং ল্যাঙ্গুয়েজের প্রোভাইডেড ফিচারগুলো পাবেন। একবার IDE বা টেক্সট এডিটর পেয়ে গেলে আপনাকে এবার Dart Extension এবং Flutter Extension ইন্সটল করতে হবে। এই এক্সটেনশনগুলোতে আমাদের IDE/Text Editor খুবই নিখুঁত এরোর চেকিং, টাইপ চেকিং (type checking), নাল সেফটি চেক (null safety checks), আর ফরম্যাটিং করবে, যাতে করে আমাদের ডেভেলপার জীবন হবে আরও সহজ।

এনভায়রনমেন্ট সাজানো শেষ হলে, এবার সামনে যাওয়ার পালা!

Dart শেখা

ফ্লাটার ফ্রেমওয়ার্কের মেরুদণ্ড হচ্ছে ডার্ট (Dart), যা গুগলের তৈরি একটি ল্যাঙ্গুয়েজ। এই ভাষা ফ্লাটার ফ্রেমওয়ার্কে অ্যাপ বানাতে কাজে লাগবে।

ডার্টের বেসিক কিছু অংশ নিয়ে আলোচনা করা যাক।

আউটলাইন:

  • ভ্যারিয়েবল (variables)
  • ফাংশন (functions)
  • কন্ডিশনাল (conditionals)
  • লুপ (loops)
  • ক্লাস, অবজেক্ট ও কন্সট্রাক্টর (Classes, Objects, and Constructors)
  • অন্যান্য ডার্ট রিসোর্স

ভ্যারিয়েবলস (Variables)

ডার্টের ভ্যারিয়েবলগুলো টাইপ-চেকড, যার মানে হল প্রত্যেকটি ভ্যারিয়েবলকে স্পেসিফিক টাইপ দ্বারা ডিক্লেয়ার করতেই হবে, আর ওই টাইপটাকে অবশ্যই পুরো প্রোগ্রামে আপনি যে ভ্যারিয়েবল অ্যাসাইন করেছেন তার সাথে ম্যাচ করতে হবে।

কিছু বেসিক টাইপ আর উদাহরণ দেখা যাক:

String foo = 'foo';

int bar = 0;

double foobar= 12.454;

bool isCool = true;

List<String> foobarList = ['foo', 'bar'];

Dictionariesকে (যেই ডেটাটাইপ Key এবং valueদের map করে) ‘Map’ টাইপ দিয়ে ডার্টে স্পেসিফাই করা হয়। আপনাকে নিচের মত করে key type ও value type নির্ধারণ করে দিতে হবে:

Map<String, int> grades = {

  'John': 99,

  'Doe': 30,

};

আপনি যদি একই ভ্যারিয়েবলে ইনকমপ্যাটিবল টাইপ অ্যাসাইন করেন তাহলে এরোর পাবেন।

String errorExample = 'foo';

errorExample = 2; // ERROR

আপনি চাইলে ‘var’ আর ‘dynamic’ ব্যবহার করে একটি ভ্যারিয়েবল টাইপ ডায়নামিক করতে পারেন, তবে এটা সাধারণত খুব একটা ভালো সিদ্ধান্ত নয়, কারণ এর ফলে পরবর্তীতে প্রচুর এরোর আসতে পারে।

এছাড়াও, ডার্টের একটা ইউনিক ‘final’ আর ‘const’ অপারেটর আছে যেটা ব্যবহার করে ভ্যারিয়েবল ডিক্লেয়ার করা যায়। ‘final’ সাধারণত ব্যবহৃত হয় এমন সব ভ্যারিয়েবল ডিক্লেয়ার করার জন্য যেগুলো পরে আর চেঞ্জ করা হবে না। উদাহরণ হিসেবে, যদি কোনো ইউজার তার নাম ইনপুট করে আর আমরা সেটাকে একটা ভ্যারিয়েবলে সেভ করে রাখি, তাহলে জানা কথা যে এই ভ্যারিয়েবল (তার নাম) পরিবর্তিত হবে না, আর সেক্ষেত্রে আমরা এটাকে এভাবে ইনিশিয়ালাইজ/ডিক্লেয়ার করতে পারি:

final String name;

‘const’ কিওয়ার্ডটা আরেকটু স্পেসিফিক কাজে লাগে– এটা কেবল কম্পাইল-টাইম থেকে ভ্যারিয়েবলকে কনস্ট্যান্ট বানায়। এটা নিয়ে আমরা পরে কাজ করবো, আপাতত এটা নিয়ে ভাববার দরকার নেই।

ফাংশনস (Functions)

ফাংশন ডিক্লেয়ার করা হয় return typeকে স্পেসিফাই করা, ফাংশনের নাম দেয়া আর প্যারেনথিসিসের (parentheses) মধ্যে প্যারামিটার সেট করার মাধ্যমে। Void দিয়ে রিটার্ন টাইপকে স্পেসিফাই করা হয় যদি কিছুই রিটার্ন না করা হয়।

// doesn't return anything but still executes some code

void main() {

  print('hello world');

}

// prints 'hello' but also returns the string 'complete'

String hello(int reps) {

  for (int i = 0; i < reps; i++) {

    print('hello');

  }

  return 'complete';

}

// returns a list of strings (List<String>)

List<String> people() {

  return ['John', 'Doe'];

}

অ্যাসিনক্রোনাস (Asynchronous) ফাংশন হচ্ছে এমন সব ফাংশন যেগুলো দিয়ে একই সময়ে বিভিন্ন কমান্ড এক্সিকিউট করা যায় অ্যাসিনক্রোনাসভাবে (asynchronously)।

এটা কীভাবে কার্যকর হবে সেটা বুঝানোর জন্য একটা উদাহরণ হতে পারে API কল করা (মূলত, ওয়েব থেকে অন্যদের প্রোগ্রাম করা প্রয়োজনীয় কিছু ইনফরমেশন উদ্ধার করা)। যদি আমাদের ফাংশন একটা এপিআই কল করে আর ওই এপিআই’র রেসপন্সে একটা ভ্যারিয়েবল অ্যাসাইন করে, কিন্তু আমাদের পুরো অ্যাপটা ওই ফাংশনটার এক্সিকিউশন শেষ করার জন্য অপেক্ষা করতে থাকে যাতে করে সে কিছু একটা করতে পারে, তাহলে সেটা খুব একটা কার্যকর নয়। আমরা যদি এই ফাংশনটাকে অ্যাসিনক্রোনাস করে দেই, ফাংশনের এপিআই কল করার মাধ্যমে তাহলে অ্যাপটা ওই ফাংশন এক্সিকিউট করার সময় অন্য ফাংশন এক্সিকিউশন বা অন্য কোনো কাজও করতে পারবে।

একটা অ্যাসিনক্রোনাস ফাংশনের মধ্যে, আমাদের যদি কখনো কোনো কোড শেষ হবার জন্য অপেক্ষা করা লাগে যাতে আমরা পরের কাজে আগাতে পারি, তাহলে আমরা ওই কোডে আগে শুধু ‘await’ কিওয়ার্ডটা লিখে দিলেই হবে।

ডার্টে অ্যাসিনক্রোনাস ফাংশনের জন্য, parentheses আর curly braces এর মাঝে ‘async’ কিওয়ার্ড যুক্ত করে, ‘Future<[return type]>’ দিয়ে return type এনক্লোজ করে দিতে হবে।

Future<String> retrieveData() async {

  String response = await someAPICall(); // assuming the api call returns a string

  return response;

}

কন্ডিশনালস (Conditionals)

If statement এভাবে লেখা হয়ে থাকে:

bool someCondition = true;

if (someCondition) {

  print('someCondition is true');

} else {

  print('someCondition is false');

}

লুপস (Loops)

For লুপ সব প্রোগ্রামিং ভাষাতেই সমানভাবে গুরুত্বপূর্ণ, এবং ডার্টে এদেরকে ইমপ্লেমেন্ট করার কিছু উপায় রয়েছে:

List words = ['hello', 'world', '!'];

// 1st way

// declare an int i, increment it by 1 until it is no longer 

// less than words.length (3 in this case)

for (int i = 0; i < words.length; i++) {

  print(words[i]);

// 2nd way

// for each element in word, dart will take that element (in this case, a string, word)

// and will allow you to execute code using that element (here, we just print it out)

// the rocket notation (=>) allows us to write only a single statement to execute

// on the right side. otherwise, we would do (word) { print('hey!'); print(word); }

words.forEach((word) => print(word));

// 3rd way

// very similar to the 2nd way but a different syntax

for (String word in words) {

  print(word);

}

দারুণ!

ক্লাস, অবজেক্ট ও কন্সট্রাক্টর (Classes, Objects, and Constructors)

ক্লাস হচ্ছে মূলত ব্লুপ্রিন্ট, অথবা টেমপ্লেট, যা দিয়ে আপনি আপনার প্রোগ্রামে নিজের ডেটাটাইপ বানিয়ে নিতে পারেন। উদাহরণ হিসেবে, আপনি যদি গাড়ির সম্পর্কে প্রোগ্রাম লিখতে চান, স্বাভাবিকভাবেই String, int, bool ইত্যাদি প্রচলিত ডেটাটাইপ দিয়ে আপনি সেটা লিখতে পারবেন না।

ক্লাস ব্যবহার করে আমরা চাইলে আমাদের নিজেদের ডেটাটাইপ অথবা মডেল বানিয়ে নিতে পারি একটা ক্লাস আর তার অ্যাট্রিবিউটস (attributes) ডিফাইন করে। এসকল অ্যাট্রিবিউটস হচ্ছে প্রচলিত ডেটাটাইপ, তবে রেজাল্টিং ক্লাসটা আমাদের আরও বেশি জটিল কোড সহজে লিখতে সাহায্য করে।

আমাদের যখন একটা ক্লাসের একটা স্পেসিফিক ইন্সট্যান্স (instance) বানাতে হয় (অর্থাৎ যদি আমরা একটা গাড়ি বানানোর জন্য ব্লুপ্রিন্টের ব্যবহার করতে চাই), আমরা আমাদের প্রয়োজনীয় অ্যাট্রিবিউটগুলোর সাথে সেটাকে ‘instantiate’ করি, আর ফলাফল হিসেবে যা আসে তার নাম অবজেক্ট (Object)।

অবজেক্ট হচ্ছে একটা ক্লাসের একটা স্পেসিফিক ইন্সট্যান্স– ক্লাস যদি ‘Car’ হয়, তাহলে অবজেক্ট হবে Tesla Model S বা এরকম কিছু একটা। ধরা যাক আরেকটা অবজেক্ট বানাতে চাচ্ছি, সেটার নাম হতে পারে Lamborghini Aventador। আপনি একটা ক্লাসের জন্য যত ইচ্ছা অবজেক্ট বানাতে পারবেন।

নিচের মত করে ক্লাস বানানো আর ব্যবহার করা যায়। দেখুন কীভাবে instantiated অবজেক্টের টাইপ ডিক্লেয়ার করা হয়েছে, আর কীভাবে অবজেক্টটাকে instantiated করা হয়েছে।

class Car {

  String name;

  int price;

  bool isMadeByElonMusk;

}

void main() {

  // type 'Car'

  Car tesla = Car(); // class is instantiated with parentheses, ()

  // populating each of the attributes we defined in the above class

  tesla.name = 'Model S';

  tesla.price = 50000;

  tesla.isMadeByElonMusk = true;

}

এখন, একটা অবজেক্ট বানানোর পর ম্যানুয়ালি তার সবগুলো অ্যাট্রিবিউট সেট করা খুবই ক্লান্তিকর আর ব্যয়বহুল কাজ হবে। tesla.name, tesla.price,..., … এভাবে কাজ করা মোটেই আমাদের জন্য ভালো না।

আর এখানেই এসেছে কন্সট্রাকটরের (constructors) কাজ। কনস্ট্রাক্টর দিয়ে আমরা আমাদের ক্লাসে একটা ফাংশন ডিক্লেয়ার করতে পারি, যেটা সব অ্যাট্রিবিউট সেট করার কাজ করে দেবে। এরপর একটা ক্লাসকে instantiate করতে হলে আমাদের শুধু প্যারামিটার পাস (pass) করলেই চলবে। নিচের উদাহরণ দেখুন।

ক্লাসের সাথে সম্পর্কিত আরেকটা গুরুত্বপূর্ণ বিষয় হচ্ছে মেথড (methods)।

মেথড হচ্ছে আমাদের ক্লাসের মধ্যে ডিফাইনড ফাংশন, যা আমাদের ক্লাসের সাথে সম্পর্কিত ডাটা ও স্পেশাল অপারেশন নিয়ে কাজ করে। উদাহরণস্বরূপ, আমরা চেক করতে চাইতে পারি আমাদের গাড়িটা দামি কি না। সেটা করার জন্য আমরা একটা isExpensive() মেথড ডিফাইন করলেই হবে।

মনে রাখতে হবে, একটা নির্দিষ্ট ক্লাসে ডিফাইনড মেথড সেই অবজেক্টে কল করা হয় তার সাথে অ্যাসোসিয়েটেড সকল অ্যাট্রিবিউটসের অ্যাক্সেস ওই মেথডের কাছে থাকে। যদি isExpensive() মেথড আমাদের tesla অবজেক্টে কল করা হয়, তাহলে এই মেথডের tesla.price ভ্যালুর উপরেও অ্যাক্সেস থাকবে।

// define a class named car

class Car {

  // define a constructor that takes in a String name, int price and bool isMadeByElonMusk

  Car(String name, int price, bool isMadeByElonMusk) {

    // set all the object's attributes equal to the inputs passed in

    this.name = name;

    this.price = price;

    this.isMadeByElonMusk = isMadeByElonMusk;

  }

  // defining the attributes of the class

  String name;

  int price;

  bool isMadeByElonMusk;

  // defining the method 'isExpensive' that returns type bool

  bool isExpensive() {

    // 'this.price' refers specifically to the price value of the object it was called upon

    if (this.price > 30000) {

      return true;

    } else {

      return false;

    }

  }

}

void main() {

  // instantiate the class by using its constructor, passing in the expected parameters

  // we defined already

  Car tesla = Car('Model S', 50000, true);

  // returns true by using the Car class's method, isExpensive, because tesla.price = 50,000

  bool isCarExpensive = tesla.isExpensive();

}

Flutter UI শেখা

এখন যেহেতু আপনি ডার্ট সম্পর্কে বেসিক ধারণা রাখেন, চলুন ঘুরে আসা যাক ফ্লাটার ফ্রেমওয়ার্ক।

আউটলাইন:

  • ইন্সটলেশন (Installation)
  • উইজেটস (Widgets)
  • লেআউট (Layout)
  • ফরম্যাটিং (Formatting)
  • স্টেটলেস উইজেটস (Stateless Widgets)
  • স্টেটফুল উইজেটস (Stateful Widgets)
  • নাল সেফটি (Null Safety)
  • আরও ফ্লাটার রিসোর্স (More Flutter Resources)

প্রথমে আমরা ফ্লাটারের জন্য একটা প্রোগ্রামিং এনভায়রনমেন্ট ইন্সটল করব।

ইন্সটলেশন (Installation)

ওএসের ভিত্তিতে কিছু ইউজারের কাছে ইন্সটল প্রক্রিয়াটা একটু ঝামেলার মনে হতে পারে, তবে এটা তেমন একটা খারাপও না। অনলাইন থেকে সাহায্য নিয়ে ফ্লাটার এবং আপনার ওএসের জন্য অন্যান্য প্রয়োজনীয় টুলস ইন্সটল করে ফেলুন, আপনার আরও লাগবে একটা এমুলেটর/ভার্চুয়াল ফোন যেটা দিয়ে আপনি আপনার অ্যাপ টেস্ট করবেন।

একবার ইন্সটল শেষ হয়ে গেলে, নিচের কমান্ডটা রান করে আপনার এনভায়রনমেন্ট ঠিক আছে কি না বুঝে নিন।

$ flutter doctor

এবার নিচের কমান্ডটা চালিয়ে একটা ফ্লাটার প্রজেক্ট বানান।

$ flutter create <project_name>

ফোল্ডার স্ট্রাকচারটা দেখতে কিছুটা নিচের মত হবে। আমরা আমাদের সব কোড ‘lib’ ফোল্ডারে রাখব, আর পরবর্তীতে বাকি ফোল্ডার নিয়ে আলাপ করা হবে। আপাতত শুধু কোডগুলো অনুসরণ করতে থাকুন, এখনই প্রজেক্ট সেটআপ নিয়ে ভাববার দরকার নেই।

দারুণ! এবার যেহেতু আমাদের ফ্লাটার এনভায়রনমেন্ট তৈরি হয়ে গেছে, এবার ফ্লাটার দিয়ে কীভাবে অ্যাপ সাজানো হয়ে সেটা দেখে আসা যাক।

উইজেটস (Widgets)

ফ্লাটার অ্যাপ উইজেট নামক একটা জিনিস দিয়ে বিল্ড করা হয়। আপনি যদি frontend javascript framework সম্পর্কে ধারণা রাখেন, তাহলে বুঝবেন এগুলো কম্পোনেন্টসের সদৃশ, তবে এগুলো ফ্রেমওয়ার্কের ভেতরে আগে থেকেই দেয়া থাকে। উইজেটক্কে HTML elements যেমন ‘p’ (paragraph এর জন্য), ‘h1’ (header 1 এর জন্য) ইত্যাদির সাথে তুলনা করা যায়।

উইজেটস কার্যত অ্যাপের বেসিক এলেমেন্টস বা বিল্ডিং ব্লকস যেগুলো ফ্লাটার আমাদের জন্যে বানিয়ে রেখেছে। এগুলোতে নির্দিষ্ট প্রপার্টিজ বা প্যারামিটার দেয়া থাকে যেগুলো ফ্লাটার আমাদের কাছ থেকে আশা করে। উদাহরণস্বরূপ, অ্যাপ স্ক্রিনে টেক্সট দেখানো জন্য আমরা Text widget নামক একটা উইজেট ব্যবহার করি, অনেকটা এইচটিএমএলের ‘p’ এলেমেন্টের মত, যেটাতে একটা স্ট্রিং (string) পাস করতে হয়। দেখা যাক এটা কোডে এবং অ্যাপে কেমন দেখা যায়।

 // displays the text on the app screen

Text('Some string here');

ফ্লাটার লাইব্রেরিতে একটা প্রিবিল্ট উইজেটও আছে, যার নাম ElevatedButton (একটা Material theme বাটন) যেটা একটা onPressed প্রপার্টি (যাতে বাটন প্রেস করে কোডটাকে এক্সিকিউট করা যায়) আর একটা চাইল্ড প্রপার্টি (Text উইজেট যেটা বাটনের টেক্সট দেখায়) নেয়। আরেকটা হচ্ছে TextField যেটা ইনপুট টেক্সট হ্যান্ডেল করে।

লেআউট (Layout)

টেক্সট ডিসপ্লে বা বাটন প্রেস করার থেকে জটিলতর কাজ করার ক্ষেত্রেও উইজেট কাজে লাগে। ফ্লাটার যেভাবে অ্যাপের ভেতরে লেআউট তৈরি করে সেটাও উইজেট দিয়ে করা হয়। যেমন, এইচটিএমএলের ‘div’ এলেমেন্টের সদৃশ Container উইজেট দিয়ে একটা কন্টেইনারের ভেতরে আরেকটা চাইল্ড উইজেট র‍্যাপ (wrap) করা যায়, যাতে করে প্যাডিং (padding), মার্জিন (margins), রঙ (colors) যুক্ত করা যায়। ভেতরের উইজেটটাকে সাধারণত চাইল্ড (child) উইজেট বলা হয়, আর কন্টেইনারটাকে বলা হয় প্যারেন্ট (parent)। বিষয়টা সেন্স মেক করে, তাই না?

Container(

  child: Text('hello!' )

),

আরও কিছু গুরুত্বপূর্ণ লেআউট উইজেট হচ্ছে রো (row) আর কলাম (column) উইজেট। এই উইজেটগুলো আপনাকে উইজেটদের লম্বালম্বি (vertically) বা পাশাপাশি (horizontally) স্ট্যাক (stack) করতে সাহায্য করে। এই উইজেটগুলোতে চাইল্ড উইজেটগুলোর লিস্ট পাস করতে হয়। নিচের মত করে এরা কাজ করে।

Row(

  children: [

    // in the app, child widgets of a row are laid out left to right like so

    Text('left text'),

    Text('middle text'),

    Text('right text'),

  ],

)

Column(

  children: [

    // child widgets of a column are laid out top to bottom like so

    Text('top text'),

    Text('middle text'),

    Text('bottom text'),

  ],

)

Row:

Column:

কিছু লেআউট উইজেট আমরা স্ক্রিনে যেসব অন্য উইজেট লাগাই সেসবকে র‍্যাপ করে থাকে। যেমন, Scaffold উইজেট মূলত ব্যবহার করা হয় স্ক্রিনকে আমাদের জন্য ‘scaffold’ বা লেআউট করার জন্য,আর এটাকে ব্যবহার করা হয় এভাবে:

Scaffold(

  body: Container(

    child: Text('hi!'),

  ),

)

নোট: Scaffold পেজে বেসিক স্টাইলিং অ্যাপ্লাই করে, যেমন ব্যাকগ্রাউন্ড রঙ সাদা করা, টেক্সট কালো করা, ডিফল্ট ফন্ট সাইজ সেট করা ইত্যাদি। Scaffold ছাড়া আপনি আসলে ফাঁকা স্ক্রিনে pure আর raw উইজেটস রেন্ডার করছেন যেটার আসলে কোনো ডাটা বা স্টাইল নেই।

Scaffold সহ:

Scaffold ছাড়া:

আরেকটা কার্যকর উইজেট হচ্ছে ListView.builder । ListView.builder উইজেট দুটি আর্গুমেন্ট (arguments) নেয়- itemCount (কতগুলো লিস্ট আইটেম বিল্ড করতে হবে) আর itemBuilder (যা বানানো হয়েছে তাকে রিটার্ন করার জন্য)। নিচের মত হবে কোডটা।

List<String> people = ['John', 'Doe', 'Jane'];

ListView.builder(

  itemCount: people.length, // 3

  // index is the current index that the builder is iterating on. think of it like the 

  // 'i' in the for loop,  for (int i = 0; i < whatever; i++) 

  itemBuilder: (context, index) {

    return Container(

      child: Text(people[index]),

    );

  },

)

এগুলো কেমন দেখা যায় তা আমরা পরে স্ক্রিনশটে দেখব।

প্রপার্টি/প্যারামিটার (properties/parameters)

ফ্লাটারের বানানো প্রত্যেকটা উইজেটে নির্দিষ্ট সংখ্যক প্যারামিটার বা আর্গুমেন্ট পাস করানো যায়। পূর্বে যেমন দেখেছিলাম, Container উইজেট একটা ‘child’ প্রপার্টি নেয়, আর এটা একটা ‘color’ প্রপার্টিও নিতে পারে যেটা দিয়ে কন্টেইনারের ব্যাকগ্রাউন্ড কালার নির্ধারণ করা যায়।

প্রত্যেক উইজেটের নির্দিষ্ট সংখ্যক প্যারামিটার থাকবে যেটা আপনি ফ্লাটার ডকুমেন্টেশন পড়ে কিংবা আপনার IDE/Text Editor এর IntelliSense ব্যবহার করে শিখতে পারবেন। উদাহরণস্বরূপ, VS Code এ আপনি Ctrl+Space চেপে অথবা একটা উইজেটে লেখার পর তার উপর মাউস হোভার (hover) করলে দেখতে পাবেন কী কী প্রপার্টিস ওখানে ব্যবহার করা যাবে।

সাধারণত, আপনি চাইলে প্যারামিটার দিয়ে উইজেটে আপনার সব স্টাইল পাস করতে পারবেন। 

এসব প্যারামিটারের অনেকগুলো কেবল নির্দিষ্ট ধরণের টাইপ বা অবজেক্ট গ্রহণ করে। কন্টেইনারের ‘child’ প্রপার্টি কেবল আরেকটা ফ্লাটার উইজেটই অ্যাক্সেপ্ট করবে। ‘color’ প্রপার্টি শুধুমাত্র সেসকল অবজেক্টই অ্যাক্সেপ্ট করবে যেগুলো ফ্লাটার আগে থেকে নির্ধারণ করে রেখেছে (যেমন Colors.black, Colors.blue ইত্যাদি)। অথবা যেসকল অবজেক্ট আগে থেকে নির্দিষ্টভাবে ইন্সট্যানশিয়েট করা আছে (Color(0xFFFFFFFF), hex codes ব্যবহার করে ব্যবহার করা)।

Text উইজেটে আমরা টেক্সটকে স্টাইল করতে পারি একটা ‘TextStyle’ অবজেক্ট ইন্সট্যানশিয়েট করার মাধ্যমে, টেক্সট উইজেটের ‘style’ প্রপার্টির মধ্যে পাস করার মাধ্যমে। খেয়াল করুন কীভাবে color প্রপার্টি ফ্লাটার লাইব্রেরি থেকে Colors.purple নামক একটা Color অবজেক্ট নেয়। এছাড়াও, fontWeight প্রপার্টি একটা FontWeight অবজেক্ট নেয়।

Text(

  'text to display',

  style: TextStyle(

    // font color

    color: Colors.purple,

    // font size

    fontSize: 16.0,

    // font weight

    fontWeight: FontWeight.bold,

  ),

)

কন্টেইনার উইজেটে স্টাইলিং করার জন্য আমরা ‘decoration’ প্রপার্টি ব্যবহার করি আর আমাদের স্টাইলের সাথে ইন্সট্যানশিয়েট করা'BoxDecoration' অবজেক্ট পাস করি।

Container(

  // styling the container

  decoration: BoxDecoration(

    // you can define the background color in this object instead

    color: Colors.blue,

    // border radius - valid arguments must be of class BorderRadius

    borderRadius: BorderRadius.circular(20.0),

  ),

  height: 50.0,

  width: 50.0,

  // margin of the container - argument must be of class EdgeInsets

  margin: EdgeInsets.all(8.0),

  // child element (using the Center widget centers the Text widget)

  child: Center(

    Text('hello!')

  ),

)

Column উইজেটে আপনার লম্বালম্বিভাবে আপনার অবজেক্টগুলো পেজের মাঝ বরাবর অ্যালাইন করার দরকার হতে পারে। আপনি সেটা করতে পারেন Column উইজেটের ‘mainAxisAlignment’ প্রপার্টি ব্যবহার করে (কলামের মূল অক্ষ বা axis লম্বালম্বি)। এছাড়াও আপনি টেক্সটকে কলামের মধ্যে হরাইজন্টালি অ্যালাইন করতে ‘crossAxisAlignment’ প্রপার্টি ব্যবহার করতে পারেন।

Column(

  // argument passed in must use the MainAxisAlignment object 

  // can you start to see the practices and conventions Flutter everywhere?

  mainAxisAlignment: MainAxisAlignment.center,

  children: [

    Text('top text'),

    Text('center text'), 

    Text('bottom text'),

  ],

)

MainAxisAlignment.center ছাড়া:

MainAxisAlignment.center সহ (যেমনটা কোডে করা হয়েছে):

Column এর অন্যান্য প্রপার্টির মধ্যে রয়েছে crossAxisAlignment, mainAxisSize, এবং আরও অনেক। সম্ভাবনা আছে যে, যদি আপনার মনে হয় আপনার উইজেটস স্টাইল করা প্রয়োজন, আপনি তাহলে শুধু গুগল করে প্রপার্টিগুলো বের করে নিতে পারেন, অথবা কোন কাজে কী প্রপার্টি ব্যবহার করতে হবে এটা জেনে নিতে পারেন।

যে পরিমাণ প্রপার্টি আর ক্লাস আপনার লাগবে, তা দেখে মনে হবে শেখার কাজটা ভয়ানক কঠিন, তবে ধীরে ধীরে আপনি এ ব্যাপারে ভালো ধারণা লাভ করবেন (তাছাড়া গুগল তো আছেই)।

ফরম্যাটিং (Formatting)

এখন আপনি মনে করতে পারেন সব জায়গায় এইসব কমা আর নিউ লাইন দিয়ে বুঝাচ্ছেটা কী? যে কারণে আমি এই কোডগুলো এভাবে লিখেছি তার কারণ হচ্ছে আপনার IDE আপনার কোড কীভাবে ফরম্যাট করবে। IDE কাজটি করে trailing কমা ডিটেক্ট করা এবং সাথে সাথে নতুন লাইন যুক্ত করার মাধ্যমে।

ফরম্যাটার মেনে চলার মাধ্যমে আপনি কোডকে নিজ এবং অন্যের জন্য আরও রিডেবল করে তুলতে পারবেন। এখানে একটা উদাহরণ দেখানো হল।

// weird code you might write totally without a formatter

// not very good, is it?

Column(children:[

  Container

  (child: Text

  (

    'hi!'

  )),

  Text(

    'hi'

  )

]

)

// code you might write with the formatter, but without adhering to the formatting guidelines

Column(children: [

  Container(color: Color(0xFFFFFF), child: Text('hey there'), margin: EdgeInsets.all(5.0), padding: EdgeInsets.all(5.0)),

  Text('hi')])

// code you write with the formatter, that adheres to the formatter

Column(

  children: [

    Container(

      color: Color(0xFFFFFF),

      child: Text('hey there'),

      margin: EdgeInsets.all(5.0),// add a trailing comma to the last parameter (margin)

    ), // add a trailing comma to the Widget

    Text('hi'), // add a trailing comma to the last child of the Column

  ], // add a trialing comma to the children parameter

)

আপনি কি আমার সাথে একমত হবেন যে শেষ উদাহরণটা সবচেয়ে বেশি পাঠযোগ্য এবং এভাবে কোড করা সবচেয়ে সহজ (কমেন্টগুলো বাদে)?

শুধু আপনার উইজেট এবং প্যারামিটারে ট্রেইলিং কমা অ্যাড করুন, সেভ চাপুন, আর বাকি কাজ ফরম্যাটার করে দেবে। ধীরে ধীরে আপনি এতে পারদর্শী হয়ে উঠবেন।

স্টেটলেস উইজেট (Stateless Widgets)

স্টেটলেস উইজেট হল মূলত এমন উইজেট যেটি পরিবর্তিৎ হয় না– অর্থাৎ এরা স্ট্যাটিক। স্টেটলেস উইজেটের একটা উদাহরণ হতে পারে একটা পেজ যেখানে ইউএসের স্টেটগুলোর নামের লিস্ট দেখা যাবে। আরও একটি সিম্পল উদাহরণ দেখে আসা যাক যেখানে একটা স্টেটলেস উইজেট বানিয়ে একটা সাদা কন্টেইনার রিটার্ন করা যাবে। নিচে স্টেটলেস উইজেট ডিফাইন করার সিনট্যাক্স দেখানো হল।

class ListOfStates extends StatelessWidget {

  // this is the constructor, but don't worry about it right now

  const ListOfStates({Key? key}) : super(key: key);

  // @override is good practice to tell us that the following method (in this case,

  // the build method) is being overriden from the default build method

  @override

  // this build function returns a Widget

  Widget build(BuildContext context) {

    return Container(color: Color(0xFFFFFFFF));

  }

}

ভালো খবর হচ্ছে বেশিরভাগ IDEতে স্নিপেট (snippet) থাকে যা দিয়ে আপনার জন্য অটোম্যাটিকভাবে স্টেটলেস উইজেট ক্রিয়েট করা যাবে। আপনার IDEতে stless টাইপ করে TAB বা Enter চেপে সব প্রয়োজনীয় কোড জেনারেট করুন।

আপনি যদি আপনার স্টেটলেস উইজেটে প্যারামিটার যোগ করতে চান (যেমন একটা ‘message’ প্যারামিটার যা দিয়ে একটা স্টেটলেস উইজেটে ম্যাসেজ ডিসপ্লে করা সায়), তাহলে আপনার একটা কন্সট্রাক্টর লাগবে, ঠিক যেমনটা করে ক্লাস কন্সট্রাক্ট করা হয়। এভাবে-

class DisplayMessage extends StatelessWidget {

  // add it to the constructor here after the key, as 'required this.<parameter>'

  DisplayMessage({ Key? key, required this.message }) : super(key: key);

  // initialize it as a 'final' variable (it won't change)

  final String message

  @override

  Widget build(BuildContext context) {

    return Container(

      child: Text(message),

    );

  }

}

এই উইজেট এরপর আরেকটা প্যারেন্ট উইজেটে ইন্সট্যানশিয়েট হবে এভাবে-

Scaffold(

  body: Column(

    children: [

      ...

      // instantiating the stateless widget we just created (which is in another file) 

      // with string, the message we want to display

      DisplayMessage(message: 'Hello there!'),

      ...

    ],

  ),

)

স্টেটফুল উইজেট (Stateful Widgets)

স্টেটফুল উইজেট হল এমন উইজেট যা নির্দিষ্ট পরিবর্তনে প্রতিক্রিয়া করে এবং এরপর রিবিল্ট হয়। আমাদের অ্যাপ যদি আমরা ইন্টার‍্যাক্টিভ বানাতে চাই তাহলে এটা কাজে লাগবে। ধরা যাক, আমাদের অ্যাপে একটা কাউন্টার রাখতে চাচ্ছি। যখনই ইউজার একটা ‘+’ বাটন চাপবে, আমরা স্ক্রিনে আমাদের ডিফাইনড একটা ভ্যারিয়েবল ‘count’ এর বৃদ্ধি দেখব। নিচের মত করে কাজটি করা যাবে।

নোট: যখনই আমরা আমাদের স্টেটফুল উইজেটকে যেকোনো পরিবর্তনের প্রতি রিয়্যাক্ট করাতে চাইব (যেখানে ফ্লাটার পেজটাকে রিবিল্ড করবে), আমরা setState(() {}) ব্যবহার করব।

lass DisplayCount extends StatefulWidget {

  const DisplayCount({Key? key}) : super(key: key); 

  @override

  _DisplayCountState createState() => _DisplayCountState();

}

class _DisplayCountState extends State<DisplayCount> {

  // defining a variable, count, inside our widget

  int count = 0;    

  @override

  Widget build(BuildContext context) {

    return Column(

      children: [

        // display the count as a string

        Text(count.toString()),

        ElevatedButton(

          // the text displayed on the button

          child: Text('Click me to add +'),

          // the code that will execute when the button is pressed

          onPressed: () {

            // setState is called to signal to Flutter to rebuild the widget

            // count is incremented by 1, so the widget will be rebuilt with 

            // a new value displayed in the text widget above

            setState(() {

                count += 1;

            });

          },

        ),

      ],

    );

  }

}

স্টেটফুল উইজেটের জন্য IDE Snippetএও আমাদের অ্যাক্সেস রয়েছে। সেটার জন্য লিখুন stful ।

স্টেটফুল উইজেটে কন্সট্রাক্টরও একই জিনিস, তবে তাদের কেবল DisplayCount উইজেটে ডিক্লেয়ার করা হয়, _DisplayCountState এ নয়। _DisplayCountState উইজেট, যেখানে আপনি আপনার কোড লিখবেন, সেখানে আপনি আপনার ভ্যারিয়েবলকে (widget.[variable]) বলতে পারেন।

class DisplayCount extends StatefulWidget {

  const DisplayCount({Key? key, required this.message}) : super(key: key); 

  final String message;

  @override

  _DisplayCountState createState() => _DisplayCountState();

}

class _DisplayCountState extends State<DisplayCount> {

  ...

  @override

  Widget build(BuildContext context) {

    return Column(

      children: [

        // refer to the 'message' attribute defined above as widget.message

        Text(widget.message),

        ...

      ],

    );

  }

  ...

}

স্টেটফুল উইজেট স্টেটলেস উইজেটের মত করেই ইন্সট্যানশিয়েট করা হয়।

স্টেটফুল উইজেট কাজে লাগে বিজনেস লজিক সম্পর্কিত যেকোনো কাজে, ইন্টার‍্যাক্টিভ ফিচারে, এবং ব্যাকএন্ডে ডাটা স্ট্রিম শোনার ক্ষেত্রে, যেগুলো আমরা পরে দেখব।

নাল সেফটি (Null Safety)

বিরক্তিকর নাল এরোর সামলানোর জন্য ফ্লাটারের সাম্প্রতিক ভার্সনে  নাল সেফটি আনা হয়েছে।

মূলত, যদি কোনোকিছু, যেমন একটা স্ট্রিং ডিক্লেয়ার করা হয় এবং সেটার একটা ভ্যালিড ভ্যালু দেয়ার কথা থাকে যেমন ‘Hi!’, সেখানে যদি একটা নাল ভ্যালু অ্যাসাইন করা হয় (মূলত একটা ভ্যালু যা আসলে nothing), সেক্ষেত্রে সব ধরণের সমস্যা শুরু হতে থাকে- কিছু অংশ টেক্সট মিসিং হতে শুরু করে, কিছু অংশ ফাংশনালিটি হারায়, ইত্যাদি।

ফ্লাটারের নাল সেফটি ডেভেলপারদের IDE ফিচারের সাহায্যে এসব ইস্যু ফিক্স করতে সাহায্য করে, যেসব ফিচার ডেভেলপারদের আরও শক্তভাবে নাল চেক করতে বাধ্য করে। এর মানে ডেভেলপারদের দায় পড়ে নাল ভ্যালু ডিক্লেয়ারের কারণে ঘটিত অবস্থা সামলানোর।

নাল সেফটিতে্, ৩টি গুরুত্বপূর্ণ সিম্বল জানতে হবে। ‘?’, ‘!’, এবং ‘??’ সিম্বল।

‘?’

আমরা যদি এমন একটা ভ্যারিয়েবল ডিক্লেয়ার করি যেটা আমরা মনে করি যে কোনোভাবে নাল ভ্যালু দিতে পারে, আমরা সেখানে টাইপ ডিক্লেয়ারেশনের শেষে ‘?’ অপারেটর যুক্ত করে দেই যাতে করে আমরা নিজেরা মনে রাখতে পারি এবং IDEকে বলে রাখতে পারি এখানকার ভ্যারিয়েবলে একটা স্ট্রিক্ট নাল চেকিং করতে। একটা উদাহরণ দেখা যাক।

// initializing a string wih a nullable type and assigning it to the 

// return value of this function, fetchSomeDataOrSomething()

String? response = await fetchSomeDataOrSomething(); 

// in the case that the function returned something null and response has a null value,

// it is now safely accounted for with this conditional statement

if (response != null) {

  print(response);

} else {

  print('error');

}

‘!’ 

যদি আমরা একটা ভ্যারিয়েবলের জন্য একটা নালেবল (nullable) টাইপ ডিক্লেয়ার করি কিন্তু আমাদের জানা থাকে যে এটা নাল হবে না, ভ্যারিয়েবল নামের পরে আমরা তখন ‘!’ অপারেটর ব্যবহার করি। নোট: এটা ব্যবহার উপেক্ষা করার চেষ্টা করুন কারণ এটা IDE এর নাল সেফটি চেক বাইপাস করে ফেলে। 

// fetchSomeData() returns type bool

bool? response = fetchSomeData(); 

// declaring that response will always be a valid value and not null

if (response! == True) { 

  print('function has returned true');

} else {

  print('function has returned false');

}

‘??’

আমরা যখন একটা ভ্যারিয়েবলে একটা ভ্যালু অ্যাসাইন করছি, আমরা চাইলে চেক করতে পারি সেটা নাল কি না এবং সেখানে একটা ভ্যালু অ্যাসাইন করতে পারি। যদি অ্যাসাইনড ভ্যালুটা নাল হয়, তাহলে আমরা ‘??’ অপারেটর লাগাতে পারি এবং যদি সেটা নাল হয় তাহলে একটা ডিফল্ট ভ্যালু ডানপাশে অ্যাড করে দিতে পারি।

String? response = fetchSomething();

// if response is not null, the 'something' variable will take on the value of response'

// if response is null, the 'something' variable with take on the value on the right side

String something = response ?? 'defaultValue';

ফায়ারবেজ (Firebase) শেখা

ফায়ারবেজ মোবাইল এবং ওয়েব অ্যাপ্লিকেশন ডেভেলপের প্ল্যাটফর্ম। ব্যাকএন্ড বা ডাটা নিয়ে কাজ করার জন্য এটা একটা সুবিধাজনক পথ, আপনার নিজের কোনো সার্ভার বা এপিআই বানানো ছাড়াই।

ফ্লাটার এবং ফায়ারবেজ দুটোই যেহেতু গুগলের বানানো (ফায়ারবেজ মূলত তৈরি করা হয়েছিল অ্যাপ বানানো জন্য), এ দুটি একে অপরের ফ্রন্টএন্ড ও ব্যাকএন্ড হিসেবে বেশ ভালো কাজ করে।

বেশিরভাগ প্রজেক্টের মূল ব্যাকএন্ড একটা ডাটাবেজ ব্যবহার করে, যেটা ফায়ারবেজ তার ক্লাউড Firestore database দিয়ে সরবরাহ করে। এই ফায়ারস্টোর ডাটাবেজের মূল কাঠামো খুবই সাধারণ, কিন্তু প্রচলিত, রিয়েল-টাইম ডাটাবেজ যেমন SQL এর থেকে খুবই আলাদা। ফায়ারস্টোর বরং একটা No-SQL ডাটাবেজ।

এই সিরিজটি ফায়ারবেজের স্ট্রাকচারের ব্যাপারে ধারনা লাভের জন্য খুবই ভালো, কাজেই একটু দেখে নেয়া যাক। এপিসোড ১, ২ আর চার বিশেষভাবে গুরুত্বপূর্ণ।

মূলত, একটা ফায়ারবেজ ফায়ারস্টোর বানানো হয় টপ-লেভেল ‘collections’এর মাধ্যমে, যেটা হতে পারে ‘Users', 'Messages', 'Products' ইত্যাদি। এই কালেকশনগুলোর ভেতরে ডকুমেন্টও থাকতে পারে।

ডকুমেন্ট হল এর প্যারেন্ট কালেকশনের স্পেসিফিক ইন্সট্যান্স যেগুলোতে অনুরূপ ভ্যালুসহ ‘fields’ এর নাম্বার অ্যাসাইন করা যায়। উদাহরণস্বরূপ, নিচে কীভাবে প্রোডাক্ট কালেকশনে ম্যাকবুক প্রো ডকুমেন্ট দেখা যায় সেটা দেখানো হল।

বামে: কালেকশন, মাঝে: কালেকশনের ডকুমেন্ট, ডানে: ডকুমেন্টটার ফিল্ড

নোট: আমি আমার বানানো একটা প্রজেক্ট থেকে ফায়ারবেজ কন্সোলের মাধ্যমে একটা ক্লাউড ফায়ারস্টোর ডাটাবেজ অ্যাক্সেস করছি।

No-SQL database এর একটা ব্যাপার হচ্ছে আপনি একই ফিল্ড ছাড়াই একই কালেকশনে একাধিক ডকুমেন্ট বানাতে পারবেন। উদাহরণ হিসেবে, ‘Pencil’ ডকুমেন্টে ‘rating’ ফিল্ডটি মিসিং থাকতে পারে, তবে এতে কোনো এরোর হবে না।

ফায়ারবেজ সম্পর্কে কিছু গুরুত্বপূর্ণ জানার জিনিস হচ্ছে এর বিলিং এবং সিকিউরিটি রুলস।

ফায়ারস্টোরে ডাটাবেজের আকারের ভিত্তিতে বিলিং চার্জ করা হয় না, বরং ডাটাবেজে রাইট ও রিডের সংখ্যার ভিত্তিতে হয়। উদাহরণস্বরূপ, আপনি যদি একটা Electronics প্রোডাক্ট বানান (ডকুমেন্ট আকারে) এবং সেটাকে ডাটাবেজে রাখেন, সেটাকে ১টা রাইট হিসেবে গোনা হয়। আপনি যদি প্রোডাক্টের দামও বাড়ান, তাও এটাকে ১টা রাইট হিসেবেই গোনা হবে।

আপনার যদি সব ‘Food’ কালেকশনের প্রোডাক্টকে লোড করতে হত, ফায়ারবেজ আপনাকে প্রতি ডকুমেন্টে ১ রিড করে চার্জ করত।

যাই হোক, ফায়ারবেজ তার সীমা সম্পর্কে বেশ উদার। তবে আপনি যদি আপনার অ্যাপকে প্রোডাকশনে নিতে চান (বাস্তব পৃথিবীতে ছাড়তে চান) তাহলে সবচেয়ে ভালো হয় বিলিং কীভাবে কাজ করে সে ব্যাপারে সতর্ক থাকা, যাতে আপনি আপনার ডাটাবেজ কল অপটিমাইজ করতে পারেন।

ফ্লাটারের সাথে ফায়ারবেজ সংযোগ

এখন যেহেতু আপনার ফায়ারবেজের সবচেয়ে গুরুত্বপূর্ণ অংশের ব্যাপারে জেনে গিয়েছি (ফায়ারস্টোর ডাটাবেজ), আমরা ফ্লাটার দিয়ে সেটার ডাটার অ্যাক্সেস কীভাবে নেবো?

StreamBuilder

আমরা এই কাজের জন্য একটা StreamBuilder ব্যবহার করতে পারি। একটা ‘স্ট্রিম’ মূলত একটা ডাটার স্ট্রিম যেটার পরিবর্তন আমরা একাধারে পর্যবেক্ষণ করছি। স্ট্রিমের একপাশে থাকে ফায়ারস্টোর ডাটাবেজ। আরেকপাশে থাকে আমাদের অ্যাপ।

কাজেই, যখন ফায়ারস্টোর ডাটাবেজে কিছু চেঞ্জ হয় (ধরা যাক একটা নতুন প্রোডাক্ট অ্যাড করা), ওই চেঞ্জটা ডাটা স্ট্রিম দিয়ে ফ্লাটার অ্যাপে চলে যায়। একবার চেঞ্জটা লক্ষ করা হলে StreamBuilder উইজেট নিজেকে রিবিল্ড করে যাতে করে সেই পরিবর্তনের সাথে সংযুক্ত হতে পারে।

নিচে সিনট্যাক্স দেখানো হল:

StreamBuilder(

  // gets an instance of a Firestore database and retrieves 'snapshots' of the Macbook Pro document

  stream: FirebaseFirestore.instance.collection('Products').doc('Macbook Pro').snapshots(),

  // builder defines what will be built on the app using this 'snapshot' data (the stream data)

  // Firestore collections are of type QuerySnapshot

  // Firestore documents are of type DocumentSnapshot

  // Both are referred to as AsyncSnapshots because they are asynchronous snapshots

  builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {

    // check that there is data in the stream and that it has finished loading

    if (snapshot.hasData) {

      return Container(

        // snapshot.data gives you access to all the fields in the document

        // get the values of the fields by using square brackets and the 

        // name of the field, like so

        child: Text(snapshot.data['name'])

      ),

    }, else {

      // if there's no data yet, show a loading sign

      return CircularProgressIndicator();

    }

  },

)

এটা দেখে জটিল মনে হলেও আসলে তেমন জটিল নয়। একপাশে আপনি একটা ডাটার স্ট্রিম অ্যাক্সেস করছেন, সেটা হতে পারে একটা ডকুমেন্টের কালেকশন অথবা হতে পারে আপনি ‘snapshot’ ভ্যারিয়েবলের মাধ্যমে অ্যাক্সেস পাওয়া যায় এমন একটা উইজেট বানাচ্ছেন। যদি StreamBuilder ফায়ারস্টোরের ভেতরে কোনো পরিবর্তন সনাক্ত করে, উইজেটটা রিবিল্ড করা হবে।

FutureBuilder

StreamBuilders দারুণ, তবে কেমন হত যদি আপনাকে ফায়ারস্টোরের চেঞ্জ সম্পর্কে জানতেই না হত? যদি আপনি কিছু ইনফরমেশন উদ্ধার করতে চাইতেন, যেমন ম্যাকবুকের দাম, আর কিছু না (আপনি জানেন যে ওই ভ্যালুটা চেঞ্জ হবে না)?

FutureBuilder দিয়ে আমরা সেটাই করতে পারি।

FutureBuilders  প্যারামিটার হিসেবে একটা অ্যাসিনক্রোনাস ফাংশনকে নেয় আর নেয় একটা বিল্ডার, যেটা দিয়ে ফাংশন এক্সিকিউশনের পর কিছু বিল্ড করা যাবে (StreamBuilder এর মত)। আমাদের উদাহরণে, আমাদের অ্যাসিনক্রোনাস ফাংশন অথবা ‘future’ (FutureBuilder যে নামে এটাকে ডাকে) ম্যাকবুকের দাম উদ্ধার করবে, আর উইজেট হয়ে সেই দামটাকে ডিসপ্লে করবে আমাদের বিল্ডার।

// defining an async function that returns an int

Future<int> retrieveMacbookPrice() async {

  // PS here's how to retrieve a single document from Firestore - 

  // in our case, the Macbook document

  var document = await FirebaseFirestore.instance.collection('Products').doc('Macbook Pro').get();

  // The data you get back (the document and its fields) will be a dictionary that maps 

  // keys (type String) to values (type dynamic)

  Map<String, dynamic> macbookData = document.data();

  int macbookPrice = macbookData['price'];

}

FutureBuilder(

  // builder will only build after this 'future' function is done executing

  future: retrieveMacbookPrice(),

  // the 'snapshot' here refers to what is returned from the future!

  builder: (BuildContext context, AsyncSnapshot<int> snapshot) {

    if (snapshot.hasData) {

      // data from the snapshot is accessed like so

      int price = snaphot.data['price']

      return Container(

        // convert int to string

        child: Text(price.toString()),

      );

    } else {

      // if there's no data yet, show a loading sign

      return CircularProgressIndicator();

    }

  }

)

অনেক হয়ে গেল! তবে ভেবে দেখুন, এখন যেহেতু আপনি ফায়ারবেজ, FutureBuilders, StreamBuilders এর কাজ সম্পর্কে জানেন, শক্তিশালী অ্যাপ বানানোর যাত্রায় আপনি অনেক দূর এগিয়ে গেছেন।

স্টেট ম্যানেজমেন্ট (State management)

স্টেট ম্যানেজমেন্ট ফ্লাটারের খুবই গুরুত্বপূর্ণ একটা বিষয় যেটা এমন:

ধরা যাক আপনি একটা অ্যাপ বানাতে চান যেটা আপনার ইউজারের প্রোফাইল এবং ইনফরমেশনের ট্র্যাক রাখবে। ইউজারনেম এবং পাসওয়ার্ড দিয়ে লগইন করার পরে আপনি চান শুভেচ্ছা হিসেবে অ্যাপের প্রতিটি পেজে তাদের নাম বলে শুভেচ্ছা জানানো হবে (যেমন, ‘Hello, [name]!’)। এটা আপনি কীভাবে করবেন? আপনি কীভাবে ইউজারের 'username 'আর 'password' ভ্যালুগুলো পুরো অ্যাপজুড়ে পাস করবেন?

আপনি চাইলে ইউজারনেমটাকে একটা প্যারামিটার হিসেবে পাস করতে পারেন আলাদা আলাদা পেজের সকল স্টেটলেস আর স্টেটফুল উইজেটের জন্য। তবে বাস্তবে, আপনি চাইবেন আপনার ইউজারনেম ভ্যালু এমনভাবে হোল্ড করা থাকবে যাতে সেটাকে সব স্ক্রিন/পেজ থেকে অ্যাক্সেস করা যায়।

এই কাজটা করা যাবে একটা ‘Provider’ উইজেট ব্যবহার করে, যা একটি বিল্ট-ইন স্টেট ম্যানেজমেন্ট সলিউশন।

Providerকে ‘প্রোভাইডার’ বলার কারণ এটা চাইল্ড উইজেটে একটা ভ্যালু পাস ডাউন করে প্রোভাইড করে, যাতে করে চাইল্ড উইজেটটি ওই ভ্যালু/এনটিটি থেকে সবকিছুর অ্যাক্সেস পায়। আমাদের উদাহরণে, যদি আমাদের একটা ‘Cart’ ক্লাস থাকত যেটা আমরা চাইল্ড উইজেট দিয়ে অ্যাক্সেস করতে চাইতাম, তাহলে এটা দেখাত এমন:

Provider(

  create: (context) => CartModel(),

  child: MyApp(),

)

অর্থাৎ, MyApp চাইল্ড উইজেটে আমরা CartModel এবং এর সব মেথডের অ্যাক্সেস পেতাম। আপনি চাইলে ডাটা অ্যাক্সেসের জন্য CartModel ক্লাসকে ইন্সট্যানশিয়েট করতে পারেন দুইভাবে:

/ 1st way

Provider.of<CartModel>(context).removeAllItems();

// 2nd way

// context.watch listens for changes in CartModel - if data changes, the parent will rebuild

// whatever is necessary

context.watch<CartModel>().removeAllItems();

// context.read returns CartModel / the model of interest without listening to changes in 

// the data

context.read<CartModel>().removeAllItems();

এটা CartModel টাইপ দেখার জন্য Provider কল করে, আর removeAllItems() মেথড কল করে। দ্বিতীয় পদ্ধতিতে, CartModel এর অবজেক্ট (< > এর ভেতরে যা আছে) ইন্সট্যানশিয়েট করা করা হয় প্যারেনথিসিস দিয়ে -> context.read< >() ।

কী হত যদি আমরা আরেকটা ডাটা অ্যাক্সেস করতে চাইতাম যেটার জন্য স্টেট ম্যানেজমেন্ট দরকার হয়, যেমন ধরা যাক কালার থিমের জন্য অ্যাপে ইউজারের পছন্দ? আমরা সেক্ষেত্রে একটা ক্লাস বানাতে পারতাম 'UserPreferences' নামে, তবে সেটাকে CartModel ক্লাসের উপর থেকে অ্যাক্সেস করতাম কীভাবে?

একটা উপায় হতে পারে Providerদের nest করা।

Provider(

  create: (context) => CartModel(),

  child: Provider(

    create: (context) => UserPreferences(),

    child: MyApp(),

  ),

)

তাহলে আমরা MyApp থেকে UserPreferences এবং CartModel উভয়ের অ্যাক্সেসই পেতাম। তবে আপনি সম্ভবত ধরতে পারছেন যে এটা অসম্ভব ফাস্ট হয়ে যায়, তাই না? এখানেই MultiProvider এর খেলা শুরু।

‘MultiProvider’ উইজেট আমাদের অ্যাপের একদম উপরে (main.dart এ) একাধিক ‘provider’ ডিফাইন করতে সাহায্য করে, যেখানে সবগুলো চাইল্ড একে অপরের প্রোভাইডারকে অ্যাক্সেস করতে পারে।

MultiProvider(

  providers: [

    Provider<CartModel>(create: (_) => CartModel()),

    Provider<UserPreferences>(create: (_) => UserPreferences()),

  ],

  child: MyApp(),

)

কী সুন্দর ন্যাচারাল প্রগ্রেশন!

এই গেল স্টেট ম্যানেজমেন্টের বেসিক। এবার চলুন বিবিধ কিছু শিখে আসা যাক।

দারুণ কিছু অভ্যাস

ফ্লাটারে বড় বড় প্রজেক্ট বানানোর সময়ে এই অভ্যাস বা প্র্যাক্টিসগুলো মাথায় রাখা অতি গুরুত্বপূর্ণ। 

ফোল্ডার স্ট্রাকচার

একটা বৃহৎ প্রজেক্ট মেইনটেইন কর‍তে হলে নিশ্চিত করতে হবে আপনার ফোল্ডার স্ট্রাকচার সঠিকভাবে সাজানো আছে কি না।

ফোল্ডার সাধারণত এভাবে স্ট্রাকচার করা হয়:

পূর্বে যেমন দেখেছি, lib হচ্ছে যেখানে আপনি আপনার সব ফ্লাটার কোড রাখবেন। ফ্লাটার এরপর এই কোডগুলোকে অ্যান্ড্রয়েড ও আইওএস কোডে রূপান্তর করে নেটিভ অ্যাপস বানায়, যেগুলো সহজেই অ্যান্ড্রয়েড ও আইওএস ফোল্ডারে পাওয়া যাবে। আপনার ব্যবহৃত যেকোনো image, svg ফাইল অথবা ছবি আপনাকে 'assets' নামক ফোল্ডার বানিয়ে সেখানে স্থানান্তরিত করতে হবে।

lib ফোল্ডারে আপনাকে কোডগুলোকে screens, screens, models, services, widgets, এবং constants হিসেবে আলাদা করতে হবে। Main.dart হবে আপনার wrapper ফাইল।

Constants ব্যবহৃত হয় constants.dart রাখার জন্য, যেটা মূলত আপনার অ্যাপের জন্য ThemeData এবং কালার স্কিম (color scheme) ডিফাইন করে, যাতে করে আপনার অ্যাপ একটা নির্দিষ্ট স্টাইল ধারণ করতে পারে। উদাহরণস্বরূপ, আমি সাধারণত constants.dart ফাইলে kPrimaryColor এবং kSecondaryColor ডিফাইন করি। আপনি চাইলে একটা theme.dart ফাইল দিয়ে ThemeData অবজেক্ট বানাতে পারেন।

মডেল হচ্ছে এমন ক্লাস যেগুলো আপনি ফ্লাটারে ডাটা নিয়ে কাজে সুবিধার জন্য বানাতে চান। যেমন, আপনি হয়ত চাইতে পারেন একটা User ক্লাস তৈরি করতে যেখানে 'username', 'nickname', 'age' ইত্যাদি প্রপার্টি রয়েছে। models ফোল্ডারে আপনার পছন্দমত ক্লাসের নাম দিয়ে ফোল্ডার বানিয়ে নিন। আমি যদি বানাতে চাইতাম…

class User {

  String username;

  String nickname;

  int age;

}

তাহলে আমি ফাইলের নাম দিতাম user.dart (যদি দুইটি শব্দ হয় তাহলে একটা আন্ডারস্কোর '_' দিয়ে দিন স্পেসের জায়গায়, যেমন food_item.dart)।

ফোল্ডারে স্ক্রিন হল যেইখানে আপনি আপনার বেশিরভাগ কোড রাখবেন - সব স্ক্রিনের জন্য ইউআই কোড। নতুন স্ক্রিন বানাতে, screen নামের নতুন একটা ফোল্ডার খুলে সেই সাবফোল্ডারে আপনার কোড রাখুন। এতে করে আপনার সব স্ক্রিন 'screens' ফোল্ডার হিসেবে পৃথক থাকবে। আপনার নির্দিষ্ট স্ক্রিন ফোল্ডারে মূল ফাইলকে (স্ক্রিনের_নাম).dart নাম দিন।

আপনার স্ক্রিনের যদি অনেক কম্পোনেন্ট থাকে তাহলে screenএর ডিরেক্টরিতে একটা components ফোল্ডার খুলুন।

Services ব্যবহৃত হয় বিজনেস লজিক কন্টেইন করে এমন সব ক্লাস রাখতে। এরাও models ফোল্ডারের অনুরূপ নিয়ম মেনে চলে।

Widgets ব্যবহৃত হয় আপনি মাল্টিপল স্ক্রিনের জন্য ব্যবহার করেন এমন সব কাস্টমভাবে বানানো উইজেট রাখার জন্য। উদাহরণ হিসেবে, যদি আপনি login ও sign_in দুই স্ক্রিনের জন্য আপনার নিজের Button উইজেট বানান, সেটাকে widgets ফোল্ডারে রাখুন।

ফ্রন্টএন্ড থেকে বিজনেস লজিক আলাদা করা

বিজনেস লজিক মূলত এমন কোড যার সরাসরি অ্যাপের লেআউটের সাথে কোনো লেনদেন নেই। উদাহরণস্বরূপ, আপনার যদি একটা login স্ক্রিন থাকে, ইউআই হত Column, TextField আর ElevatedButton উইজেট। বিজনেস লজিক হত কীভাবে ইউজার আপনার ব্যাকএন্ড সার্ভারে সাইন ইন করবে (যেমন ফায়ারবেজ)।

সাধারণত এই দুটোকে আলাদা রাখাই বুদ্ধিমানের কাজ হবে, কারণ এতে করে আপনি ফ্রন্টএন্ড এবং ব্যাকএন্ড ডাটা গুলিয়ে ফেলে এলোমেলো আর দুর্বোধ্য কোড বানিয়ে না ফেলেন। আমি যদি product_details স্ক্রিনের কোড দেখতে চাই, আমি কেন প্রোডাক্টটা ব্যাকএন্ডে কীভাবে কাজ করে সেটা দেখব? এজন্য এদেরকে আলাদা করে রাখাটাই পরিচ্ছন্নতা।

এসব কথার মানে হচ্ছে আমরা যথাসম্ভব চেষ্টা করব 'services' ফোল্ডারে আমাদের বিজনেস লজিক/ব্যাকএন্ড কোড রাখতে, 'screens' এ নয়। আমি সাধারণত কাজটা করি একটা 'APIServices' ক্লাস ডিফাইন করার মাধ্যমে যেখানে বিজনেস লজিক নিয়ে কাজ করে এমন বেশ কিছু মেথড রয়েছে।

বেশি বেশি অ্যাবস্ট্রাক্ট করুন (বেশি বেশি উইজেটস বানান)

ফ্লাটারে আপনি যতটা সম্ভব কোড এক্সট্র্যাক্ট করতে চাইবেন। এর মানে হচ্ছে যখনই আপনি সিঙ্গেল ইউজ কেসের জন্যে তৈরি একটা উইজেট ট্রি-র অংশ পাবেন , তখনই সেটাকে এক্সট্র্যাক্ট করে নিজের আলাদা উইজেটে নিয়ে অন্য কোথাও রাখবেন। এখানে একটা উদাহরণ দেখানো হল।

// products_screen.dart

Scaffold(

  // Column widget to lay out everything on the page vertically

  body: Column(

    children: [

      // nested column widget dedicated to displaying electronics

      Column(

        children: [

          Container(child: Text('Electronics')),

          Text('Macbook pro'),

          Text('iPhone'),

          Text('Galaxy Buds'),

        ],

      ),

      // nested column widget dedicated to displaying food

      Column(

        children: [

          Container(child: Text('Food items')),

          Text('Jelly beans'),

          Text('Peanut Butter'),

          Text('Apples'),

        ],

      ),

    ],

  ),

)

এখানে 'Food Items’ এবং 'Electronics’ সেকশন একটা উইজেট ট্রিতে রাখা হয়েছে, ফলে প্রজেক্ট যত বড় হবে এটা দেখতে অগোছালো আর দুর্বোধ্য লাগবে। ভালো হবে যদি এটা বানানো যায় এভাবে।

// screens/products/products_screen.dart

Scaffold(

  body: Column(

    children: [

      // Extracted widgets (put the widgets into their own file in the 'components' directory of this screen's directory)

      ElectronicsSection(),

      FoodItemsSection(),

    ],

  ),

)

// screens/products/components/electronics_section.dart

class ElectronicsSection extends StatelessWidget {

  const ElectronicsSection({ Key? key }) : super(key: key);

  // same widgets, just put into the build function as a returned value

  @override

  Widget build(BuildContext context) {

    return Column(

      children: [

        Container(child: Text('Electronics')),

        Text('Macbook pro'),

        Text('iPhone'),

        Text('Galaxy Buds'),

      ],

    );

  }

}


// screens/products/components/food_items_section.dart

class FoodItemsSection extends StatelessWidget {

  const FoodItemsSection({ Key? key }) : super(key: key);

  // same widgets, just put into the build function as a returned value

  @override

  Widget build(BuildContext context) {

    return Column(

      children: [

        Container(child: Text('Food items')),

        Text('Jelly beans'),

        Text('Peanut Butter'),

        Text('Apples'),

      ],

    );

  }

}

এই রেজাল্টটা আগের চেয়ে বেশি পরিচ্ছন্ন আর এখানে ডিবাগ ও কোড করা সহজতর।

টেস্টিং (Testing) 

ফ্লাটারে তৈরি অ্যাপের জন্য ইউনিট টেস্ট বানানোর মাধ্যমে সহজে আপনি কোনোপ্রকার কোড ব্রেক ছাড়াই নতুন ফিচার অ্যাড করতে পারবেন।

টেস্ট লেখা হয় আপনার অ্যাপের নির্দিষ্ট ফাংশনালিটি চেক করার প্রসেসকে অটোমেট করতে। উদাহরণস্বরূপ, আপনি চাইলে লগইন স্ক্রিন এবং বিজনেস লজিক কাজ করছে কি না চেক করার জন্য একটা ইউনিট টেস্ট লিখতে পারেন, আর এটা আপনি চাইলে প্রত্যেকবার অ্যাপে কোনো চেঞ্জ করার সাথে সাথে রান করে দেখতে পারেন

সহায়ক রিসোর্স

যেহেতু আপনি সিনট্যাক্স শিখে ফেলেছেন এবং একটা ফ্লাটার প্রজেক্ট নিজে নিজে বানানোর চেষ্টা করেছেন, আপনাকে অভিনন্দন ও শুভকামনা, যাতে আপনি এবার নিজের উদ্যোগে ফ্লাটার অ্যাপ বানাতে পারেন।

ফ্লাটার প্র্যাক্টিস করতে করতে ক্লান্ত হয়ে গেলে উৎসাহ হারাবেন না। অনেকবার আমিও আটকে গিয়েছিলাম, কিন্তু যখন থেকে আমি একটা নতুন বিষয় বুঝতে পারতাম, আমি খুব দ্রুত অনেকটা এগোতে পারতাম। শেখার কোনো সময় নেই। আপনি যে এখানে প্রথমবারের মত এসেছেন এতেই খুশি হন।

আশা করি এই বিশাল গাইড আপনার ভালো লেগেছে।