Skip to main content
Firstlight

Firstlight / Docs

Recipes for the screens a forms app needs, setup for your coding agent, Laravel on native screens, and accessibility.

View source
Build a list and detail flow

Build a List and Detail Flow

This recipe gives an app its shell: a layout with the platform's own title bar and back navigation, a list screen, and a detail screen it pushes. Every other recipe runs inside this layout.

This recipe runs unchanged in the showcase app, where ListDetailRecipeTest exercises it.

Firstlight does not draw navigation. NativePHP Mobile renders it natively when a layout opts in with usesNativeChrome():

Behaviour iOS Android
Stack SwiftUI NavigationStack Material 3 Scaffold
Title Large title that collapses to inline on scroll LargeTopAppBar / TopAppBar
Back System back button and edge swipe Top app bar back arrow and system back

PHP stays in charge of the screen stack. A back swipe or back press pops PHP's stack, so mount() and screen state behave the same on both platforms.

1. Write the layout

Save this as app/NativeComponents/Layouts/AppLayout.php:

<?php

namespace App\NativeComponents\Layouts;

use FirstlightUI\NativeComponent as FirstlightScreen;
use Native\Mobile\Edge\Layouts\Builders\NavBar;
use Native\Mobile\Edge\Layouts\NativeLayout;
use Native\Mobile\Edge\NativeComponent;

class AppLayout extends NativeLayout
{
    public function usesNativeChrome(): bool
    {
        return true;
    }

    public function navBar(NativeComponent $screen): ?NavBar
    {
        $isRoot = $screen instanceof FirstlightScreen && $screen->isRootScreen();

        return NavBar::make()
            ->title($screen->navTitle())
            ->displayMode($isRoot ? 'large' : 'inline')
            ->back(! $isRoot);
    }
}

isRootScreen() is true when nothing is below the screen on PHP's stack. The first screen gets a large title and no back button; every pushed screen gets an inline title and a back button. Android shows a back arrow only when the bar asks for one, so pushed screens must pass back(true). Each screen supplies its title through navTitle().

2. Register the routes

use App\NativeComponents\Layouts\AppLayout;
use App\NativeComponents\PostScreen;
use App\NativeComponents\PostsScreen;
use Illuminate\Support\Facades\Route;

Route::native('/posts', PostsScreen::class)->layout(AppLayout::class);
Route::native('/posts/{post}', PostScreen::class)->layout(AppLayout::class);

3. Write the list screen

Use the searchable list recipe. Its row press pushes the detail screen:

public function openPost(int $id): void
{
    $this->navigate('/posts/'.$id);
}

navigate() pushes. Use replace() to swap the current screen without adding to the stack, and back() to pop it from PHP, for example after saving.

4. Write the detail screen

<?php

namespace App\NativeComponents;

use App\Models\Post;
use FirstlightUI\NativeComponent;
use Illuminate\View\View;

class PostScreen extends NativeComponent
{
    public Post $post;

    public function navTitle(): string
    {
        return $this->post->title;
    }

    public function render(): View
    {
        return view('native.post');
    }
}

NativePHP fills a typed public property from the route parameter with the same name, using Laravel route model binding: {post} becomes a Post loaded by id. No mount() is needed.

Save the view as resources/views/native/post.blade.php:

<native:scroll-view class="w-full flex-1 bg-theme-background">
    <native:column class="w-full p-4 gap-3">
        <native:text class="text-sm text-theme-on-surface-variant">{{ $post->created_at->diffForHumans() }}</native:text>
        <native:text class="text-base text-theme-on-surface">{{ $post->body }}</native:text>
    </native:column>
</native:scroll-view>

Rules to keep

  • Return true from usesNativeChrome(). Without it, NativePHP falls back to drawn bars that do not support large titles or the iOS back swipe.
  • Do not add the custom attribute to <native:top-bar> or <native:bottom-nav>. It keeps the drawn bar.
  • Pass stable ids in URLs, never list positions.
  • Let the platform draw the back button. Do not add your own back Button.

Known limits

  • Tabs added with tabBar() share PHP's single stack. Switching tabs replaces the screen, so each tab does not keep its own history.
  • Android receives back only when the gesture completes. It does not show the predictive-back preview while the user drags.

Served from firstlightui/nativephp on GitHub.