Простое приложение Flutter с использованием remove bg apis

Посмотрите эту статью в моем блоге

Полный код этого поста можно посмотреть на github.

Для того чтобы познакомиться с flutter, я решил создать проект flutter 3, который позволял бы удалять фоновые изображения с фотографий.

Используя отличный апи remove bg api (ограничение на 50 вызовов апи в месяц для бесплатного уровня), я могу просто отправить изображение и затем показать его пользователю.

Из-за моего незнания экосистемы flutter я столкнулся с рядом проблем.

Первая связана с библиотекой http dio во flutter. Вызов remove.bg api возвращает файл в байтах, но если в http-запросе не указать байты, вы получаете строку, которую нельзя использовать ни для чего/.

    var formData = FormData();
    var dio = Dio();
    // flutter add api token
    // hardcoded free access token
    dio.options.headers["X-Api-Key"] = "<API_KEY>";
    try {
      if (kIsWeb) {
        var _bytes = await image.readAsBytes();
        formData.files.add(MapEntry(
          "image_file",
          MultipartFile.fromBytes(_bytes, filename: "pic-name.png"),
        ));
      } else {
        formData.files.add(MapEntry(
          "image_file",
          await MultipartFile.fromFile(image.path, filename: "pic-name.png"),
        ));
      }
      Response<List<int>> response = await dio.post(
          "https://api.remove.bg/v1.0/removebg",
          data: formData,
          options: Options(responseType: ResponseType.bytes));
      return response.data;
Вход в полноэкранный режим Выход из полноэкранного режима

remove.bg api ожидает form_data со свойством image data.

Для http-клиента dio

      Response<List<int>> response = await dio.post(
          "https://api.remove.bg/v1.0/removebg",
          data: formData,
          options: Options(responseType: ResponseType.bytes));
Войти в полноэкранный режим Выйти из полноэкранного режима

вы должны определить тип ответа для изображений, иначе вы получите запутанную строку в двоичной кодировке, с которой сложно взаимодействовать.

Это вызывает проблемы и сбои с загруженным изображением, очень трудно отлаживать, возможно, было бы проще, если бы вы знали больше о flutter и знали, как и где отслеживать ошибки.

Еще одна интересная особенность — логика загрузки. dart:io не поддерживается в вебе, в результате мне нужно было найти обходной путь для логики загрузки, в случае, если элемент якоря выдает ошибку для мобильного усложнения, мне нужно будет условно отрисовывать его в зависимости от ситуации или использовать динамический импорт только для веба.

              downloadButton = _html.AnchorElement(
                href:
                    "$header,$base64String")
              ..setAttribute("download", "file.png")
              ..click()
Вход в полноэкранный режим Выход из полноэкранного режима

В целом, адаптация кода flutter к веб является немного сложной задачей, но я уверен, что у меня есть прочный фундамент, на который можно опираться.

Более подробную информацию можно найти на сайте https://docs.flutter.dev/cookbook/plugins/picture-using-camera.

Для тех, кому интересно, я добавил действие github для автоматического развертывания на сайте github pages.

name: Flutter Web
on:
  push:
    branches:
      - main

jobs:
  build:
    name: Build Web
    env:
      my_secret: ${{secrets.commit_secret}}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.0.3'
      - run: flutter pub get
      - run: flutter build web --release
      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4
        with:
          branch: gh-pages # The branch the action should deploy to.
          folder: build/web # The folder the action should deploy.
Вход в полноэкранный режим Выйти из полноэкранного режима

Это действие соберет web для flutter 3.0.3 и затем развернет его в ветке, которая будет отображаться на страницах github.

Для того чтобы иметь реализации, специфичные для конкретной платформы, лучше всего использовать вложенные импорты

import 'package:rm_img_bg/download_button_main.dart'
if (dart.library.html) 'package:rm_img_bg/download_button_web.dart';
Вход в полноэкранный режим Выйти из полноэкранного режима

Убедитесь, что функции и классы определены одинаково

import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:html' as _html;
import 'dart:typed_data';


class DownloadButtonProps {
    List<int> imageInBytes;
    DownloadButtonProps({ required this.imageInBytes});
  }

class DownloadButton extends StatelessWidget {

  final DownloadButtonProps data;
  const DownloadButton({Key? key, required this.data}): super(key: key);
  @override
  Widget build(BuildContext context) {
    String base64String = base64Encode(Uint8List.fromList(data.imageInBytes));
    String header = "data:image/png;base64"; 
    return ElevatedButton(
      onPressed: () => {
        // saveFile(uploadedImage.toString())
          {
            _html.AnchorElement(
              href:
                  "$header,$base64String")
            ..setAttribute("download", "file.png")
            ..click()
          }
      },
      child: const Text("Save File"),
    );
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мобильная версия (необходимо сделать)

import 'package:flutter/material.dart';

class DownloadButtonProps {
    List<int> imageInBytes;
    DownloadButtonProps({ required this.imageInBytes});
}

class DownloadButton extends StatelessWidget {

  final DownloadButtonProps data;
  const DownloadButton({Key? key, required this.data}): super(key: key);
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => {
        // saveFile(uploadedImage.toString())
          {
            print("DO SOMETHING HERE")
          }
      },
      child: const Text("Save File"),
    );
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Я решил добавить поддержку мобильных устройств для проекта flutter.

  if (kIsWeb) {
      return Scaffold(
          appBar: AppBar(title: const Text('Display the Picture')),
          // The image is stored as a file on the device. Use the `Image.file`
          // constructor with the given path to display the image.
          body: Container(
              child: Row(children: [
            Column(children: [
              Text("Original Image"),
              image,
            ]),
            Column(children: [
              Text("Background Removed Image"),
              otherImage,
              downloadButton,
            ]),
          ])));
    }

    // add bigger font and padding on the item.
    // extra padding on the save file item
    return Scaffold(
        appBar: AppBar(title: const Text('Display the Picture')),
        // The image is stored as a file on the device. Use the `Image.file`
        // constructor with the given path to display the image.
        body: SingleChildScrollView(
            child: Column(children: [
              // Original Image with 16 font and padding of 16
          Text("Original Image", style: const TextStyle(fontSize: 16)),
          Padding(padding: EdgeInsets.symmetric(vertical: 4)),
          image,
          Text("Background Removed Image", style: const TextStyle(fontSize: 16)),
          Padding(padding: EdgeInsets.symmetric(vertical: 4)),
          otherImage,
          Padding(padding: EdgeInsets.symmetric(vertical: 4)),
          downloadButton,
        ])));
Вход в полноэкранный режим Выход из полноэкранного режима

Зачастую это связано с условным рендерингом в зависимости от платформы. По сути, для настольного компьютера изображения должны располагаться рядом друг с другом, а для мобильного пользователь должен прокрутить страницу вниз, чтобы увидеть оригинал и изображение, удаленное с фона.

Я думаю, что в большинстве случаев лучше использовать сайт remove.bg, но интересно, чтобы приложение делало это на ходу.

Будущие улучшения могут включать

  • добавление кнопки к изображению для его увеличения.
  • изменить логику сохранения, чтобы пользователь мог выбрать имя файла.

Поскольку flutter поддерживает несколько платформ, некоторые функции не полностью поддерживаются кросс-платформенно.

import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

class DownloadButtonProps {
    List<int> imageInBytes;
    DownloadButtonProps({ required this.imageInBytes});
}

class DownloadButton extends StatelessWidget {

  final DownloadButtonProps data;
  const DownloadButton({Key? key, required this.data}): super(key: key);

  Future<String> getFilePath() async {
    Directory? appDocumentsDirectory; 
    try {
      appDocumentsDirectory ??= await getExternalStorageDirectory();
    } catch (e) {
      print(e);
    }
    print(appDocumentsDirectory);
    appDocumentsDirectory ??= await getApplicationDocumentsDirectory();
    String appDocumentsPath = appDocumentsDirectory.path;
    // random file name to avoid overwriting existing files.
    String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
    String filePath = '$appDocumentsPath/$fileName';
    print(filePath);
    return filePath;
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        // saveFile(uploadedImage.toString())
        {
          File file = File(await getFilePath());
          await file.writeAsBytes(data.imageInBytes);
        }
      },
      child: const Text("Save File"),
    );
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Я также обновил плавающую кнопку действия, чтобы она возвращалась, если remove.bg api возвращает строку (скорее всего, сообщение об ошибке).


 floatingActionButton: FloatingActionButton(
    // Provide an onPressed callback.
    onPressed: () async {
        // Take the Picture in a try / catch block. If anything goes wrong,
        // catch the error.
        try {
        // Ensure that the camera is initialized.
        await _initializeControllerFuture;

        // Attempt to take a picture and get the file `image`
        // where it was saved.
        final image = await _controller.takePicture();

        final uploadedImageResp = await uploadImage(image);
        // If the picture was taken, display it on a new screen.
        if (uploadedImageResp.runtimeType == String) {
            errorMessage = "Failed to upload image";
            return;
        }
        // if response is type string, then its an error and show, set message
        await Navigator.of(context).push(
            MaterialPageRoute(
            builder: (context) => DisplayPictureScreen(
                // Pass the automatically generated path to
                // the DisplayPictureScreen widget.
                imagePath: image.path,
                uploadedImage: uploadedImageResp),
            ),
        );
        } catch (e) {
        // If an error occurs, log the error to the console.
        print(e);
        }
    },
    child: const Icon(Icons.camera_alt),
Вход в полноэкранный режим Выход из полноэкранного режима

В следующей статье я расскажу о настройке fastlane для развертывания приложения в магазине google play.

Следующая статья будет доступна в блоге, подпишитесь на мою rss-ленту, чтобы узнать, когда появится следующая статья.

Оцените статью
devanswers.ru
Добавить комментарий