Flutter에서 인앱 업데이터(OTA) 빌드

이 글은 imported from my blog 입니다. 문제가 있는 경우(문자 누락으로 인한 끊어진 링크 또는 문법 오류) 이 문서에 댓글로 알려주세요.


여러 면에서 Flutter는 크로스 플랫폼 모바일 애플리케이션을 구축하기 위한 환상적인 프레임워크이지만 플랫폼에 구애받지 않는 기능을 개발할 때 많은 사람들이 플랫폼 채널 코드에 의존하는 것 같습니다.



다음 세 가지 이유로 Dart에서 가능한 한 많은 코드를 유지하려고 합니다.
  • 내 코드 베이스의 이식성을 유지합니다. 다른 플랫폼에서 기능을 구현해야 하는 경우 다시 작성할 코드가 거의 또는 전혀 없습니다.
  • 프로젝트의 학습 곡선을 줄여줍니다. 개발자는 Dart만 알면 되고 플랫폼 채널 코드를 찾아 해석할 필요가 없습니다.
  • KISS(Keep-it-simple-stupid) 방법론; 플랫폼 채널을 만지작거리기 시작하면 Dart 코드와 플랫폼 코드 간의 통신에 대해 걱정해야 합니다. 비동기 작업을 믹스에 던질 때 이것은 정말 빨리 손에서 벗어날 수 있습니다.

  • 따라서 코드를 Dart에 유지하는 데 중점을 둔 것처럼 이론적으로 우리의 주요 장애물은 파일, 시스템 권한으로 작업한 다음 인텐트를 시작해야 한다는 것입니다. Dart의 파일 지원은 실제로 문제가 아니며 편리한 플러그인으로 시스템 권한을 극복할 수 있지만 의도를 위해 플랫폼 채널에 의존해야 했지만 약 10줄의 간단한 동기 코드입니다.

    1단계: 시스템 권한



    simple_permissions 라는 Flutter 플러그인 덕분에 이것은 큰 문제가 되지 않았습니다.

    import 'package:simple_permissions/simple_permissions.dart';
    
    // ...
    
    bool permissionStatus = await SimplePermissions.checkPermission(Permission.WriteExternalStorage);
    if(!permissionStatus) permissionStatus = (await SimplePermissions.requestPermission(Permission.WriteExternalStorage)) == PermissionStatus.authorized;
    
    

    uses-permission 태그를 AndroidManifest.xml 태그에 추가해야 합니다.

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


    2단계: 파일 시스템



    내장된 dart:io 라이브러리와 path-provider 플러그인 사이에서 Flutter의 플랫폼에 구애받지 않는 특성으로 인해 이론적으로는 문제가 있지만 Flutter는 실제로 파일 조작을 위한 뛰어난 API를 제공합니다.

    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.
    
      }
    
    }
    
    


    Dart에서는 디렉토리를 참조하기 위해 Directory 클래스를 사용하고 파일을 참조하기 위해 File 클래스를 사용합니다. 제 생각에는 이것은 Java보다 훨씬 더 논리적이고 적절한 이름입니다.

    모든 것이 매우 자명하며 라이브러리에 내장된 Darts를 사용하면 파일을 다운로드하는 것이 매우 간편합니다.

    참고: Android N 지원



    Flutter와 관련이 없지만 설정하는 데 약간의 파고가 필요했기 때문에 이것을 포함했습니다.

    <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>
    



    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 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>
    
    


    Android 매니페스트 파일의 application 태그 안에 파일 공급자 클래스를 참조하는 provider 태그를 포함해야 합니다. 이 태그 안에 공급자가 액세스할 수 있는 모든 파일 경로를 나열하는 meta-data 태그가 있어야 합니다. (filepaths.xml 참조).

    FileProvider에 대한 자세한 내용은 https://developer.android.com/reference/android/support/v4/content/FileProvider을 참조하십시오.

    3단계: 플랫폼 채널



    마지막 단계는 ACTION_INSTALL_PACKAGE 인텐트를 실행하는 것입니다. 기본 플랫폼 채널을 설정하여 시작해야 합니다.

    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");
        }
      }
    
    }
    
    


    마지막으로 MainActivity.java 파일을 편집하여 MethodChannel를 선언하고 코드를 실행하여 인텐트를 호출합니다.

    파일을 외부 메모리에 다운로드했기 때문에 여기에는 특별히 고급 개념이 없으므로 액세스하고 설치를 트리거하기만 하면 됩니다.

    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;
          }
      }
    
    }
    
    


    그러면 OTA 설치가 시작됩니다!

    좋은 웹페이지 즐겨찾기