Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions resources/views/docs/mobile/4/digging-deeper/async-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,48 @@ required parameter that isn't in the payload can't be resolved, so the call fail

</aside>

## Sending events while a task runs

<x-docs.version-badge since="TODO" />

`finished()` hands back one result, at the end. To tell the screen something while the work still runs, such as
progress, fire an event that implements `BroadcastsGlobally` inside the task. It is carried to the UI thread, where
the live screen hears it in its `#[On]` methods:

```php
use App\Events\ReportProgressed;
use Native\Mobile\AsyncTask;

AsyncTask::dispatch(static function () {
foreach (ExpensiveReport::sections() as $index => $section) {
$section->build();

event(new ReportProgressed(done: $index + 1));
}
});
```

```php
use App\Events\ReportProgressed;
use Native\Mobile\Attributes\On;

#[On(ReportProgressed::class)]
public function onProgress(int $done): void
{
$this->sectionsDone = $done;
}
```

Unlike a scoped callback, the event goes to whichever screen is live, and only while a native screen is showing.
Its `Event::listen` listeners then run on the UI thread, and not in the task.

Each event from a task costs its own render on the screen. Fire one per step, as above, and not one per row of a
large loop: a large burst does not all reach the screen, see
[A burst of events](../the-basics/events#a-burst-of-events).

See [Events from queued jobs and async tasks](../the-basics/events#events-from-queued-jobs-and-async-tasks) for how
to write the event, and for the limits.

## Running several at once

Tasks run on a small pool of background PHP contexts, so several can be in flight together:
Expand Down Expand Up @@ -262,3 +304,6 @@ the callbacks run on your UI thread, in your component.
- Async tasks don't survive the app being killed. For durable background work, use [Queues](queues).
- Device APIs that need the UI (camera, dialogs, biometrics) don't belong inside async tasks. Fetch and compute
in the task; drive UI from the callbacks.
- On Android the background lane for async tasks starts 2.5 seconds after the app is ready. A task that is
dispatched before that is refused: its work does not run and `failed()` is called at once, saying that the
background lane is not available. Without a `failed()` callback, the refusal only shows in the log.
27 changes: 27 additions & 0 deletions resources/views/docs/mobile/4/digging-deeper/queues.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,33 @@ class SyncData implements ShouldQueue
}
```

## Sending Events to a Screen

<x-docs.version-badge since="TODO" />

A job runs in its own PHP runtime, so it cannot change a screen directly. Fire an event that implements
`BroadcastsGlobally` instead, and the live screen hears it in its `#[On]` methods:

```php
use App\Events\SyncFinished;

public function handle()
{
Http::post('https://api.example.com/sync', $this->payload);

event(new SyncFinished(count: count($this->payload)));
}
```

The event only reaches a screen while a native screen is showing. Its `Event::listen` listeners then run in the
runtime of the screens, and not in the job. Otherwise the event stays in the job, as any Laravel event does.

This does not work under [Jump](../the-basics/jump): a job that `php artisan queue:work` runs on your machine does
not reach the screen.

See [Events from queued jobs and async tasks](../the-basics/events#events-from-queued-jobs-and-async-tasks) for how
to write the event and the screen's method, and for the limits.

## How It Works

When your app boots, NativePHP automatically starts a dedicated PHP runtime on a separate thread. This worker
Expand Down
4 changes: 2 additions & 2 deletions resources/views/docs/mobile/4/digging-deeper/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ public function appearanceChanged(string $mode): void
}
```

`AppearanceChanged` also dispatches globally, so code anywhere in the app can listen — not just the active
screen:
`AppearanceChanged` also [broadcasts globally](../the-basics/events#broadcasting-globally), so code anywhere in the
app can listen, not just the active screen:

```php
use Illuminate\Support\Facades\Event;
Expand Down
4 changes: 3 additions & 1 deletion resources/views/docs/mobile/4/testing/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ To audit a tree for screen-reader accessibility rather than a specific element,

## Render-count guards

Every interaction re-renders. When performance matters, assert on exactly how many frames a screen produced.
Every interaction re-renders, except an `emitNative()` that misses every `when` filter of the screen while nothing
else is there for the event (see [Testing a when filter](native-events#testing-a-when-filter)). When performance
matters, assert on exactly how many frames a screen produced.

- `renderCount()` — frames rendered so far (the initial mount counts as 1).
- `assertRenderCount($count)` — an exact frame count.
Expand Down
4 changes: 3 additions & 1 deletion resources/views/docs/mobile/4/testing/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ it('increments the count', function () {
`tap()` presses the nearest pressable element whose subtree shows the given text (or one carrying a matching `ref` —
more on that in [Interactions](interactions)). `assertSet()` reads a public or `#[Computed]` property and compares it,
while `assertNotSet()` asserts a property holds anything but a given value. Every interaction re-renders the component,
so the assertions that follow see the fresh frame.
so the assertions that follow see the fresh frame. The exception is an `emitNative()` that misses every `when` filter
of the screen while nothing else is there for the event, see
[Testing a when filter](native-events#testing-a-when-filter).

## Passing params and data

Expand Down
104 changes: 104 additions & 0 deletions resources/views/docs/mobile/4/testing/native-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ responses. You reach its assertions through the harness, or the bridge itself th
plugin pushes an event. It fires `#[On]` listeners, fluent `->on()` closures, and any pending `then()` / `catch()`
callbacks the component chained onto its bridge call, then re-renders.

No re-render follows an event that the screen hears only through `when` filters, when none of them matches and
nothing else is there for the event. See [Testing a when filter](#testing-a-when-filter).

Pass the event class and its payload:

```php
Expand All @@ -32,6 +35,107 @@ it('counts device shakes delivered as native events', function () {
});
```

## Testing a when filter

<x-docs.version-badge since="TODO" />

`emitNative()` hands the payload to the screen as you wrote it, so a
[`when` filter](../the-basics/events#filtering-with-when) sees the values and types that you pass. The comparison is
strict: write `42` and not `'42'` when the device sends a number. Only two numbers match across types, so `5` passes a
filter on `5.0`.

Deliver one event that the filter lets through and one that it does not:

```php
use Native\Mobile\Events\Alert\ButtonPressed;

// ItemScreen: #[On(ButtonPressed::class, when: ['id' => 'delete-confirm', 'label' => 'Delete'])]
it('deletes the item only when Delete is pressed', function () {
Native::test(ItemScreen::class)
->emitNative(ButtonPressed::class, ['index' => 0, 'label' => 'Cancel', 'id' => 'delete-confirm'])
->assertSet('deleted', false)
->assertNotRerendered()
->emitNative(ButtonPressed::class, ['index' => 1, 'label' => 'Delete', 'id' => 'delete-confirm'])
->assertSet('deleted', true)
->assertRerendered();
});
```

A screen that listens for an event only through filters does not re-render when none of them matches, and the
harness does the same: `emitNative()` then renders no new frame. Pin that down with `assertNotRerendered()` or
`assertRenderCount()`, see [Render-count guards](advanced#render-count-guards).

The exception is an event that something else is there for: a closure registered with `->on()`, a fluent callback
that waits for it, or a Laravel listener that is registered for the event. The screen then re-renders, also on a
miss.

The package itself listens for `AppearanceChanged`, `OrientationChanged` and `ThermalStateChanged`, so do not expect
`assertNotRerendered()` to pass after those three. An `Event::listen` in your app for an event that broadcasts
globally has the same effect, and `Event::fake()` does not change that.

## Events that broadcast globally

An event that implements [`BroadcastsGlobally`](../the-basics/events#broadcasting-globally) is heard by the live
screen and by Laravel's listeners. Test each direction from the side where the event starts.

### From the device

`emitNative()` delivers the event to the screen, and a marked event is also sent through `event()`. Fake Laravel's
events to assert on that half. The screen still hears the event:

```php
use Illuminate\Support\Facades\Event;
use Native\Mobile\Events\System\AppearanceChanged;

it('hears a theme change on the screen and in Laravel', function () {
Event::fake();

Native::test(SettingsScreen::class)
->emitNative(AppearanceChanged::class, ['mode' => 'dark'])
->assertSet('mode', 'dark');

Event::assertDispatched(AppearanceChanged::class);
});
```

### Fired from PHP

<x-docs.version-badge since="TODO" />

The screen under test is the live screen. Fire the event with `event()` in the body of the test, and the harness
delivers it to the screen's `#[On]` methods before the next assertion, with one render:

```php
use App\Events\OrderUpdated;

it('shows an order that shipped', function () {
$screen = Native::test(OrdersScreen::class);

event(new OrderUpdated(orderId: 42, status: 'shipped'));

$screen->assertSet('statuses', [42 => 'shipped'])
->assertRerendered();
});
```

When a method of the screen fires the event, its `#[On]` method runs after that method has returned, and the
interaction still renders once:

```php
it('renders once when a handler fires the event', function () {
Native::test(OrdersScreen::class)
->call('ship', 42) // ship() fires event(new OrderUpdated(42, 'shipped'))
->assertSet('statuses', [42 => 'shipped'])
->assertRenderCount(2);
});
```

Two things to keep in mind:

- `Event::fake()` holds a marked event back, so it reaches no screen and no listener. To test the screen's side,
leave the event out of the fake, for example with `Event::fake([OtherEvent::class])`.
- After `follow()`, the destination is the live screen. The screen underneath hears nothing, as on a device.

## Asserting on native calls

After an interaction, assert on what the component sent to the bridge:
Expand Down
34 changes: 34 additions & 0 deletions resources/views/docs/mobile/4/the-basics/device.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ $battery['isCharging']; // bool
| `batteryLevel` | Battery level from `0.0` to `1.0` |
| `isCharging` | Whether the device is charging |

## Events

The device sends two events that a screen hears with `#[On]`:

| Event | Fires when | Arguments |
| --- | --- | --- |
| `Native\Mobile\Events\Device\ThermalStateChanged` | The thermal state of the device changes | `state`, `previous` |
| `Native\Mobile\Events\Motion\ShakeDetected` | The user shakes the device | None |

`state` and `previous` are cases of the `Native\Mobile\ThermalState` enum: `Normal`, `Warm`, `Hot` or `Critical`.
`Device::thermalState()` returns the current case at any time.

```php
use Native\Mobile\Attributes\On;
use Native\Mobile\Events\Device\ThermalStateChanged;
use Native\Mobile\Events\Motion\ShakeDetected;
use Native\Mobile\ThermalState;

#[On(ThermalStateChanged::class)]
public function onThermal(ThermalState $state, ThermalState $previous): void
{
$this->throttled = $state->isHot();
}

#[On(ShakeDetected::class)]
public function onShake(): void
{
$this->shakes++;
}
```

Both events [broadcast globally](events#broadcasting-globally). Next to the `#[On]` methods of the live screen,
`Event::listen` hears them anywhere in your app.

## Permissions

| Platform | Permissions |
Expand Down
Loading