আপনার ফ্লটার অ্যাপটিকে বিরক্তিকর থেকে সুন্দরের দিকে নিয়ে যান

আপনার ফ্লটার অ্যাপটিকে বিরক্তিকর থেকে সুন্দরের দিকে নিয়ে যান

এই কোডল্যাব সম্পর্কে

subjectজুন ২৪, ২০২৫-এ শেষবার আপডেট করা হয়েছে
account_circleThe Flutter Team-এর লেখা

1. ভূমিকা

Flutter হল Google-এর UI টুলকিট যা একটি একক কোডবেস থেকে মোবাইল, ওয়েব এবং ডেস্কটপের জন্য সুন্দর, স্থানীয়ভাবে সংকলিত অ্যাপ্লিকেশন তৈরির জন্য। ফ্লাটার বিদ্যমান কোডের সাথে কাজ করে, বিশ্বজুড়ে বিকাশকারী এবং সংস্থাগুলি ব্যবহার করে এবং এটি বিনামূল্যে এবং ওপেন সোর্স।

এই কোডল্যাবে, আপনি একটি ফ্লাটার মিউজিক অ্যাপ্লিকেশান বাড়ান, এটিকে বিরক্তিকর থেকে সুন্দরের দিকে নিয়ে যান। এটি সম্পন্ন করার জন্য, এই কোডল্যাবটি উপাদান 3 এ প্রবর্তিত সরঞ্জাম এবং API ব্যবহার করে।

আপনি কি শিখবেন

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

পূর্বশর্ত

এই কোডল্যাব ধরে নেয় যে আপনার কিছু ফ্লটার অভিজ্ঞতা আছে। যদি তা না হয়, আপনি প্রথমে মৌলিক বিষয়গুলো শিখতে চাইতে পারেন। নিম্নলিখিত লিঙ্কগুলি সহায়ক:

আপনি কি নির্মাণ করবেন

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

নিম্নলিখিত ভিডিওগুলি দেখায় যে এই কোডল্যাবের সমাপ্তিতে অ্যাপটি কীভাবে কাজ করে:

আপনি এই কোডল্যাব থেকে কি শিখতে চান?

2. আপনার ফ্লটার ডেভেলপমেন্ট এনভায়রনমেন্ট সেট আপ করুন

এই ল্যাবটি সম্পূর্ণ করার জন্য আপনার দুটি টুকরো সফ্টওয়্যার প্রয়োজন - ফ্লাটার SDK এবং একটি সম্পাদক

আপনি এই ডিভাইসগুলির যেকোনো একটি ব্যবহার করে কোডল্যাব চালাতে পারেন:

  • আপনার কম্পিউটারের সাথে সংযুক্ত এবং বিকাশকারী মোডে সেট করা একটি শারীরিক Android বা iOS ডিভাইস৷
  • আইওএস সিমুলেটর (এক্সকোড সরঞ্জাম ইনস্টল করা প্রয়োজন)।
  • অ্যান্ড্রয়েড এমুলেটর (অ্যান্ড্রয়েড স্টুডিওতে সেটআপ প্রয়োজন)।
  • একটি ব্রাউজার (ডিবাগিংয়ের জন্য Chrome প্রয়োজন)।
  • একটি Windows , Linux , বা macOS ডেস্কটপ অ্যাপ্লিকেশন হিসাবে। আপনি যে প্ল্যাটফর্মে স্থাপন করার পরিকল্পনা করছেন সেখানে আপনাকে অবশ্যই বিকাশ করতে হবে। সুতরাং, আপনি যদি একটি উইন্ডোজ ডেস্কটপ অ্যাপ বিকাশ করতে চান, তাহলে যথাযথ বিল্ড চেইন অ্যাক্সেস করতে আপনাকে অবশ্যই উইন্ডোজে বিকাশ করতে হবে। অপারেটিং সিস্টেম-নির্দিষ্ট প্রয়োজনীয়তা রয়েছে যা docs.flutter.dev/desktop- এ বিস্তারিতভাবে কভার করা হয়েছে।

3. কোডল্যাব স্টার্টার অ্যাপ পান

GitHub থেকে এটি ক্লোন করুন

GitHub থেকে এই কোডল্যাব ক্লোন করতে, নিম্নলিখিত কমান্ডগুলি চালান:

git clone https://github.com/flutter/codelabs.git
cd codelabs/boring_to_beautiful/step_01/

সবকিছু কাজ করছে কিনা তা নিশ্চিত করতে, নীচে দেখানো হিসাবে একটি ডেস্কটপ অ্যাপ্লিকেশন হিসাবে Flutter অ্যাপ্লিকেশনটি চালান। বিকল্পভাবে, আপনার IDE-তে এই প্রকল্পটি খুলুন এবং অ্যাপ্লিকেশন চালানোর জন্য এর টুলিং ব্যবহার করুন।

flutter run

সফলতার ! MyArtist এর হোম স্ক্রিনের জন্য স্টার্টার কোড চলমান হওয়া উচিত। আপনি MyArtist হোম স্ক্রীন দেখতে হবে. এটি ডেস্কটপে সুন্দর দেখায়, কিন্তু মোবাইল... দুর্দান্ত নয়। এক জিনিসের জন্য, এটি খাঁজকে সম্মান করে না। চিন্তা করবেন না, আপনি এটি ঠিক করবেন!

1e67c60667821082.pngd1139cde225de452.png

কোড ঘুরে দেখুন

এরপরে, কোডটি ঘুরে দেখুন।

lib/src/features/home/view/home_screen.dart খুলুন, যাতে নিম্নলিখিতগুলি রয়েছে:

lib/src/features/home/view/home_screen.dart

import 'package:flutter/material.dart';

import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';

class HomeScreen extends StatefulWidget {
 
const HomeScreen({super.key});

 
@override
 
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
 
@override
 
Widget build(BuildContext context) {
   
final PlaylistsProvider playlistProvider = PlaylistsProvider();
   
final List<Playlist> playlists = playlistProvider.playlists;
   
final Playlist topSongs = playlistProvider.topSongs;
   
final Playlist newReleases = playlistProvider.newReleases;
   
final ArtistsProvider artistsProvider = ArtistsProvider();
   
final List<Artist> artists = artistsProvider.artists;
   
return LayoutBuilder(
     
builder: (context, constraints) {
       
return Scaffold(
         
body: SingleChildScrollView(
           
child: AdaptiveColumn(
             
children: [
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     
children: [
                       
Expanded(
                         
child: Text(
                           
'Good morning',
                           
style: context.displaySmall,
                         
),
                       
),
                       
const SizedBox(width: 20),
                       
const BrightnessToggle(),
                     
],
                   
),
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
children: [
                     
const HomeHighlight(),
                     
LayoutBuilder(
                       
builder: (context, constraints) => HomeArtists(
                         
artists: artists,
                         
constraints: constraints,
                       
),
                     
),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
crossAxisAlignment: CrossAxisAlignment.start,
                   
children: [
                     
Padding(
                       
padding: const EdgeInsets.all(2),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

এই ফাইলটি material.dart আমদানি করে এবং দুটি শ্রেণী ব্যবহার করে একটি রাষ্ট্রীয় উইজেট প্রয়োগ করে:

  • import বিবৃতি উপাদান উপাদান উপলব্ধ করে তোলে.
  • HomeScreen ক্লাসটি প্রদর্শিত পুরো পৃষ্ঠাটিকে উপস্থাপন করে।
  • _HomeScreenState ক্লাসের build() পদ্ধতি উইজেট ট্রির মূল তৈরি করে, যা UI-তে সমস্ত উইজেট কীভাবে তৈরি হয় তা প্রভাবিত করে।

4. টাইপোগ্রাফির সুবিধা নিন

পাঠ্য সর্বত্র আছে। পাঠ্য ব্যবহারকারীর সাথে যোগাযোগ করার একটি কার্যকর উপায়। আপনার অ্যাপটি কি বন্ধুত্বপূর্ণ এবং মজাদার, বা সম্ভবত বিশ্বস্ত এবং পেশাদার হওয়ার উদ্দেশ্যে? আপনার প্রিয় ব্যাঙ্কিং অ্যাপ কমিক সানস ব্যবহার না করার একটি কারণ রয়েছে। কীভাবে পাঠ্য উপস্থাপন করা হয় তা আপনার অ্যাপ সম্পর্কে ব্যবহারকারীর প্রথম ছাপকে আকার দেয়। পাঠ্যটি আরও ভেবেচিন্তে ব্যবহার করার কিছু উপায় এখানে রয়েছে।

দেখাও, বলো না

যেখানে সম্ভব, "বলো" এর পরিবর্তে "দেখান"। উদাহরণস্বরূপ, স্টার্টার অ্যাপে NavigationRail প্রতিটি প্রধান রুটের জন্য ট্যাব রয়েছে, তবে অগ্রণী আইকনগুলি একই রকম:

86c5f73b3aa5fd35.png

এটি সহায়ক নয় কারণ ব্যবহারকারীকে এখনও প্রতিটি ট্যাবের পাঠ্য পড়তে হবে। ভিজ্যুয়াল ইঙ্গিত যোগ করে শুরু করুন যাতে ব্যবহারকারী আপনার পছন্দের ট্যাবটি খুঁজে পেতে শীর্ষস্থানীয় আইকনগুলিতে দ্রুত নজর দিতে পারে। এটি স্থানীয়করণ এবং অ্যাক্সেসযোগ্যতার সাথেও সহায়তা করে।

lib/src/shared/router.dart এ, প্রতিটি নেভিগেশন গন্তব্যের জন্য স্বতন্ত্র লিডিং আইকন যোগ করুন (বাড়ি, প্লেলিস্ট এবং মানুষ):

lib/src/shared/router.dart

const List<NavigationDestination> destinations = [
  NavigationDestination(
    label: 'Home',
    icon: Icon(Icons.home),                                      // Modify this line
    route: '/',
  ),
  NavigationDestination(
    label: 'Playlists',
    icon: Icon(Icons.playlist_add_check),                        // Modify this line
    route: '/playlists',
  ),
  NavigationDestination(
    label: 'Artists',
    icon: Icon(Icons.people),                                    // Modify this line
    route: '/artists',
  ),
];

23278e4f4610fbf4.png

সমস্যা?

আপনার অ্যাপটি সঠিকভাবে না চললে, টাইপোর জন্য দেখুন। প্রয়োজনে, ট্র্যাকে ফিরে পেতে নিম্নলিখিত লিঙ্কগুলিতে কোডটি ব্যবহার করুন৷

ভেবেচিন্তে ফন্ট নির্বাচন করুন

ফন্ট আপনার অ্যাপ্লিকেশনের ব্যক্তিত্ব সেট করে, তাই সঠিক ফন্ট নির্বাচন করা অত্যন্ত গুরুত্বপূর্ণ। একটি ফন্ট নির্বাচন করার সময়, এখানে কয়েকটি বিষয় বিবেচনা করতে হবে:

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

এটি মাথায় রেখে, Google Fonts-এ যান এবং Montserrat-এর মতো একটি sans-serif ফন্ট বেছে নিন, যেহেতু মিউজিক অ্যাপটি কৌতুকপূর্ণ এবং মজাদার হওয়ার উদ্দেশ্যে।

কমান্ড লাইন থেকে, google_fonts প্যাকেজ টানুন। এটি pubspec.yaml ফাইলটিকে একটি অ্যাপ নির্ভরতা হিসাবে ফন্টগুলি যোগ করতে আপডেট করে।

flutter pub add google_fonts

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Make sure the following two lines are present -->
        <key>com.apple.security.network.client</key>
        <true/>
</dict>
</plist>

lib/src/shared/extensions.dart এ, নতুন প্যাকেজ আমদানি করুন:

lib/src/shared/extensions.dart

import 'package:google_fonts/google_fonts.dart'; // Add this line.

মন্টসেরাট TextTheme:

TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line

গরম পুনরায় লোড 7f9a9e103c7b5e5.png পরিবর্তনগুলি সক্রিয় করতে। (আপনার IDE-এ বোতামটি ব্যবহার করুন বা, কমান্ড লাইন থেকে, হট রিলোড করতে r লিখুন।):

1e67c60667821082.png

মন্টসেরাট ফন্টে প্রদর্শিত পাঠ্য সহ আপনার নতুন NavigationRail আইকনগুলি দেখতে হবে।

সমস্যা?

আপনার অ্যাপটি সঠিকভাবে না চললে, টাইপোর জন্য দেখুন। প্রয়োজনে, ট্র্যাকে ফিরে পেতে নিম্নলিখিত লিঙ্কগুলিতে কোডটি ব্যবহার করুন৷

5. থিম সেট করুন

থিমগুলি রঙ এবং পাঠ্য শৈলীগুলির একটি সেট সিস্টেম নির্দিষ্ট করে একটি অ্যাপে একটি কাঠামোগত নকশা এবং অভিন্নতা আনতে সহায়তা করে। থিমগুলি আপনাকে প্রতিটি একক উইজেটের জন্য সঠিক রঙ নির্দিষ্ট করার মতো ছোটখাটো বিবরণের উপর চাপ না দিয়ে দ্রুত একটি UI বাস্তবায়ন করতে সক্ষম করে।

ফ্লটার ডেভেলপাররা সাধারণত দুটি উপায়ের একটিতে কাস্টম-থিমযুক্ত উপাদান তৈরি করে:

  • প্রতিটি নিজস্ব থিম সহ স্বতন্ত্র কাস্টম উইজেট তৈরি করুন।
  • ডিফল্ট উইজেটগুলির জন্য স্কোপড থিম তৈরি করুন।

এই উদাহরণটি lib/src/shared/providers/theme.dart এ অবস্থিত একটি থিম প্রদানকারী ব্যবহার করে সমগ্র অ্যাপ জুড়ে ধারাবাহিকভাবে-থিমযুক্ত উইজেট এবং রঙ তৈরি করতে:

lib/src/shared/providers/theme.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';

class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
 
const NoAnimationPageTransitionsBuilder();

 
@override
 
Widget buildTransitions<T>(
   
PageRoute<T> route,
   
BuildContext context,
   
Animation<double> animation,
   
Animation<double> secondaryAnimation,
   
Widget child,
 
) {
   
return child;
 
}
}

class ThemeSettingChange extends Notification {
 
ThemeSettingChange({required this.settings});
 
final ThemeSettings settings;
}

class ThemeProvider extends InheritedWidget {
 
const ThemeProvider({
   
super.key,
   
required this.settings,
   
required this.lightDynamic,
   
required this.darkDynamic,
   
required super.child,
 
});

 
final ValueNotifier<ThemeSettings> settings;
 
final ColorScheme? lightDynamic;
 
final ColorScheme? darkDynamic;

 
final pageTransitionsTheme = const PageTransitionsTheme(
   
builders: <TargetPlatform, PageTransitionsBuilder>{
     
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
     
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
     
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
     
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
     
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
   
},
 
);

 
Color custom(CustomColor custom) {
   
if (custom.blend) {
     
return blend(custom.color);
   
} else {
     
return custom.color;
   
}
 
}

 
Color blend(Color targetColor) {
   
return Color(
     
Blend.harmonize(
       
targetColor.toARGB32(),
       
settings.value.sourceColor.toARGB32(),
     
),
   
);
 
}

 
Color source(Color? target) {
   
Color source = settings.value.sourceColor;
   
if (target != null) {
     
source = blend(target);
   
}
   
return source;
 
}

 
ColorScheme colors(Brightness brightness, Color? targetColor) {
   
final dynamicPrimary = brightness == Brightness.light
       
? lightDynamic?.primary
       
: darkDynamic?.primary;
   
return ColorScheme.fromSeed(
     
seedColor: dynamicPrimary ?? source(targetColor),
     
brightness: brightness,
   
);
 
}

 
ShapeBorder get shapeMedium =>
     
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8));

 
CardThemeData cardTheme() {
   
return CardThemeData(
     
elevation: 0,
     
shape: shapeMedium,
     
clipBehavior: Clip.antiAlias,
   
);
 
}

 
ListTileThemeData listTileTheme(ColorScheme colors) {
   
return ListTileThemeData(
     
shape: shapeMedium,
     
selectedColor: colors.secondary,
   
);
 
}

 
AppBarTheme appBarTheme(ColorScheme colors) {
   
return AppBarTheme(
     
elevation: 0,
     
backgroundColor: colors.surface,
     
foregroundColor: colors.onSurface,
   
);
 
}

 
TabBarThemeData tabBarTheme(ColorScheme colors) {
   
return TabBarThemeData(
     
labelColor: colors.secondary,
     
unselectedLabelColor: colors.onSurfaceVariant,
     
indicator: BoxDecoration(
       
border: Border(bottom: BorderSide(color: colors.secondary, width: 2)),
     
),
   
);
 
}

 
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
   
return BottomAppBarTheme(color: colors.surface, elevation: 0);
 
}

 
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
   
return BottomNavigationBarThemeData(
     
type: BottomNavigationBarType.fixed,
     
backgroundColor: colors.surfaceContainerHighest,
     
selectedItemColor: colors.onSurface,
     
unselectedItemColor: colors.onSurfaceVariant,
     
elevation: 0,
     
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
   
);
 
}

 
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
   
return const NavigationRailThemeData();
 
}

 
DrawerThemeData drawerTheme(ColorScheme colors) {
   
return DrawerThemeData(backgroundColor: colors.surface);
 
}

 
ThemeData light([Color? targetColor]) {
   
final colorScheme = colors(Brightness.light, targetColor);
   
return ThemeData.light().copyWith(
     
colorScheme: colorScheme,
     
appBarTheme: appBarTheme(colorScheme),
     
cardTheme: cardTheme(),
     
listTileTheme: listTileTheme(colorScheme),
     
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
     
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
     
navigationRailTheme: navigationRailTheme(colorScheme),
     
tabBarTheme: tabBarTheme(colorScheme),
     
drawerTheme: drawerTheme(colorScheme),
     
scaffoldBackgroundColor: colorScheme.surface,
   
);
 
}

 
ThemeData dark([Color? targetColor]) {
   
final colorScheme = colors(Brightness.dark, targetColor);
   
return ThemeData.dark().copyWith(
     
colorScheme: colorScheme,
     
appBarTheme: appBarTheme(colorScheme),
     
cardTheme: cardTheme(),
     
listTileTheme: listTileTheme(colorScheme),
     
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
     
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
     
navigationRailTheme: navigationRailTheme(colorScheme),
     
tabBarTheme: tabBarTheme(colorScheme),
     
drawerTheme: drawerTheme(colorScheme),
     
scaffoldBackgroundColor: colorScheme.surface,
   
);
 
}

 
ThemeMode themeMode() {
   
return settings.value.themeMode;
 
}

 
ThemeData theme(BuildContext context, [Color? targetColor]) {
   
final brightness = MediaQuery.of(context).platformBrightness;
   
return brightness == Brightness.light
       
? light(targetColor)
       
: dark(targetColor);
 
}

 
static ThemeProvider of(BuildContext context) {
   
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
 
}

 
@override
 
bool updateShouldNotify(covariant ThemeProvider oldWidget) {
   
return oldWidget.settings != settings;
 
}
}

class ThemeSettings {
 
ThemeSettings({required this.sourceColor, required this.themeMode});

 
final Color sourceColor;
 
final ThemeMode themeMode;
}

Color randomColor() {
 
return Color(Random().nextInt(0xFFFFFFFF));
}

const linkColor = CustomColor(name: 'Link Color', color: Color(0xFF00B0FF));

class CustomColor {
 
const CustomColor({
   
required this.name,
   
required this.color,
   
this.blend = true,
 
});

 
final String name;
 
final Color color;
 
final bool blend;

 
Color value(ThemeProvider provider) {
   
return provider.custom(this);
 
}
}

প্রদানকারী ব্যবহার করতে, একটি উদাহরণ তৈরি করুন এবং এটিকে lib/src/shared/app.dart এ অবস্থিত MaterialApp এ স্কোপড থিম অবজেক্টে পাঠান। এটি কোনো নেস্টেড Theme অবজেক্ট দ্বারা উত্তরাধিকারসূত্রে প্রাপ্ত হবে:

lib/src/shared/app.dart

import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'playback/bloc/bloc.dart';
import 'providers/theme.dart';
import 'router.dart';

class MyApp extends StatefulWidget {
 
const MyApp({super.key});

 
@override
 
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
 
final settings = ValueNotifier(ThemeSettings(
   
sourceColor:  Colors.pink,
   
themeMode: ThemeMode.system,
 
));
 
@override
 
Widget build(BuildContext context) {
   
return BlocProvider<PlaybackBloc>(
     
create: (context) => PlaybackBloc(),
     
child: DynamicColorBuilder(
       
builder: (lightDynamic, darkDynamic) => ThemeProvider(
           
lightDynamic: lightDynamic,
           
darkDynamic: darkDynamic,
           
settings: settings,
           
child: NotificationListener<ThemeSettingChange>(
             
onNotification: (notification) {
               
settings.value = notification.settings;
               
return true;
             
},
             
child: ValueListenableBuilder<ThemeSettings>(
               
valueListenable: settings,
               
builder: (context, value, _) {
                 
final theme = ThemeProvider.of(context); // Add this line
                 
return MaterialApp.router(
                   
debugShowCheckedModeBanner: false,
                   
title: 'Flutter Demo',
                   
theme: theme.light(settings.value.sourceColor), // Add this line
                   
routeInformationParser: appRouter.routeInformationParser,
                   
routerDelegate: appRouter.routerDelegate,
                 
);
               
},
             
),
           
)),
     
),
   
);
 
}
}

এখন থিম সেট আপ করা হয়েছে, অ্যাপ্লিকেশনের জন্য রং নির্বাচন করুন.

রঙের সঠিক সেট নির্বাচন করা কঠিন হতে পারে। আপনার প্রাথমিক রঙ সম্পর্কে ধারণা থাকতে পারে, তবে আপনি আপনার অ্যাপে শুধুমাত্র একটি রঙের চেয়ে বেশি চান। টেক্সট কি রঙ হওয়া উচিত? শিরোনাম? বিষয়বস্তু? লিঙ্ক? পটভূমির রঙ সম্পর্কে কি? ম্যাটেরিয়াল থিম বিল্ডার হল একটি ওয়েব-ভিত্তিক টুল (উপাদান 3 এ প্রবর্তিত), যা আপনাকে আপনার অ্যাপের জন্য পরিপূরক রঙের একটি সেট নির্বাচন করতে সাহায্য করে।

অ্যাপ্লিকেশনের জন্য একটি উত্স রঙ চয়ন করতে, উপাদান থিম নির্মাতা খুলুন এবং UI এর জন্য বিভিন্ন রং অন্বেষণ করুন৷ ব্র্যান্ডের নান্দনিক বা আপনার ব্যক্তিগত পছন্দের সাথে মানানসই একটি রঙ নির্বাচন করা গুরুত্বপূর্ণ।

একটি থিম তৈরি করার পরে, প্রাথমিক রঙের বুদ্বুদে ডান-ক্লিক করুন - এটি প্রাথমিক রঙের হেক্স মান ধারণকারী একটি ডায়ালগ খোলে। এই মান অনুলিপি করুন. (আপনি এই ডায়ালগ ব্যবহার করে রঙ সেট করতে পারেন।)

থিম প্রদানকারীর কাছে প্রাথমিক রঙের হেক্স মান পাস করুন। উদাহরণস্বরূপ, হেক্স রঙ #00cbe6 Color(0xff00cbe6) হিসাবে নির্দিষ্ট করা হয়েছে। ThemeProvider একটি ThemeData তৈরি করে যাতে পরিপূরক রঙের সেট থাকে যা আপনি মেটেরিয়াল থিম বিল্ডারে পূর্বরূপ দেখেছেন:

final settings = ValueNotifier(ThemeSettings(
   sourceColor:  Color(0xff00cbe6), // Replace this color
   themeMode: ThemeMode.system,
 ));

হট অ্যাপ রিস্টার্ট করুন। প্রাথমিক রঙের জায়গায়, অ্যাপটি আরও অভিব্যক্তিপূর্ণ অনুভব করতে শুরু করে। প্রেক্ষাপটে থিম উল্লেখ করে এবং ColorScheme দখল করে সমস্ত নতুন রঙ অ্যাক্সেস করুন:

final colors = Theme.of(context).colorScheme;

একটি নির্দিষ্ট রঙ ব্যবহার করতে, colorScheme এ একটি রঙের ভূমিকা অ্যাক্সেস করুন। lib/src/shared/views/outlined_card.dart এ যান এবং OutlinedCard একটি বর্ডার দিন:

lib/src/shared/views/outlined_card.dart

class _OutlinedCardState extends State<OutlinedCard> {
  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: widget.clickable
          ? SystemMouseCursors.click
          : SystemMouseCursors.basic,
      child: Container(
        // Add from here...
        decoration: BoxDecoration(
          border: Border.all(
            color: Theme.of(context).colorScheme.outline,
            width: 1,
          ),
        ),
        // ... To here.
        child: widget.child,
      ),
    );
  }
}

উপাদান 3 একে অপরের পরিপূরক এবং অভিব্যক্তির নতুন স্তর যুক্ত করতে UI জুড়ে ব্যবহার করা যেতে পারে এমন সূক্ষ্ম রঙের ভূমিকাগুলি প্রবর্তন করে৷ এই নতুন রঙের ভূমিকাগুলির মধ্যে রয়েছে:

  • Primary , OnPrimary , PrimaryContainer , OnPrimaryContainer
  • Secondary , OnSecondary , SecondaryContainer , OnSecondaryContainer
  • Tertiary , OnTertiary , TertiaryContainer , OnTertiaryContainer
  • Error OnError ErrorContainer OnErrorContainer
  • Background , OnBackground
  • Surface , OnSurface , SurfaceVariant , OnSurfaceVariant
  • Shadow , Outline , InversePrimary

এছাড়াও, নতুন ডিজাইনের টোকেনগুলি হালকা এবং অন্ধকার উভয় থিমকে সমর্থন করে:

7b51703ed96196a4.png

এই রঙের ভূমিকাগুলি UI এর বিভিন্ন অংশে অর্থ এবং জোর দেওয়ার জন্য ব্যবহার করা যেতে পারে। এমনকি যদি একটি উপাদান বিশিষ্ট না হয়, তবুও এটি গতিশীল রঙের সুবিধা নিতে পারে।

ব্যবহারকারী ডিভাইসের সিস্টেম সেটিংসে অ্যাপের উজ্জ্বলতা সেট করতে পারেন। lib/src/shared/app.dart এ, যখন ডিভাইসটি ডার্ক মোডে সেট করা থাকে, তখন MaterialApp এ একটি ডার্ক থিম এবং থিম মোড ফিরিয়ে দিন।

lib/src/shared/app.dart

return MaterialApp.router(
  debugShowCheckedModeBanner: false,
  title: 'Flutter Demo',
  theme: theme.light(settings.value.sourceColor),
  darkTheme: theme.dark(settings.value.sourceColor), // Add this line
  themeMode: theme.themeMode(), // Add this line
  routeInformationParser: appRouter.routeInformationParser,
  routerDelegate: appRouter.routerDelegate,
);

অন্ধকার মোড সক্ষম করতে উপরের ডানদিকের কোণায় চাঁদের আইকনে ক্লিক করুন।

সমস্যা?

আপনার অ্যাপটি সঠিকভাবে না চললে, ট্র্যাকে ফিরে পেতে নিম্নলিখিত লিঙ্কে কোডটি ব্যবহার করুন।

6. অভিযোজিত নকশা যোগ করুন

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

উপাদানগুলি অভিযোজিত বিন্যাসগুলির সাথে কাজ করা সহজ করার জন্য প্যাকেজগুলি অফার করে — আপনি GitHub-এ এই Flutter প্যাকেজগুলি খুঁজে পেতে পারেন৷

ক্রস-প্ল্যাটফর্ম, অভিযোজিত অ্যাপ্লিকেশন তৈরি করার সময় নিম্নলিখিত প্ল্যাটফর্মের পার্থক্যগুলি মাথায় রাখুন:

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

lib/src/shared/views/adaptive_navigation.dart ফাইলটিতে একটি নেভিগেশন ক্লাস রয়েছে যেখানে আপনি বডি রেন্ডার করার জন্য গন্তব্য এবং বিষয়বস্তুর একটি তালিকা প্রদান করতে পারেন। যেহেতু আপনি একাধিক স্ক্রিনে এই লেআউটটি ব্যবহার করেন, তাই প্রতিটি শিশুর মধ্যে পাস করার জন্য একটি ভাগ করা বেস লেআউট রয়েছে। নেভিগেশন রেলগুলি ডেস্কটপ এবং বড় স্ক্রিনের জন্য ভাল, তবে পরিবর্তে মোবাইলে নীচের নেভিগেশন বার দেখিয়ে লেআউটটিকে মোবাইল বন্ধুত্বপূর্ণ করে তুলুন।

lib/src/shared/views/adaptive_navigation.dart

import 'package:flutter/material.dart';

class AdaptiveNavigation extends StatelessWidget {
 
const AdaptiveNavigation({
   
super.key,
   
required this.destinations,
   
required this.selectedIndex,
   
required this.onDestinationSelected,
   
required this.child,
 
});

 
final List<NavigationDestination> destinations;
 
final int selectedIndex;
 
final void Function(int index) onDestinationSelected;
 
final Widget child;

 
@override
 
Widget build(BuildContext context) {
   
return LayoutBuilder(
     
builder: (context, dimens) {
       
if (dimens.maxWidth >= 600) {                               // Add this line
         
return Scaffold(
           
body: Row(
             
children: [
               
NavigationRail(
                 
extended: dimens.maxWidth >= 800,
                 
minExtendedWidth: 180,
                 
destinations: destinations
                     
.map(
                       
(e) => NavigationRailDestination(
                         
icon: e.icon,
                         
label: Text(e.label),
                       
),
                     
)
                     
.toList(),
                 
selectedIndex: selectedIndex,
                 
onDestinationSelected: onDestinationSelected,
               
),
               
Expanded(child: child),
             
],
           
),
         
);
       
}                                                           // Add this line
       
// Mobile Layout
       
// Add from here...
       
return Scaffold(
         
body: child,
         
bottomNavigationBar: NavigationBar(
           
destinations: destinations,
           
selectedIndex: selectedIndex,
           
onDestinationSelected: onDestinationSelected,
         
),
       
);
       
// ... To here.
     
},
   
);
 
}
}

a8487a3c4d7890c9.png

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

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

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

নিম্নলিখিত এক্সটেনশন পদ্ধতিগুলি ( lib/src/shared/extensions.dartMyArtist প্রকল্পে সংজ্ঞায়িত), বিভিন্ন লক্ষ্যগুলির জন্য অপ্টিমাইজ করা লেআউট ডিজাইন করার সময় শুরু করার জন্য একটি ভাল জায়গা।

lib/src/shared/extensions.dart

extension BreakpointUtils on BoxConstraints {
 
bool get isTablet => maxWidth > 730;
 
bool get isDesktop => maxWidth > 1200;
 
bool get isMobile => !isTablet && !isDesktop;
}

730 পিক্সেলের চেয়ে বড় একটি স্ক্রীন (দীর্ঘতম দিকে), কিন্তু 1200 পিক্সেলের চেয়ে ছোট, একটি ট্যাবলেট হিসাবে বিবেচিত হয়। 1200 পিক্সেলের চেয়ে বড় যেকোনো কিছুকে ডেস্কটপ হিসেবে বিবেচনা করা হয়। যদি একটি ডিভাইস ট্যাবলেট বা ডেস্কটপ না হয়, তাহলে এটি মোবাইল হিসাবে বিবেচিত হয়৷ আপনি material.io এ অভিযোজিত ব্রেকপয়েন্ট সম্পর্কে আরও জানতে পারেন।

হোম স্ক্রিনের প্রতিক্রিয়াশীল লেআউট 12-কলামের গ্রিডের উপর ভিত্তি করে AdaptiveContainer এবং AdaptiveColumn ব্যবহার করে।

একটি অভিযোজিত বিন্যাসের জন্য দুটি লেআউট প্রয়োজন: একটি মোবাইলের জন্য, এবং একটি বড় পর্দার জন্য একটি প্রতিক্রিয়াশীল বিন্যাস। এই মুহুর্তে, LayoutBuilder একটি ডেস্কটপ লেআউট প্রদান করে। lib/src/features/home/view/home_screen.dart এ মোবাইল লেআউটটিকে একটি TabBar এবং TabBarView 4টি ট্যাব সহ তৈরি করুন।

lib/src/features/home/view/home_screen.dart

import 'package:flutter/material.dart';

import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';

class HomeScreen extends StatefulWidget {
 
const HomeScreen({super.key});

 
@override
 
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
 
@override
 
Widget build(BuildContext context) {
   
final PlaylistsProvider playlistProvider = PlaylistsProvider();
   
final List<Playlist> playlists = playlistProvider.playlists;
   
final Playlist topSongs = playlistProvider.topSongs;
   
final Playlist newReleases = playlistProvider.newReleases;
   
final ArtistsProvider artistsProvider = ArtistsProvider();
   
final List<Artist> artists = artistsProvider.artists;
   
return LayoutBuilder(
     
builder: (context, constraints) {
       
// Add from here...
       
if (constraints.isMobile) {
         
return DefaultTabController(
           
length: 4,
           
child: Scaffold(
             
appBar: AppBar(
               
centerTitle: false,
               
title: const Text('Good morning'),
               
actions: const [BrightnessToggle()],
               
bottom: const TabBar(
                 
isScrollable: true,
                 
tabs: [
                   
Tab(text: 'Home'),
                   
Tab(text: 'Recently Played'),
                   
Tab(text: 'New Releases'),
                   
Tab(text: 'Top Songs'),
                 
],
               
),
             
),
             
body: LayoutBuilder(
               
builder: (context, constraints) => TabBarView(
                 
children: [
                   
SingleChildScrollView(
                     
child: Column(
                       
children: [
                         
const HomeHighlight(),
                         
HomeArtists(
                           
artists: artists,
                           
constraints: constraints,
                         
),
                       
],
                     
),
                   
),
                   
HomeRecent(playlists: playlists, axis: Axis.vertical),
                   
PlaylistSongs(playlist: topSongs, constraints: constraints),
                   
PlaylistSongs(
                     
playlist: newReleases,
                     
constraints: constraints,
                   
),
                 
],
               
),
             
),
           
),
         
);
       
}
       
// ... To here.

       
return Scaffold(
         
body: SingleChildScrollView(
           
child: AdaptiveColumn(
             
children: [
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     
children: [
                       
Expanded(
                         
child: Text(
                           
'Good morning',
                           
style: context.displaySmall,
                         
),
                       
),
                       
const SizedBox(width: 20),
                       
const BrightnessToggle(),
                     
],
                   
),
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
children: [
                     
const HomeHighlight(),
                     
LayoutBuilder(
                       
builder: (context, constraints) => HomeArtists(
                         
artists: artists,
                         
constraints: constraints,
                       
),
                     
),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
crossAxisAlignment: CrossAxisAlignment.start,
                   
children: [
                     
Padding(
                       
padding: const EdgeInsets.all(2),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

377cfdda63a9de54.png

সমস্যা?

আপনার অ্যাপটি সঠিকভাবে না চললে, ট্র্যাকে ফিরে পেতে নিম্নলিখিত লিঙ্কে কোডটি ব্যবহার করুন।

7. হোয়াইটস্পেস ব্যবহার করুন

হোয়াইটস্পেস হল আপনার অ্যাপের জন্য একটি গুরুত্বপূর্ণ ভিজ্যুয়াল টুল, বিভাগগুলির মধ্যে একটি সাংগঠনিক বিরতি তৈরি করে৷

পর্যাপ্ত না হওয়ার চেয়ে খুব বেশি হোয়াইটস্পেস থাকা ভাল। আপনার ফন্টের আকার বা ভিজ্যুয়াল উপাদানগুলিকে স্থানটিতে আরও ফিট করার জন্য আরও সাদা স্থান যোগ করা বাঞ্ছনীয়।

শ্বেতস্থানের অভাব যাদের দৃষ্টি সমস্যা রয়েছে তাদের জন্য একটি অসুবিধা হতে পারে। অত্যধিক হোয়াইটস্পেসে সমন্বয়ের অভাব হতে পারে এবং আপনার UI খারাপভাবে সংগঠিত দেখাতে পারে। উদাহরণস্বরূপ, নিম্নলিখিত স্ক্রিনশটগুলি দেখুন:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

এর পরে, আপনি হোম স্ক্রিনে এটিকে আরও জায়গা দেওয়ার জন্য সাদা স্থান যোগ করবেন। তারপরে আপনি ব্যবধানটি সূক্ষ্ম সুর করতে লেআউটটিকে আরও পরিবর্তন করবেন।

সেই উইজেটের চারপাশে সাদা স্থান যোগ করতে একটি Padding বস্তু দিয়ে একটি উইজেট মোড়ানো। lib/src/features/home/view/home_screen.dart এ সমস্ত প্যাডিং মান 35-এ বৃদ্ধি করুন:

lib/src/features/home/view/home_screen.dart

return Scaffold(
  body: SingleChildScrollView(
    child: AdaptiveColumn(
      children: [
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.all(35),                   // Modify this line
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    'Good morning',
                    style: context.displaySmall,
                  ),
                ),
                const SizedBox(width: 20),
                const BrightnessToggle(),
              ],
            ),
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            children: [
              const HomeHighlight(),
              LayoutBuilder(
                builder: (context, constraints) => HomeArtists(
                  artists: artists,
                  constraints: constraints,
                ),
              ),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.all(35),               // Modify this line
                child: Text(
                  'Recently played',
                  style: context.headlineSmall,
                ),
              ),
              HomeRecent(playlists: playlists),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.all(35),                   // Modify this line
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.all(35),       // Modify this line
                        child: Text(
                          'Top Songs Today',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: topSongs,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.all(35),       // Modify this line
                        child: Text(
                          'New Releases',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: newReleases,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
);

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

lib/src/features/home/view/home_highlight.dart এ, ব্যানারের প্যাডিং 15 এ পরিবর্তন করুন:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(15),                   // Modify this line
            child: Clickable(
              child: SizedBox(
                height: 275,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Image.asset(
                    'assets/images/news/concert.jpeg',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

অ্যাপটি হট রিলোড করুন। নীচের দিকে থাকা দুটি প্লেলিস্টের মধ্যে কোনো হোয়াইটস্পেস নেই, তাই সেগুলিকে একই টেবিলের অন্তর্গত বলে মনে হচ্ছে৷ এটি এমন নয়, এবং আপনি পরবর্তীতে এটি ঠিক করবেন।

df1d9af97d039cc8.png

Row একটি আকারের উইজেট সন্নিবেশ করে প্লেলিস্টগুলির মধ্যে হোয়াইটস্পেস যোগ করুন যেটিতে সেগুলি রয়েছে৷ lib/src/features/home/view/home_screen.dart এ, 35 প্রস্থের একটি SizedBox যোগ করুন:

lib/src/features/home/view/home_screen.dart

AdaptiveContainer(
  columnSpan: 12,
  child: Padding(
    padding: const EdgeInsets.all(35),
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Flexible(
          flex: 10,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.all(35),
                child: Text(
                  'Top Songs Today',
                  style: context.titleLarge,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    PlaylistSongs(
                      playlist: topSongs,
                      constraints: constraints,
                    ),
              ),
            ],
          ),
        ),
        const SizedBox(width: 35),                                  // Add this line
        Flexible(
          flex: 10,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.all(35),
                child: Text(
                  'New Releases',
                  style: context.titleLarge,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    PlaylistSongs(
                      playlist: newReleases,
                      constraints: constraints,
                    ),
              ),
            ],
          ),
        ),
      ],
    ),
  ),
),

অ্যাপটি হট রিলোড করুন। অ্যাপ্লিকেশন নিম্নলিখিত হিসাবে দেখতে হবে:

d8b2a3d47736dbab.png

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

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

  • EdgeInsets.LTRB() পৃথকভাবে বাম, উপরে, ডান এবং নীচে সেট করে
  • EdgeInsets.symmetric() প্যাডিংকে উল্লম্ব (উপর এবং নীচে) সমতুল্য এবং অনুভূমিক (বাম এবং ডান) সমতুল্য করার জন্য সেট করে
  • EdgeInsets.only() শুধুমাত্র নির্দিষ্ট প্রান্ত সেট করে।

lib/src/features/home/view/home_screen.dart

return Scaffold(
  body: SingleChildScrollView(
    child: AdaptiveColumn(
      children: [
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.fromLTRB(20, 25, 20, 10),  // Modify this line
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    'Good morning',
                    style: context.displaySmall,
                  ),
                ),
                const SizedBox(width: 20),
                const BrightnessToggle(),
              ],
            ),
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            children: [
              const HomeHighlight(),
              LayoutBuilder(
                builder: (context, constraints) => HomeArtists(
                  artists: artists,
                  constraints: constraints,
                ),
              ),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(             // Modify from here...
                  horizontal: 15,
                  vertical: 10,
                ),                                               // To here.
                child: Text(
                  'Recently played',
                  style: context.headlineSmall,
                ),
              ),
              HomeRecent(playlists: playlists),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.all(15),                   // Modify this line
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'Top Songs Today',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: topSongs,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 25),                       // Modify this line
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'New Releases',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: newReleases,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
);

lib/src/features/home/view/home_highlight.dart এ, ব্যানারে বাম এবং ডান প্যাডিং 35 এবং উপরের এবং নীচের প্যাডিং 5 এ সেট করুন:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            // Modify the following line
            padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
            child: Clickable(
              child: SizedBox(
                height: 275,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Image.asset(
                    'assets/images/news/concert.jpeg',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

অ্যাপটি হট রিলোড করুন। লেআউট এবং ব্যবধান অনেক ভাল চেহারা! সমাপ্তি স্পর্শের জন্য, কিছু গতি এবং অ্যানিমেশন যোগ করুন।

7f5e3514a7ee1750.png

সমস্যা?

আপনার অ্যাপটি সঠিকভাবে না চললে, ট্র্যাকে ফিরে পেতে নিম্নলিখিত লিঙ্কে কোডটি ব্যবহার করুন।

8. গতি এবং অ্যানিমেশন যোগ করুন

গতি এবং অ্যানিমেশন হল নড়াচড়া এবং শক্তি প্রবর্তন করার এবং ব্যবহারকারী অ্যাপের সাথে ইন্টারঅ্যাক্ট করার সময় প্রতিক্রিয়া প্রদানের দুর্দান্ত উপায়।

পর্দার মধ্যে অ্যানিমেট করুন

ThemeProvider মোবাইল প্ল্যাটফর্মের (iOS, Android) জন্য স্ক্রীন ট্রানজিশন অ্যানিমেশন সহ একটি PageTransitionsTheme সংজ্ঞায়িত করে। ডেস্কটপ ব্যবহারকারীরা ইতিমধ্যেই তাদের মাউস বা ট্র্যাকপ্যাড ক্লিক থেকে প্রতিক্রিয়া পান, তাই একটি পৃষ্ঠা রূপান্তর অ্যানিমেশন প্রয়োজন হয় না।

Flutter স্ক্রীন ট্রানজিশন অ্যানিমেশন প্রদান করে যা আপনি lib/src/shared/providers/theme.dart এ দেখানো লক্ষ্য প্ল্যাটফর্মের উপর ভিত্তি করে আপনার অ্যাপের জন্য কনফিগার করতে পারেন:

lib/src/shared/providers/theme.dart

final pageTransitionsTheme = const PageTransitionsTheme(
  builders: <TargetPlatform, PageTransitionsBuilder>{
    TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
    TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
    TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
    TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
  },
);

lib/src/shared/providers/theme.dart এ হালকা এবং অন্ধকার উভয় থিমে PageTransitionsTheme পাস করুন

lib/src/shared/providers/theme.dart

ThemeData light([Color? targetColor]) {
  final colorScheme = colors(Brightness.light, targetColor);
  return ThemeData.light().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

ThemeData dark([Color? targetColor]) {
  final colorScheme = colors(Brightness.dark, targetColor);
  return ThemeData.dark().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

iOS এ অ্যানিমেশন ছাড়াই

iOS এ অ্যানিমেশন সহ

সমস্যা?

আপনার অ্যাপটি সঠিকভাবে না চললে, ট্র্যাকে ফিরে পেতে নিম্নলিখিত লিঙ্কে কোডটি ব্যবহার করুন।

9. হোভার স্টেট যোগ করুন

একটি ডেস্কটপ অ্যাপে গতি যোগ করার একটি উপায় হল হোভার স্টেটস , যেখানে একটি উইজেট তার অবস্থা (যেমন রঙ, আকৃতি বা বিষয়বস্তু) পরিবর্তন করে, যখন ব্যবহারকারী এটির উপর কার্সার ঘোরায়।

ডিফল্টরূপে, _OutlinedCardState ক্লাস ("সম্প্রতি বাজানো" প্লেলিস্ট টাইলগুলির জন্য ব্যবহৃত), একটি MouseRegion প্রদান করে — যা কার্সার তীরটিকে হোভারে একটি পয়েন্টারে পরিণত করে — তবে আপনি আরও ভিজ্যুয়াল প্রতিক্রিয়া যোগ করতে পারেন৷

lib/src/shared/views/outlined_card.dart খুলুন এবং একটি _hovered অবস্থা চালু করতে নিম্নলিখিত বাস্তবায়নের সাথে এর বিষয়বস্তু প্রতিস্থাপন করুন।

lib/src/shared/views/outlined_card.dart

import 'package:flutter/material.dart';

class OutlinedCard extends StatefulWidget {
 
const OutlinedCard({super.key, required this.child, this.clickable = true});

 
final Widget child;
 
final bool clickable;

 
@override
 
State<OutlinedCard> createState() => _OutlinedCardState();
}

class _OutlinedCardState extends State<OutlinedCard> {
 
bool _hovered = false;

 
@override
 
Widget build(BuildContext context) {
   
final borderRadius = BorderRadius.circular(_hovered ? 20 : 8);
   
const animationCurve = Curves.easeInOut;
   
return MouseRegion(
     
onEnter: (_) {
       
if (!widget.clickable) return;
       
setState(() {
         
_hovered = true;
       
});
     
},
     
onExit: (_) {
       
if (!widget.clickable) return;
       
setState(() {
         
_hovered = false;
       
});
     
},
     
cursor: widget.clickable
         
? SystemMouseCursors.click
         
: SystemMouseCursors.basic,
     
child: AnimatedContainer(
       
duration: kThemeAnimationDuration,
       
curve: animationCurve,
       
decoration: BoxDecoration(
         
border: Border.all(
           
color: Theme.of(context).colorScheme.outline,
           
width: 1,
         
),
         
borderRadius: borderRadius,
       
),
       
foregroundDecoration: BoxDecoration(
         
color: Theme.of(
           
context,
         
).colorScheme.onSurface.withAlpha(_hovered ? 30 : 0),
         
borderRadius: borderRadius,
       
),
       
child: TweenAnimationBuilder<BorderRadius>(
         
duration: kThemeAnimationDuration,
         
curve: animationCurve,
         
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
         
builder: (context, borderRadius, child) => ClipRRect(
           
clipBehavior: Clip.antiAlias,
           
borderRadius: borderRadius,
           
child: child,
         
),
         
child: widget.child,
       
),
     
),
   
);
 
}
}

অ্যাপটি হট রিলোড করুন এবং তারপরে সাম্প্রতিক প্লেলিস্ট টাইলগুলির একটির উপর হোভার করুন৷

OutlinedCard অস্বচ্ছতা পরিবর্তন করে এবং কোণগুলিকে বৃত্তাকার করে।

অবশেষে, lib/src/shared/views/hoverable_song_play_button.dart এ সংজ্ঞায়িত HoverableSongPlayButton উইজেট ব্যবহার করে একটি প্লেলিস্টের গান নম্বরটিকে একটি প্লে বোতামে অ্যানিমেট করুন। lib/src/features/playlists/view/playlist_songs.dart এ, একটি HoverableSongPlayButton দিয়ে Center উইজেট (যাতে গানের নম্বর রয়েছে) মোড়ানো করুন:

lib/src/features/playlists/view/playlist_songs.dart

rowBuilder: (context, index) => DataRow.byIndex(
  index: index,
  cells: [
    DataCell(
      HoverableSongPlayButton(                                      // Modify from here...
        hoverMode: HoverMode.overlay,
        song: playlist.songs[index],
        child: Center(
          child: Text(
            (index + 1).toString(),
            textAlign: TextAlign.center,
          ),
        ),
      ),                                                            // To here.
    ),
    DataCell(
      Row(
        children: [
          Padding(
            padding: const EdgeInsets.all(2),
            child: ClippedImage(playlist.songs[index].image.image),
          ),
          const SizedBox(width: 10),
          Expanded(child: Text(playlist.songs[index].title)),
        ],
      ),
    ),
    DataCell(Text(playlist.songs[index].length.toHumanizedString())),
  ],
),

অ্যাপটি হট রিলোড করুন এবং তারপরে টপ গান টুডে বা নতুন রিলিজ প্লেলিস্টের গান নম্বরের উপরে কার্সারটি ঘোরান।

সংখ্যাটি একটি প্লে বোতামে অ্যানিমেট হয় যা আপনি এটি ক্লিক করলে গানটি বাজবে৷

GitHub এ চূড়ান্ত প্রকল্প কোড দেখুন।

10. অভিনন্দন!

আপনি এই কোডল্যাব সম্পূর্ণ করেছেন! আপনি শিখেছেন যে অনেকগুলি ছোট পরিবর্তন রয়েছে যা আপনি একটি অ্যাপকে আরও সুন্দর করতে এবং আরও অ্যাক্সেসযোগ্য, আরও স্থানীয়করণযোগ্য এবং একাধিক প্ল্যাটফর্মের জন্য আরও উপযুক্ত করতে একীভূত করতে পারেন৷ এই কৌশল অন্তর্ভুক্ত, কিন্তু সীমাবদ্ধ নয়:

  • টাইপোগ্রাফি: টেক্সট শুধুমাত্র একটি যোগাযোগের হাতিয়ারের চেয়েও বেশি কিছু। ব্যবহারকারীদের অভিজ্ঞতা এবং আপনার অ্যাপের উপলব্ধিতে ইতিবাচক প্রভাব তৈরি করতে পাঠ্যটি যেভাবে প্রদর্শিত হয় তা ব্যবহার করুন।
  • থিমিং: একটি ডিজাইন সিস্টেম স্থাপন করুন যা আপনি প্রতিটি উইজেটের জন্য ডিজাইনের সিদ্ধান্ত না নিয়েই নির্ভরযোগ্যভাবে ব্যবহার করতে পারেন।
  • অভিযোজন: ব্যবহারকারী আপনার অ্যাপটি চালাচ্ছেন এমন ডিভাইস এবং প্ল্যাটফর্ম এবং এর ক্ষমতা বিবেচনা করুন। স্ক্রিনের আকার এবং আপনার অ্যাপ কীভাবে প্রদর্শিত হয় তা বিবেচনা করুন।
  • গতি এবং অ্যানিমেশন: আপনার অ্যাপে গতিবিধি যোগ করা ব্যবহারকারীর অভিজ্ঞতায় শক্তি যোগ করে এবং আরও কার্যত, ব্যবহারকারীদের জন্য প্রতিক্রিয়া প্রদান করে।

কয়েকটি ছোট পরিবর্তনের মাধ্যমে আপনার অ্যাপ বিরক্তিকর থেকে সুন্দর হতে পারে:

আগে

1e67c60667821082.png

পরে

পরবর্তী পদক্ষেপ

আমরা আশা করি আপনি Flutter-এ সুন্দর অ্যাপ তৈরি করার বিষয়ে আরও শিখেছেন!

আপনি যদি এখানে উল্লিখিত কোনো টিপস বা কৌশল প্রয়োগ করেন (বা শেয়ার করার জন্য আপনার নিজস্ব একটি টিপ থাকে), তাহলে আমরা আপনার কাছ থেকে শুনতে চাই! @rodydavis এবং @khanhnwin- এ টুইটারে আমাদের সাথে যোগাযোগ করুন!

এছাড়াও আপনি নিম্নলিখিত সংস্থান সহায়ক খুঁজে পেতে পারেন.

থিমিং

অভিযোজিত এবং প্রতিক্রিয়াশীল সংস্থান:

সাধারণ নকশা সম্পদ:

এছাড়াও, ফ্লটার সম্প্রদায়ের সাথে সংযোগ করুন !

এগিয়ে যান এবং অ্যাপ বিশ্ব সুন্দর করুন!