১. ভূমিকা
ফ্লাটার হলো গুগলের একটি UI টুলকিট, যা দিয়ে একটিমাত্র কোডবেস থেকে মোবাইল, ওয়েব এবং ডেস্কটপের জন্য অ্যাপ্লিকেশন তৈরি করা যায়। এই কোডল্যাবে, আপনি নিম্নলিখিত ফ্লাটার অ্যাপ্লিকেশনটি তৈরি করবেন:
অ্যাপ্লিকেশনটি "নিউস্টে", "লাইটস্ট্রিম", "মেইনব্রেক" বা "গ্রেপাইন"-এর মতো চমৎকার নাম তৈরি করে। ব্যবহারকারী পরবর্তী নামের জন্য অনুরোধ করতে, বর্তমান নামটিকে পছন্দের তালিকায় যুক্ত করতে এবং একটি আলাদা পৃষ্ঠায় পছন্দের নামগুলোর তালিকা পর্যালোচনা করতে পারেন। অ্যাপটি বিভিন্ন স্ক্রিন সাইজের সাথে সামঞ্জস্যপূর্ণ।
আপনি যা শিখবেন
- ফ্লাটার কীভাবে কাজ করে তার মূল বিষয়গুলো
- ফ্লাটারে লেআউট তৈরি করা
- ব্যবহারকারীর কার্যকলাপকে (যেমন বোতাম চাপা) অ্যাপের আচরণের সাথে সংযুক্ত করা
- আপনার ফ্লাটার কোড সুসংগঠিত রাখা
- আপনার অ্যাপকে রেসপন্সিভ করা (বিভিন্ন স্ক্রিনের জন্য)
- আপনার অ্যাপের একটি সামঞ্জস্যপূর্ণ রূপ ও অনুভূতি অর্জন করা
আপনি একটি প্রাথমিক কাঠামো দিয়ে শুরু করবেন, যাতে আপনি সরাসরি আকর্ষণীয় অংশগুলোতে যেতে পারেন।

আর এই যে ফিলিপ আপনাদেরকে পুরো কোডল্যাবটি ঘুরিয়ে দেখাচ্ছেন!
ল্যাবটি শুরু করতে নেক্সট-এ ক্লিক করুন।
২. আপনার ফ্লাটার পরিবেশ সেট আপ করুন।
সম্পাদক
এই কোডল্যাবটিকে যথাসম্ভব সহজবোধ্য করার জন্য, আমরা ধরে নিচ্ছি যে আপনি আপনার ডেভেলপমেন্ট এনভায়রনমেন্ট হিসেবে ভিজ্যুয়াল স্টুডিও কোড (ভিএস কোড) ব্যবহার করবেন। এটি বিনামূল্যে পাওয়া যায় এবং সব প্রধান প্ল্যাটফর্মে কাজ করে।
অবশ্যই, আপনি আপনার পছন্দের যেকোনো এডিটর ব্যবহার করতে পারেন: অ্যান্ড্রয়েড স্টুডিও, অন্যান্য ইন্টেলিজ আইডিই, ইম্যাক্স, ভিম, বা নোটপ্যাড++। এগুলো সবই ফ্লাটারের সাথে কাজ করে।
এই কোডল্যাবের জন্য আমরা ভিএস কোড (VS Code) ব্যবহার করার পরামর্শ দিই, কারণ এর নির্দেশাবলীতে ডিফল্টভাবে ভিএস কোড-এর নির্দিষ্ট শর্টকাটগুলো দেওয়া থাকে। "X কাজটি করার জন্য আপনার এডিটরে উপযুক্ত কাজটি করুন"-এর মতো কিছু বলার চেয়ে "এখানে ক্লিক করুন" বা "এই কী-টি চাপুন"-এর মতো কথা বলা বেশি সহজ।

একটি উন্নয়ন লক্ষ্য নির্বাচন করুন
ফ্লাটার একটি মাল্টি-প্ল্যাটফর্ম টুলকিট। আপনার অ্যাপটি নিম্নলিখিত যেকোনো অপারেটিং সিস্টেমে চলতে পারে:
- আইওএস
- অ্যান্ড্রয়েড
- জানালা
- ম্যাকওএস
- লিনাক্স
- ওয়েব
তবে, একটিমাত্র অপারেটিং সিস্টেম বেছে নেওয়া একটি প্রচলিত রীতি, যার জন্য আপনি প্রাথমিকভাবে ডেভেলপ করবেন। এটিই আপনার "ডেভেলপমেন্ট টার্গেট"—অর্থাৎ সেই অপারেটিং সিস্টেম, যেটিতে আপনার অ্যাপটি ডেভেলপমেন্ট চলাকালীন চলে।

উদাহরণস্বরূপ, ধরুন আপনি একটি ফ্লাটার অ্যাপ তৈরি করার জন্য একটি উইন্ডোজ ল্যাপটপ ব্যবহার করছেন। যদি আপনি আপনার ডেভেলপমেন্ট টার্গেট হিসেবে অ্যান্ড্রয়েড বেছে নেন, তাহলে সাধারণত আপনি একটি ইউএসবি ক্যাবলের মাধ্যমে আপনার উইন্ডোজ ল্যাপটপের সাথে একটি অ্যান্ড্রয়েড ডিভাইস সংযুক্ত করেন এবং আপনার নির্মাণাধীন অ্যাপটি সেই সংযুক্ত অ্যান্ড্রয়েড ডিভাইসে চলে। কিন্তু আপনি ডেভেলপমেন্ট টার্গেট হিসেবে উইন্ডোজও বেছে নিতে পারেন, যার অর্থ হলো আপনার নির্মাণাধীন অ্যাপটি আপনার এডিটরের পাশাপাশি একটি উইন্ডোজ অ্যাপ হিসেবে চলবে।
আপনার ডেভেলপমেন্টের লক্ষ্য হিসেবে ওয়েবকে বেছে নেওয়াটা লোভনীয় মনে হতে পারে। এই পছন্দের অসুবিধা হলো, আপনি ফ্লাটারের অন্যতম দরকারি একটি ডেভেলপমেন্ট ফিচার—স্টেটফুল হট রিলোড—হারিয়ে ফেলবেন। ফ্লাটার ওয়েব অ্যাপ্লিকেশন হট-রিলোড করতে পারে না।
এখনই আপনার সিদ্ধান্ত নিন। মনে রাখবেন: আপনি পরবর্তীতে যেকোনো সময় অন্যান্য অপারেটিং সিস্টেমে আপনার অ্যাপটি চালাতে পারবেন। তবে, উন্নয়নের একটি সুস্পষ্ট লক্ষ্য মাথায় রাখলে পরবর্তী পদক্ষেপটি আরও সহজ হয়।
ফ্লাটার ইনস্টল করুন
ফ্লাটার এসডিকে ইনস্টল করার সবচেয়ে হালনাগাদ নির্দেশাবলী সর্বদা docs.flutter.dev- এ পাওয়া যায়।
ফ্লাটার ওয়েবসাইটের নির্দেশাবলীতে শুধু এসডিকে (SDK) ইনস্টলেশনই নয়, বরং ডেভেলপমেন্ট টার্গেট-সম্পর্কিত টুলস এবং এডিটর প্লাগইনগুলোও অন্তর্ভুক্ত রয়েছে। মনে রাখবেন যে, এই কোডল্যাবের জন্য আপনাকে শুধুমাত্র নিম্নলিখিতগুলো ইনস্টল করতে হবে:
- ফ্লাটার এসডিকে
- ফ্লাটার প্লাগইন সহ ভিজ্যুয়াল স্টুডিও কোড
- আপনার নির্বাচিত ডেভেলপমেন্ট টার্গেটের জন্য প্রয়োজনীয় সফটওয়্যার (উদাহরণস্বরূপ: উইন্ডোজের জন্য ভিজ্যুয়াল স্টুডিও , অথবা ম্যাকওএস-এর জন্য এক্সকোড )।
পরবর্তী অংশে আপনি আপনার প্রথম ফ্লাটার প্রজেক্ট তৈরি করবেন।
এখন পর্যন্ত যদি আপনার কোনো সমস্যা হয়ে থাকে, তাহলে সমস্যা সমাধানের জন্য স্ট্যাকওভারফ্লো থেকে নেওয়া এই প্রশ্ন ও উত্তরগুলো আপনার কাজে আসতে পারে।
প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী
- আমি ফ্লাটার এসডিকে-এর পাথ কীভাবে খুঁজে পাব?
- ফ্লাটার কমান্ড খুঁজে না পেলে আমি কী করব?
- "Waiting for another flutter command to release startup lock" সমস্যাটি আমি কীভাবে সমাধান করব?
- আমার অ্যান্ড্রয়েড এসডিকে ইনস্টলেশনটি কোথায় আছে, তা আমি ফ্লাটারকে কীভাবে জানাব?
-
flutter doctor --android-licensesচালানোর সময় যে জাভা ত্রুটি দেখা দেয়, তা আমি কীভাবে সমাধান করব? - Android-এ
sdkmanagerটুল খুঁজে না পাওয়ার সমস্যাটি আমি কীভাবে সমাধান করব? - "
cmdline-toolscomponent is missing" ত্রুটিটি আমি কীভাবে সমাধান করব? - আমি কীভাবে অ্যাপল সিলিকন (M1)-এ কোকোপডস চালাব?
- VS Code-এ সেভ করার সময় অটোফরমেটিং কীভাবে বন্ধ করব?
৩. একটি প্রকল্প তৈরি করুন
আপনার প্রথম ফ্লাটার প্রজেক্ট তৈরি করুন
ভিজ্যুয়াল স্টুডিও কোড চালু করুন এবং কমান্ড প্যালেটটি খুলুন ( F1 অথবা Ctrl+Shift+P অথবা Shift+Cmd+P চেপে)। "flutter new" টাইপ করা শুরু করুন। Flutter: New Project কমান্ডটি নির্বাচন করুন।
এরপরে, অ্যাপ্লিকেশন (Application) নির্বাচন করুন এবং তারপর আপনার প্রজেক্ট তৈরি করার জন্য একটি ফোল্ডার বেছে নিন। এটি আপনার হোম ডিরেক্টরি হতে পারে, অথবা C:\src\ মতো কোনো ডিরেক্টরিও হতে পারে।
অবশেষে, আপনার প্রজেক্টটির একটি নাম দিন। যেমন namer_app বা my_awesome_namer ।

