cranpose_core/hooks.rs
1use std::{hash::Hash, rc::Rc};
2
3use crate::{
4 composer_context,
5 owned::Owned,
6 runtime,
7 state::{
8 DerivedState, MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State,
9 StructuralEqual,
10 },
11};
12
13/// Returns the value `init` made on the first composition at this call site. Later compositions
14/// return the same value until the call site leaves the composition. Compose's `remember`.
15#[track_caller]
16pub fn remember<T: 'static>(init: impl FnOnce() -> T) -> Owned<T> {
17 let source = crate::caller_location_key();
18 composer_context::with_composer(|composer| composer.remember_at(source, init))
19}
20
21/// Like [`remember`], but recomputes whenever `key` changes.
22///
23/// A plain `remember` is keyed only by composition position: when a slot is
24/// reused with different inputs (a list row that changes identity, an icon
25/// whose path prop changes), the stale value survives. `rememberKeyed`
26/// stores the key beside the value and re-runs `init` on mismatch — the JC
27/// `remember(key1) { ... }` contract.
28#[expect(non_snake_case)]
29#[track_caller]
30pub fn rememberKeyed<K, T>(key: K, init: impl FnOnce(&K) -> T) -> T
31where
32 K: PartialEq + 'static,
33 T: Clone + 'static,
34{
35 let slot = remember(|| std::cell::RefCell::new(None::<(K, T)>));
36 slot.with(|cell| {
37 let mut stored = cell.borrow_mut();
38 match &*stored {
39 Some((stored_key, value)) if *stored_key == key => value.clone(),
40 _ => {
41 let value = init(&key);
42 *stored = Some((key, value.clone()));
43 value
44 }
45 }
46 })
47}
48
49/// Returns a [`MutableState`] that always holds the latest value.
50///
51/// The state **reference** is stable across recompositions; only the **value** updates.
52/// This allows closures to capture a stable reference while reading fresh values.
53///
54/// # Use Case
55/// Use when a `remember`ed closure needs to read a value that changes each recomposition
56/// without recreating the closure itself.
57///
58/// # Example
59/// ```rust,ignore
60/// let config = build_config(); // Rebuilt each recomposition
61/// let config_state = rememberUpdatedState(config);
62///
63/// // This closure is created once, reads latest config via state
64/// let callback = remember(|| {
65/// let cfg = config_state;
66/// Rc::new(move || do_something(&cfg.value()))
67/// }).with(|c| c.clone());
68/// ```
69///
70/// # JC Equivalent
71/// ```kotlin
72/// @Composable
73/// fun <T> rememberUpdatedState(newValue: T): State<T> =
74/// remember { mutableStateOf(newValue) }.apply { value = newValue }
75/// ```
76#[expect(non_snake_case)]
77#[track_caller]
78pub fn rememberUpdatedState<T: Clone + 'static>(value: T) -> MutableState<T> {
79 let source = crate::caller_location_key();
80 composer_context::with_composer(|composer| {
81 let runtime = composer.runtime_handle();
82 let state = composer.remember_at(source, || {
83 OwnedMutableState::with_runtime(value.clone(), runtime)
84 });
85 state.with(|s| {
86 s.set(value);
87 s.handle()
88 })
89 })
90}
91
92/// Runs `callback` once on the next frame with the frame time in nanoseconds. Dropping the returned
93/// registration before the frame cancels the callback. Must be called during composition.
94#[cfg(feature = "internal")]
95#[expect(non_snake_case)]
96pub fn withFrameNanos(
97 callback: impl FnOnce(u64) + 'static,
98) -> crate::internal::FrameCallbackRegistration {
99 composer_context::with_composer(|composer| {
100 composer
101 .runtime_handle()
102 .frame_clock()
103 .with_frame_nanos(callback)
104 })
105}
106
107/// Runs `callback` once on the next frame with the frame time in milliseconds. Dropping the
108/// returned registration before the frame cancels the callback. Must be called during composition.
109#[cfg(feature = "internal")]
110#[expect(non_snake_case)]
111pub fn withFrameMillis(
112 callback: impl FnOnce(u64) + 'static,
113) -> crate::internal::FrameCallbackRegistration {
114 composer_context::with_composer(|composer| {
115 composer
116 .runtime_handle()
117 .frame_clock()
118 .with_frame_millis(callback)
119 })
120}
121
122/// Creates a new `MutableState` initialized with the given value.
123///
124/// `MutableState` is a cheap copyable observable handle. Reads are tracked by the
125/// current composer or snapshot, and writes trigger recomposition of scopes that
126/// read it.
127///
128/// Writing a value equal to the one already there is not a change, so it does
129/// not recompose anything -- Jetpack Compose's `structuralEqualityPolicy()`,
130/// which is its default too. Use [`mutableStateOfNeverEqual`] for a value that
131/// cannot be compared, or one whose every write must count.
132///
133/// # When to use
134/// Use `mutableStateOf` when:
135/// 1. You are creating state properties inside a struct or class (not a composable function).
136/// 2. You are implementing a custom state management solution.
137///
138/// **Inside a `#[composable]` function this must sit inside a `remember`**,
139/// either [`rememberMutableStateOf`] for a single state or [`remember`] around
140/// the struct that holds several. Called straight from a composable body it
141/// makes a new state every pass and loses the value, exactly as in Kotlin.
142///
143/// # Example
144///
145/// ```rust,ignore
146/// struct MyViewModel {
147/// name: MutableState<String>,
148/// }
149///
150/// impl MyViewModel {
151/// fn new() -> Self {
152/// Self {
153/// name: mutableStateOf(String::from("Alice")),
154/// }
155/// }
156/// }
157///
158/// #[composable]
159/// fn rememberMyViewModel() -> MyViewModel {
160/// remember(MyViewModel::new).with(|model| model.clone())
161/// }
162/// ```
163///
164/// A state made while a [`remember`] is building its value belongs to that
165/// slot and is released with it, which is what lets the struct above be
166/// remembered whole. Made anywhere else it is owned by the runtime and lives
167/// as long as the runtime does; to tie that to a Rust owner instead, store an
168/// [`OwnedMutableState`] or call [`MutableState::retain`] on the handle.
169#[expect(non_snake_case)]
170pub fn mutableStateOf<T: Clone + PartialEq + 'static>(initial: T) -> MutableState<T> {
171 current_runtime("mutableStateOf")
172 .alloc_persistent_state_with_policy(initial, Rc::new(StructuralEqual))
173}
174
175/// Like [`mutableStateOf`], for a value that cannot be compared or whose every
176/// write must count as a change.
177///
178/// This is Jetpack Compose's `neverEqualPolicy()`, and the non-remembered form
179/// of [`rememberMutableStateOfNeverEqual`].
180#[expect(non_snake_case)]
181pub fn mutableStateOfNeverEqual<T: Clone + 'static>(initial: T) -> MutableState<T> {
182 current_runtime("mutableStateOfNeverEqual").alloc_persistent_state(initial)
183}
184
185/// Creates an [`OwnedMutableState`] holding `initial`, which is released when its owner is dropped.
186/// Writing an equal value is not a change. Panics without an active runtime.
187#[expect(non_snake_case)]
188pub fn ownedMutableStateOf<T: Clone + PartialEq + 'static>(initial: T) -> OwnedMutableState<T> {
189 OwnedMutableState::with_runtime_structural_eq(initial, current_runtime("ownedMutableStateOf"))
190}
191
192/// Like [`ownedMutableStateOf`], for a value that cannot be compared.
193#[expect(non_snake_case)]
194pub fn ownedMutableStateOfNeverEqual<T: Clone + 'static>(initial: T) -> OwnedMutableState<T> {
195 OwnedMutableState::with_runtime(initial, current_runtime("ownedMutableStateOfNeverEqual"))
196}
197
198fn current_runtime(what: &str) -> runtime::RuntimeHandle {
199 composer_context::try_with_composer(super::composer::Composer::runtime_handle)
200 .or_else(runtime::current_runtime_handle)
201 .unwrap_or_else(|| {
202 panic!(
203 "{what} requires an active runtime. Create state inside a composition or after a Runtime is created."
204 )
205 })
206}
207
208/// Like [`mutableStateOf`] but returns `None` if no runtime is available.
209///
210/// Use this when you want to lazily initialize reactive state and gracefully
211/// handle the case where the runtime isn't yet available.
212#[expect(non_snake_case)]
213pub fn try_mutableStateOf<T: Clone + PartialEq + 'static>(initial: T) -> Option<MutableState<T>> {
214 let runtime = composer_context::try_with_composer(super::composer::Composer::runtime_handle)
215 .or_else(runtime::current_runtime_handle)?;
216 Some(runtime.alloc_persistent_state_with_policy(initial, Rc::new(StructuralEqual)))
217}
218
219/// Creates a snapshot-aware list holding `values`. Must be called during composition. Compose's
220/// `mutableStateListOf`.
221#[expect(non_snake_case)]
222pub fn mutableStateListOf<T, I>(values: I) -> SnapshotStateList<T>
223where
224 T: Clone + 'static,
225 I: IntoIterator<Item = T>,
226{
227 composer_context::with_composer(move |composer| composer.mutable_state_list_of(values))
228}
229
230/// Creates an empty snapshot-aware list. Must be called during composition.
231#[expect(non_snake_case)]
232pub fn mutableStateList<T: Clone + 'static>() -> SnapshotStateList<T> {
233 mutableStateListOf(std::iter::empty::<T>())
234}
235
236/// Creates a snapshot-aware map holding `pairs`. Must be called during composition. Compose's
237/// `mutableStateMapOf`.
238#[expect(non_snake_case)]
239pub fn mutableStateMapOf<K, V, I>(pairs: I) -> SnapshotStateMap<K, V>
240where
241 K: Clone + Eq + Hash + 'static,
242 V: Clone + 'static,
243 I: IntoIterator<Item = (K, V)>,
244{
245 composer_context::with_composer(move |composer| composer.mutable_state_map_of(pairs))
246}
247
248/// Creates an empty snapshot-aware map. Must be called during composition.
249#[expect(non_snake_case)]
250pub fn mutableStateMap<K, V>() -> SnapshotStateMap<K, V>
251where
252 K: Clone + Eq + Hash + 'static,
253 V: Clone + 'static,
254{
255 mutableStateMapOf(std::iter::empty::<(K, V)>())
256}
257
258/// A composable hook that creates and remembers a `MutableState`.
259///
260/// This is the primary way to define local state in a composable function.
261/// It combines `remember` and `mutableStateOf`.
262///
263/// # Arguments
264///
265/// * `init` - A closure that provides the initial value. This is only called once
266/// when the composable enters the composition.
267///
268/// # Example
269///
270/// ```rust,ignore
271/// #[composable]
272/// fn Counter() {
273/// // "count" persists across recompositions.
274/// // If we used mutableStateOf directly, it would reset to 0 every frame.
275/// let count = rememberMutableStateOf(|| 0);
276///
277/// Button(
278/// Modifier::empty(),
279/// ButtonSpec::default(),
280/// move || count.set(count.value() + 1),
281/// || Text(format!("Count: {}", count.value()))
282/// );
283/// }
284/// ```
285#[expect(non_snake_case)]
286#[track_caller]
287pub fn rememberMutableStateOf<T: Clone + PartialEq + 'static>(
288 init: impl FnOnce() -> T,
289) -> MutableState<T> {
290 let source = crate::caller_location_key();
291 composer_context::with_composer(|composer| {
292 let runtime = composer.runtime_handle();
293 composer
294 .remember_at(source, || {
295 OwnedMutableState::with_runtime_structural_eq(init(), runtime)
296 })
297 .with(super::state::OwnedMutableState::handle)
298 })
299}
300
301/// Like [`rememberMutableStateOf`], for a value that cannot be compared or whose every write must
302/// count as a change.
303#[expect(non_snake_case)]
304#[track_caller]
305pub fn rememberMutableStateOfNeverEqual<T: Clone + 'static>(
306 init: impl FnOnce() -> T,
307) -> MutableState<T> {
308 let source = crate::caller_location_key();
309 composer_context::with_composer(|composer| {
310 let runtime = composer.runtime_handle();
311 composer
312 .remember_at(source, || OwnedMutableState::with_runtime(init(), runtime))
313 .with(super::state::OwnedMutableState::handle)
314 })
315}
316
317/// A state computed from other states, as in Kotlin.
318///
319/// `compute` runs in a scope of its own: the states it reads subscribe that
320/// scope, not the composable reading the result, and a change to them runs
321/// `compute` again without recomposing anyone. The readers recompose only
322/// when the result differs from the last one. A composable that reads
323/// `derivedStateOf(|| index_under(offset.get()))` as an offset animates
324/// recomposes once per new index rather than once per frame.
325#[expect(non_snake_case)]
326#[track_caller]
327pub fn derivedStateOf<T: 'static + Clone + PartialEq>(
328 compute: impl Fn() -> T + 'static,
329) -> State<T> {
330 let source = crate::caller_location_key();
331 let compute: Rc<dyn Fn() -> T> = Rc::new(compute);
332 composer_context::with_composer(|composer| {
333 composer.with_group(source, |composer| {
334 if let Some(scope) = composer.current_recompose_scope() {
335 scope.mark_derivation();
336 }
337 composer.set_recompose_callback(move |composer| {
338 composer
339 .remember_at(source, || None::<DerivedState<T>>)
340 .update(|slot| {
341 if let Some(derived) = slot {
342 derived.recompute();
343 }
344 });
345 });
346 let runtime = composer.runtime_handle();
347 composer
348 .remember_at(source, || None::<DerivedState<T>>)
349 .update(|slot| {
350 let derived = match slot.take() {
351 Some(mut derived) => {
352 derived.set_compute(compute);
353 derived.recompute();
354 derived
355 }
356 None => DerivedState::new(runtime, compute),
357 };
358 let state = derived.state.as_state();
359 *slot = Some(derived);
360 state
361 })
362 })
363 })
364}