डेवलपर रिलेशंस इंजीनियर
Compose का स्नैपशॉट रनटाइम, ऑटोमैटिक डिपेंडेंसी ट्रैकिंग, और ऐटॉमिक अपडेट, यूज़र इंटरफ़ेस (यूआई) लेयर पर नहीं रुकते. Jetpack AppState की मदद से, Compose स्टेट को अपस्ट्रीम में ले जाया जा सकता है. इससे, स्क्रीन पर दिखने वाले कॉन्टेंट को बेहतर बनाया जा सकता है. इसके लिए, यूज़र इंटरफ़ेस (यूआई) पर पहुंचने से पहले, स्नैपशॉट सिस्टम की मदद से स्टेट को होस्ट और ड्राइव किया जाता है.
इस सेशन में, आपको AppState की दो मुख्य लाइब्रेरी के बारे में जानकारी मिलेगी. हम स्टेट को होस्ट करने के लिए StateStore और कंपोज़िशन का इस्तेमाल करके स्टेट को पाने के लिए Transform के बारे में जानेंगे. इसके लिए, Kotlin के सामान्य कंट्रोल फ़्लो का इस्तेमाल किया जाएगा. इससे Flow को जोड़ने वाली जटिल चेन की ज़रूरत नहीं पड़ेगी और आपका यूज़र इंटरफ़ेस (यूआई) आसानी से सिंक हो जाएगा.
combine जैसी जटिल फ़्लो ऑपरेटर चेन को बदल देता है. साथ ही, यह शुरुआती रिटर्न और नेस्ट किए गए स्टेट डेरिवेशन के साथ काम करता है.ट्रांसक्रिप्ट
नमस्ते. Jetpack AppState की मदद से, यूज़र इंटरफ़ेस (यूआई) से परे Compose स्टेट को इस्तेमाल करने के बारे में जानकारी देने वाले सेशन में आपका स्वागत है.
मेरा नाम बेन है और मैं डेवलपर रिलेशंस इंजीनियर हूं. मैं Jetpack आर्किटेक्चर लाइब्रेरी पर काम करता हूं.
मेरा नाम एलिफ़ है. मैं उन इंजीनियर में से एक हूं जो ऐप्लिकेशन आर्किटेक्चर लाइब्रेरी पर काम करते हैं.
आज हम आपसे एक ऐसे भविष्य के बारे में बात करने आए हैं जहां Compose, यूज़र इंटरफ़ेस (यूआई) से आगे बढ़कर, ऐप्लिकेशन के डेटा और डोमेन लेयर में शामिल हो जाएगा.
मुझे पूरा भरोसा है कि अब तक आपको यह पता चल गया होगा कि Android पर यूज़र इंटरफ़ेस (यूआई) बनाने के लिए, Compose सबसे सही टूलकिट है.
फ़ोन और टैबलेट से लेकर विजेट, स्मार्टवॉच, टीवी, और एक्सआर डिवाइसों तक, Compose सभी को बेहतर बनाता है.
हालांकि, जैसे-जैसे आपका ऐप्लिकेशन इन यूज़र इंटरफ़ेस (यूआई) प्लैटफ़ॉर्म और बैकग्राउंड कॉम्पोनेंट पर बढ़ता है, वैसे-वैसे उनके पीछे की स्थिति को मैनेज करना मुश्किल हो जाता है. खास तौर पर, दो सामान्य समस्याओं के बारे में.
पहला है, जटिल स्टेट को डिराइव करना.
कई डेटा स्ट्रीम, लोकल यूज़र इंटरफ़ेस (यूआई) इनपुट, और ऐंबियंट स्टेट (जैसे, इंटरनेट कनेक्टिविटी) को एक साथ जोड़ना. यह अक्सर combine, flatMapLatest, और distinctUntilChanged जैसे ऑपरेटर के उलझाव में बदल जाता है.
दूसरा है, कॉम्पोनेंट के बीच स्टेट शेयर करना: किसी गतिविधि, सेवा, ऐप्लिकेशन विजेट वगैरह को सिंक करके रखना. साथ ही, यह पक्का करना कि ज़रूरत पड़ने पर, वह स्टेट प्रोसेस बंद होने पर भी बनी रहे.
जब हमने इन चुनौतियों को समझने की कोशिश की, तो हमें पता चला कि Compose को कैसे बनाया गया है.
ज़्यादातर लोग Compose को यूज़र इंटरफ़ेस (यूआई) टूलकिट मानते हैं. हालांकि, इसमें दो अलग-अलग लेयर होती हैं: सबसे ऊपर Compose यूआई और उसके नीचे Compose रनटाइम.
Compose यूज़र इंटरफ़ेस (यूआई), लेआउट, ड्रॉइंग, और इनपुट को मैनेज करता है. वहीं, Compose रनटाइम, स्टेट मैनेजमेंट और रीकंपोज़िशन के लिए इस्तेमाल किया जाने वाला एक सामान्य इंजन है.
यह यूज़र इंटरफ़ेस (यूआई) पर निर्भर नहीं करता.
इसी वजह से, हमने Jetpack AppState बनाया.
यह लाइब्रेरी का एक ऐसा सेट है जो यूज़र इंटरफ़ेस (यूआई) के अलावा, Compose स्टेट का ज़्यादा से ज़्यादा फ़ायदा पाने पर फ़ोकस करता है.
AppState के बारे में जानने से पहले, आइए यह पक्का करें कि Compose स्टेट के बारे में हमारी समझ एक जैसी हो.
इसका जवाब androidx.compose.runtime.State है.
/**
* A value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value.
*/
@Stable
public interface State<out T> {
public val value: T
}इसके अलावा, MutableState का इंटरफ़ेस भी है, जिससे शायद आप ज़्यादा परिचित हों.
/**
* A mutable value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value. When the
* [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
* will be scheduled. If [value] is written to with the same value, no recompositions will be
* scheduled.
*/
@Stable
public interface MutableState<T> : State<T> {
override var value: T
public operator fun component1(): T
public operator fun component2(): (T) -> Unit
}बुनियादी तौर पर, ये सिर्फ़ बुनियादी कंटेनर हैं.
Compose स्नैपशॉट सिस्टम की वजह से, ये टेस्ट सही तरीके से काम करते हैं.
इसलिए, जब हम Compose स्टेट कहते हैं, तो हमारा मतलब आम तौर पर स्नैपशॉट स्टेट से होता है.
इसलिए, mutableStateOf को कॉल करने पर, एक ऐसा कंटेनर बनता है जो स्नैपशॉट सिस्टम के ज़रिए रीड और राइट को रूट करता है.
जब भी आपका कोड, कंपोज़िशन जैसे स्नैपशॉट ऑब्ज़र्वर में चलता है, तो आपको दो ऐसी प्रॉपर्टी मिलती हैं जो फ़्लो जैसी स्ट्रीम में नहीं मिलतीं.
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
}पहला, अपने-आप और डाइनैमिक तरीके से पढ़ने की सुविधा को ट्रैक करना.
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}इसलिए, अपने-आप होने का मतलब है कि सदस्यता लेने या इकट्ठा करने की ज़रूरत नहीं है.
.value को पढ़ने के लिए सदस्यता लेनी होगी.
इसलिए, अगर हम यहां काउंटर को देखें, तो कंपोज़ेबल के अंदर सीधे count.value को पढ़ने से, कंपोज़िशन को पढ़ने की जानकारी मिलती है. साथ ही, यह काउंटर को रीडर की सूची में रजिस्टर करता है.
अब AddButton को देखें.
@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}
@Composable
fun AddButton(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("Add")
}
}यह सिर्फ़ क्लिक की गिनती करने के लिए लिखता है.
इसलिए, यह कोई रीडर नहीं है.
जब कोई उपयोगकर्ता AddButton पर क्लिक करता है और संख्या में बदलाव करता है, तो स्नैपशॉट सिस्टम उस बदलाव को ट्रैक करता है. इससे कंपोज़िशन को पता चलता है कि काउंटर को अमान्य करना है, लेकिन AddButton को नहीं.
डाइनैमिक का मतलब है कि रीड ट्रैकिंग, रनटाइम के दौरान होती है. यह इस बात पर निर्भर करती है कि कौनसे कोड पाथ असल में चल रहे हैं.
@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(true) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}इसलिए, यहां हम काउंटर टेक्स्ट को isVisible फ़्लैग के पीछे सुरक्षित रख रहे हैं.
isVisible के सही होने पर, isVisible और संख्या, दोनों को पढ़ा जाता है.
इसलिए, Counter को दोनों के रीडर के तौर पर रजिस्टर किया गया है.
और जब isVisible गलत होता है, तो if ब्रांच नहीं चलती है, count को नहीं पढ़ा जाता है, इसलिए Counter को सिर्फ़ isVisible के रीडर के तौर पर रजिस्टर किया जाता है, count के तौर पर नहीं.
@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(false) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}इसलिए, जब AddButton वोटों की गिनती के लिए लिखता है, तो अमान्य करने के लिए कुछ नहीं बचता.
दूसरी प्रॉपर्टी यह है कि आपको एक जैसी जानकारी और ज़्यादा जानकारी में से किसी एक को चुनने की ज़रूरत नहीं है.
कंसिस्टेंसी का मतलब है कि जब किसी स्नैपशॉट में कई वैल्यू अपडेट की जाती हैं, तो कोड चलने के दौरान वे बदलाव अलग रहते हैं. भले ही, कोई दूसरा थ्रेड उसी समय उन पर लिख रहा हो.
स्नैपशॉट पूरा होने पर, वे अपडेट एक साथ दिखते हैं.
ग्रैन्युलैरिटी का मतलब है कि अलग-अलग प्रॉपर्टी को अलग-अलग डिक्लेयर और ट्रैक किया जा सकता है. खास तौर पर, तब जब वे एक-दूसरे से जुड़ी न हों.
इसलिए, किसी एक प्रॉपर्टी को अपडेट करने से सिर्फ़ उन रीडर को अमान्य किया जाता है जो असल में उस पर निर्भर होते हैं.
आइए, इसे कुछ असल कोड में देखते हैं.
उदाहरण के लिए, हम एक ऐसे गेम को देखते हैं जिसमें स्टेट के तीन हिस्से हैं: लेवल का नाम, गेम में बिताया गया समय, और स्कोर.
val levelName: String = "Level 1 - Stay Alive!"
val timeSurvived: Int = 0
val score: Double = 0.0तो चलिए, सबसे पहले एक फ़्लो लागू करने के तरीके को देखते हैं. इसमें हर चीज़ को GameUiState क्लास में बंडल करके, बारीकियों के बजाय एक जैसा होने को चुना जाता है.
data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)इसका मतलब है कि जब भी updateScore रन होता है, तब पूरा GameUiState कॉपी हो जाता है, ताकि timeSurvived और स्कोर, दोनों एक साथ सिंक होकर अपडेट हो सकें.
data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}यूज़र इंटरफ़ेस (यूआई) में, GameScreen यूज़र इंटरफ़ेस (यूआई) की स्थिति को इकट्ठा करता है और उसे पढ़ता है, ताकि हेडर और ScoreBadge को काम की प्रॉपर्टी पास की जा सकें.
data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Column {
Header(levelName = state.levelName)
ScoreBadge(
timeSurvived = state.timeSurvived,
score = state.score
)
}
}GameScreen हर स्टेट को पढ़ता है. इसलिए, updateScore को किया गया हर कॉल, GameScreen को अमान्य कर देता है. भले ही, हेडर का डेटा कभी न बदला हो.
अब हर प्रॉपर्टी को उसके अपने फ़्लो में बांटा जा सकता है, ताकि ज़्यादा जानकारी मिल सके. हालांकि, ऐसा करने से आपको एक जैसा डेटा नहीं मिलेगा, क्योंकि जब कोई दूसरा अपडेट ब्लॉक चल रहा हो, तब कोई दूसरा थ्रेड किसी वैल्यू को अपडेट कर सकता है.
अब राज्य के हिसाब से लागू किए गए एक अन्य तरीके पर विचार करते हैं.
@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}इसमें हम फिर से GameUiState का इस्तेमाल करते हैं.
हालांकि, इस उदाहरण में यह थोड़ा अलग है, क्योंकि हर प्रॉपर्टी mutableStateOf को डेलिगेट करती है.
यहां, updateScore अपने राइट को withMutableSnapshot में रैप करता है. इससे हमें सभी राइट में एक जैसा डेटा मिलता है.
@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}इसके बाद, यूज़र इंटरफ़ेस (यूआई) में, GameScreen स्टेट को सीधे हेडर और ScoreBadge को पास करता है.
@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state = viewModel.uiState
Column {
Header(state = state)
ScoreBadge(state = state)
}
}यह किसी भी प्रॉपर्टी को नहीं पढ़ता. इसलिए, जब updateScore चलता है, तो GameScreen रीडर नहीं होता.
इस मामले में, सिर्फ़ असल कॉम्पोनेंट ScoreBadge को अमान्य किया जाता है.
आखिर में, ध्यान दें कि इनमें से कोई भी चीज़ यूज़र इंटरफ़ेस (यूआई) से नहीं जुड़ी है.
यूज़र इंटरफ़ेस (यूआई) एक ऐसी जगह है जहां ये प्रॉपर्टी वाकई काम आती हैं.
इसके साथ ही, हमारी बातचीत में शामिल होने के लिए धन्यवाद.
चलिए, अब मज़ाक़ बंद करते हैं और एलीफ़ को बुलाते हैं, ताकि वे हमें पहली लाइब्रेरी, ट्रांसफ़ॉर्म के बारे में बता सकें.
ठीक है।
धन्यवाद, बेन.
जैसा कि बेन ने बताया, Compose स्नैपशॉट स्टेट हमें अपने-आप और डाइनैमिक तौर पर रीड ट्रैकिंग, एक जैसा अनुभव, और ज़्यादा जानकारी देता है. इससे मेरे मन में एक सवाल आता है कि क्या होगा अगर हमारे डेटा फ़्लो को भी Compose स्टेट स्नैपशॉट-- स्नैपशॉट स्टेट से कंट्रोल किया जा सके?
अगर स्टेट हमारा डेटा मॉडल है, तो हम इसके सभी उपभोक्ताओं के लिए स्टेट की गणना और उसे ट्रांसफ़ॉर्म कैसे करते हैं?
यहीं पर Jetpack AppState Transform की नई लाइब्रेरी काम आती है.
लाइब्रेरी के बारे में जानने से पहले, हमें यह जानना होगा कि लाइब्रेरी कैसे बनी.
इसलिए, हेडलेस कंपोज़िशन में स्टेट वैल्यू जनरेट करने के लिए, Compose इंजन का फ़ायदा लेने का आइडिया नया नहीं है. Compose इंजन, रनटाइम और कंपाइलर है.
आपमें से कई लोग पहले से ही Molecule लाइब्रेरी का इस्तेमाल कर रहे होंगे या इसके बारे में जानते होंगे.
हेडलेस का मतलब है कि इसमें कोई यूज़र इंटरफ़ेस (यूआई) नहीं है.
Molecule ने Compose Runtime की क्षमता को दिखाने में अहम भूमिका निभाई. इसने हमें यह दिखाया कि ऐप्लिकेशन बनाने की सुविधा को यूआई नोड एमिट करने की ज़रूरत नहीं है. साथ ही, इसका इस्तेमाल ऐप्लिकेशन बनाने की सुविधा को लिखने के लिए किया जा सकता है, जो एक प्योर डेटा मॉडल दिखाता है.
Molecule के मामले में, यह Kotlin Flow या StateFlow था.
इसका मतलब है कि कंबाइन जैसे रिएक्टिव ऑपरेटर को चेन करने के बजाय, सामान्य Kotlin कोड लिखा जा सकता है.
हालांकि, फ़्लो के बजाय, अगर आपका डेटा मॉडल State है, तो क्या होगा? ऐसा इसलिए, क्योंकि आपका यूज़र इंटरफ़ेस (यूआई) पहले से ही Compose में है.
ट्रांसफ़ॉर्म की मदद से, आपका डेटा मॉडल स्टेट बन जाता है.
आइए, असल एपीआई पर एक नज़र डालते हैं.
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R>यह पूरा एपीआई है.
यह बहुत आसान है, इसमें सिर्फ़ एक फ़ंक्शन है.
यह एक सामान्य ऑब्ज़र्वेबल स्टेट दिखाता है, न कि कोई फ़्लो या StateFlow.
साथ ही, इस डेटा का इस्तेमाल करने वाले व्यक्ति को यह पता नहीं होता या उसे इस बात से कोई फ़र्क़ नहीं पड़ता कि इसे ट्रांसफ़ॉर्म ने जनरेट किया है.
यहां सबसे दिलचस्प बात यह ब्लॉक है.
यह कंपोज़ेबल है.
इसका मतलब है कि आपको पहले से जो भी पता है वह इसमें काम करता है.
याद रखें कि LaunchedEffect, collectAsState, अर्ली रिटर्न, प्लेन if, और ऐसी कोई भी चीज़ जिसे Composable के अंदर से पढ़ा जा सकता है, उसे Transform में भी इस्तेमाल किया जा सकता है.
हमें यह ध्यान रखना चाहिए कि इस प्रज़ेंटेशन में आज हम जो भी एपीआई देख रहे हैं वे सभी ऐल्फ़ा वर्शन में हैं. साथ ही, इनमें बदलाव हो सकता है. हालांकि, इसके बावजूद, आइए एक उदाहरण देखें.
चलिए, फ़ोकस टाइमर वाले एक छोटे और आसान ऐप्लिकेशन का उदाहरण लेते हैं.
कोई टास्क चुनें, 25 मिनट का टाइमर सेट करें, और घड़ी की टिक-टिक शुरू हो जाती है.
जब भी कोई सिंगल स्क्रीन बनाई जाती है, भले ही वह इस तरह की कोई आसान सिंगल स्क्रीन हो, आपको कई इंडिपेंडेंट डेटा स्ट्रीम को जोड़ना और मैनेज करना पड़ सकता है, ताकि आप अपने स्टेट को हुड के नीचे असेंबल कर सकें.
फ़ोकस स्क्रीन के लिए, मान लें कि हमारे पास दो स्क्रीन हैं: फ़ोकस सेशन, जो यह बताएगा कि हम किस फ़ेज़ में हैं. जैसे, सेशन रुका हुआ है या नहीं, कितने सेकंड बचे हैं. यह सारी जानकारी, टाइमर इंजन से StateFlow के तौर पर मिलेगी. यह हर एक सेकंड में टिक करेगी.
दूसरी डेटा स्ट्रीम, वह मौजूदा टास्क होगा जिस पर उपयोगकर्ता काम कर रहा है. यह डेटा, Room डेटाबेस से फ़्लो के तौर पर आएगा.
इसलिए, आखिर में दो डेटा स्ट्रीम और एक स्क्रीन स्टेट दिखेगी.
ViewModel में, इन दोनों को एक साथ रखा जाएगा. इसके आउटपुट को हम समरी कहेंगे. यह कोड का वह हिस्सा होगा जिसे हम आगे लिखेंगे.
ViewModel में, आपको इन फ़्लो को चेन ऑपरेटर का इस्तेमाल करके जोड़ना होगा.
class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: StateFlow<FocusSummary> =
combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle,
)
}इसलिए, दोनों को एक साथ जोड़ें और दोनों फ़्लो को एक साथ स्ट्रीम में ज़िप करें. लैम्ब्डा के अंदर ब्रांचिंग करें, ताकि अगर ऐसा हो, तो ऐसा हो और फिर आखिर में एक स्टेट हो.
Transform की मदद से, हम छोटे-मोटे बदलाव वाले इस पूरे कोड को हटा सकते हैं.
class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)यह कॉल कुछ जाना-पहचाना लगता है, क्योंकि हमारे पास पास किए जा रहे स्कोप में एक शुरुआती वैल्यू है. हालांकि, चेन करने के लिए अपस्ट्रीम में कुछ भी नहीं है. डेरिवेशन, ब्लॉक ही है.
stateIn के उलट, ट्रांसफ़ॉर्म उस स्कोप के लाइफ़टाइम के दौरान लगातार चलता रहता है. में, अपस्ट्रीम स्ट्रीम को रोकने और फिर से शुरू करने के लिए, आपको शायद WhileSubscribed टाइम आउट कॉन्फ़िगर करना पड़े.
हमारे सोर्स फ़्लो हैं और फ़्लो का स्वागत है.
class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}हम हर एक को collectAsState से जोड़ेंगे. सेशन एक StateFlow है, इसलिए हमें शुरुआती वैल्यू की ज़रूरत नहीं है. हालांकि, Room Flow ठंडा है, इसलिए हम इसे एक वैल्यू देते हैं.
सदस्यता के बारे में बस इतना ही.
पढ़ने से ही, हमें सदस्यता मिलती है.
यह अपने-आप रीड ट्रैकिंग की सुविधा है, जिसके बारे में बेन ने पहले उदाहरण में बताया था. हालांकि, यह ViewModel में है.
जब हम आइडल स्टेट में हों, तब हम यहां इस तरह के शुरुआती नतीजे भी दिखा सकते हैं.
मान लें कि हमारे पास कोई नया टास्क नहीं है, तो हम बाकी टास्क भी प्रोसेस नहीं करेंगे.
एक बार फिर, डाइनैमिक रीड ट्रैकिंग की सुविधा का फ़ायदा मिला.
जब हम कुछ नहीं कर रहे होते हैं, तब रूम फ़्लो का डेटा इकट्ठा नहीं किया जाता.
इसके बाद, हम खास जानकारी दिखाते हैं.
इसलिए, अगर उपयोगकर्ता टास्क को रोकता है या टाइमर टिक करता है, तो सब कुछ अपने-आप अपडेट हो जाता है.
इसलिए, जब हम ViewModel के एक उदाहरण से बाहर निकलते हैं, तो चीज़ें थोड़ी और जटिल हो जाती हैं.
मान लें कि इस सेशन को दो और प्लैटफ़ॉर्म पर दिखाना है. पहला, हर स्क्रीन पर ऐप्लिकेशन की स्क्रीन के सबसे नीचे मौजूद मिनी-टाइमर बार और दूसरा, होम स्क्रीन विजेट.
इन दोनों को सेशन की खास जानकारी के बारे में एक जैसी जानकारी की ज़रूरत होगी. जैसे, कौनसा फ़ेज़ है, सेशन को रोका गया है या नहीं, टाइमर में कितना समय बचा है.
इसका मतलब है कि अगर हर प्लैटफ़ॉर्म अपनी कॉपी खुद असेंबल करता है, तो एक ही चीज़ को तीन बार और तीन अलग-अलग जगहों पर बनाना होगा. साथ ही, इन सभी को एक-दूसरे के साथ सहमत होना होगा.
इसलिए, हमें ऐप्लिकेशन के स्कोप में एक बार समरी जनरेट करनी होगी. इसके बाद, डाउनस्ट्रीम में मौजूद सभी कॉम्पोनेंट इसका इस्तेमाल करेंगे.
तो, हम इस तरह की सुविधा को कैसे लागू करें?
जहां हमें लागू करने के लिए, शायद तीन चीज़ों की ज़रूरत होगी.
class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
...
}
class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
...
}
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
...
}
}
}इसलिए, ऐप्लिकेशन के स्कोप में एक सेशन मैनेजर होगा, जहां से खास जानकारी मिलेगी. साथ ही, एक मिनी-टाइमर ViewModel और एक Glance विजेट होगा. ये दोनों, इस फ़ोकस सेशन मैनेजर से मिली खास जानकारी का इस्तेमाल करेंगे.
आइए, हर एक को पारंपरिक तरीके से और फिर ट्रांसफ़ॉर्म की मदद से बनाते हैं.
इसलिए, पारंपरिक तौर पर, हम ऐप्लिकेशन के स्कोप में एक बार StateFlow के तौर पर समरी जनरेट करके शुरुआत करेंगे.
class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: StateFlow<FocusSummary> =
combine(
sessionFlow,
taskRepository.tasksFlow
) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}.stateIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle
)
}यह वही combine और stateIn कॉल है जो हमने पहले ViewModel के उदाहरण में देखा था. हालांकि, इसे एक लेवल ऊपर ले जाया गया है.
ट्रांसफ़ॉर्म की मदद से, हम ViewModel के उसी डिक्लेरेशन का फिर से इस्तेमाल कर सकते हैं जिसे हमने पहले देखा था.
class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = scope,
container = handle.getOrCreateContainer("transform")
) {
val session by sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow
.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}सिर्फ़ एक बदलाव हुआ है. वह यह कि अब यह ऐप्लिकेशन के स्कोप में मौजूद है.
इसके बाद, हमारे पास मिनी-टाइमर का ViewModel है.
class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: StateFlow<MiniTimerUiState> =
combine(
focusManager.summary,
isExpanded
) { summary, expanded ->
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = MiniTimerUiState()
)
fun toggleExpanded() { isExpanded.update { !it } }
}मान लें कि हमें यहां भी खुद को थोड़ा चैलेंज देना है.
मान लें कि मिनी-टाइमर में, हमारे पास पहले से मौजूद दो डेटा स्ट्रीम, StateFlow और रूम फ़्लो के अलावा, हमें यह भी ट्रैक करना है कि टाइमर बार को बड़ा किया गया है या नहीं.
इसके लिए, हम MutableStateFlow का इस्तेमाल करते हैं.
इसके लिए, हमें समरी फ़्लो और isExpanded StateFlow को ट्रैक करने के लिए, एक और कंबाइन का इस्तेमाल करना होगा.
इसके बाद, हम दूसरे राज्य में जाते हैं.
ट्रांसफ़ॉर्म की मदद से, हमें डेटा स्ट्रीम के बीच सिंक करने के लिए, इनमें से किसी भी चेन ऑपरेटर का फिर से इस्तेमाल करने की ज़रूरत नहीं पड़ती.
class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: State<MiniTimerUiState> =
transform(
initialValue = MiniTimerUiState(),
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val summary by focusManager.summary
val expanded by isExpanded.collectAsState()
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}
fun toggleExpanded() { isExpanded.update { !it } }
}हम नेस्टेड ट्रांसफ़ॉर्म कर सकते हैं. यह वाकई बहुत शानदार है.
इसलिए, यहां ViewModel, समरी को पढ़ता है. यह अपस्ट्रीम ट्रांसफ़ॉर्म का आउटपुट है, जो किसी अन्य स्टेट की तरह ही है. इसके बाद, यह collectAsState के साथ isExpanded को इकट्ठा करता है.
इसलिए, हम एक ही ब्लॉक में StateFlow में अपस्ट्रीम स्टेट और डाउनस्ट्रीम फ़्लो को मिक्स कर सकते हैं.
साथ ही, जानकारी के स्तर को ध्यान में रखें.
इसलिए, अगर isExpanded को टॉगल किया जाता है, तो सिर्फ़ यह ब्लॉक फिर से चलेगा.
ऐप्लिकेशन-लेवल ब्लॉक और विजेट को इस बारे में पता भी नहीं चलता.
आखिर में, हमारे पास विजेट है.
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
.collectAsState()
GlanceTheme { FocusWidgetContent(summary) }
}
}
}Glance, Compose है. इसलिए, हम किसी भी अन्य Composable की तरह, provideContent में collectAsState के साथ StateFlow इकट्ठा कर सकते हैं.
Transform की मदद से, यह काम और भी आसान हो जाता है.
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
GlanceTheme { FocusWidgetContent(summary) }
}
}
}हमारा फ़ोकस मैनेजर हमें Compose स्टेट उपलब्ध करा रहा है. इसलिए, हमें यहां कुछ भी बदलने की ज़रूरत नहीं है.
आखिर में, मिनी-टाइमर बार और होम स्क्रीन विजेट में कभी भी अलग-अलग जानकारी नहीं दिख सकती, क्योंकि वे एक ही स्थिति को पढ़ रहे हैं.
इतना ही नहीं, उस स्टेट में किया गया कोई भी बदलाव, उस स्टेट के सभी उपभोक्ताओं को दिखेगा.
इसका क्या मतलब है?
इसलिए, अगर आपने यहां विजेट पर जाकर 'रोकें' पर क्लिक किया, तो यह मैनेजर से सेशन स्ट्रीम में जाएगा. इसके बाद, यह वापस समरी में जाएगा. साथ ही, ये तीनों स्क्रीन सिंक होकर अपडेट हो जाएंगी.
अब जब हमने ट्रांसफ़ॉर्म टूल को इस्तेमाल करने का तरीका थोड़ा-बहुत जान लिया है, तो आइए इसके बारे में ज़्यादा जानकारी हासिल करते हैं.
हमने पहले जिस ट्रांसफ़ॉर्म एपीआई के बारे में जाना था वह असल में एक हेल्पर फ़ंक्शन है. इसका इस्तेमाल, मैनेज किए जा रहे बुनियादी ट्रांसफ़ॉर्म को बनाने के लिए किया जाता है.
public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R> {
val transformScope = TransformScope()
val transform = transformScope
.launchTransform(initialValue, onUpdate)
scope.launch(
dispatcher,
start = CoroutineStart.UNDISPATCHED
) {
try { transformScope.run() } finally {
transform.dispose()
}
}
return transform
}असल में, यह TransformScope और launchTransform एक्सटेंशन फ़ंक्शन का इस्तेमाल करता है. यह इस TransformScope में कंपोज़ेबल ट्रांसफ़ॉर्मेशन को लॉन्च करता है और डिसपोज़ेबल स्टेट दिखाता है.
इसे ऊपर से नीचे तक पढ़ने पर, आपको पता चलेगा कि यह आपके लिए सब कुछ करता है.
यह TransformScope बनाता है और आपके ट्रांसफ़ॉर्म को इसमें लॉन्च करता है. यह वह स्थिति होगी जो आपको वापस मिलेगी.
इसके बाद, यह आपके दिए गए डिस्पैचर पर TransformScope में रन भी लॉन्च करता है, ताकि स्कोप का लाइफ़टाइम और थ्रेड आपका हो.
इसके बाद, स्कोप खत्म होने पर ट्रांसफ़ॉर्मेशन को हटा देता है.
यह Transform की पूरी लाइफ़-साइकल की कहानी है.
चलिए, TransformScope के इंटरफ़ेस पर एक नज़र डालते हैं.
public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope
public abstract class TransformScope {
public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
public abstract suspend fun run()
}
public fun <T> TransformScope.launchTransform(
initialState: T,
onUpdate: @Composable (T) -> T,
): DisposableState<T>
public interface DisposableState<T> : State<T> {
public fun dispose()
}हम देख सकते हैं कि इसमें createTransform फ़ंक्शन है, जो इस TransformScope से जुड़ा ट्रांसफ़ॉर्मेशन बनाएगा.
इसमें एक सस्पेंडिंग रन फ़ंक्शन भी होता है, जो TransformScope को शुरू करता है और मौजूदा स्टेट ट्रांसफ़ॉर्मेशन को अपडेट करने की प्रोसेस शुरू करता है.
इसका क्या मतलब है?
इसलिए, run असल में Recomposer और कंपोज़िशन को सेट अप करता है.
स्कोप में मौजूद हर ट्रांसफ़ॉर्म, उस कंपोज़िशन के अंदर मौजूद होता है. साथ ही, उसका अपना रीकंपोज़ स्कोप होता है.
इसलिए, हम स्कोप शेयर करना चाहते हैं. साथ ही, हमारे पास TransformScope है.
इससे, जब भी कोई स्टेट बदलता है, तो सिर्फ़ उसे पढ़ने वाले ट्रांसफ़ॉर्म फिर से चलाए जाते हैं.
जैसा कि हमने पहले बताया था, run एक suspend फ़ंक्शन है.
override suspend fun run() {
val context = currentCoroutineContext()
val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
...
val recomposer = Recomposer(context)
val composition = Composition(UnitApplier, recomposer)
try {
withContext(clock) {
if (clock is HeadlessTransformClock) {
launch(start = CoroutineStart.UNDISPATCHED) {
clock.runClock()
}
}
...
}
} finally {
// dispose of composition, cancel the clock, close the recomposer
}
}इसलिए, यह उस कोरूटीन पर चलता है जिससे इसे कॉल किया जाता है. यह तब तक चलता है, जब तक कि उस कोरूटीन को रद्द नहीं कर दिया जाता.
अब आपके मन में यह सवाल आ सकता है कि Recomposer को फ़्रेम क्लॉक की ज़रूरत क्यों होती है. दरअसल, फ़्रेम क्लॉक से Recomposer को यह पता चलता है कि उसे फ़्रेम कब बनाना है.
Compose यूज़र इंटरफ़ेस (यूआई) में, यह डिसप्ले है. हालांकि, जैसा कि हमने पहले बताया था, हम हेडलेस एनवायरमेंट में हैं. इसलिए, कोई यूज़र इंटरफ़ेस (यूआई) नहीं है.
इसलिए, अगर आपने हमें घड़ी नहीं दी, तो हम खुद ही घड़ी बना लेंगे.
डिसप्ले फ़्रेम क्लॉक से रीकंपोज़िशन को अलग करके, Transform अपने सभी ट्रांसफ़ॉर्मेशन और स्टेट डेरिवेशन को यूज़र इंटरफ़ेस (यूआई) थ्रेड से बाहर चलाता है.
इसके लिए, हमने एक खास HeadlessTransformClock बनाया है.
फ़्रेम क्लॉक को ट्रांसफ़ॉर्म करें.
कोई विजेट, सेवा या अन्य ट्रांसफ़ॉर्म डाउनस्ट्रीम, ट्रांसफ़ॉर्म से मिलने वाली वैल्यू का इंतज़ार कर सकता है. इसलिए, हमारी घड़ी तेज़ी से काम करती है.
इसका मतलब है कि ट्रांसफ़ॉर्म के इस्तेमाल किए जा रहे किसी भी स्टेट में बदलाव होने पर, तुरंत एक नया फ़्रेम जनरेट होता है.
यह एक छोटा लॉक-फ़्री स्टेट मशीन है. इसलिए, फ़्रेम का अनुरोध करना, तुलना करने और सेट करने की एक आसान प्रोसेस है.
जब स्टेट में कोई बदलाव नहीं होता, तो घड़ी पार्क हो जाती है.
इसलिए, ऐसी स्थिति का इस्तेमाल करने वाले ट्रांसफ़ॉर्म के लिए आपको कुछ भी खर्च नहीं करना पड़ता जिसमें कोई बदलाव नहीं किया गया है.
इससे पहले कि मैं बेन को वापस बुलाऊं, हमें यह भी ध्यान रखना चाहिए कि ट्रांसफ़ॉर्म अब rememberSaveable के साथ काम करता है. इससे यह पक्का किया जा सकता है कि कॉन्फ़िगरेशन में बदलाव होने पर भी आपका डेटा सेव रहे.
यह नए SavedStateContainer API की मदद से ऐसा करता है. यह एपीआई अब वह बिल्डिंग ब्लॉक है जिस पर Compose का SaveableStateRegistry, SavedStateHandle, और SavedStateRegistry, सभी बने हैं.
इनमें से किसी को भी SavedStateContainer में बदला जा सकता है या चाइल्ड कंटेनर बनाकर उन्हें अपने ट्रांसफ़ॉर्म में पास किया जा सकता है, ताकि यह पक्का किया जा सके कि आपका डेटा सेव हो गया है.
आखिर में, हम आपको फिर से याद दिलाना चाहते हैं कि आज हम जिन एपीआई के बारे में बात कर रहे हैं वे सभी ऐल्फ़ा वर्शन में हैं. इसलिए, इनमें बदलाव हो सकते हैं.
अब मैं बेन को माइक सौंपता हूं.
ठीक है।
ठीक है, धन्यवाद एलिफ़.
अब हमने देखा कि Compose स्टेट कितना असरदार है और Transform की मदद से, इसे यूज़र इंटरफ़ेस (यूआई) के बाहर कैसे इस्तेमाल किया जा सकता है. आपने अब तक के उदाहरण में एक बात पर ध्यान दिया होगा कि हमारे इनपुट, फ़्लो थे जिन्हें हमने collectAsState का इस्तेमाल करके जोड़ा था.
लेकिन क्या ऐसा करना हमेशा ज़रूरी होता है?
स्टेट होल्डर में सीधे तौर पर Compose स्टेट का इस्तेमाल क्यों नहीं करना चाहिए?
इसलिए, इस सवाल का जवाब देने के लिए, मुझे स्टेट को तीन मुख्य कैटगरी में बांटना मददगार लगता है.
पहला है लाइव स्टेट, जो सिर्फ़ मेमोरी में सेव होता है.
जैसे, नेटवर्क या ब्लूटूथ कनेक्शन का स्टेटस.
आपको सेव की गई वैल्यू का इस्तेमाल नहीं करना है.
दूसरा, ट्रांज़िएंट स्टेट. यह ऐप्लिकेशन की प्रोसेस बंद होने के बाद भी बना रहना चाहिए. हालांकि, इसे हमेशा के लिए सेव करने की ज़रूरत नहीं होती.
यूज़र इंटरफ़ेस (यूआई) की स्थिति, जैसे कि कोई कार्ड बड़ा किया गया है या नहीं. साथ ही, सेशन की स्थिति, जैसे कि नेविगेशन की पिछली गतिविधियां या फ़ॉर्म का ड्राफ़्ट.
तीसरा है, स्थायी स्थिति. इसमें डेटा को हमेशा के लिए सेव किया जाता है.
जैसे, उपयोगकर्ता की पसंद या डेटाबेस में मौजूद मीडिया लाइब्रेरी.
चलिए, सबसे पहले यह जानते हैं कि लाइव और ट्रांज़िएंट स्टेट, दोनों के लिए Compose स्टेट का इस्तेमाल कैसे किया जा सकता है.
अब तक, आर्किटेक्चर से जुड़े हमारे दिशा-निर्देशों के मुताबिक, Compose की स्थिति को यूज़र इंटरफ़ेस (यूआई) लेयर में या कभी-कभी ViewModels में रखा जाता था. साथ ही, हमने आपको हर जगह Flow का इस्तेमाल करने के लिए कहा था.
लेकिन ऐसा क्यों था?
ऐसा इसलिए, क्योंकि जब भी आपको किसी स्टेट प्रॉपर्टी को Flow के साथ जोड़ना होता था, तब आपको snapshotFlow का इस्तेमाल करके उसे वापस कन्वर्ट करना पड़ता था.
हालांकि, Transform की मदद से, इस कन्वर्ज़न की ज़रूरत नहीं होती.
आइए, देखते हैं कि ViewModel से यह कैसा दिखेगा या इसका क्या मतलब है.
@HiltViewModel
class TaskListViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
private val networkMonitor: NetworkMonitor,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
...
}लोकल स्क्रीन इनपुट के लिए, जैसे कि यहां selectedTaskIds, MutableStateFlow के बजाय सामान्य mutableStateOf प्रॉपर्टी का एलान किया जा सकता है.
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
}यह ऐसी चीज़ है जिसे ऐप्लिकेशन की प्रोसेस बंद होने के बाद भी बनाए रखने की ज़रूरत नहीं है. इसलिए, यह ठीक है कि यह सिर्फ़ mutableStateOf का इस्तेमाल नहीं कर रहा है.
सर्च क्वेरी जैसी जिन चीज़ों को आपको ऐप्लिकेशन की प्रोसेस बंद होने के बाद भी सेव रखना है उनकी बदली जा सकने वाली स्थिति को सीधे savedStateHandle.saveable के साथ सेव किया जा सकता है.
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
}इसके बाद, गेम के उदाहरण में updateScore की तरह ही, जब कोई तरीका एक साथ कई स्टेट वैल्यू को अपडेट करता है, तो withMutableSnapshot में राइट को रैप करने से एक जैसा अनुभव मिलता है.
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
fun updateSearchQuery(query: String) {
Snapshot.withMutableSnapshot {
searchQuery = query
selectedTaskIds = emptySet()
}
}
}इसके बाद, searchQuery को रूम फ़्लो के साथ जोड़ने के लिए, फ़्लो को इकट्ठा करते समय उसे सीधे ट्रांसफ़ॉर्म में पढ़ें.
val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}तो, नेटवर्क कनेक्टिविटी जैसी लाइव स्थिति के बारे में क्या?
@Singleton
class NetworkMonitor @Inject constructor(
@ApplicationContext context: Context,
) {
private val connectivityManager =
context.getSystemService(ConnectivityManager::class.java)
...
}आम तौर पर, यह सिंगलटन में मौजूद होता है. इसकी वजह यह है कि इसे कई स्क्रीन पर या ऐप्लिकेशन के कई कॉम्पोनेंट, जैसे कि सेवाओं से पढ़ा जाता है.
हालांकि, इसका मतलब यह नहीं है कि इसे सेव करने के लिए, Compose State का इस्तेमाल नहीं किया जा सकता.
असल में, यह अक्सर फ़्लो-आधारित वर्शन से ज़्यादा आसान होता है.
आपको अडैप्ट करने के लिए, कॉलबैक फ़्लो या किसी और चीज़ की ज़रूरत नहीं है. आपको बस कॉलबैक में सीधे तौर पर डॉट वैल्यू को अपडेट करना होगा.
@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
}अगर किसी उपभोक्ता को अब भी फ़्लो की ज़रूरत है, तो snapshotFlow का इस्तेमाल करके उसे वापस लाया जा सकता है.
@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { _isOnline.value = true }
override fun onLost(network: Network) { _isOnline.value = false }
}
init {
connectivityManager.registerDefaultNetworkCallback(callback)
}
}आइए, देखते हैं कि ViewModel में यह कैसा दिखता है.
isOnline को सीधे तौर पर ट्रांसफ़ॉर्म में पढ़ा जा सकता है. साथ ही, वैल्यू बदलने पर यह फिर से कंपोज़ हो जाएगा.
val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
if (!networkMonitor.isOnline.value) {
return@transform TaskListState.Offline
}
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}यह सब सीधे तौर पर हो रहा है. स्टेट को अपडेट करने के लिए, आपको ऐप्लिकेशन स्कोप या कोरूटीन स्कोप देने की ज़रूरत नहीं है. यह सिर्फ़ एक डायरेक्ट और सिंक्रोनस अपडेट है.
इसलिए, Transform की मदद से, आपका ViewModel, सिंगलटन मैनेजर से स्टेट, ViewModel में स्टेट, और रिपॉज़िटरी से फ़्लो को एक ही Transform ब्लॉक में जोड़ सकता है.
चलिए, अब ट्रांज़िएंट स्टेट पर वापस चलते हैं.
मैंने पहले दो तरह के स्टेट बताए थे: यूज़र इंटरफ़ेस (यूआई) स्टेट और सेशन स्टेट.
यूज़र इंटरफ़ेस (यूआई) की स्थिति और किसी एक ऐक्टिविटी में मौजूद सेशन की स्थिति के लिए, ViewModel और SavedState का इस्तेमाल करना बेहतर होता है.
इन्हें इसी मकसद से बनाया गया था.
हालांकि, समस्या तब शुरू होती है, जब उस सेशन स्टेट को कई अलग-अलग यूज़र इंटरफ़ेस (यूआई) और ऐप्लिकेशन कॉम्पोनेंट के साथ शेयर करना पड़ता है.
जब कोई राज्य ViewModel से बाहर निकल जाता है, तो हमारे मौजूदा एपीआई और दिशा-निर्देशों में कुछ अंतर रह जाता है.
इसलिए, SavedState एपीआई सबसे पहले किसी गतिविधि के बंडल में सेव होते हैं. हालांकि, यह अन्य ऐप्लिकेशन कॉम्पोनेंट में उपलब्ध नहीं होता, जैसे कि कोई सेवा.
दूसरा, mutableStateOf को सिंगलटन में सेव किया जाता है, जो सिर्फ़ मेमोरी में रहता है. इसलिए, ऐप्लिकेशन की प्रोसेस बंद होने पर यह सेव नहीं रहता.
Jetpack AppState, StateStore इसी कमी को पूरा करता है.
यह Compose स्टेट का टाइप-सेफ़ मैप है. इसे ऐप्लिकेशन के कॉम्पोनेंट के साथ शेयर किया जा सकता है. साथ ही, ऐप्लिकेशन की प्रोसेस बंद होने पर, इसे DataStore की मदद से वापस लाया जा सकता है.
आपके पास हर सिंगलटन में DataStore को खुद वायर करने का विकल्प है. हालांकि, इसका मतलब है कि हर प्रॉपर्टी के लिए, सेव और रीस्टोर करने वाला बॉयलरप्लेट लिखना होगा.
StateStore DataStore इंटिग्रेशन की मदद से, DataStore को एक बार हुक अप किया जाता है. इसके बाद, आपको हर उस स्टेट के लिए शामिल होना होता है जिसे आपको बनाए रखना है.
चलिए, इसे अपने फ़ोकस टाइमर ऐप्लिकेशन में बनाते हैं.
हम पहले इन-मेमोरी से शुरुआत करेंगे. इसके बाद, DataStore परसिस्टेंस जोड़ेंगे.
इसकी शुरुआत करने के लिए, हम FocusSession को एक सील किए गए इंटरफ़ेस के तौर पर मॉडल कर रहे हैं. इसमें आइडल, रनिंग, और पॉज़्ड स्टेट शामिल हैं.
sealed interface FocusSession {
data object Idle : FocusSession
data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}इसके बाद, इस स्थिति को StateStore में सेव करने के लिए, हमने StateStoreKey को इसके टाइप, FocusSession, और डिफ़ॉल्ट वैल्यू, idle के साथ तय किया.
sealed interface FocusSession { ... }
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)इसके बाद, हम Hilt में एक सिंगलटन StateStore उपलब्ध कराते हैं. इसे हम उन अलग-अलग कॉम्पोनेंट में इंजेक्ट कर सकते हैं जहां हमें इसकी ज़रूरत होती है. जैसे, फ़ोकस सेशन मैनेजर.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(): StateStore = StateStore()
}
@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
...
}इसलिए, यह ठीक उसी तरह काम करता है जिस तरह ViewModel, SavedStateHandle को इंजेक्ट करता है.
ठीक है।
अब हमारे पास StateStore है, लेकिन हम इसका इस्तेमाल कैसे करें?
अच्छी बात यह है कि यह एक छोटा एपीआई है.
इसके सिर्फ़ तीन तरीके हैं. इसलिए, हम एक-एक करके उनके बारे में जानेंगे.
पहला है setState, जो किसी कुंजी के लिए नई वैल्यू लिखता है.
fun startFocus(taskId: String, durationMinutes: Int = 25) {
val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
stateStore.setState(
FocusSessionKey,
FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
)
}
fun endFocus() {
stateStore.setState(FocusSessionKey, FocusSession.Idle)
}जब कोई सेशन शुरू होता है, तो हम उसे 'चल रहा है' पर सेट करते हैं. सेशन खत्म होने पर, हम उसे 'कुछ नहीं कर रहा है' पर सेट करते हैं.
बस इतना ही.
दूसरा, हमारे पास अब भी updateState है. इसकी मदद से, मौजूदा वैल्यू के आधार पर नई वैल्यू का हिसाब लगाया जा सकता है. जैसे, रनिंग और रोके गए के बीच स्विच करते समय, बचे हुए समय का हिसाब लगाना.
fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Running) return@updateState session
FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
}
}
fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Paused) return@updateState session
FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
}
}आखिर में, getState है, जो किसी कुंजी के लिए रीड-ओनली स्टेट दिखाता है. इसमें डिफ़ॉल्ट वैल्यू को बदलने का विकल्प होता है. ऐसा तब किया जा सकता है, जब आपको किसी खास कॉल साइट पर उसे बदलना हो.
@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
...
}इस मैनेजर में FocusSession स्टेट को एक्सपोज़ करने का मतलब है कि उपभोक्ताओं को कभी भी StateStore या उसकी कुंजियों को सीधे ऐक्सेस करने की ज़रूरत नहीं पड़ेगी.
आपके पास डेटा को एन्कैप्सुलेट करने का विकल्प होता है. इससे डेटा को पढ़ने और उसमें बदलाव करने की अनुमति को मैनेज किया जा सकता है.
इसके बाद, उपभोक्ताओं के बजाय वे सिर्फ़ Focus Session Manager को इंजेक्ट करते हैं. यह शायद उनके पास पहले से ही होता है, क्योंकि उन्हें सेशन को कंट्रोल करना होता है. इसके बाद, वे State को पढ़ सकते हैं.
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() { ... }
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
fun focusSessionManager(): FocusSessionManager
}इसके बाद, ये रीडर सीधे तौर पर स्टेट को फिर से एक्सपोज़ कर सकते हैं या ट्रांसफ़ॉर्म का इस्तेमाल करके, नई और अपडेट की गई स्टेट बना सकते हैं. ठीक वैसे ही, जैसे हमने पहले मिनी-टाइमर ViewModel के उदाहरण में देखा था.
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}इसके बाद, यूज़र इंटरफ़ेस (यूआई) कंपोज़ेबल, उस स्टेट को किसी भी अन्य कंपोज़ स्टेट की तरह पढ़ सकते हैं. साथ ही, ज़रूरत पड़ने पर उसे फिर से कंपोज़ कर सकते हैं.
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}
@Composable
fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
val session by viewModel.session
if (session !is FocusSession.Idle) {
TimerReadout(session = session)
}
}हालांकि, अब तक FocusSession सिर्फ़ यादों में ही ज़िंदा है.
इसलिए, आइए देखें कि ऐप्लिकेशन की प्रोसेस बंद होने की समस्या से बचने के लिए क्या करना चाहिए.
इसलिए, सबसे पहले हमें स्टोर बनाते समय syncToDataStore को लॉन्च करके, कुछ सेटअप करना होगा.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(
@ApplicationContext context: Context,
@ApplicationScope scope: CoroutineScope,
): StateStore = StateStore().apply {
val path = context.dataStoreFile("app_state.json").absolutePath
scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
}
}इसके बाद, हम उन सभी कुंजियों को शामिल करते हैं जिन्हें हमें बनाए रखना है.
@Serializable
sealed interface FocusSession {
@Serializable data object Idle : FocusSession
@Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
@Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
PersistToDataStoreहमें बस यह पक्का करना होगा कि टाइप को सीरियलाइज़ किया जा सके और कुंजी, PersistToDataStore इंटरफ़ेस को लागू करे.
अब आपको लग रहा होगा कि यह तो बहुत आसान है.
तो, कुंजी को बनाए रखने के लिए यह असल में क्या करता है?
इसलिए, syncToDataStore, Transform के लिसनर एपीआई का इस्तेमाल करता है. यह बिलकुल Transform की तरह ही है. हालांकि, यह कुछ भी रिटर्न नहीं करता. इसका इस्तेमाल सिर्फ़ निगरानी और खराब असर के लिए किया जाता है.
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener { … }
}इसके बाद, उस लिसनर में, यह स्टोर में मौजूद हर कुंजी को पढ़ता है और जांच करता है कि क्या कोई कुंजी PersistToDataStore है.
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener {
for (key in keys) {
if (key is PersistToDataStore) {
val value = getState(key).value
LaunchedEffect(key) {
// Restore saved value from dataStore on first access
}
LaunchedEffect(key, value) {
// Flush updated value to dataStore on change
}
}
}
}
}जब ऐसा होता है, तो यह LaunchedEffect का इस्तेमाल करके, उन वैल्यू को DataStore के साथ सिंक करता है. ऐसा तब होता है, जब उन वैल्यू में कोई बदलाव होता है.
मैं यहां यह बताना चाहता हूं कि StateStore जैसे स्टैंडर्ड कंटेनर की मदद से, हम ये ड्रॉप-इन इंटिग्रेशन उपलब्ध करा पाते हैं.
DataStore, पहला ऐसा टूल है जिसके बारे में हम बात कर रहे हैं.
स्टेट की कैटगरी पर वापस आते हैं. अब हमें साफ़ तौर पर पता है कि पूरे ऐप्लिकेशन में Compose State का इस्तेमाल कैसे किया जा सकता है.
लाइव स्टेट के लिए mutableStateOf से लेकर, स्क्रीन-लोकल प्रज़ेंटेशन स्टेट के लिए SavedState API, और क्रॉस-कॉम्पोनेंट सेशन स्टेट के लिए StateStore तक. इसके बाद, हमारे पास Flows के साथ इंटरऑपरेबिलिटी के लिए collectAsState है.
हालांकि, इंटरऑपरेबिलिटी किसी एक कैटगरी तक सीमित नहीं है. जब भी आपको ऑब्ज़र्वेबल टाइप को Compose स्टेट में बदलना हो, तब collectAsState जैसे हेल्पर का इस्तेमाल किया जा सकता है.
हालांकि, परमानेंट स्टेट के लिए, हम Compose के साथ काम करने के तरीकों पर भी विचार कर रहे हैं. इसके अलावा, हम DataStore और Room के लिए भी काम कर रहे हैं, ताकि Compose स्टेट को सीधे तौर पर एक्सपोज़ किया जा सके. इससे आपको इसके लिए, बेहतर सिंटैक्स जैसा अपना कोड नहीं लिखना पड़ेगा.
मुझे यह बताते हुए खुशी हो रही है कि Transform, StateStore, और DataStore इंटिग्रेशन, आज से ऐल्फ़ा वर्शन में उपलब्ध हैं. हमने developer.android.com पर सभी दस्तावेज़ भी पब्लिश कर दिए हैं. साथ ही, GitHub पर हमारे पास AppState Recipes का एक डेटा स्टोर करने की जगह है. इसमें StateStore और Transform, दोनों को इस्तेमाल करने के उदाहरण दिए गए हैं.
[versions]
appstate = "1.0.0-alpha01"
...
[libraries]
...
transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }साथ ही, इन्हें अपने ऐप्लिकेशन में आज़माएं.
हमें सुझाव/राय दें या हमसे शिकायत करें.
जैसे, हम ऐल्फ़ा वर्शन में हैं. इसलिए, हम इस समय बदलाव कर सकते हैं और उन्हें दोहरा सकते हैं. इससे यह पक्का किया जा सकेगा कि वे आपके इस्तेमाल के उदाहरणों के लिए काम करें.
/**
* A value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value.
*/
@Stable
public interface State<out T> {
public val value: T
}/**
* A mutable value holder where reads to the [value] property during the execution of a [Composable]
* function, the current [RecomposeScope] will be subscribed to changes of that value. When the
* [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
* will be scheduled. If [value] is written to with the same value, no recompositions will be
* scheduled.
*/
@Stable
public interface MutableState<T> : State<T> {
override var value: T
public operator fun component1(): T
public operator fun component2(): (T) -> Unit
}@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
}@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}@Composable
fun Counter() {
val count = remember { mutableStateOf(0) }
Column {
Text("Count: ${count.value}")
AddButton(onClick = { count.value++ })
}
}
@Composable
fun AddButton(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("Add")
}
}@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(true) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}@Composable
fun Counter() {
val isVisible = remember { mutableStateOf(false) }
val count = remember { mutableStateOf(0) }
Column {
if (isVisible.value) {
Text("Count: ${count.value}")
} else {
Text("Hidden")
}
AddButton(onClick = { count.value++ })
}
}val levelName: String = "Level 1 - Stay Alive!"
val timeSurvived: Int = 0
val score: Double = 0.0data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}data class GameUiState(
val levelName: String = "Level 1 - Stay Alive!",
val timeSurvived: Int = 0,
val score: Double = 0.0
)
class GameViewModel : ViewModel() {
val uiState = MutableStateFlow(GameUiState())
fun updateScore() = uiState.update {
it.copy(
timeSurvived = it.timeSurvived + 1,
score = it.score + 10 + .01 * it.timeSurvived
)
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Column {
Header(levelName = state.levelName)
ScoreBadge(
timeSurvived = state.timeSurvived,
score = state.score
)
}
}@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}@Stable
class GameUiState {
var levelName by mutableStateOf("Level 1 - Stay Alive!")
var timeSurvived by mutableStateOf(0)
var score by mutableStateOf(0.0)
}
class GameViewModel : ViewModel() {
val uiState = GameUiState()
fun updateScore() = Snapshot.withMutableSnapshot {
uiState.timeSurvived += 1
uiState.score += 10 + .01 * uiState.timeSurvived
}
}
@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
val state = viewModel.uiState
Column {
Header(state = state)
ScoreBadge(state = state)
}
}public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R>class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: StateFlow<FocusSummary> =
combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle,
)
}class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find { it.id == session.activeTaskId }
FocusSummary.Active(...)class FocusScreenViewModel(
focusManager: FocusSessionManager,
taskRepository: TaskRepository
) : ViewModel() {
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val session by focusManager.sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
...
}
class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
...
}
class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
...
}
}
}class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: StateFlow<FocusSummary> =
combine(
sessionFlow,
taskRepository.tasksFlow
) { session, tasks ->
if (session.phase == FocusPhase.Idle) {
FocusSummary.Idle
} else {
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}.stateIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle
)
}class FocusSessionManager(
taskRepository: TaskRepository,
scope: CoroutineScope
) {
val sessionFlow: StateFlow<FocusSession> = ...
val summary: State<FocusSummary> =
transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = scope,
container = handle.getOrCreateContainer("transform")
) {
val session by sessionFlow.collectAsState()
if (session.phase == FocusPhase.Idle)
return@transform FocusSummary.Idle
val tasks by taskRepository.tasksFlow
.collectAsState(emptyList())
val task = tasks.find {
it.id == session.activeTaskId
}
FocusSummary.Active(...)
}
}class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: StateFlow<MiniTimerUiState> =
combine(
focusManager.summary,
isExpanded
) { summary, expanded ->
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = MiniTimerUiState()
)
fun toggleExpanded() { isExpanded.update { !it } }
}class MiniTimerViewModel(
focusManager: FocusSessionManager
) : ViewModel() {
val isExpanded = MutableStateFlow(false)
val uiState: State<MiniTimerUiState> =
transform(
initialValue = MiniTimerUiState(),
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
val summary by focusManager.summary
val expanded by isExpanded.collectAsState()
MiniTimerUiState(
summary = summary,
showControls = expanded && summary is FocusSummary.Active
)
}
fun toggleExpanded() { isExpanded.update { !it } }
}class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
.collectAsState()
GlanceTheme { FocusWidgetContent(summary) }
}
}
}class FocusWidget : GlanceAppWidget() {
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
val focusManager = ...
provideContent {
val summary by focusManager.summary
GlanceTheme { FocusWidgetContent(summary) }
}
}
}public fun <R> transform(
initialValue: R,
scope: CoroutineScope,
onUpdate: @Composable (R) -> R,
): State<R> {
val transformScope = TransformScope()
val transform = transformScope
.launchTransform(initialValue, onUpdate)
scope.launch(
dispatcher,
start = CoroutineStart.UNDISPATCHED
) {
try { transformScope.run() } finally {
transform.dispose()
}
}
return transform
}public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope
public abstract class TransformScope {
public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
public abstract suspend fun run()
}
public fun <T> TransformScope.launchTransform(
initialState: T,
onUpdate: @Composable (T) -> T,
): DisposableState<T>
public interface DisposableState<T> : State<T> {
public fun dispose()
}override suspend fun run() {
val context = currentCoroutineContext()
val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
...
val recomposer = Recomposer(context)
val composition = Composition(UnitApplier, recomposer)
try {
withContext(clock) {
if (clock is HeadlessTransformClock) {
launch(start = CoroutineStart.UNDISPATCHED) {
clock.runClock()
}
}
...
}
} finally {
// dispose of composition, cancel the clock, close the recomposer
}
}@HiltViewModel
class TaskListViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
private val networkMonitor: NetworkMonitor,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
...
}@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
}@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
}@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
var selectedTaskIds by mutableStateOf(emptySet<String>())
private set
var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
private set
fun updateSearchQuery(query: String) {
Snapshot.withMutableSnapshot {
searchQuery = query
selectedTaskIds = emptySet()
}
}
}val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}@Singleton
class NetworkMonitor @Inject constructor(
@ApplicationContext context: Context,
) {
private val connectivityManager =
context.getSystemService(ConnectivityManager::class.java)
...
}@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
}@Singleton
class NetworkMonitor @Inject constructor(...) {
...
private val _isOnline = mutableStateOf(checkCurrentConnectivity())
val isOnline: State<Boolean> = _isOnline
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { _isOnline.value = true }
override fun onLost(network: Network) { _isOnline.value = false }
}
init {
connectivityManager.registerDefaultNetworkCallback(callback)
}
}val tasksState: State<TaskListState> = transform(
initialValue = TaskListState.Loading,
scope = viewModelScope,
) {
if (!networkMonitor.isOnline.value) {
return@transform TaskListState.Offline
}
val tasks = remember(searchQuery) {
tasksRepository.getTasks(searchQuery)
}.collectAsState(initial = null).value
?: return@transform TaskListState.Loading
TaskListState.Success(tasks = tasks)
}sealed interface FocusSession {
data object Idle : FocusSession
data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}sealed interface FocusSession { ... }
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(): StateStore = StateStore()
}
@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
...
}fun startFocus(taskId: String, durationMinutes: Int = 25) {
val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
stateStore.setState(
FocusSessionKey,
FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
)
}
fun endFocus() {
stateStore.setState(FocusSessionKey, FocusSession.Idle)
}fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Running) return@updateState session
FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
}
}
fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
stateStore.updateState(FocusSessionKey) { session ->
if (session !is FocusSession.Paused) return@updateState session
FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
}
}@Singleton
class FocusSessionManager @Inject constructor(
private val stateStore: StateStore,
) {
val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
...
}@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() { ... }
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
fun focusSessionManager(): FocusSessionManager
}@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}@HiltViewModel
class MiniTimerViewModel @Inject constructor(
focusSessionManager: FocusSessionManager,
) : ViewModel() {
val session: State<FocusSession> = focusSessionManager.session
}
@Composable
fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
val session by viewModel.session
if (session !is FocusSession.Idle) {
TimerReadout(session = session)
}
}@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideStateStore(
@ApplicationContext context: Context,
@ApplicationScope scope: CoroutineScope,
): StateStore = StateStore().apply {
val path = context.dataStoreFile("app_state.json").absolutePath
scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
}
}@Serializable
sealed interface FocusSession {
@Serializable data object Idle : FocusSession
@Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
@Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}
object FocusSessionKey :
StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
PersistToDataStoresuspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener { … }
}suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
listener {
for (key in keys) {
if (key is PersistToDataStore) {
val value = getState(key).value
LaunchedEffect(key) {
// Restore saved value from dataStore on first access
}
LaunchedEffect(key, value) {
// Flush updated value to dataStore on change
}
}
}
}
}[versions]
appstate = "1.0.0-alpha01"
...
[libraries]
...
transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }