Search by

momotombo / nativephp-pull-to-refresh

momotombo.dev

Customizable animated pull-to-refresh UI component for NativePHP Mobile on iOS and Android.

Package info

github.com/momotombodevs/nativephp-pull-to-refresh

Type:nativephp-plugin

pkg:composer/momotombo/nativephp-pull-to-refresh

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.2 2026-11-09 18:03 UTC

This package is auto-updated.

Last update: 2026-11-09 18:05:33 UTC


README

Native pull-to-refresh EDGE container for NativePHP Mobile 4.5+ on iOS and Android. The component owns one vertical scroll container, renders an animated native indicator, and invokes a method on the parent Native Component when a refresh is accepted.

The consuming app owns data fetching and publishes the refreshing and result states. This package does not make network requests and does not provide a JavaScript API or custom bridge functions.

Requirements

  • PHP 8.3+
  • NativePHP Mobile ^4.5
  • NativePHP Mobile UI ^0.6
  • iOS 18.2+
  • Android API 26+

Installation

Published package

Install the stable release from Composer:

composer require momotombo/nativephp-pull-to-refresh:^1.1

Register the NativePHP plugin in the consuming app. Publish the NativePHP plugin provider first if the app does not have one yet:

php artisan vendor:publish --tag=nativephp-plugins-provider
php artisan native:plugin:register momotombo/nativephp-pull-to-refresh
php artisan native:plugin:list

The provider publishing command is normally a one-time setup. Do not register the same plugin repeatedly in the same app.

Rebuild the native app after the plugin is registered and whenever nativephp.json, Kotlin, Swift, or native dependencies change:

php artisan native:run ios
php artisan native:run android

Local package development

Add the package as a Composer path repository in the host app's composer.json:

{
  "repositories": [
    {
      "type": "path",
      "url": "packages/momotombo/nativephp-pull-to-refresh",
      "options": { "symlink": true }
    }
  ]
}

Then install the development version and register it:

composer require momotombo/nativephp-pull-to-refresh:@dev
php artisan native:plugin:register momotombo/nativephp-pull-to-refresh

PHP changes are available immediately through the path repository. Native changes require a new native build; restarting PHP or refreshing Composer does not replace the installed Kotlin or Swift renderer.

Blade usage

Use the component as the only vertical scroll container for the screen. Give it available height so short or empty content can still be pulled.

<native:pull-to-refresh
    :refreshing="$refreshing"
    :result="$refreshResult"
    on-refresh="refreshFeed"
    class="w-full flex-1"
>
    @forelse ($items as $item)
        <native:row class="w-full p-4">
            <native:text>{{ $item['title'] }}</native:text>
        </native:row>
    @empty
        <native:column class="w-full items-center justify-center p-8">
            <native:text>No items yet. Pull down to check again.</native:text>
        </native:column>
    @endforelse
</native:pull-to-refresh>

on-refresh names a method on the parent Native Component. The app should reject re-entry, set refreshing before the request, reset result to idle, and publish success or error when the request finishes:

use Native\Mobile\Edge\NativeComponent;
use Throwable;

class FeedScreen extends NativeComponent
{
    public bool $refreshing = false;
    public string $refreshResult = 'idle';
    public array $items = [];

    public function refreshFeed(): void
    {
        if ($this->refreshing) {
            return;
        }

        $this->refreshing = true;
        $this->refreshResult = 'idle';

        try {
            $this->items = app(FeedRepository::class)->latest();
            $this->refreshResult = 'success';
        } catch (Throwable $exception) {
            report($exception);
            $this->refreshResult = 'error';
        } finally {
            $this->refreshing = false;
        }
    }
}

Replace FeedRepository with the app's data source. The package only coordinates the native gesture and callback; it does not manage application data, caching, authentication, or retries.

PHP fluent API

The same element can be created programmatically when building an EDGE tree in PHP:

use Momotombo\NativePhpPullToRefresh\Elements\PullToRefresh;
use Momotombo\NativePhpPullToRefresh\Elements\PullToRefreshAppearance;
use Momotombo\NativePhpPullToRefresh\Enums\IndicatorShape;
use Momotombo\NativePhpPullToRefresh\Enums\Motion;
use Momotombo\NativePhpPullToRefresh\Enums\Preset;
use Momotombo\NativePhpPullToRefresh\Enums\ProgressStyle;
use Native\Mobile\Edge\Elements\Row;
use Native\Mobile\Edge\Elements\Text;

PullToRefresh::make()
    ->class('w-full flex-1')
    ->preset(Preset::Orbital)
    ->appearance(
        PullToRefreshAppearance::make()
            ->shape(IndicatorShape::Blob)
            ->progressStyle(ProgressStyle::Arc)
            ->motion(Motion::Spring)
    )
    ->refreshing($this->refreshing)
    ->result($this->refreshResult)
    ->onRefresh('refreshFeed')
    ->addChild(Row::make(Text::make('Recent activity')));

Every option that takes a fixed set of values accepts either the enum case or its string value ('blob' and IndicatorShape::Blob are equivalent). The enums live in Momotombo\NativePhpPullToRefresh\Enums: Preset, IndicatorShape, ProgressStyle, Motion, ProgressMode, RefreshResult, AndroidIndicatorIcon, PhaseName, and LayerRole.

Public builder methods are:

preset, appearance, shape, progressStyle, motion, progressMode, indicator, threshold, indicatorSize, indicatorStrokeWidth, animationDuration, indicatorIconIos, indicatorIconAndroid, indicatorAsset, indicatorLottie, accentColor, containerColor, trackColor, refreshing, result, resultDuration, refreshActionLabel, pullLabel, releaseLabel, refreshingLabel, successLabel, errorLabel, and onRefresh.

Options

Attribute Default Description
preset spring spring, liquid, minimal, orbital, wave, or pulse. Supplies the default progress-style and motion (see below).
shape circle circle, capsule, rounded, or blob. Radii scale with the container on both platforms; capsule makes the container 1.6× wider than tall so it reads as a pill.
progress-style From preset ring, arc (orbital arc with a satellite), wave (liquid fill), dots (eight dots), or none (glyph only). Used by the built-in indicator.
motion From preset none, spring (bouncy), liquid (eased), pulse (breathes while loading), or spin (linear).
progress-mode scrub scrub follows the gesture, indeterminate loops as soon as the pull starts, none draws a full indicator without tracking the gesture.
threshold 88 Pull distance in points/dp. Android uses it as the activation distance; iOS uses it to scale custom progress while UIKit controls activation.
indicator-size 32 Indicator diameter in points/dp. The container around it is indicator-size + 20 on iOS and max(40, indicator-size + 8) on Android, where Material's default box is 40dp.
indicator-stroke-width 2.5 Progress and status mark thickness.
animation-duration 240 Native transition duration in milliseconds. 0 disables non-essential motion.
accent-color Theme accent Progress, glyph, and artwork color. Accepts a color string or ARGB integer.
container-color Theme surface variant Indicator background color.
indicator-track-color Theme outline variant Inactive progress track color.
refreshing false App-controlled loading state. Keep it true until the request finishes.
result idle idle, success, or error.
result-duration 500 Success/error feedback duration in milliseconds. 0 hides the feedback.
on-refresh — Parent Native Component method called after an accepted gesture or accessibility action.
indicator-icon-ios — iOS SF Symbol name.
indicator-icon-android — Android icon: refresh, sync, arrow_downward, download, cloud_download, or local_shipping.
indicator-asset — Relative path under the consuming app's public/ directory to a PNG, SVG, or GIF.
indicator-lottie — Relative path under the consuming app's public/ directory to a Lottie JSON file.
Accessibility labels English defaults refresh-action-label, pull-label, release-label, refreshing-label, success-label, and error-label.

Each preset resolves progress-style and motion unless you set them yourself; empty Blade bindings also fall back to the preset.

Preset progress-style motion
spring ring spring
liquid wave liquid
minimal ring (thin) spin
orbital arc spin
wave wave liquid
pulse dots pulse

A composed native:pull-to-refresh-indicator inherits motion and progress-mode from the container unless it declares its own. While looping, progress and track layers spin, artwork spins with motion="spin" and breathes with motion="pulse". Reduced-motion settings and motion="none" stop every loop, including Lottie and GIF playback, which then show a still frame.

Example:

<native:pull-to-refresh
    preset="orbital"
    shape="blob"
    progress-style="arc"
    motion="spring"
    accent-color="#7C3AED"
    container-color="#F5F3FF"
    :refreshing="$refreshing"
    :result="$refreshResult"
    on-refresh="refreshFeed"
    class="w-full flex-1"
>
    <!-- EDGE children -->
</native:pull-to-refresh>

Composed indicators in Blade

For a fully custom visual indicator, add exactly one native:pull-to-refresh-indicator. It is a declarative sentinel: it is consumed by the parent and never becomes a row in the scrollable content. Its pulling phase is required as the base (the element throws without one); missing phases reuse that composition.

<native:pull-to-refresh threshold="88" on-refresh="refreshFeed" class="w-full flex-1">
    <native:pull-to-refresh-indicator shape="blob" motion="spring" progress-mode="scrub">
        <native:pull-to-refresh-phase name="pulling">
            <native:pull-to-refresh-layer role="background">
                <native:stack class="items-center justify-center">
                    <native:image src="refresh/background.svg" />
                </native:stack>
            </native:pull-to-refresh-layer>
            <native:pull-to-refresh-layer role="artwork">
                <native:image src="refresh/logo.svg" width="42" height="42" />
            </native:pull-to-refresh-layer>
            <native:pull-to-refresh-layer role="label">
                <native:text>Desliza para actualizar</native:text>
            </native:pull-to-refresh-layer>
        </native:pull-to-refresh-phase>

        <native:pull-to-refresh-phase name="release">
            <native:pull-to-refresh-layer role="progress">
                <native:icon name="arrow-upward" />
            </native:pull-to-refresh-layer>
        </native:pull-to-refresh-phase>

        <native:pull-to-refresh-phase name="refreshing">
            <native:pull-to-refresh-layer role="artwork" lottie="refresh/loading.json" />
        </native:pull-to-refresh-phase>

        <native:pull-to-refresh-phase name="success">
            <native:pull-to-refresh-layer role="status">
                <native:icon name="check-circle" />
                <native:text>Actualizado</native:text>
            </native:pull-to-refresh-layer>
        </native:pull-to-refresh-phase>

        <native:pull-to-refresh-phase name="error">
            <native:pull-to-refresh-layer role="status">
                <native:icon name="error" />
                <native:text>Error al actualizar</native:text>
            </native:pull-to-refresh-layer>
        </native:pull-to-refresh-phase>
    </native:pull-to-refresh-indicator>

    <native:column>
        <!-- scrollable EDGE content -->
    </native:column>