ফ্লাটার এখন আপনার প্রজেক্ট ফোল্ডার তৈরি করে এবং ভিএস কোড সেটি খোলে।
এখন আপনি অ্যাপটির একটি প্রাথমিক কাঠামো দিয়ে ৩টি ফাইলের বিষয়বস্তু ওভাররাইট করবেন।
প্রাথমিক অ্যাপটি কপি ও পেস্ট করুন
VS Code-এর বাম প্যানে Explorer সিলেক্ট করা আছে কিনা তা নিশ্চিত করুন এবং pubspec.yaml ফাইলটি খুলুন।

এই ফাইলের বিষয়বস্তু নিম্নলিখিত দ্বারা প্রতিস্থাপন করুন:
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
pubspec.yaml ফাইলটি আপনার অ্যাপের মৌলিক তথ্য নির্দিষ্ট করে, যেমন এর বর্তমান সংস্করণ, এর নির্ভরতা এবং যে অ্যাসেটগুলোর সাথে এটি সরবরাহ করা হবে।
এরপরে, প্রোজেক্টে থাকা আরেকটি কনফিগারেশন ফাইল, analysis_options.yaml খুলুন।

এর বিষয়বস্তু নিম্নলিখিত দ্বারা প্রতিস্থাপন করুন:
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_print: false
prefer_const_constructors_in_immutables: false
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_final_fields: false
unnecessary_breaks: true
use_key_in_widget_constructors: false
এই ফাইলটি নির্ধারণ করে যে আপনার কোড বিশ্লেষণ করার সময় ফ্লাটার কতটা কঠোর হবে। যেহেতু ফ্লাটারে এটি আপনার প্রথম কাজ, তাই আপনি অ্যানালাইজারকে সহজভাবে কাজ করতে বলছেন। আপনি এটি পরে যেকোনো সময় পরিবর্তন করতে পারেন। প্রকৃতপক্ষে, আপনি যখন একটি প্রকৃত প্রোডাকশন অ্যাপ প্রকাশ করার কাছাকাছি চলে আসবেন, তখন আপনি প্রায় নিশ্চিতভাবেই অ্যানালাইজারকে এর চেয়ে আরও কঠোর করতে চাইবেন।
অবশেষে, lib/ ডিরেক্টরির অধীনে থাকা main.dart ফাইলটি খুলুন।

এই ফাইলের বিষয়বস্তু নিম্নলিখিত দ্বারা প্রতিস্থাপন করুন:
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
এই ৫০ লাইনের কোডই এখন পর্যন্ত অ্যাপটির সম্পূর্ণ অংশ।
পরবর্তী অংশে, অ্যাপ্লিকেশনটি ডিবাগ মোডে চালান এবং উন্নয়ন শুরু করুন।
৪. একটি বাটন যোগ করুন
এই ধাপে একটি নতুন শব্দজোড়া তৈরি করার জন্য একটি ' পরবর্তী' বাটন যুক্ত করা হয়।
অ্যাপটি চালু করুন
প্রথমে, lib/main.dart খুলুন এবং নিশ্চিত করুন যে আপনার টার্গেট ডিভাইসটি সিলেক্ট করা আছে। VS Code-এর নিচের ডান কোণায় আপনি একটি বাটন দেখতে পাবেন, যেখানে বর্তমান টার্গেট ডিভাইসটি দেখানো হয়। এটি পরিবর্তন করতে ক্লিক করুন।
lib/main.dart খোলা থাকা অবস্থায়, 'play' খুঁজুন
VS Code উইন্ডোর উপরের ডানদিকের কোণায় থাকা বাটনটিতে ক্লিক করুন।
প্রায় এক মিনিট পর, আপনার অ্যাপটি ডিবাগ মোডে চালু হবে। এখনও এটি তেমন আকর্ষণীয় দেখাচ্ছে না:

