Overview
In-App Updates asks Android players to update your game without leaving it, using Google's
official Play In-App Updates plugin. Drop
the InAppUpdater prefab into your first scene: on launch it asks Google Play for an update, picks
Flexible (a background download while the player keeps playing) or Immediate (a full-screen,
blocking update) with an update policy, and raises UnityEvents for your UI.
Prefer code? The static InAppUpdate API gives you the same flows with C# events, all on Unity's main
thread. In the Editor a simulator plays Google Play's side, so every flow, including declines, failures and resuming
after a restart, can be tested in Play Mode without uploading a build.
Drop in, done
One prefab checks on launch and on resume, and starts the right Play flow.
A smart policy
Urgent releases go Immediate by priority or staleness; everything else stays Flexible.
Test in the Editor
A simulator answers like Google Play: dialogs, progress, declines, failures, resume.
Or script it
A small static API with C# events, on the main thread, safe on every platform.
Requirements
| Item | Version | Notes |
|---|---|---|
| Unity | 2022.3+ | With Android Build Support for device builds |
| External Dependency Manager | com.unity.external-dependency-manager 2.0+ | Resolves Google's com.google.android.play:app-update 2.1.0 for Android builds (see Android dependencies). Google's EDM4U works too. The simulator works without it. |
| Android device | Android 5.0+ with the Play Store | Google Play only offers in-app updates to an app installed from Google Play. |
com.unity.ugui | Any | The demo's UI only |
Quick start
- Import the package Everything lands in
Assets/HardArtcore/In-App Updates. - Add the External Dependency Manager Select
Prefabs/InAppUpdater.prefaband click Install External Dependency Manager in the Inspector, if it shows (details in Android dependencies). - Add the prefab Drag
InAppUpdater.prefabinto your first scene. On launch it checks for an update and starts the flow the policy picks. - Ask to restart Hook On Ready To Install to a "Restart to update?" panel whose button calls
InAppUpdater.InstallUpdate(). Or tick Auto Install to restart straight away. - Press Play The simulator plays the whole flow in the Editor. Change its answers under Simulator on the component.
A Flexible update is downloaded in the background, but installing it restarts the app. Google recommends asking the player first, so Auto Install is off by default.
The demo
Open Demo/DemoScene and press Play. The status panel shows the check result, the download progress and a
restart banner; the event log lists every event as it arrives. The Update tiles call
Check(), StartFlexibleUpdate() and StartImmediateUpdate(); the Simulator
tiles change how the simulated player answers, the release priority, and whether the next download fails. The demo
works in portrait and landscape.
Demo/DemoScene/Scripts/DemoUI.cs shows both APIs: the restart banner listens to the component's
On Ready To Install, and the panel and log listen to the static InAppUpdate events.
Android dependencies
Device builds call Google's official Play In-App Updates library (com.google.android.play:app-update)
directly. The package declares it in Editor/InAppUpdatesDependencies.xml, and the
External Dependency Manager
(EDM) adds it to your Android build, together with the libraries it needs.
<dependencies>
<androidPackages>
<androidPackage spec="com.google.android.play:app-update:2.1.0">
<repositories>
<repository>https://maven.google.com/</repository>
</repositories>
</androidPackage>
</androidPackages>
</dependencies>
- Add the External Dependency Manager Select any
InAppUpdaterand click Install External Dependency Manager in the Inspector, or addcom.unity.external-dependency-managerfrom Window > Package Manager > Unity Registry. The button only shows when no dependency manager is in the project. - Switch to Android The resolver runs on its own when dependencies are missing. To run it by hand: Assets > External Dependency Manager > Android Resolver > Resolve (or Force Resolve).
- Accept its prompts If EDM offers to patch
mainTemplate.gradleor enable custom Gradle templates, accept.
- Already using Google's External Dependency Manager for Unity (EDM4U), as a package or a
.unitypackage? It reads the same file; keep it, and keep only one dependency manager in the project. - With Minify on, the package adds keep rules for the Play classes to the generated Gradle project, so R8 doesn't strip them.
- Other plugins that use Google's Play libraries (Play Review, Play Asset Delivery, ...) resolve together with this one, so versions don't clash.
The InAppUpdater component
The drop-in way: it checks, decides and starts the flow, and turns everything into UnityEvents.
- Check On Start
- Ask Google Play when the component starts. On by default.
- Check On Resume
- Ask again when the app returns to the foreground. Finds Flexible downloads that finished while the game was in the background. On by default.
- Auto Start
- Start the Play flow automatically when an update is found, once per session (reloading the scene doesn't show the dialog again). On by default.
- Default Mode
- The mode used unless the update is urgent: Flexible (default) or Immediate.
- Immediate From Priority
- Treat the update as urgent when its priority is at least this (0-5). 6 = never. Default 4.
- Immediate After Days
- Treat the update as urgent when Google Play has known about it for at least this many days. 0 = never (default).
- Flexible Reprompt Days
- After the player declines a Flexible update, don't offer it automatically for this many days. Default 3.
- Auto Install
- Install a downloaded Flexible update right away, which restarts the app. Off = raise On Ready To Install so your UI can ask first.
- Simulator
- The Editor's scenario, see Editor simulator.
Events and methods
- On Update Available (int)
- An update is available. Argument: its version code.
- On No Update
- Google Play has no newer version.
- On Download Progress (float)
- Download progress from 0 to 1, for a progress bar.
- On Ready To Install
- A Flexible update is downloaded. Show a prompt and call
InstallUpdate(). - On Declined
- The player dismissed the Play update dialog.
- On Failed (string)
- Something went wrong. Argument: the
UpdateErrorname. - Check(), StartFlexibleUpdate(), StartImmediateUpdate(), InstallUpdate()
- For UI buttons.
- StartUpdateByPolicy()
- Starts the update in the mode the policy picks for the last check. Returns false if nothing was started.
The update policy
When an update is found, InAppUpdater picks the mode like this:
- Urgent? The update is urgent when its priority is at least Immediate From Priority, or when it is at least Immediate After Days old.
- Immediate If the update is urgent, or Default Mode is Immediate, and Google Play allows Immediate for it.
- Flexible Otherwise, if Google Play allows Flexible and the player didn't decline it in the last Flexible Reprompt Days.
- Nothing If neither applies. The policy never turns a non-urgent update into a blocking one.
Priority (0-5) is set per release through the Google Play Developer API (inAppUpdatePriority on an edit's track release). Play Console has no field for it.
Scripting API
Skip the component entirely if you prefer code. Every event is raised on Unity's main thread.
using Hardartcore.InAppUpdates;
using UnityEngine;
using UnityEngine.UI;
public class UpdateFlow : MonoBehaviour
{
public Slider progressBar;
public GameObject restartPanel; // its button calls InAppUpdate.CompleteUpdate()
void OnEnable()
{
InAppUpdate.CheckCompleted += OnCheckCompleted;
InAppUpdate.StateChanged += OnStateChanged;
InAppUpdate.Declined += mode => Debug.Log("The player said no to " + mode);
InAppUpdate.Failed += error => Debug.LogWarning(error);
}
void OnDisable()
{
InAppUpdate.CheckCompleted -= OnCheckCompleted;
InAppUpdate.StateChanged -= OnStateChanged;
}
void Start() => InAppUpdate.Check();
void OnCheckCompleted(UpdateInfo info)
{
if (info.IsUpdateAvailable && info.FlexibleAllowed)
InAppUpdate.StartUpdate(UpdateMode.Flexible);
}
void OnStateChanged(InstallState state)
{
progressBar.value = state.Progress;
if (state.Status == InstallStatus.Downloaded) restartPanel.SetActive(true);
}
}
InAppUpdate
- Check()
- Asks Google Play whether an update is available. The result arrives through
CheckCompleted; a check already running is not repeated. - StartUpdate(mode)
- Starts the Play update flow in
UpdateMode.FlexibleorUpdateMode.Immediate. Returns false if a flow is already running or the platform isn't supported. - CompleteUpdate()
- Installs a downloaded Flexible update. On a device Google Play restarts the app.
- event CheckCompleted (UpdateInfo)
- The result of a check.
- event StateChanged (InstallState)
- Progress and status of the running update.
- event Declined (UpdateMode)
- The player dismissed the Play update dialog.
- event Failed (UpdateError)
- A check, a flow or an install failed.
- LastInfo, State
- The last check result and the last install state.
- CurrentMode
- The mode of the running or most recent flow, kept across app restarts.
- IsChecking, IsFlowActive
- Whether a check or a flow is running.
- IsSupported, IsSimulated
- Whether this build has a backend (Google Play or the simulator), and whether it is the simulator.
- Simulator
- The
SimulatorSettingsthe simulator plays back.InAppUpdaterassigns its own.
UpdateInfo and InstallState
- UpdateInfo.Availability
Unknown,NotAvailable,Available, orInProgress(a flow started earlier is still running).IsUpdateAvailableis true forAvailable.- UpdateInfo.AvailableVersionCode
- The version code of the update.
- UpdateInfo.Priority
- 0-5, from the Play Developer API.
- UpdateInfo.StalenessDays
- Days since Google Play learned about the update, or null if unknown.
- UpdateInfo.FlexibleAllowed, ImmediateAllowed, IsAllowed(mode)
- Which modes Google Play allows for this update.
- UpdateInfo.InstallStatus, BytesDownloaded, TotalBytesToDownload
- The state of a download in progress.
- InstallState.Status
Pending,Downloading,Downloaded,Installing,Installed,FailedorCanceled.- InstallState.Progress
- 0 to 1. Google Play sometimes reports a total of 0, so it falls back to the status.
- InstallState.BytesDownloaded, TotalBytesToDownload
- Byte counts of the download.
UpdateError
The values cover Google's InstallErrorCode: ApiNotAvailable, InvalidRequest,
UpdateUnavailable, UpdateNotAllowed, DownloadNotPresent,
UpdateInProgress, InternalError, UpdateFailed, PlayStoreNotFound,
AppNotOwned and more. UserCanceled is reported through Declined, not
Failed. NotSupported is the package's own: the platform or build has no backend.
On iOS and other platforms every call raises Failed with UpdateError.NotSupported and does nothing else, so the same code ships everywhere.
Editor simulator
In the Editor every call goes to a simulator that behaves like Google Play. Configure it under Simulator on
the InAppUpdater; changes apply live in Play Mode. While playing, the Inspector also shows the live
state (backend, last check, install state, running flow) and buttons for Check, Flexible,
Immediate and Install.
- Availability, Available Version Code
- What a check reports. In Progress tests resuming an update after an app restart.
- Priority, Staleness Days
- The release's priority (0-5) and age in days (-1 = unknown), for testing the policy.
- Flexible Allowed, Immediate Allowed
- Which modes Google Play allows.
- User Choice
- How the simulated player answers the update dialog: Ask shows a stand-in dialog in the Game view; Accept or Decline answer by themselves.
- Check Error
- An error every check returns. None = success.
- Fail Download
- Fails the download half way.
- Network Delay, Download Seconds, Download Size Mb
- Timing and size of the simulated check and download.
- Use In Development Builds
- Runs the simulator on a device too, in Development builds only: handy for testing your UI without a Play release. Release builds always use Google Play.
Testing on a device
Real updates only appear for an app installed from Google Play.
- Install from Play Upload a build to an internal testing track and install it from Google Play on the device, as a tester.
- Publish a newer build Upload a second build with a higher version code.
- Let Play notice Open the Play Store on the device so it sees the new version, then launch your installed build.
Internal app sharing is faster: install one version through a sharing link, then open the link of a higher version code without installing it. Google Play caches what it knows, so clearing the Play Store's storage can help.
How it works
The package handles the parts of Google's API that are easy to get wrong:
- Resuming. If the app was closed during an update, the next check reports
InProgressand the flow is resumed in the same mode, as Google requires. - Background downloads. A Flexible update that finished while the app was closed or in the background is found by the next check and reported as
Downloaded. - Single-use info. Google lets each update info start one flow only; the package fetches fresh info when needed.
- One flow at a time.
StartUpdaterefuses a second flow while one runs. - Not nagging. After a Flexible decline,
InAppUpdaterwaits Flexible Reprompt Days before offering it again. - Domain reload off. Static state is reset on entering Play Mode, so Enter Play Mode Options works.
Assemblies
- Hardartcore.InAppUpdates
- The runtime:
InAppUpdate,InAppUpdater, the types, the Google Play backend (Google's Java API through JNI) and the simulator. No references to other assemblies and no scripting defines. - Hardartcore.InAppUpdates.Editor
- The Inspector (live state, test buttons, the EDM installer),
InAppUpdatesDependencies.xml, and the R8 keep rules for Android builds. - Hardartcore.InAppUpdates.Tests
- PlayMode tests of the update policy and the simulator flows.
Folder layout
Assets/HardArtcore/In-App Updates/ ├── Documentation.html this documentation ├── Runtime/ InAppUpdate, InAppUpdater, types, Play backend, simulator ├── Editor/ Inspector and the Google plugin installer ├── Prefabs/InAppUpdater.prefab the drop-in component ├── Tests/ policy and simulator tests └── Demo/ DemoScene, its scripts, Nunito font and icons
Upgrading from 1.x
Version 2 replaces 1.x (UpdateManager.AndroidUpdateManager, AutoUpdater) entirely.
- Delete the old files Importing doesn't delete them. Under
Assets/hardartcore/In-App Updates/, deletePlugins/(the oldupdate-release.aarbreaks the Android build against the new Play libraries),Scripts/AutoUpdater.csandPrefabs/AutoUpdateManager.prefab. - Clean the manifest Remove the old Game Package Registry entry from
Packages/manifest.json(Google shut that registry down), then add the External Dependency Manager. Google'scom.google.play.*Unity packages are no longer needed: remove them unless other plugins use them. - Review the components Existing
AndroidUpdateManagercomponents becomeInAppUpdaterautomatically (same script GUID). AnAutoUpdaterbecomes anInAppUpdaterwith Auto Start and Auto Install ticked.
| 1.x | 2.0 |
|---|---|
OnUpdateAvailable | InAppUpdate.CheckCompleted |
OnUpdateDownloading, OnStateUpdate, OnUpdateDownloaded | InAppUpdate.StateChanged |
OnErrorReceived | InAppUpdate.Failed |
CheckForAnUpdate() | InAppUpdate.Check() |
StartUpdate() | InAppUpdate.StartUpdate(mode) |
CompleteUpdate() | InAppUpdate.CompleteUpdate() |
Troubleshooting
- No update is ever found on the device
- The app must be installed from Google Play, and Google Play must know about a higher version code. See Testing on a device; clearing the Play Store's storage refreshes its cache.
- "com.google.android.play:app-update is missing from the build" on Android
- The Android dependencies weren't resolved: Assets > External Dependency Manager > Android Resolver > Force Resolve, then build again (see Android dependencies). With Minify on and your own Gradle setup, check that the keep rules reached the build.
- Failed: AppNotOwned or PlayStoreNotFound
- The build was sideloaded, or the device has no Play Store. Install from a Play track or internal app sharing.
- Failed: ApiNotAvailable
- The Play Store on the device is too old, or the device doesn't support in-app updates.
- Duplicate classes when building
- Left-over 1.x files: delete
Plugins/update-release.aar(see Upgrading from 1.x), then resolve with External Dependency Manager. - The dialog shows only once
- By design: Auto Start offers an update once per session, and a declined Flexible update waits Flexible Reprompt Days. Call
StartFlexibleUpdate()from a button to offer it again.
For device issues, attach Logcat output filtered by Unity and PlayCore when you write to support.
Third-party notices
The demo includes two third-party components. Google's Play In-App Updates library is not included: the External Dependency Manager downloads it from Google's Maven repository, under Google's own terms. Everything else in the package is original work by HardArtcore.
| Component | Location | License |
|---|---|---|
| Nunito font Copyright 2014 The Nunito Project Authors (github.com/googlefonts/nunito) | Demo/UI/Fonts/ | SIL Open Font License 1.1 |
| Material Symbols icons Copyright Google LLC (github.com/google/material-design-icons) | Demo/UI/Icons/ | Apache License 2.0 |
The full licence texts ship in the package's Documentation.html, and online: SIL Open Font License 1.1, Apache License 2.0.
Release notes
2.0.0
- Rewritten on Google's Play In-App Updates library (
com.google.android.play:app-update2.1.0), declared for the External Dependency Manager: no Google Unity packages to install. InAppUpdatercomponent with an update policy: Immediate by priority or staleness, Flexible otherwise, no re-prompting for a few days after a decline.- Static
InAppUpdateAPI with C# events on the main thread; a safe no-op on iOS and other platforms. - Editor simulator with a stand-in Play dialog, live Inspector state and test buttons, and an option for Development builds.
- Resumes interrupted updates and finds downloads that finished in the background.
- New demo scene, assembly definitions and automated tests.
1.x
AndroidUpdateManagerandAutoUpdateron the Play Core library.
Need help?
Questions, bug reports or feature requests: I usually reply within a day.
Last updated November 9, 2026.