Building an in-app updater (OTA) in Flutter
Tags: updater, flutter, android, apps
Published: 2022-08-25 22:18:35

---
title: Building an in-app updater (OTA) in Flutter
published: true
date: 2019-02-25 22:10:28 UTC
tags: updater,flutter,android,apps
canonical_url: https://blog.samjakob.com/building-an-in-app-updater-ota-in-flutter-b0becddee8ca
cover_image: https://blog.samjakob.com/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1628740466771%2F9imC_jzXj.png%3Fw%3D1600%26h%3D840%26fit%3Dcrop%26crop%3Dentropy%26auto%3Dcompress%2Cformat%26format%3Dwebp&w=3840&q=75
---

This article is [imported from my blog](https://blog.samjakob.com/building-an-in-app-updater-ota-in-flutter-b0becddee8ca). Please let me know if there are any issues (broken links or grammatical errors due to missing characters) by commenting on this article.

***

In many ways, Flutter is a fantastic framework for building cross-platform mobile applications however when it comes to developing features that arent platform-agnostic many people seem to resort to platform channel code.

![Example Screenshot of use in application](https://cdn.hashnode.com/res/hashnode/image/upload/v1628740465239/5xb-dn8K4.png)

I try to keep as much code as possible in Dart for three reasons:

1. It maintains the portability of my code base; should I need to implement a feature in another platform, I will have little to no code to rewrite.

2. It reduces the learning curve for our projects; developers only have to know Dart and they dont have to locate and interpret platform channel code.

3. Keep-it-simple-stupid (KISS) methodology; when you start messing around with platform channels you then have to worry about communicating between the Dart code and the platform code. This can get out of hand really quickly when you throw asynchronous operations into the mix.

So, as were focused on keeping our code in Dart, theoretically our main obstacles are that we need to work with files, system permissions and then we need to launch an intent. File support in Dart is actually not a problem and system permissions can be overcome with a handy plugin, however we did have to resort to platform channels for the intent, but thats about 10 lines of simple synchronous code.

### Step 1: System Permissions

Thanks to a Flutter plugin called [`simple_permissions`](https://pub.dartlang.org/packages/simple\_permissions) this wasn't much of an issue.

```dart
import 'package:simple_permissions/simple_permissions.dart';

// ...

bool permissionStatus = await SimplePermissions.checkPermission(Permission.WriteExternalStorage);
if(!permissionStatus) permissionStatus = (await SimplePermissions.requestPermission(Permission.WriteExternalStorage)) == PermissionStatus.authorized;

```

Remember to add the `uses-permission` tag to your `AndroidManifest.xml`

```xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```

### Step 2: Filesystem

Whilst theoretically a challenge because of the platform-agnostic nature of Flutter, between the built in `dart:io` library and the `path-provider` plugin, Flutter actually provides an excellent API for manipulating files.

```dart
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http;

// Don't forget to check that you have Filesystem permissions or this will fail!
class FileIO {

  static const downloadDirReference = "/.apollo";

  ///
  /// Download and install the app at [url]. 
  /// You should call this method *after* querying your server for any available updates
  /// and getting the download link for the update.
  ///
  static Future<void> runInstallProcedure(String url) async {

    / **************************************** /
    /* Setup and clean directories */
    / **************************************** /

    // Instantiate a directory object
    final downloadDir = new Directory(
      (await getExternalStorageDirectory()).path + downloadDirReference
    );

    // Create the directory if it doesn't already exist.
    if(!await downloadDir.exists()) await downloadDir.create();

    // Instantiate a file object (in this case update.apk within our download folder)
    final downloadFile = new File("${downloadDir.path}/update.apk");

    // Delete the file if it already exists.
    if(await downloadFile.exists()) await downloadFile.delete();

    / **************************************** /
    /* Download the APK */
    / **************************************** /

    // Instantiate an HTTP client
    http.Client client = new http.Client();

    // Make a request and get the response bytes.
    var req = await client.get(url); // (The link to your APK goes here)
    var bytes = req.bodyBytes;

    // Write the response bytes to our download file.
    await downloadFile.writeAsBytes(bytes);

    // TODO: Trigger intent.

  }

}

```

The key thing you probably noticed is that in Dart, you use the`Directory` class to refer to a directory, and the `File` class to refer to a file; in my opinion, this is much more logical and aptly-named than it is in Java.

Everything is pretty self-explanatory and downloading files is an absolute breeze with Darts built in libraries.

### NOTE: Android N support

_Whilst nothing to do with Flutter, I've included this as it did take a bit of digging for me to get set up._

```xml
<manifest >
  <application>

    <!-- ... -->

    <provider
      android:name="xyz.apollotv.kamino.OTAFileProvider"
      android:authorities="xyz.apollotv.kamino.provider"
      android:exported="false"
      android:grantUriPermissions="true">

      <!-- The @xml/filepaths file (see below) is located at /android/app/src/main/res/xml/filepaths.xml
            relative to the Flutter project root. -->

      <meta-data
          android:name="android.support.FILE_PROVIDER_PATHS"
          android:resource="@xml/filepaths" />
    </provider>

  </application>
</manifest>
```

```java
package xyz.apollotv.kamino;

import android.support.v4.content.FileProvider;

// You need to reference this FileProvider in your AndroidManifest.xml
public class OTAFileProvider extends FileProvider {}
```

```xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- In our example, the APK is downloaded to the /storage/emulated/0/.apollo/ folder. -->
    <external-path name=".apollo" path=".apollo/"/>

</paths>

```

Inside your `application` tag in your Android manifest file, you should include a `provider` tag that references your File Provider class. Inside this tag, you should have a `meta-data` tag that lists all of the file paths the provider is allowed to access. (See `filepaths.xml`).

For more information about the FileProvider, see [https://developer.android.com/reference/android/support/v4/content/FileProvider](https://developer.android.com/reference/android/support/v4/content/FileProvider)

### Step 3: Platform Channel

The final step, is to launch an `ACTION_INSTALL_PACKAGE` intent. You should begin by setting up a basic platform channel.

```dart
OTAHelper.installOTA(downloadFile.path);


class OTAHelper {

  // Replace xyz.apollotv.kamino with your package.
  static const platform = const MethodChannel('xyz.apollotv.kamino/ota');

  static Future<void> installOTA(String path) async {
    try {
      await platform.invokeMethod('install', <String, dynamic>{
        "path": path
      });
    } on PlatformException catch (e) {
      print("Error installing update: $e");
    }
  }

}

```

Finally, edit your `MainActivity.java` file to declare the `MethodChannel` and execute the code to call our intent.

There arent any particularly advanced concepts here, as weve downloaded the file to external memory, so all we need to do is access it and trigger an installation.

```java
public class MainActivity extends FlutterActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {

    // ...

    new MethodChannel(getFlutterView(), "xyz.apollotv.kamino/ota").setMethodCallHandler((methodCall, result) -> {
        if(methodCall.method.equals("install")){
            if(installOTA(methodCall.argument("path"))){
                result.success(true);
            }else{
                result.error("ERROR", "An error occurred whilst installing OTA updates.", null);
            }
            return;
        }
        result.notImplemented();
    });

  }

  private boolean installOTA(String path){
      try {
          Uri fileUri = Uri.parse("file://" + path);

          Intent intent;
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

              // This line is important: after Android N, an authority must be provided to access files for an app.
              Uri apkUri = OTAFileProvider.getUriForFile(getApplicationContext(), "xyz.apollotv.kamino.provider", new File(path));

              intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
              intent.setData(apkUri);
              intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
          } else {
              intent = new Intent(Intent.ACTION_VIEW);
              intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
              intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
          }

          getApplicationContext().startActivity(intent);
          return true;
      }catch(Exception ex){
          System.out.println("[Platform] Error during OTA installation.");
          System.out.println(ex.getMessage());
          return false;
      }
  }

}

```

And with that, the OTA installation is started!