UI লেয়ার

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

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

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

একটি প্রাথমিক কেস স্টাডি

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

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

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

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

UI বলতে UI এলিমেন্টকে বোঝায়, যেমন কন্টেনার ও কম্পোজেবল ফাংশন যা ডেটা দেখায়। Android 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 এলিমেন্ট বাইন্ডিংয়ের ফলাফল।

কেস স্টাডি বিবেচনা করুন: News অ্যাপের প্রয়োজনীয়তা পূরণ করতে, 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 স্টেট সম্পর্কে আরও তথ্যের জন্য, স্টেট ও Jetpack Compose দেখুন।

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

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

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

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

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

functionality + UiState.

যেমন, খবর দেখানো স্ক্রিনের স্ট্যাটাসকে NewsUiState বলা যেতে পারে এবং খবরের আইটেমের তালিকায় কোনও খবরের আইটেমের স্ট্যাটাসকে NewsItemUiState বলা যেতে পারে।

ইউনিডিরেকশনাল ডেটা ফ্লো-এর মাধ্যমে স্টেট ম্যানেজ করা

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

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

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

স্টেট হোল্ডার

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

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

There are many ways to model the codependency between the UI and its state producer. তবে, UI এবং এর ViewModel ক্লাসের মধ্যে ইন্টার‍্যাকশনকে মূলত ইভেন্ট ইনপুট এবং এর পরবর্তী অবস্থা আউটপুট হিসেবে বোঝা যেতে পারে, তাই সম্পর্কটি নিম্নলিখিত ডায়াগ্রামে দেখানো হয়েছে:

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

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

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

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

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

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

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

নিম্নলিখিত বিভাগগুলি সেইসব ইভেন্টকে আরও ঘনিষ্ঠভাবে দেখে যেগুলি স্ট্যাটাস পরিবর্তন করে এবং কীভাবে সেগুলি UDF ব্যবহার করে প্রসেস করা যেতে পারে।

লজিকের ধরন

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

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

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

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

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

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

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

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

UI স্টেট এক্সপোজ করুন

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

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

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

    val uiState: NewsUiState = /* ... */
}

Kotlin ফ্লো সম্পর্কে প্রাথমিক ধারণা পেতে, Android-এ Kotlin ফ্লো দেখুন। StateFlow কীভাবে পর্যবেক্ষণযোগ্য ডেটা হোল্ডার হিসেবে ব্যবহার করতে হয় তা জানতে, Jetpack Compose-এ উন্নত State ও Side Effects কোডল্যাব দেখুন।

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

UiState-এর স্ট্রিম তৈরি করার একটি সাধারণ উপায় হল private set-এর সাথে mutableStateOf প্রপার্টি এক্সপোজ করা, ViewModel-এর মধ্যে স্টেট মিউটেবল রাখা কিন্তু UI-এর জন্য শুধু-পড়ার-উপযোগী।

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-কে 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 যদি দীর্ঘ সময় ধরে চলা অপারেশন পারফর্ম করে, তাহলে সেই লজিক ব্যাকগ্রাউন্ড থ্রেডে সরিয়ে নিয়ে যাওয়ার দায়িত্বও তার। Kotlin coroutines হল কনকারেন্ট অপারেশন ম্যানেজ করার একটি দুর্দান্ত উপায় এবং Jetpack আর্কিটেকচার কম্পোনেন্ট তাদের জন্য বিল্ট-ইন সহায়তা প্রদান করে। Android অ্যাপে coroutines ব্যবহার করা সম্পর্কে আরও জানতে, Android-এ Kotlin coroutines দেখুন।

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

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")
        }
    }
}

অ্যানিমেশন

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

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

অতিরিক্ত রিসোর্স

কন্টেন্ট দেখে

নমুনা

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