</native:pull-to-refresh>

Available phases are pulling, release, refreshing, success, and error. Layer roles are background, track, progress, artwork, label, and status; the active phase decides which layers draw, and each role reacts to progress and motion: background grows with the pull, progress turns with it (or spins while looping), track spins while looping, artwork follows the motion (spring/liquid scale, spin, pulse), label stays hidden until the pull starts, and status draws as-is for success/error phases. Layers can contain existing visual EDGE components, including stack, row, image, icon, text, progress-bar, and canvas.

Do not add a scroll view, another pull-to-refresh, or interactive callbacks inside a layer. The PHP validator rejects those structures. A custom composition replaces only the visual indicator: gesture activation, callbacks, accessibility, reduced-motion behavior, refreshing, result, and result-duration remain owned by the parent container.

The typed API creates the identical EDGE tree:

use Momotombo\NativePhpPullToRefresh\Elements\PullToRefreshIndicator;
use Momotombo\NativePhpPullToRefresh\Elements\PullToRefreshLayer;
use Momotombo\NativePhpPullToRefresh\Elements\PullToRefreshPhase;
use Native\Mobile\Edge\Elements\Image;

PullToRefresh::make()
    ->indicator(
        PullToRefreshIndicator::make()
            ->phase(
                PullToRefreshPhase::make('pulling')
                    ->layer(
                        PullToRefreshLayer::make('artwork')
                            ->addChild(Image::make('refresh/logo.svg'))
                    )
            )
    );

Custom artwork

Put artwork in the consuming app's public/ directory and pass a relative path:

<native:pull-to-refresh
    indicator-asset="pull-to-refresh/refresh.svg"
    :refreshing="$refreshing"
    :result="$refreshResult"
    on-refresh="refreshFeed"
    class="w-full flex-1"
>
    <!-- EDGE children -->
</native:pull-to-refresh>

Supported formats are PNG, SVG, GIF, and Lottie JSON. Use exactly one visual source: the platform icon pair, indicator-asset, or indicator-lottie. A composed layer can instead use its local asset or lottie attribute. URLs, absolute paths, parent-directory segments (..), empty path segments, and unsupported extensions are rejected. The package does not copy application assets; the native renderer resolves them from the consuming app.

Layout, accessibility, and platform behavior

  • The component owns one vertical scroll container. Do not nest another vertical scroller inside it.
  • The container needs available height, commonly class="w-full flex-1", for short or empty content to accept a pull.
  • VoiceOver and TalkBack can trigger refresh through the localized refresh-action-label.
  • Reduced-motion settings are respected by both renderers.
  • Android uses Material 3 pull-to-refresh and applies threshold to activation.
  • iOS uses UIKit UIRefreshControl for activation. threshold scales the custom visual progress but does not change UIKit's native activation distance.
  • Native dependencies are declared in nativephp.json. The host app must be rebuilt after changing that manifest or either native renderer.

If the screen already has a vertical list or scroll view, use that screen's existing refresh mechanism instead of nesting it inside this component.

A refresh whose callback changes nothing still completes: NativePHP skips publishing identical frames, so the element serializes a render_sequence that advances on every render and the renderers treat a newer render as the callback's answer. Two consequences: screens using this component always publish at least one changed node per render, and any unrelated render of the same screen that lands while a refresh is pending also completes it. Keep refresh handlers idempotent (for example, return early while $this->refreshing is true).

Validation and development commands

From the package repository:

composer validate --strict
composer install
composer test

composer test also compiles and runs the native refresh-lifecycle checks in tests/Native: the Kotlin one needs a JDK and a Kotlin compiler in the Gradle cache (any Android build leaves one), the Swift one needs Xcode. Each is skipped when its toolchain is missing. Run only the PHP tests with vendor/bin/pest --exclude-group=native.

From a NativePHP host app:

php artisan native:plugin:validate packages/momotombo/nativephp-pull-to-refresh
php artisan native:plugin:list
php artisan native:run ios
php artisan native:run android

native:plugin:validate validates the plugin manifest and declarations. It does not replace a native build or device acceptance test. After changing Kotlin, Swift, or native dependencies, rebuild the target platform before evaluating behavior.

Support

When reporting a native issue, include the package version, NativePHP Mobile version, platform, OS/API version, renderer configuration, and the relevant build log. Do not include secrets or private application data.

License

MIT; see LICENSE.