viewModelScope হলো androidx.lifecycle লাইব্রেরি থেকে একটি বিল্ট-ইন CoroutineScope যা ViewModel জীবনচক্রের সাথে সংযুক্ত এবং এটি পরিষ্কার হলে স্বয়ংক্রিয়ভাবে বাতিল হয়ে যায়। Google Android Developers, 2025 অনুসারে, viewModelScope হল MVVM আর্কিটেকচারে করুটিন চালু করার মানক পদ্ধতি, যা মেমরি লিকের ঝুঁকি ছাড়াই নিরাপদ অ্যাসিনক্রোনাস অপারেশন নিশ্চিত করে। ViewModelScope ডিফল্টরূপে Dispatchers.Main ব্যবহার করে, এবং এর ভিতরে সমস্ত IO অপারেশন withContext এর মাধ্যমে সম্পাদন করতে হবে।
মূল পয়েন্ট
viewModelScope হলো ViewModel ইন্টারফেসের একটি এক্সটেনশন প্রপার্টি, যা lifecycle-viewmodel-ktx লাইব্রেরিতে (সংস্করণ 2.1.0 থেকে) যোগ করা হয়েছে। এটি ViewModel জীবনচক্রের সাথে সংযুক্ত একটি প্রস্তুত CoroutineScope সরবরাহ করে।
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope = this.getTag(JOB_KEY)
if (scope != null) return scope
return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { setTag(JOB_KEY, it) }
}
প্রথম অ্যাক্সেসে scope লেজি (lazy) ভাবে তৈরি হয় এবং setTag এর মাধ্যমে ক্যাশে করা হয়। এটি SupervisorJob ব্যবহার করে, যার অর্থ একটি চাইল্ড করুটিনে ব্যতিক্রম অন্যগুলোকে বাতিল করে না। ডিফল্ট ডিসপ্যাচার হল Dispatchers.Main.immediate, যা ইতিমধ্যে Main থ্রেডে থাকলে অতিরিক্ত ডিসপ্যাচিং ছাড়াই মূল থ্রেডে কোড executes করে।
যখন ViewModel জীবনচক্র ত্যাগ করে (Activity শেষ হয় বা Fragment সরানো হয়), সিস্টেম clear() কল করে, যা onCleared() ট্রিগার করে। এই কলব্যাকে, viewModelScope তার Job বাতিল করে, যা পুনরাবৃত্তভাবে সমস্ত সক্রিয় করুটিন শেষ করে। প্রক্রিয়াটি Closeable ইন্টারফেসের মাধ্যমে বাস্তবায়িত হয়, যেখানে scope Job স্বয়ংক্রিয় বন্ধের জন্য একটি রিসোর্স হিসাবে নিবন্ধিত হয়।
viewModelScope-এর ViewModel জীবনচক্রের সাথে সংযোগের প্রক্রিয়াটি ট্যাগিং এবং onCleared কলব্যাকের উপর ভিত্তি করে তৈরি। চলুন ধাপে ধাপে দেখি।
যখন ViewModel viewModelScope.launch { ... } executes করে, getter চেক করে JOB_KEY ট্যাগের অধীনে আগে থেকে কোনো scope সংরক্ষিত আছে কিনা। যদি scope না থাকে, তাহলে একটি নতুন CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) instance তৈরি হয়। Scope একটি অভ্যন্তরীণ ট্যাগ ম্যাপের মাধ্যমে ViewModel-এর ভিতরে সংরক্ষিত হয়।
viewModelScope.launch বা viewModelScope.async এর মাধ্যমে চালু করা সমস্ত করুটিন scope-এর SupervisorJob-এর চাইল্ড হয়ে যায়। তারা মূল থ্রেডে চলে (যদি not withContext এর মাধ্যমে ভিন্ন ডিসপ্যাচার নির্দিষ্ট করা না থাকে)। যতক্ষণ ViewModel জীবিত, করুটিন সক্রিয়, স্থগিত বা সম্পূর্ণ থাকতে পারে।
যখন সিস্টেম ViewModel ধ্বংস করে, ViewModel.clear() কল করা হয়। clear()-এর ভিতরে নিম্নলিখিত ঘটনাগুলো ঘটে:
স্ক্রিন ঘোরালে, Activity পুনরায় তৈরি হয়, কিন্তু ViewModel টিকে থাকে (ViewModelStoreOwner-এর ধন্যবাদ)। এর অর্থ viewModelScope সক্রিয় থাকে এবং করুটিনগুলি বাধা ছাড়াই চলতে থাকে। Activity পুনরায় তৈরি হওয়ার পরে, একই ViewModel (এবং একই scope) পুনরায় ব্যবহার করা হয় — ডেটা লোডিং শুরু থেকে শুরু হয় না।
MVVM (Model-View-ViewModel) Android অ্যাপ্লিকেশনের জন্য Google-এর প্রস্তাবিত আর্কিটেকচার। viewModelScope এতে অ্যাসিনক্রোনাস অপারেশনের নির্বাহক হিসাবে কেন্দ্রীয় ভূমিকা পালন করে।
| স্তর | উপাদান | viewModelScope-এর ভূমিকা |
|---|---|---|
| UI | Activity / Fragment | ViewModel থেকে StateFlow/LiveData পর্যবেক্ষণ করে |
| ViewModel | ViewModel | viewModelScope এর মাধ্যমে করুটিন চালু করে, UI অবস্থা পরিচালনা করে |
| Repository | Repository | viewModelScope করুটিন থেকে কল করার জন্য suspend ফাংশন সরবরাহ করে |
| Data | DAO / Api | প্রকৃত অনুরোধ সম্পাদন করে (Room, Retrofit) |
ViewModel viewModelScope এর মাধ্যমে করুটিন চালু করে, যার ভিতরে এটি Repository-এর suspend ফাংশন কল করে। ফলাফল StateFlow-এ রূপান্তরিত হয়, যা UI স্তর পর্যবেক্ষণ করে। এই ডিজাইন দায়িত্বের স্পষ্ট বিভাজন এবং প্রতিটি স্তরের স্বাধীন পরীক্ষাযোগ্যতা নিশ্চিত করে।
যদি করুটিন Fragment থেকে চালু করা হতো, স্ক্রিন ঘোরালে Fragment ধ্বংসের সাথে সেগুলো বাতিল হয়ে যেত। ViewModel রোটেশনে টিকে থাকে, তাই এর scope-এ চালু করা করুটিন চলতে থাকে। ডেটা লোড করার সময় lifecycleScope-এর তুলনায় viewModelScope-এর এটাই মূল সুবিধা।
আসুন Kotlin এ Android অ্যাপ্লিকেশনে viewModelScope ব্যবহারের তিনটি ব্যবহারিক দৃশ্য দেখি।
class ProfileViewModel(
private val repo: ProfileRepository
) : ViewModel() {
private val _profile = MutableStateFlow<Profile?>(null)
val profile: StateFlow<Profile?> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val result = repo.getProfile()
_profile.value = result
}
}
}
init ব্লকে, প্রোফাইল লোডিং অবিলম্বে শুরু হয়। করুটিন মূল থ্রেডে (ডিফল্টরূপে) চলে। রিপোজিটরি তার suspend ফাংশনের ভিতরে নেটওয়ার্ক অনুরোধের জন্য withContext(Dispatchers.IO) ব্যবহার করে, তাই ViewModel-কে থ্রেড সুইচিং নিয়ে ভাবতে হয় না।
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
fun fetchItems() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val items = repo.getItems()
_state.value = UiState.Success(items)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
UI অবস্থা একটি sealed class UiState এর মাধ্যমে বর্ণিত হয়। ViewModel প্রতিটি পরিবর্তনে অবস্থা আপডেট করে। Fragment StateFlow-এ সাবস্ক্রাইব করে এবং শুধুমাত্র বর্তমান অবস্থায় প্রতিক্রিয়া জানায়, আগের রোটেশন থেকে পুরানো কল উপেক্ষা করে।
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
প্রতিটি নতুন অনুসন্ধান কোয়েরিতে, পূর্ববর্তী করুটিন বাতিল হয়। delay(300) ডিবাউন্স প্রয়োগ করে — অনুসন্ধান শুধুমাত্র 300 মিলিসেকেন্ড নিষ্ক্রিয়তার পরে সম্পাদন হয়। এটি সার্ভার লোড কমায় এবং পুরানো ফলাফল প্রতিরোধ করে।
উভয় scope AndroidX Lifecycle লাইব্রেরি দ্বারা সরবরাহিত, কিন্তু ভিন্ন ভিন্ন জীবনচক্রের সাথে সংযুক্ত। পছন্দ কাজের ধরণের উপর নির্ভর করে।
| বৈশিষ্ট্য | viewModelScope | lifecycleScope |
|---|---|---|
| মালিক | ViewModel | LifecycleOwner (Activity/Fragment) |
| রোটেশনে বাতিল | না (ViewModel টিকে থাকে) | হ্যাঁ (Activity পুনরায় তৈরি হয়) |
| ডিফল্ট ডিসপ্যাচার | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| উপলব্ধ | ViewModel | Activity, Fragment, Service |
| সাধারণ ব্যবহার | ডেটা লোডিং, ব্যবসায়িক যুক্তি | UI মিথস্ক্রিয়া, অ্যানিমেশন |
Google সমস্ত ডেটা লোডিং এবং প্রক্রিয়াকরণ কাজের জন্য viewModelScope ব্যবহার করার সুপারিশ করে। lifecycleScope UI জীবনচক্রের নির্দিষ্ট মুহূর্তের সাথে যুক্ত অপারেশনের জন্য ব্যবহার করা উচিত — উদাহরণস্বরূপ, প্রথম স্ক্রিন উপস্থিতিতে অ্যানিমেশন শুরু করা বা লোকেশন আপডেটে সাবস্ক্রাইব করা যা স্ক্রিন ছেড়ে গেলে বন্ধ হওয়া উচিত।
একটি সুপ্রতিষ্ঠিত Android API-তেও, ডেভেলপাররা সাধারণ ভুল করে থাকেন। চলুন চারটি সবচেয়ে সাধারণ সমস্যা দেখি।
সবচেয়ে মারাত্মক ভুল হল ViewModel পরিষ্কার হওয়ার পরে StateFlow বা LiveData আপডেট করার চেষ্টা করা। যদিও viewModelScope onCleared()-এ বাতিল হয়, একটি করুটিন প্রকৃত বাতিল কার্যকর হওয়ার আগে কোড executes করতে পারে। চেক করতে isActive ব্যবহার করুন বা catch-ব্লক সম্পূর্ণ হওয়ার উপর নির্ভর করুন।
viewModelScope অভ্যন্তরীণভাবে SupervisorJob ব্যবহার করে, যা করুটিনের মধ্যে ত্রুটিগুলো আলাদা করে। তবে, যদি আপনি viewModelScope.launch-এর ভিতরে নিজস্ব Job() সহ করুটিন চালু করেন, সেই করুটিন SupervisorJob-এর চাইল্ড হয় কিন্তু অন্যান্য করুটিনে ত্রুটির কারণে বাতিল থেকে সুরক্ষিত থাকবে না।
যদিও viewModelScope-এর কোনো কঠোর সীমা নেই, হাজার হাজার সক্রিয় করুটিন সিস্টেমকে ধীর করে দিতে পারে। দীর্ঘ ডেটা তালিকার জন্য, প্রতিটি আইটেমের জন্য পৃথক করুটিন তৈরি করার পরিবর্তে Flow কে collectLatest এর সাথে ব্যবহার করুন।
যদি viewModelScope-এর পরিবর্তে ভুলবশত GlobalScope ইম্পোর্ট করা হয়, তাহলে ViewModel পরিষ্কার হলে করুটিন বাতিল হবে না। এটি মেমরি লিক এবং সম্ভাব্য ক্র্যাশের দিকে নিয়ে যায়। সর্বদা নিশ্চিত করুন যে করুটিনগুলি viewModelScope এর মাধ্যমে চালু করা হয়েছে, বিশেষ করে Fragment সাবক্লাসে।
প্রায়শই জিজ্ঞাসিত প্রশ্ন
আপনি সরাসরি viewModelScope-এর ডিসপ্যাচার পরিবর্তন করতে পারবেন না — এটি Dispatchers.Main.immediate হিসেবে হার্ডকোড করা। তবে, করুটিনের ভিতরে আপনি withContext এর মাধ্যমে অন্য ডিসপ্যাচারে সুইচ করতে পারেন। পরীক্ষায় ডিসপ্যাচার পরিবর্তন করতে, Rule এর মাধ্যমে TestDispatcher ব্যবহার করুন।
Repository-তে scope পাস করবেন না — এটি আর্কিটেকচার নীতি লঙ্ঘন করে। Repository-র suspend ফাংশন সরবরাহ করা উচিত, এবং ViewModel নিজেই viewModelScope এর মাধ্যমে করুটিন পরিচালনা করে। যদি Repository-র scope প্রয়োজন হয়, তাহলে Clean Architecture-এর পক্ষে আর্কিটেকচার পুনর্বিবেচনা করুন।
SupervisorJob নিশ্চিত করে যে একটি করুটিনে ব্যতিক্রম (যেমন, একাধিক স্বাধীন অনুরোধের মধ্যে একটি লোডিং ত্রুটি) অন্য করুটিনগুলিকে বাতিল না করে। এটি ViewModel পরিস্থিতির সাথে মেলে যেখানে বিভিন্ন স্ক্রিন স্বাধীন ডেটা লোড করে।
হ্যাঁ, viewModelScope UI প্রকার (View System বা Jetpack Compose) নির্বিশেষে যেকোনো ViewModel-এ উপলব্ধ। Compose-এ, করুটিনও viewModelScope এর মাধ্যমে চালু করা হয়, যখন UI প্রভাবের জন্য LaunchedEffect এবং rememberCoroutineScope ব্যবহার করা হয়।
viewModelScope.cancel() কল করা scope তৎক্ষণাৎ বাতিল করে — সমস্ত সক্রিয় করুটিন CancellationException-এর সাথে শেষ হয়। এর পরে viewModelScope.launch কল করা হলে, getter-এ পরবর্তী অ্যাক্সেসে স্বয়ংক্রিয়ভাবে একটি নতুন scope তৈরি হয়।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন