Flutter: генерация apk (Android)

Для того чтобы попробовать получившееся приложение на «реальном» телефоне, а не на эмуляторе, необходимо проделать ряд манипуляций.

Вариант 1: создание не подписанного apk. Не пригодно для размещения в Google Play, но пригодно для того чтобу устанавливать на телефон для «потестить». Этот способ подойдет если нужно «по быстрому» или не планируется распространять приложение через Google Play

Для этого достаточно выполнить в меню: Build -> Flutter -> Build APK. Спустя несколько минут компиляции и сборки, результат можно выловить в папке C:\Users\<пользователь>\AndroidStudioProjects\invent\build\app\outputs\flutter-apk

Вариант 2: создание подписанного apk и публикация в Google Play занимает несколько шагов

  • Добавление иконки приложения (launchpad icon)
  • Настройка подписи приложения
  • Проверка манифеста приложения (AndroidManifest.xml)
  • Проверка конфигурации сборки (build.gradle)
  • Создание версии приложения для публикации (—release)
  • Публиковать в Google Play Store

Добавление иконки приложения

По умолчанию приложение собирается с иконкой Flutter. Чтобы её поменять — нужно заместить эти иконки своими в папке <app dir>/android/app/src/main/res/

Настройка подписи приложения

Для публикации приложения в Play Store необходимо подписать приложение цифровой подписью. Создание ключа:

keytool -genkey -v -keystore ~/.keystore \
	-keyalg RSA \
    -keysize 2048 \
    -validity 10000 \
    -alias key

Далее необходимо связать приложение с ключём. Для этого нужно создать файл <app dir>/android/key.properties

storePassword=<пароль>
keyPassword=<пароль>
keyAlias=key
storeFile=<путь к файлу ключа например /home/user/.keystore>

Сконфигурируем Gradle <app dir>/android/app/build.gradle

  1. заменить следующее
android {

на информацию о файле конфигурации

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
	keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {

2. Заменить конфигурацию сборки релиза

buildTypes {
	release {
    	// TODO: Add your own signing config for the release build.
        // Signing with debug keys for now,
        // so flutter run --release works
        signingConfig signingConfig.debug
    }
}

на следующую информацию, содержащую параметры подписи

signingConfigs {
	release {
    	keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile file(keystoreProperties['storeFile'])
        storePassword keystoreProperties['storePassword']
    }
}
buildTypes {
	release {
    	signingConfig signingConfigs.release
    }
}

Теперь сборка релиза будет подписываться автоматически.

Проверка манифеста приложения

Необходимо убедиться что в файле <app dir>/android/app/src/main/AndroidManifest.xml содержится верное наименование приложения, а также установлены верные uses-permissions

Проверим конфигурацию сборки

В <app dir>/android/appbuild.gradle необходимо выставить идентификатор приложения, имя и номер сборки.

Flutter: сканирование штрихкода

1. Добавим в pubspec.yaml зависимость

dependencies:
  flutter_barcode_scanner: ^2.0.0

Добавим в код предыдущего проекта:

class _Page1State extends State<Page1> { // _ впереди класса, означает чтоб скрыть доступ из других файлов
...
....
  String _scanBarcode = 'Unknown';
  Future<void> startBarcodeScanStream() async {
      FlutterBarcodeScanner.getBarcodeStreamReceiver(
          '#ff6666', 'Cancel', true, ScanMode.BARCODE)!
          .listen((barcode) => print(barcode));
  }
  Future<void> scanBar() async {
    String barcodeScanRes;
    try {
      barcodeScanRes = await FlutterBarcodeScanner.scanBarcode('#ff6666', 'Отменить', false, ScanMode.BARCODE);
      Fluttertoast.showToast(
          msg: "Прочитано:"+barcodeScanRes,
          toastLength: Toast.LENGTH_SHORT,
          gravity: ToastGravity.CENTER,
          timeInSecForIosWeb: 1,
          backgroundColor: Colors.red,
          textColor: Colors.white,
          fontSize: 16.0
      );
    } on PlatformException {
      barcodeScanRes = 'Ошибка получения версии платформы';
    }
    if (!mounted) return;
    setState(() {
      _scanBarcode = barcodeScanRes;
    });
  }
....
....
....
   ListTile(
                onTap: (){
                  print(scanBar());
                },
                leading: Icon(
                  Icons.sick,
                ),

Следует отметить, что при запуске кода на Android/IOS приложение самостоятельно попросит доступ к камере, а потому еще одной проблемой — обработкой запросов доступа меньше.

Flutter: использование пакетов

Не все йогурты одинаково полезны (с)

Иногда стандартных библиотек поставляемых с Fllutter не хватает, а самому что-то писать затруднительно — выходом может служить использование уже написанного и опакеченого кода. Все пакеты Flutter находятся на pub.dev, для добавления любого из пакетов с этого сайта в проект, достаточно добавить в pubspec.ymal проекта зависимость. Например:

dependencies:
  fluttertoast: ^8.0.9

После чего Android Studio предложит обновить зависимости. Далее добавим в main.dart импортируемую библиотеку:

import 'package:fluttertoast/fluttertoast.dart';

Далее непосредственное использование:

 ListTile(
                onTap: (){
                  Fluttertoast.showToast(
                      msg: "Здраствуйте я ваша дядя..",
                      toastLength: Toast.LENGTH_SHORT,
                      gravity: ToastGravity.CENTER,
                      timeInSecForIosWeb: 1,
                      backgroundColor: Colors.red,
                      textColor: Colors.white,
                      fontSize: 16.0
                  );
                },

Flutter: переход между экранами

Продолжаем работать с приложением из предыдущей статьи.

А что если логика работы приложения не ограничивается одним экраном? Создадим файл /lib/pages/about.dart

import 'package:flutter/material.dart';

class AboutMe extends StatefulWidget {
  _AboutMeState createState() => _AboutMeState(); // сюда передаем текущее состояние страницы
}
class _AboutMeState extends State<AboutMe> {// _ впереди класса, означает чтоб скрыть доступ из другх файлов
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('О нас'))
    );
  }
}

А в файл page1.dart добавим связку со страницей:

import 'package:invent/pages/about.dart';

И в меню строчку вызова новой страницы:

ListTile(
                onTap: (){
                  Navigator.push(context, MaterialPageRoute(builder: (context) => AboutMe()));
                },
                leading: Icon(Icons.account_balance_outlined,), title: Text('About'),
                tileColor: Color(0xFFF5F5F5),
                dense: false,
              ),

В результате получим переход на другую страницу:

Можно заметить, что срелка «назад» добавилась автоматичеки. Программно это вызов:

Navigator.pop(context);

Navigator – виджет-класс, позволяющий управлять стеком дочерних виджетов, т.е. открывать, закрывать и переключать окна или страницы. Когда мы используем MaterialApp, то экземпляр класса Navigator уже создан

Второй способ переключения между экранами

Существует второй штатный способ переключения между экранами. Особой разницы между ними нет, каждый использует по своему усмотрению. Flutter позволяет переключаться между экранами при помощи «маршрутов». Для этого в главном файле вместо вызова home: Page1() добавим маршруты:

import 'package:invent/pages/about.dart';
import 'pages/page1.dart';
...
 Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Инвентаризация ССК',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
        initialRoute: '/',
        routes: {
          '/':(BuildContext context) => Page1(),
          '/AboutMe':(BuildContext context) => AboutMe()
        }
    );
  }

И соответственно тогда вызов перехода будет теперь осуществлять вот так:

 ListTile(
                onTap: (){
                  Navigator.pushNamed(context, "/AboutMe");
                },
                leading: Icon(Icons.account_balance_outlined,), title: Text('About'),
                tileColor: Color(0xFFF5F5F5),
                dense: false,
              ),

Flutter: диалоговое окно

В предыдущей статье, мы нарисовали меню. Теперь нужно начинать реализовывать какую-то реакцию на действия в этом меню. Например показать диалоговое окно закрытия приложения. Это правило хорошего тона у приложений — спросить прежде чем окончательно выйти.

Во Flutter за показ диалогов отвечают две фунции, которые работают непосредственно друг с другом в связке:

AlertDialog — подготовка данных для отображения

showDialog — непосредственные вывод диалога пользователю

Оформим диалог в виде отдельно функции:

SureExitDialog(BuildContext context) {
    Widget cancelButton = TextButton(
      child: Text("Остаться"),
      onPressed:  () {Navigator.pop(context);},
    );
    Widget yesButton = TextButton(
      child: Text("Да"),
      onPressed:  () {
                if (Platform.isAndroid) {
                  SystemChannels.platform.invokeMethod(
                      'SystemNavigator.pop');
                } else {
                  exit(0);
                };
},
    );
    AlertDialog alert = AlertDialog(
      title: Text("Подтверждение"),
      content: Text("Вы действительно хотите выйти из приложения?"),
      actions: [
        cancelButton,
        yesButton,
      ],
    );
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return alert;
      },
    );
  }

Добавим вызов диалога из нажатия кнопки выхода:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      key: scaffoldKey,
      appBar: AppBar(
        backgroundColor: Colors.green,
        automaticallyImplyLeading: true,
        title: Text(
          'Инвентаризация'
        ),
        actions: [
          new IconButton(
              onPressed: () {
                SureExitDialog(context);
              },
              icon: new Icon(Icons.exit_to_app)
          ),
        ],
        centerTitle: true,
        elevation: 4,
      ),
...

Результат:

Для любителей диалоговых окон во весь экран (что актуально для мобильных, вот еще вариант:

showDialog(
      context: context,
      builder: (BuildContext context) {
        return Align (
          alignment: Alignment.bottomCenter,
            child:
            Container(
              width: double.infinity,
                decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(32.0),
                        topRight: Radius.circular(32.0))),
              child:Column(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Material(
                          child: Text("Подтверждение",
                              style: TextStyle(
                                  fontSize: 14.0, color: Colors.black))),
                    ),
                    Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Material(
                          child: Text("Вы действительно хотите выйти из приложения?",
                              style: TextStyle(
                                  fontSize: 14.0, color: Colors.black))),
                    ),
                    cancelButton,
                    yesButton
                  ]
              )
            )
        );
      },
    );
  }

Результат:

1 69 70 71 72 73 310