প্রথম হট রিলোড
lib/main.dart ফাইলের একদম শেষে, প্রথম Text অবজেক্টের স্ট্রিংটিতে কিছু একটা যোগ করুন এবং ফাইলটি সেভ করুন ( Ctrl+S বা Cmd+S চেপে)। উদাহরণস্বরূপ:
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
লক্ষ্য করুন, অ্যাপটি সঙ্গে সঙ্গে পরিবর্তিত হলেও র্যান্ডম শব্দটি একই থাকে। এটি হলো ফ্লাটারের বিখ্যাত স্টেটফুল হট রিলোডের কার্যকারিতা। যখন আপনি কোনো সোর্স ফাইলে করা পরিবর্তন সেভ করেন, তখন হট রিলোড ট্রিগার হয়।
প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী
- VSCode-এ হট রিলোড কাজ না করলে কী হবে?
- VSCode-এ হট রিলোড করার জন্য আমাকে কি 'r' চাপতে হবে?
- হট রিলোড কি ওয়েবে কাজ করে?
- আমি কীভাবে "ডিবাগ" ব্যানারটি সরাব?
একটি বোতাম যোগ করা
এরপরে, Column একেবারে নিচে, দ্বিতীয় Text ঠিক নিচে একটি বাটন যোগ করুন।
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
আপনি পরিবর্তনটি সেভ করলে, অ্যাপটি আবার আপডেট হয়: একটি বাটন দেখা যায় এবং তাতে ক্লিক করলে VS Code-এর ডিবাগ কনসোলে ‘ বাটন প্রেসড!’ মেসেজটি দেখায়।
৫ মিনিটে ফ্লাটারের একটি সংক্ষিপ্ত কোর্স
ডিবাগ কনসোল দেখাটা যতই মজার হোক না কেন, আপনি চাইবেন বাটনটি দিয়ে আরও অর্থবহ কিছু করা হোক। তবে, সেদিকে যাওয়ার আগে, এটি কীভাবে কাজ করে তা বোঝার জন্য lib/main.dart এর কোডটি ভালোভাবে দেখুন।
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
ফাইলের একদম উপরে আপনি main() ফাংশনটি পাবেন। এর বর্তমান রূপে, এটি শুধু Flutter-কে MyApp এ সংজ্ঞায়িত অ্যাপটি চালানোর নির্দেশ দেয়।
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
MyApp ক্লাসটি StatelessWidget এক্সটেন্ড করে। উইজেট হলো সেইসব উপাদান যা দিয়ে প্রতিটি ফ্লাটার অ্যাপ তৈরি করা হয়। যেমনটা আপনি দেখতে পাচ্ছেন, এমনকি অ্যাপটি নিজেও একটি উইজেট।
MyApp এর কোডটি পুরো অ্যাপটি সেট আপ করে। এটি অ্যাপ-ব্যাপী স্টেট তৈরি করে (এ বিষয়ে পরে আরও আলোচনা করা হবে), অ্যাপটির নামকরণ করে, ভিজ্যুয়াল থিম নির্ধারণ করে এবং "হোম" উইজেট সেট করে—যা আপনার অ্যাপের সূচনা বিন্দু।
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
এরপরে, MyAppState ক্লাসটি অ্যাপের স্টেট নির্ধারণ করে। ফ্লাটারে এটি আপনার প্রথম কাজ, তাই এই কোডল্যাবটি বিষয়টিকে সহজ এবং নির্দিষ্ট বিষয়ের উপর আলোকপাত করবে। ফ্লাটারে অ্যাপের স্টেট পরিচালনা করার অনেক শক্তিশালী উপায় রয়েছে। এর মধ্যে সবচেয়ে সহজে ব্যাখ্যা করা যায় এমন একটি হলো ChangeNotifier , যা এই অ্যাপটিতে ব্যবহার করা হয়েছে।
-
MyAppStateঅ্যাপটির কার্যকারিতার জন্য প্রয়োজনীয় ডেটা নির্ধারণ করে। এই মুহূর্তে, এতে কেবল বর্তমান র্যান্ডম শব্দজোড়া সহ একটিমাত্র ভেরিয়েবল রয়েছে। আপনি পরবর্তীতে এতে আরও তথ্য যোগ করবেন। - স্টেট ক্লাসটি
ChangeNotifierএক্সটেন্ড করে, যার মানে হলো এটি নিজের পরিবর্তন সম্পর্কে অন্যদের জানাতে পারে। উদাহরণস্বরূপ, যদি বর্তমান শব্দজোড়া পরিবর্তিত হয়, তবে অ্যাপের কিছু উইজেটের তা জানা প্রয়োজন। - একটি
ChangeNotifierProviderব্যবহার করে স্টেটটি তৈরি করা হয় এবং পুরো অ্যাপে সরবরাহ করা হয় (উপরেMyAppএর কোডটি দেখুন)। এর ফলে অ্যাপের যেকোনো উইজেট স্টেটটি অ্যাক্সেস করতে পারে।

lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2
return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
// ...
সবশেষে রয়েছে MyHomePage , যে উইজেটটি আপনি ইতিমধ্যেই পরিবর্তন করেছেন। নিচের প্রতিটি সংখ্যাযুক্ত লাইন উপরের কোডের একটি লাইন-নম্বর কমেন্টের সাথে সম্পর্কিত:
- প্রতিটি উইজেটে একটি
build()মেথড থাকে যা উইজেটের পরিস্থিতি পরিবর্তনের সাথে সাথে স্বয়ংক্রিয়ভাবে কল করা হয়, যাতে উইজেটটি সর্বদা হালনাগাদ থাকে। -
MyHomePagewatchমেথড ব্যবহার করে অ্যাপটির বর্তমান অবস্থার পরিবর্তনগুলো ট্র্যাক করে। - প্রতিটি
buildমেথডকে অবশ্যই একটি উইজেট অথবা (সাধারণত) উইজেটের একটি নেস্টেড ট্রি রিটার্ন করতে হবে। এই ক্ষেত্রে, টপ-লেভেল উইজেটটি হলোScaffold)। আপনি এই কোডল্যাবেScaffoldনিয়ে কাজ করবেন না, কিন্তু এটি একটি সহায়ক উইজেট এবং বাস্তব জগতের অধিকাংশ ফ্লাটার অ্যাপেই এটি পাওয়া যায়। -
Columnহলো ফ্লাটারের অন্যতম মৌলিক একটি লেআউট উইজেট। এটি যেকোনো সংখ্যক চাইল্ড গ্রহণ করে এবং সেগুলোকে উপর থেকে নিচে একটি কলামে সাজিয়ে রাখে। ডিফল্টভাবে, কলামটি তার চাইল্ডগুলোকে দৃশ্যত উপরে রাখে। আপনি শীঘ্রই এটি পরিবর্তন করে কলামটিকে কেন্দ্রে স্থাপন করবেন। - আপনি প্রথম ধাপে এই
Textউইজেটটি পরিবর্তন করেছেন। - এই দ্বিতীয়
TextউইজেটটিappStateগ্রহণ করে এবং সেই ক্লাসের একমাত্র মেম্বার `current(যা একটিWordPair) অ্যাক্সেস করে।WordPairবেশ কিছু দরকারি গেটার প্রদান করে, যেমনasPascalCaseবাasSnakeCase। এখানে আমরাasLowerCaseব্যবহার করেছি, কিন্তু আপনার পছন্দ অনুযায়ী বিকল্পগুলোর কোনোটি এখনই পরিবর্তন করতে পারেন। - লক্ষ্য করুন, ফ্লাটার কোডে কীভাবে ট্রেলিং কমার ব্যাপক ব্যবহার করা হয়। এই নির্দিষ্ট কমাটির এখানে থাকার কোনো প্রয়োজন নেই, কারণ
childrenহলো এই নির্দিষ্টColumnপ্যারামিটার তালিকার শেষ (এবং একমাত্র ) সদস্য। তবুও, সাধারণত ট্রেলিং কমা ব্যবহার করা একটি ভালো অভ্যাস: এগুলো আরও সদস্য যোগ করাকে অত্যন্ত সহজ করে তোলে এবং ডার্টের অটো-ফরমেটারকে সেখানে একটি নিউলাইন বসানোর জন্য ইঙ্গিত হিসেবেও কাজ করে। আরও তথ্যের জন্য, `Code formatting` দেখুন।
এরপরে, আপনি বাটনটিকে স্টেটের সাথে সংযুক্ত করবেন।
আপনার প্রথম আচরণ
MyAppState এ স্ক্রোল করুন এবং একটি getNext মেথড যোগ করুন।
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
নতুন getNext() মেথডটি current একটি নতুন র্যান্ডম WordPair দিয়ে পুনরায় অ্যাসাইন করে। এটি notifyListeners() ( ChangeNotifier) -কেও কল করে, যা নিশ্চিত করে যে MyAppState পর্যবেক্ষণকারী প্রত্যেকে যেন অবহিত হয়।
এখন শুধু বাটনের কলব্যাক থেকে getNext মেথডটি কল করতে হবে।
lib/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
অ্যাপটি এখন সেভ করে ব্যবহার করে দেখুন। প্রতিবার নেক্সট বাটন চাপলে এটি একটি নতুন এলোমেলো শব্দজোড়া তৈরি করবে।
পরবর্তী অংশে আপনি ইউজার ইন্টারফেসটিকে আরও সুন্দর করে তুলবেন।
৫. অ্যাপটিকে আরও সুন্দর করে তুলুন
এই মুহূর্তে অ্যাপটি দেখতে এইরকম।

খুব একটা ভালো না। অ্যাপটির মূল আকর্ষণ—এলোমেলোভাবে তৈরি হওয়া শব্দজোড়া—আরও স্পষ্টভাবে দেখা যাওয়া উচিত। সর্বোপরি, আমাদের ব্যবহারকারীরা তো এই অ্যাপটি মূলত এই কারণেই ব্যবহার করছেন! এছাড়াও, অ্যাপের বিষয়বস্তুগুলো অদ্ভুতভাবে কেন্দ্রচ্যুত, এবং পুরো অ্যাপটি একঘেয়েভাবে সাদাকালো।
এই অংশটি অ্যাপটির ডিজাইনের উপর কাজ করার মাধ্যমে এই সমস্যাগুলো সমাধান করে। এই অংশের চূড়ান্ত লক্ষ্যটি নিম্নরূপ:

একটি উইজেট বের করুন
বর্তমান শব্দজোড়া দেখানোর জন্য দায়ী লাইনটি এখন দেখতে এইরকম: Text(appState.current.asLowerCase) । এটিকে আরও জটিল কিছুতে পরিবর্তন করতে চাইলে, এই লাইনটিকে একটি আলাদা উইজেটে নিয়ে যাওয়া ভালো। আপনার UI-এর বিভিন্ন যৌক্তিক অংশের জন্য আলাদা উইজেট থাকা Flutter-এ জটিলতা ব্যবস্থাপনার একটি গুরুত্বপূর্ণ উপায়।
ফ্লটার উইজেট এক্সট্র্যাক্ট করার জন্য একটি রিফ্যাক্টরিং হেল্পার প্রদান করে, কিন্তু এটি ব্যবহার করার আগে নিশ্চিত হয়ে নিন যে, যে লাইনটি এক্সট্র্যাক্ট করা হচ্ছে সেটি শুধু তার প্রয়োজনীয় অংশটুকুই অ্যাক্সেস করছে। এই মুহূর্তে, লাইনটি appState অ্যাক্সেস করছে, কিন্তু এর আসলে শুধু বর্তমান শব্দজোড়াটি কী তা জানলেই চলবে।
সেই কারণে, MyHomePage উইজেটটি নিম্নরূপে পুনরায় লিখুন:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
চমৎকার। Text উইজেটটি এখন আর পুরো appState কে নির্দেশ করে না।
এখন, রিফ্যাক্টর মেনুটি খুলুন। ভিএস কোডে, আপনি দুটি উপায়ের একটিতে এটি করতে পারেন:
- যে কোডটি আপনি রিফ্যাক্টর করতে চান (এই ক্ষেত্রে
Text), সেটির উপর রাইট-ক্লিক করুন এবং ড্রপ-ডাউন মেনু থেকে Refactor... নির্বাচন করুন।
অথবা
- যে কোড অংশটি আপনি রিফ্যাক্টর করতে চান (এই ক্ষেত্রে
Text), সেটির উপর আপনার কার্সারটি নিয়ে যান এবংCtrl+.(Win/Linux) বাCmd+.(Mac) চাপুন।
রিফ্যাক্টর মেনু থেকে এক্সট্র্যাক্ট উইজেট নির্বাচন করুন। বিগকার্ড- এর মতো একটি নাম দিন এবং Enter চাপুন।
এটি স্বয়ংক্রিয়ভাবে বর্তমান ফাইলের শেষে BigCard একটি নতুন ক্লাস তৈরি করে। ক্লাসটি দেখতে অনেকটা নিচের মতো:
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
লক্ষ্য করুন, এই রিফ্যাক্টরিংয়ের পরেও অ্যাপটি কীভাবে কাজ করে চলেছে।
একটি কার্ড যোগ করুন
এখন সময় এসেছে এই নতুন উইজেটটিকে সেই আকর্ষণীয় UI কাঠামোতে পরিণত করার, যা আমরা এই অংশের শুরুতে কল্পনা করেছিলাম।
BigCard ক্লাস এবং এর ভেতরের build() মেথডটি খুঁজুন। আগের মতোই, Text উইজেটের Refactor মেনুটি খুলুন। তবে, এবার আপনি উইজেটটি এক্সট্র্যাক্ট করবেন না।
এর পরিবর্তে, 'Wrap with Padding ' নির্বাচন করুন। এটি Text উইজেটটির চারপাশে Padding নামে একটি নতুন প্যারেন্ট উইজেট তৈরি করে। সেভ করার পর, আপনি দেখতে পাবেন যে র্যান্ডম শব্দটি ইতিমধ্যেই আরও বেশি ফাঁকা জায়গা পেয়েছে।
ডিফল্ট মান 8.0 থেকে প্যাডিং বাড়ান। উদাহরণস্বরূপ, আরও প্রশস্ত প্যাডিংয়ের জন্য 20 মতো একটি মান ব্যবহার করুন।
এরপর, আরেক ধাপ উপরে যান। আপনার কার্সারটি Padding উইজেটের উপর রাখুন, রিফ্যাক্টর মেনুটি খুলুন এবং র্যাপ উইথ উইজেট... নির্বাচন করুন।
এর মাধ্যমে আপনি প্যারেন্ট উইজেট নির্দিষ্ট করতে পারবেন। 'Card' টাইপ করে এন্টার চাপুন।
এটি Padding উইজেটকে, এবং সেই সূত্রে Text , একটি Card উইজেট দিয়ে আবৃত করে।
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
অ্যাপটি এখন দেখতে অনেকটা এইরকম হবে:

থিম এবং স্টাইল
কার্ডটিকে আরও আকর্ষণীয় করে তুলতে, এটিকে আরও গাঢ় রঙ দিয়ে রাঙিয়ে দিন। আর যেহেতু একটি সামঞ্জস্যপূর্ণ রঙের বিন্যাস বজায় রাখা সবসময়ই ভালো, তাই রঙটি বেছে নেওয়ার জন্য অ্যাপের Theme ব্যবহার করুন।
BigCard এর build() মেথডে নিম্নলিখিত পরিবর্তনগুলো করুন।
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
এই দুটি নতুন লাইন অনেক কাজ করে:
- প্রথমে, কোডটি
Theme.of(context)ব্যবহার করে অ্যাপটির বর্তমান থিমটি অনুরোধ করে। - এরপর, কোডটি কার্ডের রঙকে থিমের
colorSchemeপ্রপার্টির রঙের সমান করে দেয়। কালার স্কিমে অনেকগুলো রঙ থাকে, এবংprimaryহলো অ্যাপটির সবচেয়ে সুস্পষ্ট ও পরিচয়বাহী রঙ।
কার্ডটি এখন অ্যাপটির প্রাথমিক রঙ দিয়ে রাঙানো হয়েছে:

আপনি উপরে স্ক্রল করে MyApp এ গিয়ে সেখানকার ColorScheme এর জন্য সিড কালারটি পরিবর্তন করে এই রঙটি এবং পুরো অ্যাপটির কালার স্কিম পরিবর্তন করতে পারেন।
লক্ষ্য করুন, রঙটি কত মসৃণভাবে পরিবর্তিত হচ্ছে। একে ইমপ্লিসিট অ্যানিমেশন বলা হয়। ফ্লাটারের অনেক উইজেট বিভিন্ন মানের মধ্যে মসৃণভাবে ইন্টারপোলেট করে, যাতে ইউজার ইন্টারফেসটি (UI) বিভিন্ন অবস্থার মধ্যে শুধু "লাফিয়ে" না যায়।
কার্ডের নিচে থাকা উঁচু বাটনটিরও রঙ বদলে যায়। নির্দিষ্ট মান কোড করার পরিবর্তে অ্যাপ-ব্যাপী Theme ব্যবহারের এটাই সুবিধা।
টেক্সট থিম
কার্ডটিতে এখনও একটি সমস্যা আছে: লেখাগুলো খুব ছোট এবং এর রঙ পড়া কঠিন। এটি ঠিক করতে, BigCard এর build() মেথডে নিম্নলিখিত পরিবর্তনগুলো করুন।
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
এই পরিবর্তনের পেছনের কারণ:
-
theme.textTheme,ব্যবহার করে আপনি অ্যাপের ফন্ট থিম অ্যাক্সেস করতে পারেন। এই ক্লাসেbodyMedium(মাঝারি আকারের সাধারণ টেক্সটের জন্য),caption(ছবির ক্যাপশনের জন্য), বাheadlineLarge(বড় শিরোনামের জন্য)-এর মতো মেম্বার রয়েছে। -
displayMediumপ্রপার্টিটি হলো একটি বড় স্টাইল যা টেক্সট প্রদর্শনের জন্য ব্যবহৃত হয়। এখানে 'display' শব্দটি টাইপোগ্রাফিক অর্থে ব্যবহৃত হয়েছে, যেমন 'display typeface '।displayMediumএর ডকুমেন্টেশনে বলা হয়েছে যে, "ডিসপ্লে স্টাইলগুলো সংক্ষিপ্ত ও গুরুত্বপূর্ণ টেক্সটের জন্য সংরক্ষিত"—যা আমাদের ব্যবহারের ক্ষেত্রটির সাথে হুবহু মিলে যায়। - থিমের
displayMediumপ্রপার্টিটি তাত্ত্বিকভাবেnullহতে পারে। ডার্ট, যে প্রোগ্রামিং ল্যাঙ্গুয়েজে আপনি এই অ্যাপটি লিখছেন, তা null-safe, তাই এটি আপনাকে সম্ভাব্যnullঅবজেক্টের মেথড কল করতে দেবে না। তবে, এই ক্ষেত্রে, আপনি ডার্টকে এটা নিশ্চিত করতে!অপারেটর ("ব্যাং অপারেটর") ব্যবহার করতে পারেন যে আপনি কী করছেন সে সম্পর্কে অবগত আছেন। (এই ক্ষেত্রেdisplayMediumঅবশ্যই null নয় । যদিও, আমরা কেন এটা জানি তা এই কোডল্যাবের আওতার বাইরে।) -
displayMediumএর উপরcopyWith()কল করলে, আপনার নির্ধারিত পরিবর্তনসহ টেক্সট স্টাইলের একটি অনুলিপি ফেরত আসে। এক্ষেত্রে, আপনি শুধুমাত্র টেক্সটের রঙ পরিবর্তন করছেন। - নতুন রঙটি পেতে, আপনাকে আবারও অ্যাপের থিমে প্রবেশ করতে হবে। কালার স্কিমের '
onPrimaryপ্রপার্টিটি এমন একটি রঙ নির্ধারণ করে, যা অ্যাপের প্রাইমারি কালার হিসেবে ব্যবহারের জন্য উপযুক্ত।
অ্যাপটি এখন দেখতে নিচের মতো হবে:

আপনার ইচ্ছে হলে কার্ডটি আরও পরিবর্তন করতে পারেন। এখানে কিছু ধারণা দেওয়া হলো:
-
copyWith()আপনাকে শুধু রঙই নয়, টেক্সট স্টাইলের আরও অনেক কিছু পরিবর্তন করার সুযোগ দেয়। আপনি যে সমস্ত প্রোপার্টি পরিবর্তন করতে পারবেন তার সম্পূর্ণ তালিকা পেতে,copyWith()-এর বন্ধনীর ভিতরে যেকোনো জায়গায় আপনার কার্সর রাখুন এবংCtrl+Shift+Space(Win/Linux) বাCmd+Shift+Space(Mac) চাপুন। - একইভাবে, আপনি
Cardউইজেট সম্পর্কে আরও অনেক কিছু পরিবর্তন করতে পারেন। উদাহরণস্বরূপ, আপনিelevationপ্যারামিটারের মান বাড়িয়ে কার্ডের শ্যাডো বড় করতে পারেন। - রং নিয়ে পরীক্ষা-নিরীক্ষা করে দেখুন।
theme.colorScheme.primaryছাড়াও.secondary,.surfaceএবং আরও অসংখ্য কালার রয়েছে। এই সব রঙেরইonPrimaryসমতুল্য কালার আছে।
প্রবেশগম্যতা উন্নত করুন
ফ্লাটার অ্যাপগুলোকে ডিফল্টভাবেই অ্যাক্সেসযোগ্য করে তোলে। উদাহরণস্বরূপ, প্রতিটি ফ্লাটার অ্যাপ TalkBack এবং VoiceOver-এর মতো স্ক্রিন রিডারগুলোর কাছে অ্যাপের সমস্ত টেক্সট এবং ইন্টারেক্টিভ এলিমেন্ট সঠিকভাবে প্রদর্শন করে।

তবে, কখনও কখনও কিছু কাজ করার প্রয়োজন হয়। এই অ্যাপটির ক্ষেত্রে, স্ক্রিন রিডারের কিছু তৈরি হওয়া শব্দজোড়া উচ্চারণ করতে সমস্যা হতে পারে। যদিও ‘cheaphead’ শব্দটির দুটি শব্দ শনাক্ত করতে মানুষের কোনো সমস্যা হয় না, একটি স্ক্রিন রিডার শব্দটির মাঝখানে থাকা ‘ph’- কে ‘f’ হিসেবে উচ্চারণ করতে পারে।
এর একটি সমাধান হলো pair.asLowerCase "${pair.first} ${pair.second}" দিয়ে প্রতিস্থাপন করা। পরেরটি pair এর মধ্যে থাকা দুটি শব্দ থেকে একটি স্ট্রিং (যেমন "cheap head" ) তৈরি করতে স্ট্রিং ইন্টারপোলেশন ব্যবহার করে। একটি যৌগিক শব্দের পরিবর্তে দুটি পৃথক শব্দ ব্যবহার করলে স্ক্রিন রিডারগুলো সেগুলোকে সঠিকভাবে শনাক্ত করতে পারে এবং এটি দৃষ্টি প্রতিবন্ধী ব্যবহারকারীদের আরও ভালো অভিজ্ঞতা প্রদান করে।
তবে, আপনি হয়তো pair.asLowerCase এর দৃশ্যগত সরলতা বজায় রাখতে চাইতে পারেন। স্ক্রিন রিডারদের জন্য আরও উপযুক্ত একটি শব্দার্থিক বিষয়বস্তু দিয়ে টেক্সট উইজেটের দৃশ্যগত বিষয়বস্তুকে ওভাররাইড করতে Text এর semanticsLabel প্রপার্টি ব্যবহার করুন:
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
এখন, স্ক্রিন রিডারগুলো তৈরি হওয়া প্রতিটি শব্দজোড়া সঠিকভাবে উচ্চারণ করে, অথচ ইউজার ইন্টারফেস (UI) একই থাকে। আপনার ডিভাইসে একটি স্ক্রিন রিডার ব্যবহার করে এটি বাস্তবে পরীক্ষা করে দেখুন।
UI কেন্দ্র করুন
এখন যেহেতু এলোমেলো শব্দজোড়াটি যথেষ্ট দৃষ্টিনন্দনভাবে উপস্থাপন করা হয়েছে, এটিকে অ্যাপের উইন্ডো/স্ক্রিনের কেন্দ্রে রাখার সময় হয়েছে।
প্রথমত, মনে রাখবেন যে BigCard একটি Column এর অংশ। ডিফল্টরূপে, কলামগুলো তাদের চাইল্ডগুলোকে সবার উপরে রাখে, কিন্তু আমরা এটি পরিবর্তন করতে পারি। MyHomePage এর build() মেথডে যান এবং নিম্নলিখিত পরিবর্তনটি করুন:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
এটি শিশুদেরকে Column প্রধান (উল্লম্ব) অক্ষ বরাবর কেন্দ্রে স্থাপন করে।

চাইল্ডগুলো ইতিমধ্যেই কলামের ক্রস অ্যাক্সিস বরাবর সেন্টারে রয়েছে (অন্য কথায়, সেগুলো ইতিমধ্যেই আনুভূমিকভাবে সেন্টারে আছে)। কিন্তু Column নিজে Scaffold ভিতরে সেন্টারে নেই। আমরা উইজেট ইন্সপেক্টর ব্যবহার করে এটি যাচাই করতে পারি।
উইজেট ইন্সপেক্টরটি এই কোডল্যাবের আওতার বাইরে, কিন্তু আপনি দেখতে পারেন যে যখন Column হাইলাইট করা হয়, তখন এটি অ্যাপের পুরো প্রস্থ জুড়ে থাকে না। এটি কেবল তার চাইল্ড উইজেটগুলোর প্রয়োজন অনুযায়ী আনুভূমিক স্থান দখল করে।
আপনি সরাসরি কলামটিকে মাঝখানে আনতে পারেন। আপনার কার্সারটি ‘ Column উপর রাখুন, Refactor মেনুটি খুলুন ( Ctrl+. বা Cmd+. চেপে), এবং ‘Wrap with Center’ নির্বাচন করুন।
অ্যাপটি এখন দেখতে নিচের মতো হবে:

আপনি চাইলে এটি আরও কিছুটা পরিবর্তন করতে পারেন।
- আপনি
BigCardউপরেরTextউইজেটটি সরিয়ে ফেলতে পারেন। এমন যুক্তি দেওয়া যেতে পারে যে বর্ণনামূলক লেখাটির ("একটি এলোমেলো চমৎকার ধারণা:") আর প্রয়োজন নেই, কারণ এটি ছাড়াও UI-টি বোধগম্য। আর এভাবে দেখতেও বেশি পরিচ্ছন্ন লাগে। - আপনি
BigCardএবংElevatedButtonমাঝে একটিSizedBox(height: 10)উইজেটও যোগ করতে পারেন। এভাবে, উইজেট দুটির মধ্যে আরেকটু বেশি ব্যবধান তৈরি হয়।SizedBoxউইজেটটি শুধু জায়গা নেয় এবং নিজে থেকে কিছু রেন্ডার করে না। এটি সাধারণত দৃশ্যমান 'ফাঁক' তৈরি করতে ব্যবহৃত হয়।
ঐচ্ছিক পরিবর্তনগুলোর সাথে, MyHomePage এই কোডটি থাকে:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
এবং অ্যাপটি দেখতে নিম্নরূপ:

পরবর্তী অংশে, আপনি তৈরি হওয়া শব্দগুলোকে পছন্দের তালিকায় (বা 'লাইক') রাখার সুবিধা যোগ করবেন।
৬. কার্যকারিতা যোগ করুন
অ্যাপটি কাজ করে এবং মাঝে মাঝে আকর্ষণীয় শব্দজোড়াও দেখায়। কিন্তু যখনই ব্যবহারকারী 'Next' বোতামে ক্লিক করেন, প্রতিটি শব্দজোড়া চিরতরে অদৃশ্য হয়ে যায়। সেরা পরামর্শগুলো 'মনে রাখার' কোনো উপায় থাকলে ভালো হতো: যেমন একটি 'Like' বাটন।

ব্যবসায়িক যুক্তি যোগ করুন
MyAppState এ স্ক্রোল করুন এবং নিম্নলিখিত কোডটি যোগ করুন:
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
পরিবর্তনগুলো পরীক্ষা করুন:
- আপনি
MyAppStateএfavoritesনামে একটি নতুন প্রপার্টি যোগ করেছেন। এই প্রপার্টিটি একটি খালি তালিকা:[]দিয়ে ইনিশিয়ালাইজ করা হয়েছে। - আপনি জেনেরিক ব্যবহার করে এটাও নির্দিষ্ট করে দিয়েছেন যে, লিস্টটিতে শুধুমাত্র শব্দজোড়া:
<WordPair>[]থাকতে পারবে। এটি আপনার অ্যাপকে আরও শক্তিশালী করে তোলে—আপনি যদি এতেWordPairছাড়া অন্য কিছু যোগ করার চেষ্টা করেন, তাহলে ডার্ট আপনার অ্যাপটি চালাতেই অস্বীকার করে। ফলস্বরূপ, আপনিfavoritesলিস্টটি এই জেনে ব্যবহার করতে পারেন যে, সেখানে কখনও কোনো অবাঞ্ছিত অবজেক্ট (যেমনnull) লুকিয়ে থাকতে পারবে না।
- আপনি
toggleFavorite()নামে একটি নতুন মেথডও যোগ করেছেন, যা পছন্দের তালিকায় থাকা বর্তমান শব্দজোড়াটিকে হয় তালিকা থেকে সরিয়ে দেয় (যদি এটি আগে থেকেই সেখানে থাকে), অথবা যোগ করে (যদি এটি এখনও সেখানে না থাকে)। উভয় ক্ষেত্রেই, কোডটি এরপরnotifyListeners();কল করে।
বোতামটি যোগ করুন
'বিজনেস লজিক'-এর কাজ শেষ, এবার আবার ইউজার ইন্টারফেস নিয়ে কাজ করার পালা। 'নেক্সট' বাটনের বাম পাশে 'লাইক' বাটনটি রাখতে একটি ' Row প্রয়োজন। ' Row উইজেটটি হলো Column -এর (Column) হরাইজন্টাল সংস্করণ, যা আপনি আগেই দেখেছেন।
প্রথমে, বিদ্যমান বাটনটিকে একটি Row এর মধ্যে রাখুন। MyHomePage এর build() মেথডে যান, ElevatedButton এর উপর আপনার কার্সর রাখুন, Ctrl+. বা Cmd+. চেপে Refactor মেনুটি খুলুন এবং Wrap with Row নির্বাচন করুন।
সেভ করার সময় আপনি লক্ষ্য করবেন যে, Row একইভাবে Column এর মতো কাজ করে—ডিফল্টরূপে এটি তার চাইল্ডগুলোকে বাম দিকে রাখে। ( Column তার চাইল্ডগুলোকে উপরে রাখত।) এটি ঠিক করার জন্য, আপনি আগের পদ্ধতিটিই ব্যবহার করতে পারেন, কিন্তু mainAxisAlignment সাথে। তবে, শিক্ষামূলক (শেখার) উদ্দেশ্যে, mainAxisSize ব্যবহার করুন। এটি Row উপলব্ধ সমস্ত অনুভূমিক স্থান না নিতে নির্দেশ দেয়।
নিম্নলিখিত পরিবর্তনটি করুন:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
ইউআই আবার আগের অবস্থায় ফিরে এসেছে।

এরপর, লাইক বাটনটি যোগ করুন এবং এটিকে toggleFavorite() এর সাথে সংযুক্ত করুন। একটি চ্যালেঞ্জ হিসেবে, প্রথমে নিচের কোড ব্লকটি না দেখে এটি নিজে নিজে করার চেষ্টা করুন।

নিচে যেভাবে করা হয়েছে ঠিক সেভাবে না করলেও চলবে। সত্যি বলতে, হার্ট আইকনটি নিয়ে চিন্তা করার দরকার নেই, যদি না আপনি সত্যিই একটি বড় চ্যালেঞ্জ নিতে চান।
ব্যর্থ হওয়াটাও সম্পূর্ণ স্বাভাবিক—সর্বোপরি, ফ্লাটারের সাথে এটা আপনার প্রথম ঘণ্টা।

MyHomePage এ দ্বিতীয় বাটনটি যোগ করার একটি উপায় এখানে দেওয়া হলো। এবার, একটি আইকনসহ বাটন তৈরি করতে ElevatedButton.icon() কনস্ট্রাক্টরটি ব্যবহার করুন। এবং build মেথডের শুরুতে, বর্তমান শব্দজোড়াটি আগে থেকেই favorites-এ আছে কি না, তার উপর নির্ভর করে উপযুক্ত আইকনটি বেছে নিন। এছাড়াও, দুটি বাটনকে কিছুটা দূরে রাখার জন্য আবারও SizedBox এর ব্যবহার লক্ষ্য করুন।
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
অ্যাপটি দেখতে নিম্নলিখিতের মতো হওয়া উচিত:
দুর্ভাগ্যবশত, ব্যবহারকারী পছন্দের জিনিসগুলো দেখতে পাচ্ছেন না। আমাদের অ্যাপে একটি সম্পূর্ণ আলাদা স্ক্রিন যোগ করার সময় হয়েছে। পরবর্তী বিভাগে দেখা হবে!
৭. নেভিগেশন রেল যোগ করুন
বেশিরভাগ অ্যাপে সবকিছু একটি স্ক্রিনে দেখানো সম্ভব হয় না। এই নির্দিষ্ট অ্যাপটিতে হয়তো তা সম্ভব, কিন্তু শিক্ষামূলক উদ্দেশ্যে আপনি ব্যবহারকারীর পছন্দের জিনিসগুলোর জন্য একটি আলাদা স্ক্রিন তৈরি করবেন। এই দুটি স্ক্রিনের মধ্যে পরিবর্তন করার জন্য, আপনি আপনার প্রথম StatefulWidget ইমপ্লিমেন্ট করবেন।

এই ধাপের মূল অংশে যত দ্রুত সম্ভব পৌঁছানোর জন্য, MyHomePage ২টি আলাদা উইজেটে ভাগ করুন।
MyHomePage এর সম্পূর্ণ অংশ নির্বাচন করে মুছে ফেলুন এবং নিচের কোডটি দিয়ে প্রতিস্থাপন করুন:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: 0,
onDestinationSelected: (value) {
print('selected: $value');
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
);
}
}
// ...
সেভ করার পর আপনি দেখবেন যে UI-এর ভিজ্যুয়াল অংশটি প্রস্তুত—কিন্তু এটি কাজ করে না। নেভিগেশন রেইলে থাকা ♥︎ (হার্ট) আইকনে ক্লিক করলে কিছুই হয় না।

পরিবর্তনগুলো পরীক্ষা করুন।
- প্রথমে লক্ষ্য করুন যে,
MyHomePageএর সম্পূর্ণ বিষয়বস্তুGeneratorPageনামক একটি নতুন উইজেটে স্থানান্তরিত হয়েছে। পুরোনোMyHomePageউইজেটের একমাত্র যে অংশটি স্থানান্তরিত হয়নি, তা হলোScaffold। - নতুন
MyHomePageটিতে একটিRowরয়েছে, যার দুটি চাইল্ড উইজেট আছে। প্রথম উইজেটটি হলোSafeAreaএবং দ্বিতীয়টি একটিExpandedউইজেট। -
SafeAreaনিশ্চিত করে যে এর চাইল্ড উইজেটটি কোনো হার্ডওয়্যার নচ বা স্ট্যাটাস বার দ্বারা আড়াল না হয়। উদাহরণস্বরূপ, এই অ্যাপটিতে উইজেটটিNavigationRailঘিরে রাখে, যাতে নেভিগেশন বাটনগুলো কোনো মোবাইল স্ট্যাটাস বার দ্বারা আড়াল না হয়ে যায়। - আপনি
NavigationRailএ থাকাextended: falseলাইনটিকেtrueতে পরিবর্তন করতে পারেন। এর ফলে আইকনগুলোর পাশে লেবেলগুলো দেখা যাবে। পরবর্তী ধাপে আপনি শিখবেন, অ্যাপটিতে পর্যাপ্ত আনুভূমিক স্থান থাকলে কীভাবে এটি স্বয়ংক্রিয়ভাবে করা যায়। - ন্যাভিগেশন রেইলে দুটি গন্তব্য ( হোম এবং ফেভারিটস ) রয়েছে, যেগুলোর নিজস্ব আইকন এবং লেবেল আছে। এটি বর্তমান
selectedIndexনির্ধারণ করে। নির্বাচিত ইনডেক্স শূন্য হলে প্রথম গন্তব্য, এক হলে দ্বিতীয় গন্তব্য এবং এভাবেই চলতে থাকে। আপাতত, এটি শূন্যতে হার্ড কোড করা আছে। - নেভিগেশন রেইলটি আরও নির্ধারণ করে যে, ব্যবহারকারী যখন
onDestinationSelectedব্যবহার করে গন্তব্যগুলোর মধ্যে একটি নির্বাচন করেন, তখন কী ঘটবে। বর্তমানে, অ্যাপটি কেবলprint()ব্যবহার করে অনুরোধ করা ইনডেক্স মানটি আউটপুট করে। -
Rowএর দ্বিতীয় চাইল্ড হলোExpandedউইজেট। Expanded উইজেটগুলো সারি এবং কলামে অত্যন্ত উপযোগী—এগুলো আপনাকে এমন লেআউট তৈরি করতে দেয় যেখানে কিছু চাইল্ড শুধুমাত্র তাদের প্রয়োজনীয় জায়গা নেয় (এই ক্ষেত্রেSafeArea) এবং অন্যান্য উইজেটগুলো বাকি জায়গার যতটা সম্ভব বেশি অংশ নেয় (এই ক্ষেত্রেExpanded)।Expandedউইজেটগুলোকে "লোভী" হিসেবে ভাবা যেতে পারে। আপনি যদি এই উইজেটের ভূমিকা আরও ভালোভাবে বুঝতে চান, তাহলেSafeAreaউইজেটটিকে আরেকটিExpandedদিয়ে র্যাপ করে দেখুন। এর ফলে যে লেআউটটি তৈরি হবে তা দেখতে অনেকটা এইরকম:

- দুটি
Expandedউইজেট উপলব্ধ সমস্ত আনুভূমিক স্থান নিজেদের মধ্যে ভাগ করে নিয়েছে, যদিও নেভিগেশন রেলটির জন্য বাম দিকে সামান্য একটু জায়গাই যথেষ্ট ছিল। -
Expandedউইজেটটির ভিতরে একটি রঙিনContainerআছে, এবং সেই কন্টেইনারের ভিতরেGeneratorPageরয়েছে।
স্টেটলেস বনাম স্টেটফুল উইজেট
এখন পর্যন্ত, MyAppState আপনার স্টেটের সমস্ত চাহিদা পূরণ করেছে। একারণেই আপনি এ পর্যন্ত যতগুলো উইজেট লিখেছেন, তার সবই স্টেট- বিহীন । সেগুলোর নিজস্ব কোনো পরিবর্তনযোগ্য স্টেট নেই। কোনো উইজেটই নিজেকে পরিবর্তন করতে পারে না — সেগুলোকে অবশ্যই MyAppState মধ্য দিয়ে যেতে হয়।
এটা শীঘ্রই বদলে যেতে চলেছে।
ন্যাভিগেশন রেইলের ' selectedIndex এর মান ধরে রাখার জন্য আপনার একটি উপায় প্রয়োজন। এছাড়াও, আপনি onDestinationSelected কলব্যাকের ভেতর থেকে এই মানটি পরিবর্তন করতে সক্ষম হতে চান।
আপনি MyAppState এর আরও একটি প্রপার্টি হিসেবে selectedIndex যোগ করতে পারেন । এবং এটি কাজও করবে। কিন্তু আপনি কল্পনা করতে পারেন যে, যদি প্রতিটি উইজেট তাদের মান এতে সংরক্ষণ করে, তাহলে অ্যাপ স্টেট দ্রুত অযৌক্তিকভাবে বড় হয়ে যাবে।

কিছু স্টেট শুধুমাত্র একটি উইজেটের জন্যই প্রাসঙ্গিক, তাই এটি সেই উইজেটের সাথেই থাকা উচিত।
এবার আসা যাক StatefulWidget কথায়, এটি এমন এক ধরনের উইজেট যার State আছে। প্রথমে, MyHomePage কে একটি স্টেটফুল উইজেটে রূপান্তর করুন।
MyHomePage এর প্রথম লাইনে (যেটি class MyHomePage... দিয়ে শুরু হয়েছে) আপনার কার্সারটি রাখুন এবং Ctrl+. বা Cmd+. ব্যবহার করে Refactor মেনুটি খুলুন। এরপর, Convert to StatefulWidget নির্বাচন করুন।
IDE আপনার জন্য _MyHomePageState নামে একটি নতুন ক্লাস তৈরি করে। এই ক্লাসটি State এক্সটেন্ড করে, এবং তাই এটি নিজের ভ্যালুগুলো নিজেই পরিচালনা করতে পারে। (এটি নিজেকে পরিবর্তন করতে পারে।) আরও লক্ষ্য করুন যে, পুরোনো, স্টেটলেস উইজেটের build মেথডটি উইজেটের ভেতরে না থেকে _MyHomePageState এ স্থানান্তরিত হয়েছে। এটিকে হুবহু সরানো হয়েছে — build মেথডের ভেতরে কোনো কিছুই পরিবর্তন হয়নি। এটি এখন কেবল অন্য একটি জায়গায় অবস্থান করছে।
setState
নতুন স্টেটফুল উইজেটটির শুধু একটি ভেরিয়েবল ট্র্যাক করা প্রয়োজন: selectedIndex । _MyHomePageState এ নিম্নলিখিত ৩টি পরিবর্তন করুন:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
পরিবর্তনগুলো পরীক্ষা করুন:
- আপনি
selectedIndexএকটি নতুন ভেরিয়েবল তৈরি করেন এবং এর মান0নির্ধারণ করেন। - এখন পর্যন্ত ব্যবহৃত হার্ড-কোডেড
0এর পরিবর্তে, আপনিNavigationRailসংজ্ঞায় এই নতুন ভেরিয়েবলটি ব্যবহার করবেন। - যখন
onDestinationSelectedকলব্যাকটি কল করা হয়, তখন নতুন মানটি কেবল কনসোলে প্রিন্ট না করে, একটিsetState()কলের ভিতরে সেটিকেselectedIndexএ অ্যাসাইন করা হয়। এই কলটি পূর্বে ব্যবহৃতnotifyListeners()মেথডের মতোই—এটি UI-এর আপডেট নিশ্চিত করে।
ন্যাভিগেশন রেলটি এখন ব্যবহারকারীর ইন্টারঅ্যাকশনে সাড়া দিচ্ছে। কিন্তু ডানদিকের প্রসারিত অংশটি একই থাকছে। এর কারণ হলো, কোন স্ক্রিনটি প্রদর্শিত হবে তা নির্ধারণ করতে কোডটি selectedIndex ব্যবহার করছে না।
নির্বাচিত সূচক ব্যবহার করুন
Place the following code at the top of _MyHomePageState 's build method, just before return Scaffold :
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
Examine this piece of code:
- The code declares a new variable,
page, of the typeWidget. - Then, a switch statement assigns a screen to
page, according to the current value inselectedIndex. - Since there's no
FavoritesPageyet, usePlaceholder; a handy widget that draws a crossed rectangle wherever you place it, marking that part of the UI as unfinished.

- Applying the fail-fast principle , the switch statement also makes sure to throw an error if
selectedIndexis neither 0 or 1. This helps prevent bugs down the line. If you ever add a new destination to the navigation rail and forget to update this code, the program crashes in development (as opposed to letting you guess why things don't work, or letting you publish a buggy code into production).
Now that page contains the widget you want to show on the right, you can probably guess what other change is needed.
Here's _MyHomePageState after that single remaining change:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
The app now switches between our GeneratorPage and the placeholder that will soon become the Favorites page.
Responsiveness
Next, make the navigation rail responsive. That is to say, make it automatically show the labels (using extended: true ) when there's enough room for them.

Flutter provides several widgets that help you make your apps automatically responsive. For example, Wrap is a widget similar to Row or Column that automatically wraps children to the next "line" (called "run") when there isn't enough vertical or horizontal space. There's FittedBox , a widget that automatically fits its child into available space according to your specifications.
But NavigationRail doesn't automatically show labels when there's enough space because it can't know what is enough space in every context. It's up to you, the developer, to make that call.
Say you decide to show labels only if MyHomePage is at least 600 pixels wide.
The widget to use, in this case, is LayoutBuilder . It lets you change your widget tree depending on how much available space you have.
Once again, use Flutter's Refactor menu in VS Code to make the required changes. This time, though, it's a little more complicated:
- Inside
_MyHomePageState'sbuildmethod, put your cursor onScaffold. - Call up the Refactor menu with
Ctrl+.(Windows/Linux) orCmd+.(Mac). - Select Wrap with Builder and press Enter .
- Modify the name of the newly added
BuildertoLayoutBuilder. - Modify the callback parameter list from
(context)to(context, constraints).
LayoutBuilder 's builder callback is called every time the constraints change. This happens when, for example:
- The user resizes the app's window
- The user rotates their phone from portrait mode to landscape mode, or back
- Some widget next to
MyHomePagegrows in size, makingMyHomePage's constraints smaller
Now your code can decide whether to show the label by querying the current constraints . Make the following single-line change to _MyHomePageState 's build method:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.maxWidth >= 600, // ← Here.
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// ...
Now, your app responds to its environment, such as screen size, orientation, and platform! In other words, it's responsive!.
The only work that remains is to replace that Placeholder with an actual Favorites screen. That's covered in the next section.
8. Add a new page
Remember the Placeholder widget we used instead of the Favorites page?
It's time to fix this.
If you feel adventurous, try to do this step by yourself. Your goal is to show the list of favorites in a new stateless widget, FavoritesPage , and then show that widget instead of the Placeholder .
Here are a few pointers:
- When you want a
Columnthat scrolls, use theListViewwidget. - Remember, access the
MyAppStateinstance from any widget usingcontext.watch<MyAppState>(). - If you also want to try a new widget,
ListTilehas properties liketitle(generally for text),leading(for icons or avatars) andonTap(for interactions). However, you can achieve similar effects with the widgets you already know. - Dart allows using
forloops inside collection literals. For example, ifmessagescontains a list of strings, you can have code like the following:

On the other hand, if you're more familiar with functional programming, Dart also lets you write code like messages.map((m) => Text(m)).toList() . And, of course, you can always create a list of widgets and imperatively add to it inside the build method.
The advantage of adding the Favorites page yourself is that you learn more by making your own decisions. The disadvantage is that you might run into trouble that you aren't yet able to solve by yourself. Remember: failing is okay, and is one of the most important elements of learning. Nobody expects you to nail Flutter development in your first hour, and neither should you.

What follows is just one way to implement the favorites page. How it's implemented will (hopefully) inspire you to play with the code—improve the UI and make it your own.
Here's the new FavoritesPage class:
lib/main.dart
// ...
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
Here's what the widget does:
- It gets the current state of the app.
- If the list of favorites is empty, it shows a centered message: No favorites yet.
- Otherwise, it shows a (scrollable) list.
- The list starts with a summary (for example, You have 5 favorites. ).
- The code then iterates through all the favorites, and constructs a
ListTilewidget for each one.
All that remains now is to replace the Placeholder widget with a FavoritesPage . And voilá!
You can get the final code of this app in the codelab repo on GitHub.
9. Next steps
অভিনন্দন!
Look at you! You took a non-functional scaffold with a Column and two Text widgets, and made it into a responsive, delightful little app.

What we've covered
- The basics of how Flutter works
- Creating layouts in Flutter
- Connecting user interactions (like button presses) to app behavior
- Keeping your Flutter code organized
- Making your app responsive
- Achieving a consistent look & feel of your app
এরপর কী?
- Experiment more with the app you wrote during this lab.
- Look at the code of this advanced version of the same app, to see how you can add animated lists, gradients, cross-fades, and more.
- Follow your learning journey by going to flutter.dev/learn .