Views and Widgets

View\View is the equivalent of Android's View/ViewGroup, and Widget\WidgetInterface is the equivalent of Compose/XML UI components. Every Activity builds a View in onCreate()/ onResume() and declares it as the rendered content with $this->setContentView($view).

Building a View

use Aicrion\Tandroid\View\View;
use Aicrion\Tandroid\View\ParseMode;

$view = View::message('Hello! 👋', ParseMode::HTML);

View is immutable — every method returns a new instance (similar to immutable state in Jetpack Compose):

public function attach(WidgetInterface $widget): self; // adds a widget (keyboard, table, etc.)
public function withKeyboard(WidgetInterface $keyboard): self; // alias of attach(), for readability
public function render(): array; // becomes the final sendMessage payload

The final output of render() is exactly what Kernel::handle() automatically sends to the Telegram API:

['text' => '...', 'parse_mode' => 'MarkdownV2', 'reply_markup' => [...]]

WidgetInterface

Every widget has a single contract:

interface WidgetInterface
{
    /** @return array<string, mixed> a fragment of the final sendMessage payload */
    public function render(): array;
}

Buttons and Keyboards (Button / Keyboard)

Widget\Button builds three kinds of buttons:

use Aicrion\Tandroid\Widget\Button;

Button::action('My Profile', to: ProfileActivity::class);              // navigate to another Activity
Button::action('Delete Order', to: OrderActivity::class, payload: ['id' => 42]); // with an extra
Button::url('Docs', 'https://example.com/docs');                        // external link
Button::requestContact('Share phone number');                           // request contact

An action button automatically encodes to and payload into callback_data as JSON ({"a": "...", "p": {...}}) — that exact structure is what Kernel\IntentResolver on the receiving side converts into an explicit Intent (see Activities and Intents).

Widget\Keyboard arranges buttons row by row — exactly like a vertical LinearLayout where each row is itself a horizontal LinearLayout:

use Aicrion\Tandroid\Widget\Keyboard;

$keyboard = Keyboard::inline()
    ->row(Button::action('My Profile', to: ProfileActivity::class))
    ->row(Button::url('Docs', 'https://example.com/docs'));

$view = View::message('Welcome')->attach($keyboard);

For a Reply keyboard (not Inline):

Keyboard::reply()->row(Button::requestContact());
// or directly:
Keyboard::requestContact('Send my number');

Multi-Select Checkboxes (CheckboxGroupWidget)

The equivalent of a group of CheckBoxes in Android — each option is a toggleable inline button, and the selection state is encoded directly in callback_data (no server-side state needed between taps):

use Aicrion\Tandroid\Widget\CheckboxGroupWidget;

$group = CheckboxGroupWidget::make('interests')
    ->option('sport', 'Sports')
    ->option('tech', 'Technology')
    ->checkedValues(['tech']); // pre-selected options

$view = View::message('Select your interests:')->attach($group);

Every tap on an option sends a callback_data shaped like {"w":"checkbox","n":"interests","v":"sport","c":true} — your Activity is responsible for reading n/v/c from update->callbackData (or via Intent) and updating the actual state (usually in a ViewModel).

Multi-Step Form (FormWidget)

For linear, text-based forms (e.g. "Name?" → "Age?" → "Confirm"):

use Aicrion\Tandroid\Widget\FormWidget;

$form = FormWidget::make()
    ->step('name', 'Enter your name:')
    ->step('age', 'Enter your age:', validator: static fn (string $v) => is_numeric($v));

// The Activity, after receiving the user's reply:
$result = $form->withAnswers($savedAnswers, $savedStep)->submit($update->text);

match (true) {
    $result->isInvalid  => /* re-show the same step with an error message: $result->error / $result->nextPrompt */,
    $result->isComplete => /* $result->answers holds the final data */,
    default              => /* next step: $result->nextPrompt, step: $result->nextStep */,
};

FormSubmissionResult has three states: next(), complete(), invalid() — with corresponding readonly properties for checking (isComplete, isInvalid). Persisting $savedAnswers/$savedStep between requests is your responsibility; usually kept in a ViewModel.

Multi-Page Wizard (WizardWidget)

For flows that need explicit "Previous/Next" buttons and pages with custom widgets (not just text):

use Aicrion\Tandroid\Widget\{WizardWidget, WizardPage, Button};

$wizard = WizardWidget::make(ownerActivity: SettingsActivity::class)
    ->page(new WizardPage('Choose your language', buttons: [[Button::action('English', to: SettingsActivity::class)]]))
    ->page(new WizardPage('Choose your timezone'))
    ->atIndex($currentIndex);

$view = View::message($wizard->currentPage()?->text ?? '')->attach($wizard);

The "Previous/Next" buttons automatically point back to the same ownerActivity with a wizard_step extra; the Activity must read this extra in onNewIntent()/onCreate() to set atIndex() correctly.

Rich Messages (View\Rich*)

For content that goes beyond plain text, the View\Rich namespace provides composable blocks, all producing Markdown/HTML output compatible with Telegram:

Class Purpose
Rich\TableBlock Monospace text table
Rich\CodeBlock Code block with a language hint
Rich\QuoteBlock Blockquote
Rich\ListBlock Ordered/unordered list
Rich\MapBlock Map link (static location)
Rich\SlideshowBlock A set of images as a slideshow (media group)
Rich\RichMessage Composes several blocks into a single message
use Aicrion\Tandroid\View\Rich\{RichMessage, TableBlock, CodeBlock};

$message = RichMessage::make()
    ->block((new TableBlock(['Name', 'Price']))->row('Book', '$5.00')->row('Notebook', '$1.50'))
    ->block(new CodeBlock('echo "hello";', language: 'php'));

StreamingView — Incremental Replies (like edit_message)

View\StreamingView is designed for replies that need to be completed gradually (e.g. output from a language model arriving token by token): each call to push() accumulates text, and render() ultimately produces the same standard structure as View::render(), letting an Activity switch between the initial send and subsequent edits.

Next Step

Wherever widgets or forms need to keep state across multiple user taps (like $savedAnswers/$currentStep above), read ViewModel and State Management.