I've been watching the Flutter ecosystem for a while now, and one thing that's always bugged me is the deployment workflow. You build an app, ship it to the app stores, and then sit around waiting for approval every time you need to push a fix. Sometimes that takes days or even weeks.
Shorebird solves this problem by letting you push over-the-air code push updates to your Flutter apps. The really cool part is their shorebird create command, which scaffolds Flutter projects that support instant updates right from the start.
In this tutorial, I'll walk you through building your first Flutter app with Shorebird. We'll cover everything from installation to your first release, and I'll explain the Flutter fundamentals along the way.
Getting Shorebird Installed
Before you can create a project, you need the Shorebird command-line interface. The installation process is pretty straightforward regardless of your operating system.
On macOS or Linux, open your terminal and run:
curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash
On Windows, fire up PowerShell and run:
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
iwr -UseBasicParsing 'https://raw.githubusercontent.com/shorebirdtech/install/main/install.ps1'|iex
These commands download Shorebird to ~/.shorebird/bin, including a modified Flutter engine that enables the code push stuff. This modified Flutter lives inside Shorebird's cache and won't mess with your existing Flutter installation. You'll keep using your normal Flutter SDK for development.
After installation, verify everything works by running shorebird --version. Then authenticate with shorebird login, which opens your browser so you can create a free Shorebird account or sign into an existing one.
Creating Your First OTA-Enabled Flutter App
Here's where shorebird create really shines. Instead of running flutter create and then manually configuring Shorebird later, this single command handles everything:
shorebird create my_flutter_app
Under the hood, shorebird create does two things automatically. First, it runs flutter create to scaffold a standard Flutter project with the familiar counter app template. Second, it runs shorebird init to configure your project for OTA updates and register it with Shorebird's cloud infrastructure.
You might get prompted to log in using
shorebird loginif this is your first time running the Shorebird CLI.
The command generates a unique app_id (something like 8c846e87-1461-4b09-8708-170d78331aca) that identifies your app in Shorebird's system. This ID determines which patches get delivered to which apps. Think of it as your app's fingerprint. The app_id isn't secret, so you should commit it to version control.
Beyond the standard Flutter files, shorebird create adds or modifies a few things:
-
shorebird.yaml: A new config file in your project root with your
app_id -
pubspec.yaml: Updated to include
shorebird.yamlin the assets section - AndroidManifest.xml: Updated to include INTERNET permission (required for downloading patches)
Understanding Your Flutter Project Structure
Whether you use shorebird create or flutter create, the resulting project follows Flutter's standard directory layout. I think it's worth understanding this structure because you'll be working with these files constantly.
The lib/ folder contains all your Dart code. The entry point is lib/main.dart, which has the main() function that calls runApp() with your root widget. As your app grows, you'll organize screens, widgets, and business logic into subdirectories within lib/.
The android/ folder holds Android-specific configuration, including Gradle build files and AndroidManifest.xml. Unless you're integrating native Android code or configuring platform-specific settings, you probably won't need to edit files here directly.
The ios/ folder does the same thing for Apple platforms. It contains the Xcode workspace and iOS project files. Platform-specific configuration like Info.plist lives here, and you'll need to visit this directory when setting up iOS-specific capabilities or signing certificates.
The pubspec.yaml file at the project root is probably the most important configuration file. It defines your app's name, version, dependencies, and assets. When Shorebird creates or initializes a project, it adds shorebird.yaml to the assets list here so the config file gets bundled with your app:
flutter:
uses-material-design: true
assets:
- shorebird.yaml
How Flutter's Widget Tree Works
Flutter builds user interfaces through a hierarchical tree of widgets. Everything is a widget. You compose simple widgets into complex UIs by nesting them as children of other widgets.
A typical app structure uses four fundamental widgets:
- Scaffold provides the Material Design layout structure. It's basically a container for your app's major visual elements with properties for the app bar, body content, floating action buttons, drawers, and bottom navigation.
- AppBar creates the top navigation bar. It typically displays a title, optional leading widget (like a menu icon), and trailing action buttons (like search or settings icons).
- Center is a layout widget that positions its single child in the middle of the available space. It's commonly used to center content within the Scaffold's body.
- Text displays styled text on screen. It's one of the simplest widgets, but you'll use it all the time.
Here's how these widgets nest together:
Scaffold(
appBar: AppBar(
title: const Text('My First App'),
),
body: const Center(
child: Text('Hello, world!'),
),
)
This hierarchy, Scaffold containing AppBar and Center, with Text nested inside Center, shows how Flutter builds UIs through composition rather than inheritance.
Stateless Widgets vs. Stateful Widgets
Flutter has two kinds of widgets: ones that never change and ones that can update dynamically. Understanding this distinction is crucial.
StatelessWidget represents UI that doesn't change based on user interaction. These widgets receive their configuration from parent widgets, store values in final variables, and render the same output given the same inputs. Use StatelessWidget for static content like labels, icons, or logos.
class Greeting extends StatelessWidget {
const Greeting({super.key});
@override
Widget build(BuildContext context) {
return const Text('Welcome to Flutter!');
}
}
StatefulWidget manages mutable state. When something needs to change, a counter incrementing, a form field updating, or data loading from an API, you need a StatefulWidget. It creates a companion State object that persists across rebuilds and holds the mutable data.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _increment,
child: Text('Count: $_count'),
);
}
}
The key mechanism is setState(). Calling it tells Flutter that the state has changed and triggers a rebuild of the widget, updating the UI to reflect the new values.
Hot Reload Changes Everything
One of Flutter's best features is hot reload, which injects updated code into the running Dart VM without restarting your app. When you save a file, Flutter recompiles only the changed libraries, sends them to the device, and rebuilds the widget tree. This usually takes less than a second.
The magic of hot reload is that it preserves your app's state. Your app stays on the same screen with the same data loaded while UI changes appear instantly. You don't need to re-navigate to a deeply nested screen or re-enter form data after every code change.
To trigger hot reload, just save your file in VS Code or Android Studio (the IDEs auto-reload by default), or press r in the terminal if you're running via flutter run. For changes that affect initialization logic, like modifications to main() or initState(), use hot restart (press R). This restarts the app from scratch but is still way faster than a full rebuild.
Hot reload only works in debug mode. Release builds compile Dart to native code and don't support this feature, which is exactly why Shorebird's code push capability is so valuable for production apps.
Making Your First Change
At this point, you have a fully working Flutter app with OTA updates wired in. Let's run it and see what we've got:
flutter run
Here's what the app looks like on an Android device:
Before shipping anything, let's make a small but visible change so you can see how Flutter responds to code edits. We'll tweak the app's theme color and update some text.
Open lib/main.dart in your editor. Near the top of the file, you'll find the MaterialApp widget. This is where global application settings live, including theming.
Look for the theme property. By default, it looks something like this:
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
Change the seed color to something different, like Colors.blue:
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
This single change updates the color used by Material components like the app bar, buttons, and highlights across your entire app. Flutter's theming system works top-down, so modifying the theme here affects every widget below it in the tree.
Next, scroll down to the widget that renders text on the screen. In the default counter app, you'll see a Text widget inside the body of a Scaffold:
const Text(
'You have pushed the button this many times:',
),
Replace it with something custom:
const Text(
'Welcome to my first OTA-enabled Flutter app!',
),
Save the file and run the app:
flutter run
Within seconds, you should see the updated theme color and new text:
This fast feedback loop is one of Flutter's biggest strengths. You edit Dart code, the framework rebuilds the widget tree, and the changes appear immediately.
Shipping Your App With Shorebird
Once your app is ready for users, Shorebird provides commands to build, preview, and distribute releases.
Creating a release captures a snapshot of your compiled code that Shorebird stores in the cloud. This becomes the baseline for future patches:
shorebird release android # Creates an Android release (.aab)
shorebird release ios # Creates an iOS release (.ipa)
The shorebird release command builds your app using Shorebird's modified Flutter engine, uploads the compiled Dart code to Shorebird's servers, and outputs the artifacts you'll submit to the app stores. For Android, you get an .aab file for the Play Store. For iOS, an .ipa for App Store Connect.
Previewing a release lets you test the exact build that will ship to users:
shorebird preview
➜ my_flutter_app shorebird preview
✓ Fetching releases (0.5s)
✓ Fetching releases (0.5s)
Which release would you like to preview? 1.0.0+1
✓ Fetching aab artifact (0.4s)
✓ Using stable track (0.7s)
✓ Extracting metadata (1.0s)
✓ Built apks: /Users/<username>/.shorebird/bin/cache/previews/dbad83ad-92fd-4228-af9a-014800f6efd7/android_1.0.0+1_1966896.apks (2.9s)
✓ Installing apks (6.0s)
✓ Starting app (1.8s)
The shorebird preview command downloads the release artifacts from Shorebird's cloud and installs them on a connected device or emulator. I find this particularly useful for verifying releases built on CI/CD servers before distributing them to end users.
After your initial release is live in the stores, you can push instant updates with shorebird patch whenever you fix bugs or add features. No app store review required.
For instance, update the theme of the app to use the following:
theme: ThemeData(
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
Then, run shorebird patch android. Once the command completes running, try closing and restarting an installed release of the app. The theme of the app should get updated on a fresh run:
Also, you will be able to view the patch on your Shorebird Console:
If needed, you can rollback your patches easily from here:
What's Your Flutter Story?
Starting a Flutter project with shorebird create instead of flutter create doesn't cost you anything in terms of development workflow. You still get the same project structure, the same hot reload experience, and the same widget-based UI development.
What you gain is the ability to push critical fixes to users in minutes instead of waiting days for app store approval. For any Flutter project that will eventually ship to real users, building in code push capability from day one just makes sense.
I'm curious to hear about your experience with Flutter and Shorebird. What features are you most excited to build? What challenges are you running into? Drop a comment below.





Top comments (0)