UI স্তর

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

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

একটি সাধারণ আর্কিটেকচারে, UI লেয়ারের UI এলিমেন্টগুলো স্টেট হোল্ডারদের উপর নির্ভর করে,  যারা আবার ডেটা লেয়ার অথবা  ঐচ্ছিক ডোমেইন লেয়ারের ক্লাসগুলোর উপর নির্ভর করে।
চিত্র ১. অ্যাপ আর্কিটেকচারে UI লেয়ারের ভূমিকা।

একটি মৌলিক কেস স্টাডি

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

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

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

UI লেয়ার আর্কিটেকচার

UI বলতে কন্টেইনার এবং কম্পোজেবল ফাংশনের মতো UI এলিমেন্টগুলোকে বোঝায়, যেগুলো ডেটা প্রদর্শন করে। অ্যান্ড্রয়েড UI তৈরির জন্য Jetpack Compose হলো প্রস্তাবিত টুলকিট। যেহেতু ডেটা লেয়ারের কাজ হলো অ্যাপের ডেটা ধারণ করা, পরিচালনা করা এবং তাতে অ্যাক্সেস দেওয়া, তাই UI লেয়ারকে অবশ্যই নিম্নলিখিত ধাপগুলো সম্পাদন করতে হবে:

  1. অ্যাপের ডেটা গ্রহণ করে সেটিকে এমন ডেটাতে রূপান্তর করুন যা UI সহজেই রেন্ডার করতে পারে।
  2. UI-রেন্ডারযোগ্য ডেটা গ্রহণ করুন এবং ব্যবহারকারীর কাছে উপস্থাপনের জন্য সেটিকে UI উপাদানে রূপান্তর করুন।
  3. একত্রিত UI উপাদানগুলো থেকে ব্যবহারকারীর ইনপুট ইভেন্টগুলো গ্রহণ করুন এবং প্রয়োজন অনুযায়ী UI ডেটাতে সেগুলোর প্রভাব প্রতিফলিত করুন।
  4. যতক্ষণ প্রয়োজন, ১ থেকে ৩ নম্বর ধাপগুলো পুনরাবৃত্তি করুন।

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

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

এগুলোর মধ্যে সবচেয়ে মৌলিক হলো UI অবস্থার সংজ্ঞা।

UI অবস্থা সংজ্ঞায়িত করুন

পূর্বে বর্ণিত কেস স্টাডিতে , UI প্রতিটি আর্টিকেলের কিছু মেটাডেটা সহ আর্টিকেলের একটি তালিকা প্রদর্শন করে। অ্যাপটি ব্যবহারকারীর কাছে যে তথ্য উপস্থাপন করে, সেটিই হলো UI স্টেট।

অন্য কথায়, ব্যবহারকারী যা দেখেন তা যদি UI হয়, তবে অ্যাপটি ব্যবহারকারীকে যা দেখাতে বলে, UI স্টেট হলো তা। একই মুদ্রার দুই পিঠের মতো, UI হলো UI স্টেটের চাক্ষুষ উপস্থাপনা। UI স্টেটে যেকোনো পরিবর্তন সঙ্গে সঙ্গে UI-তে প্রতিফলিত হয়।

স্ক্রিনের UI এলিমেন্টগুলোকে UI স্টেটের সাথে সংযুক্ত করার ফলেই UI তৈরি হয়।
চিত্র ৩. স্ক্রিনের UI উপাদানগুলোকে UI অবস্থার সাথে সংযুক্ত করার ফলেই UI তৈরি হয়।

কেস স্টাডিটি বিবেচনা করুন: নিউজ অ্যাপের প্রয়োজনীয়তা পূরণের জন্য, UI সম্পূর্ণরূপে রেন্ডার করার জন্য প্রয়োজনীয় তথ্য নিম্নলিখিতভাবে সংজ্ঞায়িত একটি NewsUiState ডেটা ক্লাসে আবদ্ধ করা যেতে পারে:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

UI স্টেট সম্পর্কে আরও তথ্যের জন্য, স্টেট এবং জেটপ্যাক কম্পোজ দেখুন।

অপরিবর্তনীয়তা

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

