Transform Android Jetpack-এর অংশ।
Transform হল Compose রানটাইমে তৈরি একটি লাইব্রেরি যা UI ছাড়াই একটি @Composable
ব্লক চালায় যাতে ইনপুটগুলিকে Compose State-এ রূপান্তর করা যায়।
ব্লকের মধ্যে পড়া যেকোনও State পরিবর্তন হলে, ব্লকটি অটোমেটিক
রি-কম্পোজ করে এবং রিটার্ন করা State আপডেট করে—আপনাকে ViewModels, রিপোজিটরি বা
ব্যাকগ্রাউন্ড সার্ভিসে Compose-এর ডিপেন্ডেন্সি ট্র্যাকিং এবং রি-কম্পোজিশন মডেল ব্যবহার করতে দেয়।
উদাহরণ
- রিঅ্যাক্টিভ ডেটা ট্রান্সফর্মেশন: র ডেটা স্ট্রিম (যেমন নেটওয়ার্ক রেসপন্স বা ডেটাবেস কোয়েরি) UI-রেডি স্টেটে কনভার্ট করুন।
- একাধিক ইনপুট একত্রিত করা: জটিল
Flowঅপারেটর ছাড়াই একাধিক স্বাধীন স্টেট সোর্স (যেমন ব্যবহারকারীর ইনপুট, সেটিংস এবং নেটওয়ার্ক স্ট্যাটাস) একটি একক সুসংহত স্টেটে মার্জ করুন। - ডিবাউন্সিং ও থ্রটলিং: স্ট্যান্ডার্ড Compose সাইড-ইফেক্ট
যেমন
LaunchedEffectব্যবহার করে সময়-ভিত্তিক আচরণ (যেমন সার্চ কোয়েরি ডিবাউন্স করা) প্রয়োগ করুন। - কন্ডিশনাল অপারেশন: স্ট্যান্ডার্ড
if-elseবাwhenএক্সপ্রেশন ব্যবহার করে বর্তমান পরিস্থিতির উপর ভিত্তি করে বিভিন্ন স্টেট-গ্যাদারিং লজিক রান করুন।
API ওভারভিউ
androidx.appstate.transform প্যাকেজ নিম্নলিখিত ফাংশন প্রদান করে:
package androidx.appstate.transform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable (R) -> R,
): State<R>
@Composable
public fun <R> transform(
initialValue: R,
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable (R) -> R,
): State<R>
public suspend fun listener(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
onUpdate: @Composable () -> Unit,
)
ট্রান্সফর্ম তৈরি করুন
আপনি নিজে থেকে (প্রদান করা
CoroutineScope ব্যবহার করে) অথবা আগে থেকে থাকা কম্পোজিশনের মধ্যে ট্রান্সফর্ম তৈরি করতে পারবেন। যেকোনও ট্রান্সফর্মের জন্য মূল প্যারামিটার হল
initialValue এবং onUpdate। onUpdate ল্যাম্বডা তার প্যারামিটার হিসেবে পূর্ববর্তী আউটপুট ভ্যালু R গ্রহণ করে (প্রথম কম্পোজিশনে initialValue-এ ইনিশিয়ালাইজ করা হয়)। আপনি এই প্যারামিটার ফিরিয়ে দিতে পারেন যাতে লোডিং স্টেটে রিসেট করার পরিবর্তে
শেষবার এমিট করা স্টেট বজায় রাখা যায়, যেমন
{ previous -> flow.collectAsState(initial = null).value ?: previous }।
যেমন, আপনি transform ব্যবহার করে সার্চ কোয়েরি স্ট্রিংয়ে পরিবর্তন লক্ষ্য করতে পারেন
এবং নেটওয়ার্ক কলের মতো ব্যয়বহুল কম্পিউটেশন ডিবাউন্স করতে পারেন:
var query: String by mutableStateOf("") val searchResults: List<String> by transform<List<String>>( initialValue = emptyList(), scope = coroutineScope, ) { var results by remember { mutableStateOf(emptyList<String>()) } LaunchedEffect(query) { if (query.isBlank()) { results = emptyList() return@LaunchedEffect } delay(300.milliseconds) results = api.search(query) } results }
এই স্নিপেটে:
query-এর ব্যাক-আপMutableState-এর মাধ্যমে নেওয়া হয়, যা সার্চ বার UI কম্পোজেবল পড়তে ও আপডেট করতে পারে।transformএকটি খালি তালিকারinitialValue, একটিCoroutineScopeএবংonUpdate-কে ট্রেলিং ল্যাম্বডা হিসেবে নেয়।onUpdateব্লকের মধ্যে,LaunchedEffectরান করার সময়rememberরিকম্পোজিশন জুড়েresultsস্টেট বজায় রাখে।LaunchedEffect,query-কে কী হিসেবে নেয়।query-এর ব্যাক-আপMutableState-এর মাধ্যমে নেওয়া হয় বলে,queryপরিবর্তন করা হলে প্রতিবার এফেক্ট বাতিল করে আবার চালু করা হয়।- প্রতিটি কিস্ট্রোকে API কল করা এড়াতে, এফেক্ট বডি ৩০০ মিলি সেকেন্ড অপেক্ষা করে। যদি সেই বিলম্ব সম্পূর্ণ হওয়ার আগে কোয়েরি পরিবর্তিত হয়, তাহলে এফেক্ট বাতিল করা হয় এবং কোনও নেটওয়ার্ক কল করা হয় না।
@Composable ফাংশনের মধ্যে, আপনি @Composable transform
ওভারলোডও ব্যবহার করতে পারবেন, যা ইন্টার্নালি CoroutineScope মনে রাখে এবং ডিফল্ট হিসেবে
Dispatchers.Default-এ onUpdate রান করে। এটি তখন উপযোগী হয় যখন আপনি মূল UI থ্রেড থেকে একটি ব্যয়বহুল স্টেট ডেরিভেশন (যেমন একটি বড় তালিকা ফিল্টার করা বা সার্চ করা) অফলোড করতে চান:
@Composable fun LogViewerScreen( entriesState: State<List<LogEntry>>, filterQueryState: State<String>, ) { // Calls rememberCoroutineScope() internally and runs onUpdate on // Dispatchers.Default so main-thread UI recomposition isn't blocked val matchingEntries by transform<List<LogEntry>>( initialValue = emptyList(), dispatcher = Dispatchers.Default, ) { val entries by entriesState val filterQuery by filterQueryState if (filterQuery.isBlank()) { entries } else { entries.filter { entry -> entry.matchesQuery(filterQuery) } } } // Render matchingEntries... }
listener-এর সাথে স্ট্যাটাস দেখুন
আপনি যখন কোনও নন-UI কম্পোনেন্টে (যেমন, স্টেট ম্যানেজার, রিপোজিটরি বা ব্যাকগ্রাউন্ড পরিষেবা)
Compose State-এর পরিবর্তনের প্রতিক্রিয়া জানাতে চান, তখন কোনও ডেরাইভড State<R> তৈরি না করে
listener ব্যবহার করুন।
listener হল একটি suspend ফাংশন যা কলিং কোরুটিন বাতিল না হওয়া পর্যন্ত
একটি @Composable () -> Unit ব্লক রান করে, onUpdate-এর মধ্যে রিড করা কোনও স্টেট পরিবর্তন হলেই
অটোমেটিক রিকম্পোজ করে:
class FocusSessionManager( private val stateStore: StateStore, private val notificationManager: NotificationManager, scope: CoroutineScope, ) { val session: State<FocusSession> = stateStore.getState(FocusSessionKey) init { scope.launch { listener { when (val current = session.value) { is FocusSession.Running -> { LaunchedEffect(current.endTimeMillis) { val remainingMillis = current.endTimeMillis - System.currentTimeMillis() if (remainingMillis > 0) { delay(remainingMillis) } stateStore.setState(FocusSessionKey, FocusSession.Idle) notificationManager.notify( NOTIFICATION_ID, buildSessionCompleteNotification(current.taskId), ) } } FocusSession.Idle, is FocusSession.Paused -> Unit } } } } // startFocus, pauseFocus, resumeFocus, endFocus... }
এই উদাহরণে, listener session.value লক্ষ্য করে। সেশন FocusSession.Running হলে,
LaunchedEffect সেশন FocusSession.Idle-এ রিসেট করা এবং সম্পূর্ণ হওয়ার বিজ্ঞপ্তি পোস্ট করার আগে
endTimeMillis পর্যন্ত অপেক্ষা করে। ব্যবহারকারী সেশন পজ করলে বা আগে শেষ করলে, Running ব্রাঞ্চ
কম্পোজিশন থেকে বেরিয়ে আসে এবং অটোমেটিক টাইমার বাতিল করে দেয়। scope বাতিল করা হলে, listener পর্যবেক্ষণ করা বন্ধ করে দেয়।
সাধারণ প্যাটার্ন
নিম্নলিখিত প্যাটার্নগুলি দেখায় যে কীভাবে কন্ডিশনাল লজিকের জন্য transform ব্যবহার করতে হয়,
একাধিক স্ট্রিম একত্রিত করা এবং ViewModel-এ UI স্টেট তৈরি করা।
শর্তসাপেক্ষ স্টেট প্রোডাকশন
বর্তমান অবস্থার উপর ভিত্তি করে আপস্ট্রিম স্টেট সোর্সের মধ্যে পরিবর্তন করতে
transform-এর মধ্যে স্ট্যান্ডার্ড Kotlin if-else বা when এক্সপ্রেশন ব্যবহার করুন। যেহেতু রিড
ট্র্যাকিং ডায়নামিক, তাই transform শুধুমাত্র সেই ব্রাঞ্চটি পর্যবেক্ষণ করে যা
বর্তমান রিকম্পোজিশনে চলে:
var searchQuery by mutableStateOf("") val displayedTasks: State<List<Task>> = transform( initialValue = emptyList(), scope = coroutineScope, ) { if (searchQuery.isBlank()) { val recentTasks by tasksRepository.recentTasks.collectAsState( initial = emptyList(), ) recentTasks } else { val matchingTasks by remember(searchQuery) { tasksRepository.searchTasks(searchQuery) }.collectAsState(initial = emptyList()) matchingTasks } }
একাধিক স্টেট সোর্স একত্রিত করুন
Compose অটোমেটিক স্টেট রিড ট্র্যাক করে বলে, আপনি যেকোনও সংখ্যক
State<T> প্রপার্টি এবং Flow স্ট্রিম একটিই transform ব্লকে একত্রিত করতে পারবেন। Flow.combine-এর মতো নয়, যা টাইপ না করা Array<Any?>-এ ফিরে যাওয়ার আগে সর্বাধিক পাঁচটি ফ্লোর জন্য টাইপ করা ওভারলোড প্রদান করে, transform-এ আরেকটি ইনপুট যোগ করা সবসময় একটি একক টাইপ-নিরাপদ রিড:
var showCompleted by mutableStateOf(false) var selectedCategory by mutableStateOf<Category?>(null) val dashboardState: State<DashboardUiState> = transform( initialValue = DashboardUiState(), scope = coroutineScope, ) { val tasks by tasksRepository.tasksFlow.collectAsState(initial = emptyList()) val sortOrder by userPrefsRepository.sortOrderFlow.collectAsState( initial = SortOrder.BY_DEADLINE, ) val focusSession by focusSessionManager.session val isOnline by networkMonitor.isOnline val visibleTasks = tasks .filter { task -> (showCompleted || !task.isCompleted) && (selectedCategory == null || task.category == selectedCategory) } .sortedWith(sortOrder.comparator) DashboardUiState( tasks = visibleTasks, selectedCategory = selectedCategory, focusSession = focusSession, isOnline = isOnline, ) }
ViewModel-এ transform ব্যবহার করা
নন-কম্পোজেবল transform ওভারলোড নন-UI লেয়ারে প্রতিক্রিয়াশীল লজিক চালায়
(যেমন ViewModels বা রিপোজিটরি) Compose-এর স্টেট মডেল ব্যবহার করার সময়। এটি ততক্ষণ
রান করে যতক্ষণ প্রদত্ত CoroutineScope অ্যাক্টিভ থাকে, তাই আপনি viewModelScope-কে
ViewModel-এর মধ্যে পাস করে কোনও কম্পোনেন্টের লাইফসাইকেলের সাথে এটিকে টাই করতে পারেন:
class HomeViewModel( private val mediaRepository: MediaRepository, private val networkMonitor: NetworkMonitor, ) : ViewModel() { val uiState: State<HomeUiState> = transform<HomeUiState>( initialValue = HomeUiState.Loading, scope = viewModelScope, ) { val isOnline by networkMonitor.isOnline val tracks = if (isOnline) { mediaRepository.allTracksFlow.collectAsState(initial = null).value } else { mediaRepository.downloadedTracksFlow .collectAsState(initial = null).value } ?: return@transform HomeUiState.Loading HomeUiState.Success( tracks = tracks, isOnline = isOnline, ) } }
এই ViewModel-এ, uiState networkMonitor.isOnline পড়ে এবং অফলাইন থাকাকালীন mediaRepository.downloadedTracksFlow এবং অনলাইন থাকাকালীন mediaRepository.allTracksFlow সংগ্রহ করার মধ্যে ডাইনামিকভাবে পরিবর্তন করে, একটি State<HomeUiState> প্রকাশ করে যা UI কম্পোজেবল সরাসরি পড়তে পারে। MutableState ও SavedStateHandle.saveable-এর সাথে transform
কম্বাইন করার ব্যাপারে আরও জানতে,
ViewModel-এ Compose স্টেট ব্যবহার করুন দেখুন।
transform, derivedStateOf ও Flow-এর মধ্যে থেকে বেছে নিন
transform, derivedStateOf,
এবং Kotlin Flow-এর মধ্যে বেছে নিতে নিম্নলিখিত নির্দেশিকা ব্যবহার করুন:
derivedStateOf বনাম transform
derivedStateOf এবং transform উভয়ই একটি ডেরাইভড
State<T> তৈরি করে যা এর ব্লকের মধ্যে পড়া কম্পোজ স্টেট অটোমেটিক ট্র্যাক করে।
আপনি আগে থেকে থাকা State<T> অবজেক্ট থেকে সরাসরি কোনও ভ্যালু গণনা করছেন নাকি @Composable ফিচার যেমন
collectAsState, produceState এবং LaunchedEffect প্রয়োজন তার উপর ভিত্তি করে এগুলির মধ্যে থেকে বেছে নিন:
| ক্ষমতা | derivedStateOf |
transform |
|---|---|---|
| ল্যাম্বডা টাইপ | স্ট্যান্ডার্ড Kotlin ল্যাম্বডা (() -> T) |
@Composable ল্যাম্বডা (@Composable (R) -> R) |
| যেসব ইনপুট কাজ করে | সিঙ্ক্রোনাস State<T> শুধুমাত্র রিড করে |
State<T>, Kotlin Flow (collectAsState ব্যবহার করে), suspend ফাংশন এবং কলব্যাক API (produceState ব্যবহার করে) |
| এক্সিকিউশন ও ধারাবাহিকতা | .value রিড করার সময় লেজি ও সিঙ্ক্রোনাস; initialValue বা CoroutineScope ছাড়াই একই স্ন্যাপশটের মধ্যে অ্যাটমিকভাবে আপডেট হয় |
CoroutineScope-এ একটি অ্যাক্টিভ কম্পোজিশন রান করে এবং স্ন্যাপশট কমিট করার পরে অ্যাসিঙ্ক্রোনাসভাবে রিকম্পোজ করে; initialValue প্রয়োজন |
| State ও Side-effects | remember বা সাইড-এফেক্ট API কল করা যাবে না |
remember, LaunchedEffect, produceState ও আবার ব্যবহার করা যায় এমন @Composable ফাংশন কাজ করে |
সব ইনপুট আগে থেকেই State<T> অবজেক্ট হলে এবং আপনার @Composable API-এর প্রয়োজন না হলে
derivedStateOf ব্যবহার করুন:
- সিঙ্ক্রোনাস, একই-স্ন্যাপশট ডেরিভেশন:
CoroutineScopeম্যানেজ না করে বা প্লেসহোল্ডারinitialValueপ্রদান না করে, আগে থেকে থাকাState<T>প্রপার্টি থেকে ভ্যালু বা ফিল্টার করা তালিকা গণনা করা। কারণderivedStateOf.valueপড়ার সময় সিঙ্ক্রোনাসভাবে মূল্যায়ন করে, ইনপুট স্টেট আপডেট করার সাথে সাথেই.valueপড়া কলাররা সবসময় একই স্ন্যাপশটে আপডেট করা ফলাফল দেখতে পান। - পর্যবেক্ষক কত ঘনঘন আপডেট করে তা কমানো: এমন একটি মান গণনা করা যা তার সোর্স
State<T>-এর চেয়ে কম ঘনঘন পরিবর্তিত হয় (যেমন স্ক্রল অফসেট থ্রেশহোল্ড অতিক্রম করে কিনা তা চেক করা)। আরও জানতে,derivedStateOfডকুমেন্টেশন দেখুন।
আপনার ডেরিভেশনের জন্য Compose রানটাইম প্রয়োজন হলে transform ব্যবহার করুন:
- অ্যাসিঙ্ক্রোনাস স্ট্রিমের সাথে
State<T>কম্বাইন করা:State<T>ইনপুটের পাশাপাশিcollectAsStateব্যবহার করে রিপোজিটরি বা ডেটাবেস থেকে KotlinFlowসংগ্রহ করা। - ইনপুট পরিবর্তন হলে আপস্ট্রিম কাজ রিস্টার্ট করা: ব্লকের মধ্যে
remember(key)ব্যবহার করে প্যারামিটারাইজডFlowআবার তৈরি করা, শুধুমাত্র যখন নির্দিষ্টState<T>ইনপুট আপডেট হয়। - সাইড-এফেক্ট চালানো বা ইন্টারমিডিয়েট স্টেট মনে রাখা:
LaunchedEffect(যেমন, ইনপুট ডিবাউন্স করা বা টাইমার চালানো),produceState(suspendফাংশন থেকে ডেটা লোড করা বাawaitDispose-এর সাথে কলব্যাক র্যাপ করা) বাrememberব্যবহার করে রিকম্পোজিশন জুড়ে স্টেট বজায় রাখা।
নিচের উদাহরণে derivedStateOf-এর সাথে transform ব্লকের তুলনা করা হয়েছে যা
State<T> এবং রিপোজিটরি Flow-কে একত্রিত করে:
class TaskListViewModel( private val tasksRepository: TasksRepository, private val preferencesManager: TaskPreferencesManager, ) : ViewModel() { var searchQuery by mutableStateOf("") private set // Computing a value from existing State<T>: use derivedStateOf val hasSearchQuery: State<Boolean> = derivedStateOf { searchQuery.isNotBlank() } // Combining State<T> with a Flow and remember(key): use transform val uiState: State<TaskListUiState> = transform<TaskListUiState>( initialValue = TaskListUiState.Loading, scope = viewModelScope, ) { val showCompleted by preferencesManager.showCompleted val tasks by remember(showCompleted) { tasksRepository.observeTasks(showCompleted) }.collectAsState(initial = null) val currentTasks = tasks ?: return@transform TaskListUiState.Loading val filteredTasks = currentTasks.filter { task -> task.title.contains(searchQuery, ignoreCase = true) } TaskListUiState.Success(filteredTasks) } }
এই উদাহরণে, hasSearchQuery শুধুমাত্র searchQuery স্টেট পরীক্ষা করে, তাই
derivedStateOf CoroutineScope ছাড়াই এটি লেজি কম্পিউট করে। বিপরীতে,
uiState preferencesManager.showCompleted এবং searchQuery-কে
tasksRepository থেকে অ্যাসিঙ্ক্রোনাস Flow-এর সাথে একত্রিত করে এবং শুধুমাত্র searchQuery পরিবর্তন হলে Flow-কে আবার তৈরি করা এড়াতে remember(showCompleted) ব্যবহার করে।
transform-এর পরিবর্তে কখন Flow ব্যবহার করবেন
transform-কে এমনভাবে ডিজাইন করা হয়েছে যাতে এটি ইভেন্টের স্ট্রিম নয়, বরং স্টেট-এর বর্তমান ভ্যালু দেখায়। পরবর্তী রিকম্পোজিশনের আগে কোনও স্টেট একাধিকবার পরিবর্তিত হলে, Compose শুধুমাত্র লেটেস্ট ভ্যালু ব্যবহার করে এবং নতুন ভ্যালু আগের ভ্যালুর সমান (==) হলে আপডেট এড়িয়ে যায়। আপনার লজিককে প্রতিটি ভ্যালু ক্রমানুসারে প্রসেস করতে হলে,
পরিবর্তে Kotlin Flow ব্যবহার করুন:
- রানিং মোট ও গড়: মুভিং অ্যাভারেজ, কিউমুলেটিভ
সাম বা আনডু ইতিহাস (
runningFoldবাscan) গণনা করার জন্য প্রতিটি ভ্যালু একবার করে গণনা করতে হবে।transformব্লকে, রিকম্পোজিশনের আগে দ্রুত আপডেটগুলি এড়িয়ে যাওয়া হয়, একটি সারিতে দুটি অভিন্ন রিডিং (যেমন একটি সেন্সর10.0দুবার নির্গত হয়) রিকম্পোজিশন ট্রিগার করে না এবং একই ব্লকের অন্যান্য স্টেট দ্বারা ট্রিগার করা রিকম্পোজিশন বর্তমান মান দুবার গণনা করতে পারে। প্রথমেFlow-এ রানিং ভ্যালু গণনা করুন, তারপর অন্য কোনও স্টেট-এর সাথে এটি কম্বাইন করতে হলে,transform-এ ফলাফল সংগ্রহ করুন। - ইভেন্ট লগিং ও কিউ: পাইপলাইন যা ব্যবহারকারীর ইন্টার্যাকশন লগ করে, কিউতে থাকা নেটওয়ার্ক অনুরোধ প্রসেস করে
অথবা ট্রানজ্যাকশন রেকর্ড করে যেখানে প্রতিটি মধ্যবর্তী
পরিবর্তন (
A -> B -> CবাA -> B -> A) অবশ্যই রেকর্ড করতে হবে। - একবারই ঘটে এমন ইভেন্ট: সাউন্ড চালানো বা হার্ডওয়্যার কমান্ড পাঠানোর মতো অ্যাকশন, যেখানে একই কমান্ড পরপর দুবার পাঠালে দুবারই তা রান করতে হবে।
transform-এর জন্যinitialValueপ্রয়োজন এবং পরপর একই আউটপুট থাকলে তা এড়িয়ে যায়। - ব্যাকপ্রেশার-সেনসিটিভ স্ট্রিম: স্ট্রিম (যেমন ফাইল বা সকেট রিড) যেখানে কোনও কনজিউমার পিছিয়ে পড়লে ফাস্ট প্রোডিউসারকে অবশ্যই সাসপেন্ড করতে হবে। Compose স্টেট রাইট সাসপেন্ড করে না এবং এর পরিবর্তে আগের ভ্যালু ওভাররাইট করে।
টেস্ট ট্রান্সফর্ম
যেহেতু ট্রান্সফর্ম Compose রানটাইম ও Kotlin Coroutines-এর উপর নির্ভর করে, তাই
সেগুলি পরীক্ষা করার জন্য অ্যাসিঙ্ক্রোনাস এক্সিকিউশন ও স্টেট রিকম্পোজিশন কোঅর্ডিনেট করতে হয়। ইউনিট টেস্টে
কোরুটিন শিডিউলার ও ট্রিগার রিকম্পোজিশন কন্ট্রোল করতে
androidx.appstate.transform:transform-testing আর্টিফ্যাক্ট ব্যবহার করুন।
androidx.appstate.transform.testing প্যাকেজ নিম্নলিখিত API প্রদান করে:
package androidx.appstate.transform.testing
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestResult
public fun transformTest(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend TransformTestScope.() -> Unit,
): TestResult
public sealed interface TransformTestScope : CoroutineScope {
public val testCoroutineScheduler: TestCoroutineScheduler
public val testDispatcher: TestDispatcher
public val backgroundScope: CoroutineScope
public fun advance()
public fun advanceTimeBy(duration: Duration)
public fun advanceUntilIdle()
}
transformTest runTest র্যাপ করে এবং TransformTestScope রিসিভারের সাথে আপনার টেস্ট ব্লক
এক্সিকিউট করে:
backgroundScope: ট্রান্সফর্ম চালানোর জন্য একটিCoroutineScopeযা টেস্ট সম্পূর্ণ হওয়াকে ব্লক করবে না। টেস্ট শেষ হলে এটি অটোমেটিক বাতিল হয়ে যায়, যার ফলে ট্রান্সফর্ম বন্ধ হয়ে যায়।testDispatcher:testCoroutineScheduler-এর সাথে লিঙ্ক করাTestDispatcher। এটিtransform-এরdispatcherপ্যারামিটারে পাস করুন যাতে রিকম্পোজিশনDispatchers.Default-এ ডিফল্ট হওয়ার পরিবর্তে টেস্ট শিডিউলারে রান করে।advance: টেস্ট শিডিউলারে বাকি থাকা কোরুটিন রান করে, Compose স্ন্যাপশট প্রয়োগ করার বিজ্ঞপ্তি (Snapshot.sendApplyNotifications) পাঠায় এবংtransformরিকম্পোজ করে যাতে আউটপুটStateআপডেট হয়।advanceTimeBy(duration)এবংadvanceUntilIdle: সময়ের উপর নির্ভর করে এমন অপারেশনের জন্যtestCoroutineScheduler-এ ভার্চুয়াল ঘড়ির সময় এগিয়ে দিন।
টেস্টের স্ট্যাটাস আপডেট
transform টেস্ট করতে, backgroundScope এবং testDispatcher পাস করুন
TransformTestScope থেকে, তারপর ইনপুট স্টেট বা Flow আপডেট করার পরে
advance কল করুন:
@Test fun testStateUpdate() = transformTest { var sourceState by mutableStateOf(0) val state = transform( initialValue = 0, scope = backgroundScope, dispatcher = testDispatcher, ) { sourceState } assertThat(state.value).isEqualTo(0) // Modify the source state and advance to trigger recomposition. sourceState = 1 advance() assertThat(state.value).isEqualTo(1) }
সময়-ভিত্তিক অপারেশন টেস্ট করুন
transform যখন LaunchedEffect এবং
delay-এর মতো সময়-ভিত্তিক সাইড-এফেক্ট ব্যবহার করে (যেমন ডিবাউন্সিং সার্চ কোয়েরি), তখন advanceTimeBy(duration)-এর সাথে
advance-কে একত্রিত করুন:
- ইনপুট স্টেট পরিবর্তন করার পরে,
advanceকল করুন যাতেtransformব্লক আবার কম্পোজ করে এবংLaunchedEffectশুরু করে। - ভার্চুয়াল ঘড়িকে এগিয়ে নিয়ে যেতে
advanceTimeBy(duration)কল করুন এবং তারপরে সেই টাইমস্ট্যাম্পে শিডিউল করা টাস্ক রান করতে এবংtransformপুনরায় কম্পোজ করতেadvanceকল করুন।
@Test fun testDebouncedSearch() = transformTest { var query by mutableStateOf("") val searchResults = transform<List<String>>( initialValue = emptyList(), scope = backgroundScope, dispatcher = testDispatcher, ) { var results by remember { mutableStateOf(emptyList<String>()) } LaunchedEffect(query) { if (query.isBlank()) { results = emptyList() return@LaunchedEffect } delay(300.milliseconds) results = api.search(query) } results } query = "compose" // Recompose to start the LaunchedEffect for the new query. advance() // Results don't update before the 300ms debounce delay completes. advanceTimeBy(200.milliseconds) advance() assertThat(searchResults.value).isEmpty() // Advance past the remaining 100ms and trigger recomposition. advanceTimeBy(100.milliseconds) advance() assertThat(searchResults.value).containsExactly("Compose State", "Compose UI") }
ViewModel বা স্টেট হোল্ডার টেস্ট করা
যেহেতু transform প্রদত্ত scope-এ তার কম্পোজিশন রান করে এবং
তার dispatcher প্যারামিটারকে Dispatchers.Default-এ ডিফল্ট করে, তাই যে ক্লাসগুলি একটি
transform তৈরি করে তাদের কনস্ট্রাক্টর প্যারামিটার হিসাবে একটি CoroutineScope এবং একটি CoroutineDispatcher গ্রহণ করা উচিত। ViewModel-এ, ইনজেক্ট করা
CoroutineScope-কে ViewModel(coroutineScope) কনস্ট্রাক্টরে পাস করুন এবং
CoroutineDispatcher-কে transform-এ ফরওয়ার্ড করুন:
class HomeViewModel( private val mediaRepository: MediaRepository, private val networkMonitor: NetworkMonitor, coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate), dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ViewModel(coroutineScope) { val uiState: State<HomeUiState> = transform<HomeUiState>( initialValue = HomeUiState.Loading, scope = viewModelScope, dispatcher = dispatcher, ) { val isOnline by networkMonitor.isOnline val tracks = if (isOnline) { mediaRepository.allTracksFlow.collectAsState(initial = null).value } else { mediaRepository.downloadedTracksFlow .collectAsState(initial = null).value } ?: return@transform HomeUiState.Loading HomeUiState.Success( tracks = tracks, isOnline = isOnline, ) } } @Test fun testHomeViewModelOfflineState() = transformTest { val mediaRepository = FakeMediaRepository() val networkMonitor = FakeNetworkMonitor() val viewModel = HomeViewModel( mediaRepository = mediaRepository, networkMonitor = networkMonitor, coroutineScope = backgroundScope, dispatcher = testDispatcher, ) val downloadedTrack = Track(id = "1", isDownloaded = true) val streamingTrack = Track(id = "2", isDownloaded = false) mediaRepository.emitAllTracks(listOf(downloadedTrack, streamingTrack)) mediaRepository.emitDownloadedTracks(listOf(downloadedTrack)) advance() assertThat(viewModel.uiState.value).isEqualTo( HomeUiState.Success( tracks = listOf(downloadedTrack, streamingTrack), isOnline = true, ) ) // Switch offline and verify it switches to downloadedTracksFlow. networkMonitor.setOnline(false) // First advance() recomposes into the offline branch (where collectAsState // initially returns null -> Loading) and starts collecting downloadedTracksFlow. advance() // Second advance() applies the collected downloadedTracksFlow value and // recomposes uiState with the offline Success state. advance() assertThat(viewModel.uiState.value).isEqualTo( HomeUiState.Success( tracks = listOf(downloadedTrack), isOnline = false, ) ) }
এই টেস্টে, HomeViewModel-এ backgroundScope ও testDispatcher পাস করলে,
এটি টেস্ট শিডিউলারের সাথে viewModelScope ও transform কম্পোজিশন বাইন্ড করে, যাতে প্রতিটি advance কল,
FakeMediaRepository ও FakeNetworkMonitor থেকে নির্গমনকে নির্ধারকভাবে প্রসেস করে। ViewModel-এ SavedStateHandle.saveable ও Snapshot.withMutableSnapshot
ব্যবহার করার আরও প্যাটার্ন সম্পর্কে জানতে,
ViewModel-এ Compose State ব্যবহার করা দেখুন।