buildhut_updater 0.1.0
buildhut_updater: ^0.1.0 copied to clipboard
Android in-app update checker and installer for BuildHut API.
buildhut_updater #
Android in-app update checker and APK installer for apps distributed through BuildHut.
Checks the BuildHut API for new versions, shows an update dialog with a markdown changelog, downloads the APK with a progress bar, requests the required Android permissions at runtime, and triggers the system installer — all in one call or with full control over each step.
Features #
- Version comparison — Semantic versioning with optional build numbers (
1.2.3+5) - One-call UI flow —
showBuildHutUpdateCheck()handles checking, dialogs, download, and install - Silent checks — Use
checkForUpdates()without any UI for background polling - Markdown changelog — Renders the update description from BuildHut as markdown
- Progress reporting — Real-time download progress and status callbacks
- Custom HTTP client — Pass your own
Dioinstance for interceptors, timeouts, etc. - Self-contained permissions — Requests storage and install permissions at runtime when needed
Android Manifest Setup #
Your app must declare the install permission in android/app/src/main/AndroidManifest.xml for APK installation to work. Add the following inside the <manifest> tag:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required for in-app APK updates -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Required on Android 9 and below for downloading the APK -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<!-- ... -->
</application>
</manifest>
Additionally, you need to declare a FileProvider so the system package installer can access the downloaded APK. Add this inside the <application> tag in the same manifest:
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
Then create the file android/app/src/main/res/xml/file_paths.xml:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="." />
<cache-path name="cache" path="." />
</paths>
Note: The library requests
REQUEST_INSTALL_PACKAGESand storage permissions at runtime during the install flow. If the user denies them, the install is aborted with an exception you can handle.
Installation #
Add the dependency to your pubspec.yaml:
dependencies:
buildhut_updater: ^0.1.0
Then run:
flutter pub get
Quick Start #
import 'package:buildhut_updater/buildhut_updater.dart';
import 'package:package_info_plus/package_info_plus.dart';
// 1. Get your current version
final info = await PackageInfo.fromPlatform();
final currentVersion = '${info.version}+${info.buildNumber}';
// 2. Create the update service
final updateService = BuildHutUpdateService(
appId: 'your-buildhut-app-id', // UUID from BuildHut dashboard
currentVersion: currentVersion,
);
// 3. Show the full update flow (check -> dialog -> download -> install)
await showBuildHutUpdateCheck(
context: context,
updateService: updateService,
currentVersion: currentVersion,
);
That's it. showBuildHutUpdateCheck will:
- Show a "Checking for Updates" spinner dialog
- Query
GET /api/app/{appId}on the BuildHut API - If an update is found, show a dialog with the version number and markdown changelog
- When the user taps "Update Now", download the APK with a progress bar
- Request runtime permissions if not already granted
- Trigger the Android system installer
API Reference #
BuildHutUpdateService #
The core service that communicates with the BuildHut API.
final service = BuildHutUpdateService(
appId: '51b43a77-352d-4161-b454-554edeeeaf71',
currentVersion: '1.2.3+5',
baseUrl: 'https://buildhut.fly.dev/api/app', // optional, this is the default
dio: myCustomDioInstance, // optional
);
| Parameter | Type | Default | Description |
|---|---|---|---|
appId |
String |
required | BuildHut application UUID |
currentVersion |
String |
required | Current app version for comparison |
baseUrl |
String |
https://buildhut.fly.dev/api/app |
BuildHut API base URL |
dio |
Dio? |
null |
Custom Dio instance for HTTP configuration |
checkForUpdates()
Future<BuildHutAppUpdate?> checkForUpdates()
Queries the BuildHut API and returns a BuildHutAppUpdate if a newer version exists, or null if the app is up to date. Throws on network errors.
final update = await service.checkForUpdates();
if (update != null) {
print('New version available: ${update.version}');
} else {
print('Already up to date');
}
installUpdate()
Future<void> installUpdate(
BuildHutAppUpdate update, {
ProgressCallback? onProgress,
StatusCallback? onStatusChange,
String apkNamePrefix = 'buildhut_update',
})
Downloads the APK and triggers the Android system installer. The callbacks receive:
onProgress— download progress as adoublefrom0.0to1.0onStatusChange— human-readable status string (e.g."Downloading update...","Installing update...")apkNamePrefix— prefix for the temporary APK filename
await service.installUpdate(
update,
onProgress: (progress) {
print('Download: ${(progress * 100).toInt()}%');
},
onStatusChange: (status) {
print('Status: $status');
},
);
BuildHutAppUpdate #
Data model representing an available update from the BuildHut API.
| Field | Type | Description |
|---|---|---|
app |
String |
Application name |
version |
String |
Version string (e.g. "1.2.3") |
tag |
String |
Version tag |
description |
String? |
Markdown release notes |
tags |
List<String>? |
Build tags (e.g. ["stable"]) |
downloadUrl |
String |
Presigned S3 URL for the APK |
uploadedAt |
DateTime |
Upload timestamp |
architecture |
String? |
Target architecture (e.g. "arm64-v8a") |
isNewerThan()
bool isNewerThan(String currentVersion)
Compares against the current version. Supports semver (1.2.3), semver with build number (1.2.3+5), and partial semver (1.2).
final update = BuildHutAppUpdate.fromJson(json);
if (update.isNewerThan('1.0.0')) {
// offer update
}
BuildHutUpdateDialog #
A Material dialog widget that displays the update info, handles the download with a progress bar, and triggers installation.
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => BuildHutUpdateDialog(
update: update,
updateService: service,
currentVersion: '1.0.0',
onUpdateInstalled: () {
// Called after the install is triggered
},
),
);
showBuildHutUpdateCheck() #
Future<void> showBuildHutUpdateCheck({
required BuildContext context,
required BuildHutUpdateService updateService,
required String currentVersion,
VoidCallback? onUpdateInstalled,
})
All-in-one flow: shows a checking spinner, then either the update dialog, an "up to date" dialog, or an error dialog.
Usage Patterns #
Manual check with custom handling #
final update = await service.checkForUpdates();
if (update == null) {
// No update available
return;
}
// Show your own UI, log to analytics, etc.
print('Update ${update.version} available');
print('Download URL: ${update.downloadUrl}');
print('Changelog: ${update.description}');
Background update polling #
Timer.periodic(const Duration(hours: 6), (_) async {
try {
final update = await service.checkForUpdates();
if (update != null && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Update ${update.version} available')),
);
}
} catch (_) {
// Silently ignore network errors during background checks
}
});
Custom Dio instance #
Pass your own Dio with interceptors, timeouts, or custom headers:
final dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 60),
));
dio.interceptors.add(LogInterceptor());
final service = BuildHutUpdateService(
appId: 'your-app-id',
currentVersion: '1.0.0',
dio: dio,
);
Custom BuildHut endpoint #
If you are self-hosting BuildHut:
final service = BuildHutUpdateService(
appId: 'your-app-id',
currentVersion: '1.0.0',
baseUrl: 'https://your-buildhut-instance.example.com/api/app',
);
Install with progress only (no dialog) #
final update = await service.checkForUpdates();
if (update == null) return;
await service.installUpdate(
update,
onProgress: (progress) {
// Update your own progress indicator
setState(() => _progress = progress);
},
onStatusChange: (status) {
setState(() => _statusText = status);
},
apkNamePrefix: 'my_app', // temp file will be my_app_1.2.3.apk
);
BuildHut API Compatibility #
This library targets the BuildHut GET /api/app/{appId} endpoint which returns:
{
"app": "my-app",
"version": "1.0.5",
"architecture": "arm64-v8a",
"tag": "1.0.5",
"description": "Bug fixes and performance improvements",
"tags": ["stable"],
"downloadUrl": "https://s3.../presigned-url",
"uploadedAt": "2026-04-01T10:00:00.000Z"
}
The downloadUrl is a presigned S3 URL served by BuildHut. The APK is downloaded to the app's temporary directory and then passed to the Android package installer.
Complete Example #
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:buildhut_updater/buildhut_updater.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final BuildHutUpdateService _service;
String _version = '';
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
final info = await PackageInfo.fromPlatform();
setState(() => _version = '${info.version}+${info.buildNumber}');
_service = BuildHutUpdateService(
appId: '51b43a77-352d-4161-b454-554edeeeaf71',
currentVersion: _version,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My App')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Version: $_version'),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () => showBuildHutUpdateCheck(
context: context,
updateService: _service,
currentVersion: _version,
),
icon: const Icon(Icons.system_update),
label: const Text('Check for Updates'),
),
],
),
),
);
}
}
License #
MIT