উদাহরণস্বরূপ, পূর্ববর্তী কেস স্টাডিটি বিবেচনা করুন। যদি Activity ক্লাসে UI স্টেটের একটি NewsItemUiState অবজেক্টের bookmarked ফ্ল্যাগ আপডেট করা হয়, তবে সেই ফ্ল্যাগটি একটি আর্টিকেলের বুকমার্কড স্ট্যাটাসের উৎস হিসেবে ডেটা লেয়ারের সাথে প্রতিযোগিতা করে। এই ধরনের অসামঞ্জস্যতা প্রতিরোধ করার জন্য ইমিউটেবল ডেটা ক্লাস খুবই কার্যকর।

এই নির্দেশিকায় নামকরণের নিয়মাবলী

এই নির্দেশিকায়, UI স্টেট ক্লাসগুলোর নামকরণ করা হয় স্ক্রিন বা স্ক্রিনের যে অংশকে তারা বর্ণনা করে, তার কার্যকারিতার উপর ভিত্তি করে। প্রচলিত রীতিটি নিম্নরূপ:

কার্যকারিতা + UiState .

উদাহরণস্বরূপ, সংবাদ প্রদর্শনকারী একটি স্ক্রিনের অবস্থাকে NewsUiState বলা যেতে পারে, এবং সংবাদের তালিকার কোনো একটি সংবাদ আইটেমের অবস্থা NewsItemUiState হতে পারে।

একমুখী ডেটা প্রবাহের মাধ্যমে অবস্থা পরিচালনা করুন

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

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

এই অংশে ইউনিডিরেকশনাল ডেটা ফ্লো (UDF) নিয়ে আলোচনা করা হয়েছে, যা একটি আর্কিটেকচার প্যাটার্ন এবং দায়িত্বের এই সুষ্ঠু বিভাজন নিশ্চিত করতে সাহায্য করে।

রাষ্ট্রীয় ধারক

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

পরবর্তী ক্ষেত্রে, সাধারণ বাস্তবায়ন হলো একটি ViewModel- এর ইনস্ট্যান্স, যদিও অ্যাপ্লিকেশনের প্রয়োজনীয়তার উপর নির্ভর করে একটি সাধারণ ক্লাসই যথেষ্ট হতে পারে। উদাহরণস্বরূপ, কেস স্টাডির নিউজ অ্যাপটি সেই বিভাগে প্রদর্শিত স্ক্রিনের জন্য UI স্টেট তৈরি করতে একটি NewsViewModel ক্লাসকে স্টেট হোল্ডার হিসেবে ব্যবহার করে।

UI এবং এর স্টেট প্রডিউসারের মধ্যকার পারস্পরিক নির্ভরশীলতা মডেল করার অনেক উপায় আছে। তবে, যেহেতু UI এবং এর ViewModel ক্লাসের মধ্যকার মিথস্ক্রিয়াকে মূলত ইভেন্ট ইনপুট এবং তার ফলস্বরূপ স্টেট আউটপুট হিসেবে বোঝা যায়, তাই এই সম্পর্কটিকে নিম্নলিখিত ডায়াগ্রামে দেখানো উপায়ে উপস্থাপন করা যেতে পারে:

অ্যাপ্লিকেশন ডেটা ডেটা লেয়ার থেকে ViewModel-এ প্রবাহিত হয়। UI স্টেট ViewModel থেকে UI এলিমেন্টগুলিতে প্রবাহিত হয়, এবং ইভেন্টগুলি UI এলিমেন্টগুলি থেকে আবার ViewModel-এ ফিরে আসে।
চিত্র ৪. অ্যাপ আর্কিটেকচারে ইউডিএফ কীভাবে কাজ করে তার ডায়াগ্রাম।

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

  • ViewModel, UI দ্বারা ব্যবহৃত হওয়ার জন্য স্টেট ধারণ করে এবং প্রকাশ করে। UI স্টেট হলো ViewModel দ্বারা রূপান্তরিত অ্যাপ্লিকেশন ডেটা।
  • UI ব্যবহারকারীর ইভেন্টগুলো সম্পর্কে ViewModel-কে অবহিত করে।
  • ViewModel ব্যবহারকারীর কার্যকলাপ পরিচালনা করে এবং অবস্থা হালনাগাদ করে।
  • রেন্ডার করার জন্য আপডেট করা অবস্থাটি UI-তে ফেরত পাঠানো হয়।
  • অবস্থার পরিবর্তন ঘটায় এমন যেকোনো ঘটনার ক্ষেত্রে উপরোক্ত বিষয়টির পুনরাবৃত্তি ঘটে।

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

