ViewModel and State Management
Because every BotActivity is freshly instantiated on every
request (PHP holds no memory between requests), regular class
properties never survive between two user taps. The framework's
solution is exactly what Android calls
androidx.lifecycle.ViewModel: a state holder that outlives the
Activity itself.
Creating a ViewModel
use Aicrion\Tandroid\Kernel\ViewModel\ViewModel; final class CartViewModel extends ViewModel { public function addItem(string $sku): void { $items = $this->get('items', []); $items[] = $sku; $this->set('items', $items); } public function items(): array { return $this->get('items', []); } }
The base ViewModel exposes just two protected methods to
subclasses: get(string $key, mixed $default = null) and
set(string $key, mixed $value). All state is kept in an internal
array that gets serialized/deserialized via hydrate()/
dehydrate() (called only by StateStore, never manually).
Using It in an Activity
By combining HasViewModel onto any BotActivity, the protected
viewModel() method becomes available:
use Aicrion\Tandroid\Activity\{BotActivity, HasViewModel, NavigationRequest}; use Aicrion\Tandroid\Intent\Intent; use Aicrion\Tandroid\View\View; final class CartActivity extends BotActivity { use HasViewModel; public function onCreate(Intent $intent): ?NavigationRequest { $cart = $this->viewModel(CartViewModel::class); $cart->addItem($intent->getExtra('sku')); $this->setContentView(View::message('Cart items: ' . count($cart->items()))); return null; } }
That's it. You don't need to persist anything yourself —
ActivityManager automatically calls persistViewModel() on any
Activity using HasViewModel right after every lifecycle hook
completes.
How It Works Under the Hood
Kernel::boot()builds an instance ofKernel\ViewModel\StateStoreand injects it intoActivityManager.- For every Activity instance,
ActivityManagerchecksmethod_exists($activity, 'bindStateStore'); if true (meaningHasViewModelis used), it injectsStateStore. - The first call to
$this->viewModel(FooViewModel::class)inside your Activity callsStateStore::resolve(), which creates a freshFooViewModelinstance and — if previous state exists in cache — restores it viahydrate(). - After
onCreate/onNewIntent/onResumefinishes,ActivityManagercallspersistViewModel()on that same Activity, saving the new state back intoStateStore(only ifviewModel()was actually called).
The Scope of a ViewModel
The storage key is built from a combination of the user's chat_id
and the ViewModel class's FQCN:
aicrion.viewmodel.{ViewModel_FQCN_underscored}.{chat_id}
This means a CartViewModel shared across multiple Activities (e.g.
ProductActivity → CartActivity → CheckoutActivity) all running
for the same user see the same shared state — exactly like a
ViewModel surviving Android recreating the Activity on a screen
rotation.
To explicitly clear a ViewModel from cache (e.g. after an order completes):
$this->stateStore->clear(CartViewModel::class, $this->update->chatId);
(Since HasViewModel defines this property as private within your
own class, it's directly accessible from your Activity's methods —
no separate getter needed.)
When to Use a ViewModel vs. an Extra
| Need | Right tool |
|---|---|
| A small value that only needs to reach the next Activity | Intent::putExtra() |
| State that must survive across multiple Activities/user taps | ViewModel |
| The final result of a form/wizard before reaching the confirmation step | Usually ViewModel (since FormWidget/WizardWidget are themselves stateless) |