ধারণা এবং জেটপ্যাক কম্পোজ বাস্তবায়ন
UI ইভেন্ট হলো এমন কিছু কাজ যা UI লেয়ারে, হয় UI দ্বারা অথবা ViewModel দ্বারা পরিচালিত হওয়া উচিত। সবচেয়ে সাধারণ ধরনের ইভেন্ট হলো ইউজার ইভেন্ট । ব্যবহারকারী অ্যাপের সাথে ইন্টারঅ্যাক্ট করার মাধ্যমে ইউজার ইভেন্ট তৈরি করে—উদাহরণস্বরূপ, স্ক্রিনে ট্যাপ করে বা জেসচার তৈরি করে। এরপর UI onClick() লিসেনারের মতো কলব্যাক ব্যবহার করে এই ইভেন্টগুলো গ্রহণ করে।
সাধারণত, ViewModel কোনো নির্দিষ্ট ইউজার ইভেন্টের বিজনেস লজিক পরিচালনার দায়িত্বে থাকে—উদাহরণস্বরূপ, কোনো ডেটা রিফ্রেশ করার জন্য ব্যবহারকারীর কোনো বাটনে ক্লিক করা। সাধারণত, ViewModel এমন কিছু ফাংশন উন্মুক্ত করার মাধ্যমে এই কাজটি করে থাকে, যেগুলোকে UI কল করতে পারে। ইউজার ইভেন্টগুলোর নিজস্ব UI বিহেভিয়ার লজিকও থাকতে পারে, যা UI সরাসরি পরিচালনা করতে পারে—যেমন, অন্য কোনো স্ক্রিনে নেভিগেট করা বা একটি Snackbar দেখানো।
বিভিন্ন মোবাইল প্ল্যাটফর্ম বা ফর্ম ফ্যাক্টরে একই অ্যাপের জন্য বিজনেস লজিক একই থাকলেও, UI বিহেভিয়ার লজিক হলো একটি ইমপ্লিমেন্টেশন ডিটেইল যা বিভিন্ন ক্ষেত্রে ভিন্ন হতে পারে। UI লেয়ার পেজ এই ধরনের লজিকগুলোকে নিম্নরূপে সংজ্ঞায়িত করে:
- বিজনেস লজিক বলতে বোঝায় স্টেট পরিবর্তনের ক্ষেত্রে কী করতে হবে —উদাহরণস্বরূপ, পেমেন্ট করা বা ব্যবহারকারীর পছন্দ সংরক্ষণ করা। ডোমেইন এবং ডেটা লেয়ার সাধারণত এই লজিকটি পরিচালনা করে। এই গাইড জুড়ে, বিজনেস লজিক পরিচালনাকারী ক্লাসগুলোর জন্য একটি সুনির্দিষ্ট সমাধান হিসেবে আর্কিটেকচার কম্পোনেন্টস-এর ViewModel ক্লাসটি ব্যবহার করা হয়েছে।
- UI বিহেভিয়ার লজিক বা UI লজিক বলতে বোঝায় অবস্থার পরিবর্তন কীভাবে প্রদর্শন করা হবে —উদাহরণস্বরূপ, নেভিগেশন লজিক বা ব্যবহারকারীকে কীভাবে বার্তা দেখানো হবে। UI এই লজিকটি পরিচালনা করে।
ব্যবহারকারীর ইভেন্টগুলি পরিচালনা করুন
যদি ইউজার ইভেন্টগুলো কোনো UI এলিমেন্টের অবস্থা পরিবর্তনের সাথে সম্পর্কিত হয়—উদাহরণস্বরূপ, একটি এক্সপ্যান্ডেবল আইটেমের অবস্থা—তবে UI সরাসরি সেই ইভেন্টগুলো হ্যান্ডেল করতে পারে। যদি ইভেন্টটির জন্য বিজনেস লজিক সম্পাদনের প্রয়োজন হয়, যেমন স্ক্রিনের ডেটা রিফ্রেশ করা, তবে সেটি ViewModel দ্বারা প্রসেস করা উচিত।
নিম্নলিখিত উদাহরণে দেখানো হয়েছে কিভাবে একটি UI এলিমেন্ট প্রসারিত করতে (UI লজিক) এবং স্ক্রিনের ডেটা রিফ্রেশ করতে (বিজনেস লজিক) বিভিন্ন বাটন ব্যবহার করা হয়:
class LatestNewsActivity : AppCompatActivity() {
private lateinit var binding: ActivityLatestNewsBinding
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
// The expand details event is processed by the UI that
// modifies a View's internal state.
binding.expandButton.setOnClickListener {
binding.expandedSection.visibility = View.VISIBLE
}
// The refresh event is processed by the ViewModel that is in charge
// of the business logic.
binding.refreshButton.setOnClickListener {
viewModel.refreshNews()
}
}
}
RecyclerViews-এ ব্যবহারকারীর ইভেন্ট
যদি অ্যাকশনটি UI ট্রি-এর আরও নিচের দিকে, যেমন একটি RecyclerView আইটেম বা কাস্টম View তে সংঘটিত হয়, তাহলেও ViewModel এরই ইউজার ইভেন্টগুলো হ্যান্ডেল করা উচিত।
উদাহরণস্বরূপ, ধরা যাক NewsActivity এর সমস্ত নিউজ আইটেমে একটি বুকমার্ক বাটন রয়েছে। ViewModel কে বুকমার্ক করা নিউজ আইটেমটির আইডি জানতে হবে। যখন ব্যবহারকারী কোনো নিউজ আইটেম বুকমার্ক করেন, তখন RecyclerView অ্যাডাপ্টারটি ViewModel থেকে এক্সপোজ করা addBookmark(newsId) ফাংশনটি কল করে না, কারণ এর জন্য ViewModel উপর একটি ডিপেন্ডেন্সি প্রয়োজন হবে। এর পরিবর্তে, ViewModel একটি স্টেট অবজেক্ট এক্সপোজ করে যার নাম NewsItemUiState এবং এতে ইভেন্টটি হ্যান্ডেল করার জন্য ইমপ্লিমেন্টেশন থাকে:
data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
val publicationDate: String,
val onBookmark: () -> Unit
)
class LatestNewsViewModel(
private val formatDateUseCase: FormatDateUseCase,
private val repository: NewsRepository
)
val newsListUiItems = repository.latestNews.map { news ->
NewsItemUiState(
title = news.title,
body = news.body,
bookmarked = news.bookmarked,
publicationDate = formatDateUseCase(news.publicationDate),
// Business logic is passed as a lambda function that the
// UI calls on click events.
onBookmark = {
repository.addBookmark(news.id)
}
)
}
}
এইভাবে, RecyclerView অ্যাডাপ্টারটি শুধুমাত্র তার প্রয়োজনীয় ডেটা নিয়েই কাজ করে: অর্থাৎ NewsItemUiState অবজেক্টগুলোর তালিকা। অ্যাডাপ্টারটির সম্পূর্ণ ViewModel-এ অ্যাক্সেস থাকে না, ফলে ViewModel দ্বারা উন্মুক্ত কার্যকারিতার অপব্যবহার হওয়ার সম্ভাবনা কমে যায়। যখন আপনি শুধুমাত্র অ্যাক্টিভিটি ক্লাসকে ViewModel-এর সাথে কাজ করার অনুমতি দেন, তখন আপনি দায়িত্বগুলো পৃথক করে দেন। এটি নিশ্চিত করে যে ভিউ বা RecyclerView অ্যাডাপ্টারের মতো UI-নির্দিষ্ট অবজেক্টগুলো সরাসরি ViewModel-এর সাথে ইন্টারঅ্যাক্ট করবে না।
ব্যবহারকারী ইভেন্ট ফাংশনের নামকরণের নিয়মাবলী
এই নির্দেশিকায়, ব্যবহারকারীর ইভেন্ট পরিচালনা করে এমন ViewModel ফাংশনগুলোর নামকরণ করা হয়েছে তাদের দ্বারা সম্পাদিত কাজের ওপর ভিত্তি করে একটি ক্রিয়াপদ (verb) দিয়ে—উদাহরণস্বরূপ: addBookmark(id) অথবা logIn(username, password) ।
ViewModel ইভেন্টগুলি পরিচালনা করুন
ViewModel থেকে উদ্ভূত UI অ্যাকশন—অর্থাৎ ViewModel ইভেন্ট—এর ফলে সর্বদা UI স্টেট আপডেট হওয়া উচিত। এটি একমুখী ডেটা প্রবাহের (Unidirectional Data Flow ) নীতিমালার সাথে সঙ্গতিপূর্ণ। এটি কনফিগারেশন পরিবর্তনের পরেও ইভেন্টগুলোকে পুনরায় ঘটানো সম্ভব করে এবং নিশ্চিত করে যে UI অ্যাকশনগুলো হারিয়ে যাবে না। ঐচ্ছিকভাবে, আপনি সেভড স্টেট মডিউল (saved state module) ব্যবহার করে প্রসেস বন্ধ হয়ে যাওয়ার পরেও ইভেন্টগুলোকে পুনরায় ঘটানো সম্ভব করতে পারেন।
UI অ্যাকশনগুলোকে UI স্টেটের সাথে মেলানো সবসময় একটি সহজ প্রক্রিয়া নয়, তবে এটি সহজতর লজিকের দিকে নিয়ে যায়। উদাহরণস্বরূপ, UI-কে কীভাবে একটি নির্দিষ্ট স্ক্রিনে নেভিগেট করানো যায়, শুধু তা নির্ধারণ করেই আপনার চিন্তাভাবনা শেষ হয়ে যাওয়া উচিত নয়। আপনাকে আরও ভাবতে হবে এবং আপনার UI স্টেটে সেই ইউজার ফ্লো-কে কীভাবে উপস্থাপন করা যায়, তা বিবেচনা করতে হবে। অন্য কথায়: UI-কে কী কী অ্যাকশন নিতে হবে, তা নিয়ে ভাববেন না; বরং ভাবুন সেই অ্যাকশনগুলো UI স্টেটকে কীভাবে প্রভাবিত করে।
উদাহরণস্বরূপ, ব্যবহারকারী লগইন স্ক্রিনে লগ ইন থাকা অবস্থায় হোম স্ক্রিনে যাওয়ার বিষয়টি বিবেচনা করুন। আপনি UI স্টেটে এটিকে নিম্নোক্তভাবে মডেল করতে পারেন:
data class LoginUiState(
val isLoading: Boolean = false,
val errorMessage: String? = null,
val isUserLoggedIn: Boolean = false
)
এই UI, isUserLoggedIn অবস্থার পরিবর্তনের সাথে সাথে সাড়া দেয় এবং প্রয়োজন অনুযায়ী সঠিক গন্তব্যে নেভিগেট করে:
class LoginViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
/* ... */
}
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
ইভেন্ট গ্রহণ করলে অবস্থার আপডেট শুরু হতে পারে।
UI-তে নির্দিষ্ট কিছু ViewModel ইভেন্ট ব্যবহার করার ফলে UI-এর অন্যান্য স্টেট আপডেট হতে পারে। উদাহরণস্বরূপ, ব্যবহারকারীকে কিছু একটা ঘটেছে তা জানানোর জন্য যখন স্ক্রিনে ক্ষণস্থায়ী বার্তা দেখানো হয়, তখন বার্তাটি দেখানো শেষ হলে UI-কে ViewModel-কে অবহিত করতে হয় যাতে আরেকটি স্টেট আপডেট ট্রিগার করা যায়। ব্যবহারকারী যখন বার্তাটি গ্রহণ করে (সেটি বাতিল করে বা একটি নির্দিষ্ট সময় পর), তখন যে ইভেন্টটি ঘটে, সেটিকে "ব্যবহারকারীর ইনপুট" হিসাবে গণ্য করা যেতে পারে এবং সেই হিসেবে, ViewModel-এর এ বিষয়ে অবগত থাকা উচিত। এই পরিস্থিতিতে, UI স্টেটকে নিম্নোক্তভাবে মডেল করা যেতে পারে:
// Models the UI state for the Latest news screen.
data class LatestNewsUiState(
val news: List<News> = emptyList(),
val isLoading: Boolean = false,
val userMessage: String? = null
)
যখন ব্যবসায়িক যুক্তি অনুযায়ী ব্যবহারকারীকে একটি নতুন ক্ষণস্থায়ী বার্তা দেখানোর প্রয়োজন হয়, তখন ViewModel নিম্নলিখিতভাবে UI অবস্থা আপডেট করবে:
class LatestNewsViewModel(/* ... */) : ViewModel() {
private val _uiState = MutableStateFlow(LatestNewsUiState(isLoading = true))
val uiState: StateFlow<LatestNewsUiState> = _uiState
fun refreshNews() {
viewModelScope.launch {
// If there isn't internet connection, show a new message on the screen.
if (!internetConnection()) {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = "No Internet connection")
}
return@launch
}
// Do something else.
}
}
fun userMessageShown() {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = null)
}
}
}
স্ক্রিনে UI কীভাবে বার্তাটি দেখাচ্ছে তা ViewModel-এর জানার প্রয়োজন নেই; এটি শুধু জানে যে একটি ব্যবহারকারী বার্তা আছে যা দেখানো প্রয়োজন। ক্ষণস্থায়ী বার্তাটি দেখানো হয়ে গেলে, UI-কে ViewModel-কে সে সম্পর্কে অবহিত করতে হয়, যার ফলে আরেকটি UI স্টেট আপডেট userMessage প্রপার্টিটি খালি করে দেয়:
class LatestNewsActivity : AppCompatActivity() {
private val viewModel: LatestNewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
uiState.userMessage?.let {
// TODO: Show Snackbar with userMessage.
// Once the message is displayed and
// dismissed, notify the ViewModel.
viewModel.userMessageShown()
}
...
}
}
}
}
}
যদিও বার্তাটি ক্ষণস্থায়ী, UI অবস্থাটি প্রতিটি মুহূর্তে স্ক্রিনে যা প্রদর্শিত হয় তার একটি বিশ্বস্ত প্রতিচ্ছবি। ব্যবহারকারীর বার্তাটি হয় প্রদর্শিত হয়, অথবা হয় না।
নেভিগেশন ইভেন্ট
" কনজিউমিং ইভেন্টস ক্যান ট্রিগার স্টেট আপডেটস" বিভাগে বিস্তারিতভাবে বর্ণনা করা হয়েছে যে, কীভাবে স্ক্রিনে ব্যবহারকারীর বার্তা প্রদর্শন করার জন্য UI স্টেট ব্যবহার করা হয়। নেভিগেশন ইভেন্টগুলোও একটি অ্যান্ড্রয়েড অ্যাপের সাধারণ ধরনের ইভেন্ট।
যদি ব্যবহারকারী কোনো বাটনে ট্যাপ করার কারণে UI-তে ইভেন্টটি ট্রিগার হয়, তাহলে UI নেভিগেশন কন্ট্রোলারকে কল করার মাধ্যমে সেই কাজটি সম্পন্ন করে।
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.helpButton.setOnClickListener {
navController.navigate(...) // Open help screen
}
}
}
নেভিগেট করার আগে যদি ডেটা ইনপুটের কোনো বিজনেস লজিক ভ্যালিডেশনের প্রয়োজন হয়, তাহলে ViewModel-কে সেই স্টেটটি UI-এর কাছে প্রকাশ করতে হবে। UI সেই স্টেট পরিবর্তনের সাথে সাড়া দিয়ে সেই অনুযায়ী নেভিগেট করবে। ' Handle ViewModel events' সেকশনটিতে এই ব্যবহারের ক্ষেত্রটি আলোচনা করা হয়েছে। নিচে অনুরূপ একটি কোড দেওয়া হলো:
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
if (uiState.isUserLoggedIn) {
// Navigate to the Home screen.
}
...
}
}
}
}
}
উপরের উদাহরণে, অ্যাপটি প্রত্যাশিতভাবেই কাজ করে, কারণ বর্তমান গন্তব্য, অর্থাৎ লগইন, ব্যাক স্ট্যাকে সংরক্ষিত থাকে না। ব্যবহারকারীরা ব্যাক বাটন চাপলেও সেখানে ফিরে যেতে পারেন না। তবে, যেসব ক্ষেত্রে এমনটা ঘটার সম্ভাবনা থাকে, সেসব সমাধানের জন্য অতিরিক্ত লজিকের প্রয়োজন হবে।
গন্তব্য ব্যাক স্ট্যাকে রাখা হলে নেভিগেশন ইভেন্ট
যখন একটি ViewModel এমন কোনো স্টেট সেট করে যা স্ক্রিন A থেকে স্ক্রিন B-তে একটি ন্যাভিগেশন ইভেন্ট তৈরি করে এবং স্ক্রিন A ন্যাভিগেশন ব্যাক স্ট্যাকে রাখা হয়, তখন স্বয়ংক্রিয়ভাবে B-তে অগ্রসর হওয়া বন্ধ করার জন্য আপনার অতিরিক্ত লজিকের প্রয়োজন হতে পারে। এটি বাস্তবায়ন করার জন্য, এমন একটি অতিরিক্ত স্টেট থাকা প্রয়োজন যা নির্দেশ করবে যে UI অন্য স্ক্রিনে ন্যাভিগেট করার কথা বিবেচনা করবে কি না। সাধারণত, সেই স্টেটটি UI-তে রাখা হয়, কারণ ন্যাভিগেশন লজিক হলো UI-এর কাজ, ViewModel-এর নয়। বিষয়টি ব্যাখ্যা করার জন্য, আসুন নিম্নলিখিত ব্যবহারের ক্ষেত্রটি বিবেচনা করি।
ধরা যাক, আপনি আপনার অ্যাপের রেজিস্ট্রেশন ফ্লো-তে আছেন। জন্মতারিখ যাচাইকরণ স্ক্রিনে, যখন ব্যবহারকারী একটি তারিখ ইনপুট করেন, তখন "Continue" বোতামে ট্যাপ করলে ViewModel দ্বারা তারিখটি যাচাই করা হয়। ViewModel যাচাইকরণের লজিকটি ডেটা লেয়ারের কাছে অর্পণ করে। যদি তারিখটি বৈধ হয়, তবে ব্যবহারকারী পরবর্তী স্ক্রিনে চলে যান। একটি অতিরিক্ত বৈশিষ্ট্য হিসেবে, ব্যবহারকারীরা কোনো ডেটা পরিবর্তন করতে চাইলে বিভিন্ন রেজিস্ট্রেশন স্ক্রিনের মধ্যে আসা-যাওয়া করতে পারেন। সুতরাং, রেজিস্ট্রেশন ফ্লো-এর সমস্ত গন্তব্য একই ব্যাক স্ট্যাকে রাখা হয়। এই প্রয়োজনীয়তাগুলো বিবেচনা করে, আপনি এই স্ক্রিনটি নিম্নোক্তভাবে বাস্তবায়ন করতে পারেন:
// Key that identifies the `validationInProgress` state in the Bundle
private const val DOB_VALIDATION_KEY = "dobValidationKey"
class DobValidationFragment : Fragment() {
private var validationInProgress: Boolean = false
private val viewModel: DobValidationViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = // ...
validationInProgress = savedInstanceState?.getBoolean(DOB_VALIDATION_KEY) ?: false
binding.continueButton.setOnClickListener {
viewModel.validateDob()
validationInProgress = true
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect { uiState ->
// Update other parts of the UI ...
// If the input is valid and the user wants
// to navigate, navigate to the next screen
// and reset `validationInProgress` flag
if (uiState.isDobValid && validationInProgress) {
validationInProgress = false
navController.navigate(...) // Navigate to next screen
}
}
}
return binding
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(DOB_VALIDATION_KEY, validationInProgress)
}
}
জন্মতারিখ যাচাইকরণ হলো একটি বিজনেস লজিক , যার দায়িত্ব ViewModel-এর। বেশিরভাগ সময়, ViewModel এই লজিকটি ডেটা লেয়ারের উপর ছেড়ে দেয়। ব্যবহারকারীকে পরবর্তী স্ক্রিনে নিয়ে যাওয়ার লজিকটি হলো UI লজিক, কারণ UI কনফিগারেশনের উপর নির্ভর করে এই প্রয়োজনীয়তাগুলো পরিবর্তিত হতে পারে। উদাহরণস্বরূপ, আপনি যদি একই সাথে একাধিক রেজিস্ট্রেশন ধাপ দেখান, তবে একটি ট্যাবলেটে ব্যবহারকারীকে স্বয়ংক্রিয়ভাবে অন্য স্ক্রিনে যেতে নাও চাইতে পারেন। উপরের কোডে থাকা validationInProgress ভেরিয়েবলটি এই কার্যকারিতাটি বাস্তবায়ন করে এবং জন্মতারিখ বৈধ হলে ও ব্যবহারকারী পরবর্তী রেজিস্ট্রেশন ধাপে যেতে চাইলে UI স্বয়ংক্রিয়ভাবে নেভিগেট করবে কি না, তা নিয়ন্ত্রণ করে।
আপনার জন্য প্রস্তাবিত
- দ্রষ্টব্য: জাভাস্ক্রিপ্ট বন্ধ থাকলেও লিঙ্কের লেখা প্রদর্শিত হয়।
- UI স্তর
- স্টেট হোল্ডার এবং UI স্টেট {:#mad-arch}
- অ্যাপ আর্কিটেকচারের নির্দেশিকা