কেস স্টাডি অ্যাপের একটি একক আর্টিকেল। ইউআই-তে একটি থাম্বনেইল, আর্টিকেলের শিরোনাম, লেখক, আর্টিকেলটি পড়ার আনুমানিক সময় এবং একটি বুকমার্ক আইকন দেখা যায়।
চিত্র ৫. কেস স্টাডি অ্যাপে একটি আর্টিকেল আইটেমের ইউআই।

ব্যবহারকারীর কোনো আর্টিকেল বুকমার্ক করার অনুরোধ এমন একটি ইভেন্টের উদাহরণ যা স্টেট মিউটেশন ঘটাতে পারে। স্টেট প্রডিউসার হিসেবে, UI স্টেটের সমস্ত ফিল্ড পূরণ করার জন্য প্রয়োজনীয় সমস্ত লজিক নির্ধারণ করা এবং UI-কে সম্পূর্ণরূপে রেন্ডার করার জন্য প্রয়োজনীয় ইভেন্টগুলো প্রসেস করা ViewModel-এর দায়িত্ব।

যখন ব্যবহারকারী কোনো আর্টিকেল বুকমার্ক করেন, তখন একটি UI ইভেন্ট ঘটে। ViewModel স্টেট পরিবর্তনের বিষয়ে ডেটা লেয়ারকে অবহিত করে। ডেটা লেয়ার ডেটা পরিবর্তনটি সংরক্ষণ করে এবং অ্যাপ্লিকেশন ডেটা আপডেট করে। বুকমার্ক করা আর্টিকেলসহ নতুন অ্যাপ ডেটা ViewModel-এ পাঠানো হয়, যা এরপর নতুন UI স্টেট তৈরি করে এবং প্রদর্শনের জন্য UI এলিমেন্টগুলোতে পাঠিয়ে দেয়।
চিত্র ৬. ইউডিএফ-এ ঘটনা ও তথ্যের চক্রের চিত্র।

নিম্নলিখিত বিভাগগুলিতে যে ইভেন্টগুলি অবস্থার পরিবর্তন ঘটায় এবং UDF ব্যবহার করে কীভাবে সেগুলি প্রক্রিয়া করা যায়, তা বিশদভাবে আলোচনা করা হয়েছে।

যুক্তির প্রকারভেদ

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

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

UI লজিক ViewModel-এ না রেখে UI-তেই রাখুন, বিশেষ করে যখন এতে Context মতো UI টাইপ জড়িত থাকে। যদি UI-এর জটিলতা বাড়ে এবং আপনি টেস্টেবিলিটি ও সেপারেশন অফ কনসার্নস-এর সুবিধার জন্য UI লজিক অন্য কোনো ক্লাসে অর্পণ করতে চান, তাহলে আপনি স্টেট হোল্ডার হিসেবে একটি সাধারণ ক্লাস তৈরি করতে পারেন । UI-তে তৈরি করা সাধারণ ক্লাসগুলো অ্যান্ড্রয়েড SDK ডিপেন্ডেন্সি নিতে পারে, কারণ এগুলো UI-এর লাইফসাইকেল অনুসরণ করে; ViewModel অবজেক্টগুলোর জীবনকাল দীর্ঘতর হয়।

স্টেট হোল্ডার এবং UI তৈরিতে তাদের ভূমিকা সম্পর্কে আরও তথ্যের জন্য, Jetpack Compose State গাইডটি দেখুন।

UDF কেন ব্যবহার করবেন?

UDF, চিত্র ৪-এ দেখানো অনুযায়ী স্টেট তৈরির চক্রকে মডেল করে। এটি স্টেট পরিবর্তনের উৎপত্তিস্থল, সেগুলোর রূপান্তরস্থল এবং চূড়ান্তভাবে সেগুলোর ব্যবহারের স্থানকেও আলাদা করে। এই পৃথকীকরণ UI-কে ঠিক তাই করতে দেয় যা এর নাম থেকে বোঝা যায়: স্টেট পরিবর্তন পর্যবেক্ষণ করে তথ্য প্রদর্শন করা এবং সেই পরিবর্তনগুলো ViewModel-এ পাঠিয়ে ব্যবহারকারীর অভিপ্রায় পৌঁছে দেওয়া।

অন্য কথায়, UDF নিম্নলিখিত বিষয়গুলোর সুযোগ দেয়:

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

UI অবস্থা প্রকাশ করুন

আপনার UI স্টেট সংজ্ঞায়িত করার এবং সেই স্টেটটি কীভাবে তৈরি করবেন তা নির্ধারণ করার পরে, পরবর্তী ধাপ হলো তৈরি করা স্টেটটিকে UI-তে উপস্থাপন করা।

স্টেট প্রোডাকশন ম্যানেজ করার জন্য যখন UDF ব্যবহার করা হয়, তখন উৎপাদিত স্টেটটিকে একটি স্ট্রিম হিসেবে বিবেচনা করা যেতে পারে—অন্য কথায়, সময়ের সাথে সাথে স্টেটের একাধিক সংস্করণ তৈরি হয়। StateFlow এর মতো একটি অবজার্ভেবল ডেটা হোল্ডারে UI স্টেটকে এক্সপোজ করুন। এর ফলে ViewModel থেকে সরাসরি ম্যানুয়ালি ডেটা পুল না করেই, UI স্টেটের যেকোনো পরিবর্তনে প্রতিক্রিয়া জানাতে পারে। এর আরেকটি সুবিধা হলো, UI স্টেটের সর্বশেষ সংস্করণটি সবসময় ক্যাশড থাকে, যা কনফিগারেশন পরিবর্তনের পর দ্রুত স্টেট পুনরুদ্ধারের জন্য সহায়ক।

class NewsViewModel(...) : ViewModel() {

    val uiState: NewsUiState = 
}

Kotlin flows সম্পর্কে প্রাথমিক ধারণা পেতে, Kotlin flows on Android দেখুন। একটি অবজার্ভেবল ডেটা হোল্ডার হিসেবে StateFlow কীভাবে ব্যবহার করতে হয় তা শিখতে, Jetpack Compose-এর Advanced State and Side Effects কোডল্যাবটি দেখুন।

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

UiState এর একটি স্ট্রিম তৈরি করার একটি প্রচলিত উপায় হলো একটি private set সহ একটি mutableStateOf প্রপার্টি প্রকাশ করা, যা ViewModel-এর ভিতরে স্টেটকে পরিবর্তনযোগ্য (mutable) রাখে কিন্তু UI-এর জন্য এটিকে শুধুমাত্র পঠনযোগ্য (read-only) করে।

class NewsViewModel(...) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

এরপর ViewModel এমন মেথডগুলো প্রকাশ করতে পারে যা অভ্যন্তরীণভাবে স্টেট পরিবর্তন করে এবং UI-এর ব্যবহারের জন্য আপডেটগুলো প্রকাশ করে। উদাহরণস্বরূপ, এমন একটি পরিস্থিতির কথা ভাবুন যেখানে আপনাকে একটি অ্যাসিঙ্ক্রোনাস অ্যাকশন সম্পাদন করতে হবে। আপনি ` viewModelScope ব্যবহার করে একটি কো-রুটিন চালু করতে পারেন এবং সেটি সম্পন্ন হলে পরিবর্তনযোগ্য স্টেটটি আপডেট করতে পারেন।

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

পূর্ববর্তী উদাহরণে, NewsViewModel ক্লাসটি একটি নির্দিষ্ট ক্যাটাগরির আর্টিকেল আনার চেষ্টা করে এবং তারপর সেই চেষ্টার ফলাফল—তা সফল হোক বা ব্যর্থ—UI স্টেটে প্রতিফলিত করে, যেখানে UI সে অনুযায়ী যথাযথভাবে প্রতিক্রিয়া জানাতে পারে। এরর হ্যান্ডলিং সম্পর্কে আরও তথ্যের জন্য, “ স্ক্রিনে এরর দেখান” অংশটি দেখুন।

অতিরিক্ত বিবেচ্য বিষয়

পূর্ববর্তী নির্দেশনার পাশাপাশি, UI স্টেট প্রকাশ করার সময় নিম্নলিখিত বিষয়গুলো বিবেচনা করুন:

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

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    

    এই ডিক্লারেশনে, বুকমার্ক বাটনের ভিজিবিলিটি অন্য দুটি প্রপার্টির একটি ডিরাইভড প্রপার্টি। বিজনেস লজিক যত জটিল হতে থাকে, একটি একক UiState ক্লাস থাকা ততই গুরুত্বপূর্ণ হয়ে ওঠে, যেখানে সমস্ত প্রপার্টি তাৎক্ষণিকভাবে উপলব্ধ থাকে।

  • UI স্টেট: একক স্ট্রিম নাকি একাধিক স্ট্রিম? UI স্টেট একটি একক স্ট্রিমে নাকি একাধিক স্ট্রিমে প্রকাশ করা হবে, তা বেছে নেওয়ার মূল নির্দেশক নীতি হলো নির্গত আইটেমগুলোর মধ্যকার সম্পর্ক। একক-স্ট্রিমে স্টেট প্রকাশের সবচেয়ে বড় সুবিধা হলো স্বাচ্ছন্দ্য এবং ডেটার সামঞ্জস্যতা: স্টেটের ব্যবহারকারীরা যেকোনো নির্দিষ্ট সময়ে সর্বদা সর্বশেষ তথ্য পেয়ে থাকেন। তবে, এমন কিছু ক্ষেত্র রয়েছে যেখানে ViewModel থেকে স্টেটের পৃথক স্ট্রিম উপযুক্ত হতে পারে:

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

    • UiState ডিফিং: একটি UiState অবজেক্টে যত বেশি ফিল্ড থাকে, তার কোনো একটি ফিল্ড আপডেট হওয়ার ফলে স্ট্রিমটি নির্গত হওয়ার সম্ভাবনা তত বেশি থাকে। যেহেতু UI এলিমেন্টগুলোর পরপর নির্গত হওয়া ডেটা ভিন্ন নাকি একই, তা বোঝার জন্য কোনো ডিফিং মেকানিজম নেই, তাই প্রতিটি নির্গমনের ফলেই UI এলিমেন্টটি আপডেট হয়। এর মানে হলো, distinctUntilChanged() এর মতো Flow API মেথড ব্যবহার করে এর প্রতিকারের প্রয়োজন হতে পারে।

রেন্ডারিং এবং UI অবস্থা সম্পর্কে আরও তথ্যের জন্য, কম্পোজেবল-এর জীবনচক্র দেখুন।

UI অবস্থা গ্রহণ করুন

UI-তে UiState অবজেক্টের স্ট্রিম ব্যবহার করার জন্য, আপনার ব্যবহৃত অবজার্ভেবল ডেটা টাইপের জন্য টার্মিনাল অপারেটরটি ব্যবহার করুন। উদাহরণস্বরূপ, Kotlin ফ্লো-এর জন্য collect() মেথড বা এর বিভিন্ন রূপ ব্যবহার করুন।

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

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

চলমান কার্যক্রম দেখান

একটি UiState ক্লাসে লোডিং অবস্থা প্রকাশ করার একটি সহজ উপায় হলো একটি বুলিয়ান ফিল্ড ব্যবহার করা:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

এই ফ্ল্যাগের মান UI-তে প্রোগ্রেস বারের উপস্থিতি বা অনুপস্থিতি নির্দেশ করে।

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

স্ক্রিনে ত্রুটিগুলো দেখান

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

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

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    ...
)

এরপর আপনি স্নাকবারের মতো UI এলিমেন্টের আকারে ব্যবহারকারীর কাছে এরর মেসেজগুলো উপস্থাপন করতে পারেন। UI ইভেন্টগুলো কীভাবে তৈরি ও ব্যবহৃত হয় সে সম্পর্কে আরও তথ্যের জন্য, UI ইভেন্ট দেখুন।

থ্রেডিং এবং কনকারেন্সি

নিশ্চিত করুন যে একটি ViewModel-এ সম্পাদিত সমস্ত কাজ মেইন-সেফ —অর্থাৎ মূল থ্রেড থেকে কল করার জন্য নিরাপদ। ডেটা এবং ডোমেইন লেয়ারগুলো কাজকে অন্য থ্রেডে স্থানান্তর করার জন্য দায়ী।

যদি কোনো ViewModel দীর্ঘ সময় ধরে চলা অপারেশন সম্পাদন করে, তবে সেই লজিককে একটি ব্যাকগ্রাউন্ড থ্রেডে সরিয়ে নেওয়ার দায়িত্বও তার উপরই বর্তায়। সমান্তরাল অপারেশনগুলো পরিচালনা করার জন্য কোটলিন কোরাউটিন একটি চমৎকার উপায়, এবং জেটপ্যাক আর্কিটেকচার কম্পোনেন্টস এগুলোর জন্য বিল্ট-ইন সাপোর্ট প্রদান করে। অ্যান্ড্রয়েড অ্যাপে কোরাউটিন ব্যবহার সম্পর্কে আরও জানতে, “অ্যান্ড্রয়েডে কোটলিন কোরাউটিন” দেখুন।

অ্যাপ নেভিগেশনের পরিবর্তন প্রায়শই ইভেন্টের মতো এমিশনের মাধ্যমে চালিত হয়। উদাহরণস্বরূপ, একটি SignInViewModel ক্লাস সাইন-ইন করার পরে, UiState এর isSignedIn ফিল্ডটির মান true সেট করা হতে পারে। আগের 'Consume UI state' বিভাগে আলোচিত ট্রিগারগুলোর মতোই এই ধরনের ট্রিগারগুলো কনজিউম করুন, কিন্তু কনজিউম করার বাস্তবায়নটি Navigation কম্পোনেন্টের উপর ছেড়ে দিন।

UI নেভিগেশন সম্পর্কে আরও তথ্যের জন্য, নেভিগেশন ৩ দেখুন।

পৃষ্ঠা সংখ্যা

পেজিং লাইব্রেরিটি UI-তে PagingData নামক একটি টাইপের মাধ্যমে ব্যবহৃত হয়। যেহেতু PagingData এমন আইটেমগুলোকে প্রতিনিধিত্ব করে ও ধারণ করে যা সময়ের সাথে সাথে পরিবর্তিত হতে পারে—অন্য কথায়, এটি একটি অপরিবর্তনশীল টাইপ নয়—তাই এটিকে কোনো অপরিবর্তনশীল UI স্টেটে উপস্থাপন করবেন না। এর পরিবর্তে, এটিকে ViewModel থেকে স্বাধীনভাবে এর নিজস্ব স্ট্রিমে প্রকাশ করুন।

নিম্নলিখিত উদাহরণটি পেজিং লাইব্রেরির কম্পোজ এপিআই (Compose API) প্রদর্শন করে:

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

অ্যানিমেশন

শীর্ষ-স্তরের নেভিগেশন ট্রানজিশন মসৃণ করার জন্য, অ্যানিমেশন শুরু করার আগে আপনি দ্বিতীয় স্ক্রিনে ডেটা লোড হওয়া পর্যন্ত অপেক্ষা করতে পারেন।

ন্যাভিগেশন ট্রানজিশন সম্পর্কে আরও তথ্যের জন্য, কম্পোজ-এর ন্যাভিগেশন ৩ এবং শেয়ার্ড এলিমেন্ট ট্রানজিশন দেখুন।

অতিরিক্ত সম্পদ

বিষয়বস্তু দেখুন

নমুনা

নিম্নলিখিত গুগল নমুনাগুলি UI লেয়ারের ব্যবহার প্রদর্শন করে। এই নির্দেশনাটি বাস্তবে দেখতে এগুলি ঘুরে দেখুন:

{% হুবহু %} {% endverbatim %} {% হুবহু %} {% endverbatim %}