Аутентификация в приложении Flutter

Аутентификация в приложении Flutter

Сегодня мы сделаем небольшое приложение, в котором покажем, как сделать авторизацию, выход из сеанса и удаление профиля в Firebase. И мы будем делать это в Android Studio. Кликнув по ссылке, вы узнаете как установить Flutter для Android Studio. Если вы используете другую IDE, не волнуйтесь, код не будет отличаться.

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

Наше приложение будет выглядеть как на скриншоте ниже. Оно сможет авторизовать пользователя в Firebase через Google, завершать сеанс, удалять пользователя в том случае, если он когда-либо авторизовывался в приложении. Также мы добавим круглый индикатор прогресса внизу, чтобы показать, что приложение занято.

Аутентификация в приложении Flutter

Подготовка

Вначале необходимо настроить Google Firebase.

Добавим Firebase в проект

Аутентификация в приложении Flutter
скопируйте файл .json в папку Android/app 

Перейдите в консоль Firebase, создайте новый проект. Затем скачайте файл google-services.json и скопируйте его в папку Android/app/ (как на скриншоте). Не забудьте добавить SHA-1, он понадобится для аутентификации. Кликните здесь, если не знаете, где взять SHA-1.По дальнейшей настройке Flutter документация здесь.

Добавим метод входа (Sign-in)

Аутентификация в приложении Flutter
включим Google Sign-in

Затем в Firebase Console перейдите в Authentication (слева на панели). Здесь есть список Провайдеры авторизации. Для нашего проекта достаточно будет только Google.  

Добавляем зависимости

Откройте файл pubspec.yaml в папке проекта. Добавьте следующие строки ниже dependencies:

      // ./pubspec.yaml
      dependencies:
      //...
      #AUTHENTICATIN
      firebase_auth: ^0.6.2+1
      google_sign_in: ^3.2.2

Нажмите кнопку ‘Packages get’ сверху. Супер, теперь можно перейти к разработке. 

Структура проекта

Аутентификация в приложении Flutter
структура проекта

Я создал папку /scr в /lib. В /scr создал папку /auth. Добавил файл authHelper.dart в /auth, а home.dart и startApp.dart в /src. В принципе, вы можете создать любую структуру, однако, не забудьте поменять пути до файлов в этом случае.

Main.dart

Рассмотрим этот кусок кода. Это точка входа в приложении Flutter. Здесь мы просто запускаем StartApp().

    // ./lib/main.dart
    import 'package:flutter/material.dart';
    import 'src/startApp.dart';
    void main() => runApp(StartApp());

StartApp.dart

В startApp.dart мы пропишем тему приложения. Импортируем сюда home.dart, создадим StatelessWidget, определим цвета темы и основной экран методом Home().

    // ./lib/scr/startApp.dart
    import 'package:flutter/material.dart';
    import 'home.dart';
    class StartApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Auth Tutorial',
          theme: new ThemeData(
            // theme of our application.
            primaryColor: Colors.lightBlue,
            primaryColorDark: Colors.blue,
            accentColor: Colors.orange,
          ),
          home: new Home(),      
        );
      }
    }

Home.dart

Здесь мы импортируем flutter/material.dart и наш вспомогательный модуль authHelper.dart.Затем создаём StatefulWidget. 

    // ./lib/scr/home.dart
    import 'package:flutter/material.dart';
    import 'auth/authHelper.dart';
    class Home extends StatefulWidget {
      @override
      _Home createState() => new _Home();
    }

_Home() будет таким: создаём панель app bar с надписью ‘Flutter Authentication Tutorial’. Ниже у нас будет текстовое поле и три кнопки, ‘Login’, ‘Logout’ и ‘Delete’. Можете разместить их как угодно. Я даже присвоил разные цвета. Также мы добавим круглый индикатор прогресса.

    // ./lib/scr/home.dart
    //...
    class _Home extends State<StatefulWidget> {
      @override
      Widget build(BuildContext context) {    
        return new Scaffold(
          appBar: new AppBar(
            title: new Text('Flutter Authentication Tutorial'),
          ),
          body: new Center(
              child: new Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: Text('User: $mUserName'),
              ),
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: RaisedButton(
                    color: Colors.green,
                    textColor: Colors.white,
                    splashColor: Colors.greenAccent,
                    child: const Text('LOGIN'),
                    onPressed: isLoggingIn ? null : clickLogin),
              ),
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: RaisedButton(
                    color: Colors.blue,
                    textColor: Colors.white,
                    splashColor: Colors.blueAccent,
                    child: const Text('LOGOUT'),
                    onPressed: isLoggingIn ? null : clickLogout),
              ),
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: RaisedButton(
                  color: Colors.red,
                  textColor: Colors.white,
                  splashColor: Colors.redAccent,
                  child: const Text('DELETE'),
                  onPressed: mUser == null || isLoggingIn ? null : clickDelete,
                ),
              ),
              new Opacity(
                opacity: cpbOpacity,
                child: new Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new CircularProgressIndicator(),
                    new SizedBox(width: 20.0),
                    new Text('Please wait...')
                  ],
                ),
              ),
            ],
          )),
        );
      }
    }

Добавим несколько переменных и функций. Для начала, mUser и mUserName. А также булеву isLoggingIn. При каждом успешном входе она будет true. И если она true, кнопки будут неактивны (если onPressed равен null, кнопка неактивна). При клике на кнопку Login будет вызван loginWithGoogle из модуля authHelper. Здесь же мы проверяем ошибки, и если всё хорошо, установим isLoggingIn и выведем имя пользователя (username), которое получим после авторизации.

    // ./lib/scr/home.dart
    class _Home extends State<StatefulWidget> {
      var mUser;
      var mUserName;
      var isLoggingIn = false;
      Future clickLogin() async {
        //установим isLoggingIn в true при клике на Login
        setState(() {
          isLoggingIn = true;
        });
        mUser = await loginWithGoogle().catchError((e) => setState(() {
              isLoggingIn = false;
            }));
        assert(mUser != null);
        setState(() {
          isLoggingIn = false;
          mUserName = mUser.displayName;
        });
      }
      @override
      Widget build(BuildContext context) {
      //...
      }
    }

Создадим методы clickLogout и clickDelete. В clickLogout мы вызываем logoutWithGoogle из authHelper. При успешном выполнении выводим в текстовом поле ‘No user is logged in’. При клике clickDelete, пользователь будет удалён из Firebase.

    // ./lib/scr/home.dart 
    class _Home extends State<StatefulWidget> { 
      //...
      void clickLogout() {
        setState(() {
          isLoggingIn = true;
        });
        logoutWithGoogle().whenComplete(() => setState(() {
              isLoggingIn = false;
              mUserName = 'No user is logged in!';
            }));
      }
      void clickDelete() {
        setState(() {
          isLoggingIn = true;
        });
        deleteUserWithGoogle(mUser).whenComplete(() => setState(() {
              isLoggingIn = false;
              mUser = null;
              mUserName = 'No user is logged in!';
            }));
      }
      @override
      Widget build(BuildContext context) {
        // ...
      }
    }

Допишем home.dart, добавьте эти строки в начале build. Здесь устанавливаем значение непрозрачности, в том случае, когда пользователь аутентифицирован.

    // ./lib/scr/home.dart 
    class _Home extends State<StatefulWidget> { 
      //...
      @override
      Widget build(BuildContext context) {
        var cpbOpacity;
        isLoggingIn ? cpbOpacity = 1.0 : cpbOpacity = 0.0;
        return new Scaffold(
          //...
        );
      }
    }

AuthHelper.dart

Почти закончили. Добавим немного кода в файл authHelper.dart. firebase_auth и google_sign_in необходимы для процесса аутентификации. При вызове loginWithGoogle сперва пытаемся получить пользователя. При неудаче пробуем дальнейшую авторизацию. Если и тут неудача, вызовем функцию signIn. Она откроет меню Google sign-in и если его отменят, получим ошибку. Ждём аутентификацию и пробуем получить пользователя Firebase. Если всё получилось, у нас будет пользователь, при этом он неанонимен.

    // ./lib/scr/auth/authHelper.dart
    import 'dart:async';
    import 'package:firebase_auth/firebase_auth.dart';
    import 'package:google_sign_in/google_sign_in.dart';
    final FirebaseAuth _auth = FirebaseAuth.instance;
    final GoogleSignIn _googleSignIn = new GoogleSignIn();
    Future<FirebaseUser> loginWithGoogle() async {
      GoogleSignInAccount currentUser = _googleSignIn.currentUser;
      if (currentUser == null) {
        currentUser = await _googleSignIn.signInSilently();
      }
      if (currentUser == null) {
        currentUser = await _googleSignIn.signIn();
        if (currentUser == null) {
          throw('Login Canceled');
        }
      }
      final GoogleSignInAuthentication auth = await currentUser.authentication;
      final FirebaseUser user = await _auth.signInWithGoogle(
        idToken: auth.idToken,
        accessToken: auth.accessToken,
      );
      assert(user != null);
      assert(!user.isAnonymous);
      return user;
    }
    //...

Допишем оставшиеся методы logoutWithGoogle и deleteUserWithGoogle. При выходе из сеанса, вызовем signOut из Firebase и signOut из googleSignIn. При удалении пользователя, сперва завершим его сеанс и затем полностью удалим.

    // ./lib/scr/auth/authHelper.dart
    //...
    Future<Null> logoutWithGoogle() async {
      await _auth.signOut();
      await _googleSignIn.signOut();
    }
    Future<Null> deleteUserWithGoogle(FirebaseUser user) async {
      await logoutWithGoogle();
      await user.delete();
    }

Вот и всё. Если у вас есть вопросы/уточнения, задавайте их в комментариях.

Вы пришли с Xamarin? Тогда почитайте Flutter или Xamarin: сравнение инструментов кросс-платформенной мобильной разработки.

Перевод «Flutter Authentication Tutorial»

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Аутентификация в приложении Flutter

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).

Создание первого приложения Flutter, часть 2

Создание первого приложения Flutter

Первая часть.

В этом уроке вы расширите функционал приложения и добавите в него интерактивность. Создадите вторую страницу (называемую роутом (route)), чтобы пользователь мог переходить на неё. На финальном этапе вы измените тему приложения (цвет).

Если вы пропустили первый урок, лучше начать с него.

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

В этом GIF показан финальный вид приложения:

Создание первого приложения Flutter, часть 2

1. Добавим иконки в список

Добавим иконки в виде сердечка в каждую строку. Чуть позже сделаем их кликабельными с сохранением выбранного элемента в избранное.

Добавьте набор (Set) _saved в RandomWordsState. Этот Set будет хранить список выбранных пользователем пар слов. Set был выбран из-за того, что не допускает дублей.

class RandomWordsState extends State<RandomWords> {
  final List<WordPair> _suggestions = <WordPair>[];
  final Set<WordPair> _saved = new Set<WordPair>();   // Добавьте эту строку.
  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
  ...
}

В функции _buildRow добавим проверку alreadySaved для того, чтобы быть уверенным, что ранее уже не сохранили такую пару.

Widget _buildRow(WordPair pair) {
  final bool alreadySaved = _saved.contains(pair);  // Добавьте эту строку.
  ...
}

В _buildRow() добавим иконку-сердечко к объектам ListTile для включения в избранное. Пока это статичные картинки, далее мы добавим в них интерактив:

Widget _buildRow(WordPair pair) {
  final bool alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: new Icon(   // Добавьте строки отсюда... 
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),                    // ... до сюда.
  );
}

Перезапустите приложение. Вы должны увидеть незаполненные сердечки в каждой строке.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

2. Добавляем интерактивность

Сделаем сердечки кликабельными. При клике на них в списке, у выбранного элемента переключается свойство «избранный», при этом пара слов попадает в набор с избранным.

Для этого доработаем функцию _buildRow. Если запись уже есть в избранном, при повторном клике она удаляется из избранного. При клике на строку списка, вызовется setState() для уведомления фреймворка о смене состояния.

Добавьте onTap, как указано ниже:

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: new Icon(
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),
    onTap: () {      // Добавим 9 строк отсюда...
      setState(() {
        if (alreadySaved) {
          _saved.remove(pair);
        } else { 
          _saved.add(pair); 
        } 
      });
    },               // ... до сюда.
  );
}

Замечание: Во Flutter при вызове setState() срабатывает вызов к методу build() для объекта Состояния, в результате перерисовывая UI.

Перезапустите приложение. При клике на строку списка она должна попадать в избранное или удаляться из него. Также в месте клика появляется анимация заполнения.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

3. Переход на новый экран

Сейчас мы добавим новую страницу для вывода избранного. Вы узнаете как переходить между основным (home route) и новым роутом.

Стеком, содержащим все роуты приложения, управляет Navigator. Добавление роута в стек Navigator-а, делает переход на этот роут. Удаление роута из стека Navigator-а, возвращает на предыдущий роут.

Добавим иконку списка к AppBar в методе build класса RandomWordsState. По клику на эту иконку, в Navigator добавится новый роут, содержащий элементы избранного.

Добавьте иконку и соответствующее ей действие в метод build:

class RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
        actions: <Widget>[      // Добавьте 3 строки отсюда...
          new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
        ],                      // ... до сюда.
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}

Замечание: Некоторые свойства виджетов принимают одиночный виджет (child), а другие, такие как действия, принимают массив виджетов (children), это указывается квадратными скобками ([]).

Добавим функцию _pushSaved() в класс RandomWordsState.

  void _pushSaved() {
  }

Перезапустим приложение. Иконка списка появится в верхней панели. Нажатие на неё ни к чему пока не приводит, потому что функция _pushSaved пуста.

Теперь создадим роут и положим в стек Navigator-а. Это действие отобразит новый роут на экране. Содержимое новой страницы построено в MaterialPageRoute builder, в анонимной функции.

Вызовем Navigator.push, как показано ниже. IDE покажет ошибку в коде, но мы это почти сразу поправим.

void _pushSaved() {
  Navigator.of(context).push(
  );
}

Добавим MaterialPageRoute и его построитель (builder). Код ниже создаёт список рядов ListTile. Метод divideTiles() добавляет горизонтальный разделитель между элементами. Переменная divided хранить финальный вид рядов, сконвертированный в список функцией toList().

Добавьте следующий код:

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute<void>(   // Добавьте 20 строк отсюда...
      builder: (BuildContext context) {
        final Iterable<ListTile> tiles = _saved.map(
          (WordPair pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final List<Widget> divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
          .toList();
      },
    ),                           // ... до сюда.
  );
}

В коде ниже свойство builder возвращает Scaffold, содержащий верхнюю панель нового роута с названием «Saved Suggestions.» Тело роута представлено ListView с рядами ListTiles; между рядами есть разделители:

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute<void>(
      builder: (BuildContext context) {
        final Iterable<ListTile> tiles = _saved.map(
          (WordPair pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final List<Widget> divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
              .toList();

        return new Scaffold(         // Добавьте 6 строк отсюда...
          appBar: new AppBar(
            title: const Text('Saved Suggestions'),
          ),
          body: new ListView(children: divided),
        );                           // ... до сюда.
      },
    ),
  );
}

Перезапустите приложение. Пометьте избранным какое-нибудь словосочетание в списке и затем нажмите иконку списка в верхней панели. Откроется новый экран с избранным. Navigator сам добавил кнопку «Назад» в панель. Вам даже не потребуется реализация метода Navigator.pop — просто нажав на неё, вы попадёте на основной экран.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

4. Смена UI через тему

Сейчас мы изменим тему приложения. Параметр theme отвечает за внешний вид вашего приложения. Вы можете использовать как тему по-умолчанию (зависит от устройства), так и изменить её по вашему желанию.

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

Смените цвет в классе MyApp:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      theme: new ThemeData(          // Добавьте 3 строки отсюда... 
        primaryColor: Colors.white,
      ),                             // ... до сюда.
      home: new RandomWords(),
    );
  }
}

Перезапустите приложение. Общий фон теперь белый, даже верхняя панель.

Как упражнение для практики, воспользуйтесь ThemeData для изменения других элементов UI. Класс Colors в библиотеке Material предоставляет множество цветов, а «горячий» перезапуск делает эксперименты с UI очень лёгкими.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

5. Готово!

Вы создали интерактивное приложение Flutter, работающее под iOS и Android и научились:

  • Писать код на Dart.
  • Использовать «горячий» перезапуск приложения для ускорения разработки.
  • Создавать виджеты с изменяемым состоянием (stateful), добавлять интерактивность в приложение.
  • Создавать роуты (route) и логику для перемещений между основным и новым роутами.
  • Изменять интерфейс приложения с помощью тем оформления.

Почитайте также Stateful-виджеты должны исчезнуть: Stateful Builder.

Перевод «Write Your First Flutter App, part 2»

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Создание первого приложения Flutter, часть 2

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).

Создание первого приложения Flutter, часть 1

Создание первого приложения Flutter

1. Введение

Flutter это SDK для создания мобильных приложений от Google под iOS и Android. С Flutter можно переиспользовать код, им пользуются разработчики по всему миру, он имеет открытый исходный код и бесплатен.

В этом уроке вы создадите несложное приложение Flutter. Если вы знакомы с объектно-ориентированным программированием и основами разработки, такими как переменные, циклы и условия, у вас всё должно получиться. Опыта разработки на Dart или под мобильные устройства не требуется.

Что мы узнаем в этом уроке

  • Как написать приложение на Flutter, выглядящее нативно и в  iOS, и в Android.
  • Базовую структуру приложений Flutter.
  • Поиск и использование пакетов для расширения функционала.
  • Использование горячей перезагрузки (hot reload) для удобства разработки.
  • Как реализовать stateful-виджет.
  • Как создать бесконечный список с «ленивой» загрузкой.

Во второй части мы добавим интерактивность, изменим тему приложения, добавим возможность перехода на другую страницу (называемую роутами (route) во Flutter).

Что мы сделаем в первой части

Вы сделаете простое мобильное приложение, генерирующее наименования для стартапов. Пользователь сможет выбирать и отменять выбор названий, сохраняя понравившиеся. Код будет «лениво» генерировать десять названий за один раз. По мере прокрутки пользователем списка, новые названия будут создаваться в фоне и подгружаться. Таким образом, пользователь сможет прокручивать список вечно.

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

Создание первого приложения Flutter, часть 1

2. Настройте окружение Flutter

Всё, что вам нужно для этого: Flutter SDK и редактор кода. В этом уроке предполагается, что вы выберете Android Studio, но выбор редактора остаётся за вами.

Протестировать работу приложения можно будет с этими устройствами:

  • Физическое устройство (Android или iOS), подключенное к компьютеру с настроенным режимом разработчика.
  • Симулятор iOS. (Требуется установка XCode)
  • Эмулятор Android. (Требуется установленная Android Studio)

3. Создаём каркас приложения Flutter

Для этого воспользуйтесь инструкцией Getting Started with your first Flutter app (возможно, позже её перевод будет доступен). Назовите проект startup_namer (вместо myapp). В дальнейшем мы будем дорабатывать это приложение.

Совет: Если вы не видите пункт «New Flutter Project» в вашей IDE, убедитесь, что она настроена для работы с Flutter и Dart.

В этом уроке мы будем преимущественно править файл lib/main.dart, в котором находится логика приложения.

Замените содержимое файла lib/main.dart.
Для этого удалите весь код в нём. Затем вставьте в него код ниже, выводящий надпись «Hello World» по центру экрана.

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: const Text('Welcome to Flutter'),
        ),
        body: const Center(
          child: const Text('Hello World'),
        ),
      ),
    );
  }
}

Замечание: При вставке кода могут исказиться отступы. Вот как это поправить:
   — Android Studio / IntelliJ IDEA: кликните правой кнопкой на коде и выберите Reformat Code with dartfmt.
   — VS Code: кликните правой кнопкой и выберите Format Document.
   — Terminal: запустите flutter format <имя файла>.

Запустите приложение. Вы должны увидеть приметно такое, в зависимости от того, Android или iOS у вас:

Замечание:  При первом запуске на физическом устройстве может пройти много времени. После этого будет работать hot reload. При нажатии на кнопку Save также будет запущен hot reload при работающей программе.

Подытожим

  • В этом примере создаётся приложение Material. Material — это описание визуального представления, он является стандартом в web и на мобильных платформах. Flutter содержит огромный спектр виджетов типа Material.
  • Метод main использует нотацию с большой стрелкой (=>), такая нотация используется в однострочных функциях.
  • Приложение наследует StatelessWidget, следовательно всё приложение также является виджетом. Во Flutter практически всё является виджетом, включая выравнивание, отступы, макет.
  • Виджет Scaffold из библиотеки Material предоставляет панель приложения, заголовок и свойство body, которое отвечает за иерархию виджетов на основном экране. И эта иерархия может быть иногда очень огромной.
  • Главная задача виджета — вернуть метод build, который описывает как нужно вывести виджет по отношению к соседним.
  • body в этом примере состоит из виджета Center, содержащего дочерний виджет Text. Виджет Center выравнивает всю иерархию потомков по центру экрана.

4. Используем сторонние пакеты

Сейчас вы научитесь работать с внешними пакетами, на примере пакета english_words, содержащего несколько тысяч популярных английских слов и утилиты для удобной работы с ними.

Найти пакет english_words, так же, как и многие другие пакеты с открытым исходным кодом, вы можете на pub.dartlang.org.

Файл pubspec управляет списком зависимостей приложения Flutter. В конец pubspec.yaml добавим english_words: ^3.1.0 (english_words версии 3.1.0 или выше) в список зависимостей (dependencies):

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.0
  english_words: ^3.1.0   # добавьте эту строку

Если вы открыли файл pubspec в Android Studio, кликните Packages get. Эта команда загрузит пакет из сети. В консоли вы должны увидеть что-то подобное:

flutter packages get
Running "flutter packages get" in startup_namer...
Process finished with exit code 0

Откроем lib/main.dart, импортируем пакет:

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';  // добавьте эту строку.

По мере написания, Android Studio будет предлагать библиотеки для импорта. Но добавленная строка сразу окрасится серым цветом, показывая неиспользуемый (пока) импорт.

Пришло время применить пакет english_words, пусть он генерит текст вместо текущей статичной надписи «Hello World».

Сделайте следующие изменения:

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random(); // добавьте эту строку.
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(    // Замените "const" на "new".
          //child: const Text('Hello World'),   // Замените этот текст...
          child: new Text(wordPair.asPascalCase),  // этим.
        ),
      ),
    );
  }
}

Замечание: «Pascal case» (так же известный как CamelCase («ВерблюжийРегистр»)), означает, что каждое слово в строке, включая первое, начинается с большой буквы. Таким образом, «upper camel case» становится «UpperCamelCase».

Если не заменить Center на «new Center», система типов покажет предупреждение о том, что объект Center не должен быть константой (const), потому что его потомок, объект Text больше не является константой. Поэтому и Center и Text должны создавать экземпляры с помощью new.

Если приложение уже запущено, используйте для перезапуска кнопку hot reload. При каждом перезапуске или холодном запуске проекта, вы будете видеть в приложении разные пары слов, выбранные в случайном порядке. Это происходит потому, что фразы создаются внутри метода build, запускаемого каждый раз при запросе от MaterialApp на перерисовку, или же переключении Platform в Flutter Inspector.

Проблемы?

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

5. Добавим stateful виджет

Stateless-виджеты неизменяемые (immutable), это значит, что их свойства не могут меняться  — все их значения завершённые (final).

Stateful виджеты поддерживают смену состояния. Реализация такого виджета подразумевает создание минимум двух классов: 1) класс StatefulWidget создающий экземпляр класса 2) State. Класс StatefulWidget сам по себе неизменяемый (immutable), однако класс State сохраняется на протяжении всего жизненного цикла виджета.

На этом шаге вы добавите stateful-виджет RandomWords, который создаст свой State-класс, RandomWordsState. Затем вы добавите RandomWords как дочерний элемент stateless-виджета MyApp.

Создадим минимальный класс состояния (state). Он может располагаться в любом месте файла в MyApp, но в этом решении он расположен в конце файла. Добавьте эти строки:

class RandomWordsState extends State<RandomWords> {
  // TODO Добавить метод build
}

Обратите внимание на объявление State<RandomWords>. Это указывает на то, что мы используем универсальный класс State, специализированный для использования с RandomWords. Большинство логики приложения и состояние находится здесь для поддержки состояния виджета RandomWords. Этот класс сохраняет сгенерированные пары слов, количество которых растёт бесконечно по мере прокрутки пользователя, так же как и избранных пар слов (будет в части 2), которые пользователь добавляет или удаляет в списке, нажимая кнопку в виде сердечка.

RandomWordsState зависит от класса RandomWords. Сейчас добавим его.

Добавьте stateful-виджет RandomWords в main.dart. Виджет RandomWords делает немного больше, чем просто создаёт State-класс:

class RandomWords extends StatefulWidget {
  @override
  RandomWordsState createState() => new RandomWordsState();
}

После этого IDE сообщит, что в классе отсутствует метод build. Добавим метод build, создающий пары слов, переместив этот код из MyApp в RandomWordsState. Добавьте код:

class RandomWordsState extends State<RandomWords> {
  @override                                  // Добавим с этой строки ... 
  Widget build(BuildContext context) {
    final WordPair wordPair = new WordPair.random();
    return new Text(wordPair.asPascalCase);
  }                                          // ... до этой.
}

Удалим часть кода из MyApp:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final WordPair wordPair = new WordPair.random();  // Удалите эту строку.

    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          //child: new Text(wordPair.asPascalCase), // А эту замените... 
          child: new RandomWords(),                 // ... на эту.
        ),
      ),
    );
  }
}

Перезапустите приложение. Ничего не должно измениться в поведении, приложение так же должно выводить пары слов при сохранении кода или перезапуске.

Совет: Если вы видите следующее сообщение при горячей перезагрузке, лучше полностью перезапустить его:
Reloading…
Not all changed program elements ran during view reassembly; consider restarting.

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

Проблемы?

Если ваше приложение не корректно работает, рабочий код по ссылке:

6. Создание ListView с бесконечной прокруткой

В этом шаге мы расширим RandomWordsState таким образм, чтобы выводился список пар слов. По мере его прокрутки, список (представленный виджетом ListView) будет расти бесконечно. Конструктор builder в ListView позволяет создавать списковые представления на лету, по запросу.

Добавьте список _suggestions в класс RandomWordsState для хранения предложенных пар. Также добавьте переменную _biggerFont для увеличения размера шрифта.

Замечание:  Добавление префикса в виде подчёркнутой линии к имени переменной делает её закрытой (private) в языке Dart.

class RandomWordsState extends State<RandomWords> {
  // добавьте следующие две строки.
  final List<WordPair> _suggestions = <WordPair>[];
  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0); 
  ...
}

Затем добавьте функцию _buildSuggestions() в класс RandomWordsState. Этот метод создаст ListView, который выведет сгенерированные пары слов.

Класс ListView предоставляет свойство itemBuilder, это построитель и анонимная функция с обратным вызовом. Она принимает два параметра — BuildContext, и переменную-итератор i. Итератор начинается с 0 и увеличивается при каждом вызове функции, т.е. каждый раз при генерации новой пары слов. Такая модель позволяет списку предложений расти постепенно при перемотке списка пользователем.

Добавьте функцию _buildSuggestions, описанную ниже, в класс RandomWordsState (delete the comments, if you prefer):

  Widget _buildSuggestions() {
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),
      // The itemBuilder callback is called once per suggested 
      // word pairing, and places each suggestion into a ListTile
      // row. For even rows, the function adds a ListTile row for
      // the word pairing. For odd rows, the function adds a 
      // Divider widget to visually separate the entries. Note that
      // the divider may be difficult to see on smaller devices.
      itemBuilder: (BuildContext _context, int i) {
        // Add a one-pixel-high divider widget before each row 
        // in the ListView.
        if (i.isOdd) {
          return new Divider();
        }

        // The syntax "i ~/ 2" divides i by 2 and returns an 
        // integer result.
        // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
        // This calculates the actual number of word pairings 
        // in the ListView,minus the divider widgets.
        final int index = i ~/ 2;
        // If you've reached the end of the available word
        // pairings...
        if (index >= _suggestions.length) {
          // ...then generate 10 more and add them to the 
          // suggestions list.
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }

Функция _buildSuggestions вызывает _buildRow для каждой пары слов. Она выводит созданную пару в ListTile, и это позволит вам сделать ряды более привлекательными в части 2.

Добавьте функцию _buildRow в RandomWordsState:

  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }

Обновите метод build в RandomWordsState с добавлением _buildSuggestions() вместо прямого вызова библиотеки генерации слов. (Scaffold реализует базовый визуальный стиль в Material Design)

  @override
  Widget build(BuildContext context) {
    //final wordPair = new WordPair.random(); // удалить эти... 
    //return new Text(wordPair.asPascalCase); // ... две строки.

    return new Scaffold (                   // Добавить отсюда... 
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );                                      // ... досюда.
  }

Обновим метод build в MyApp, сменим заголовок и основной экран на виджет RandomWords.

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      home: new RandomWords(),
    );
  }

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

Проблемы?

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

7. Что дальше

Поздравляем!

Вы закончили первую часть! Если хотите продолжить, переходите в часть 2, где сможете доработать приложение:

  • Добавить интерактивность.
  • Добавить навигацию.
  • Сменить цвет темы.

В конце части 2 приложение будет таким:

Создание первого приложения Flutter, часть 1

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Перевод «Write Your First Flutter App, part 1»

Создание первого приложения Flutter, часть 1

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).

Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка: Разработка кросс-платформенного мобильного приложения на голом JSON

Предыдущие несколько месяцев я посвятил работе над совершенно новым способом разработки нативных приложений для iOS и Android и назвал его Jasonette.

С Jasonette можно описать всю логику приложения всего в одном файле JSON-разметки. А если всё ваше приложение состоит из JSON, его можно загружать так же, как любые другие данные.

Нет нужды хранить логику приложения на устройстве, поэтому вы можете дорабатывать её и обновлять так часто, как требуется, простым обновлением JSON на сервере. Ваше приложение будет обновлено при следующем открытии.

В видео ниже кратко описан весь процесс (на англ.):

Jasonette состоит из многих компонентов. Там есть функции, шаблоны, стили и многое другое, и всё это в JSON разметке. Вы можете писать супер-изощрённые нативные приложения в стиле [simple_tooltip content=’Model-View-Controller (Модель-Представление-Контроллер) — схема разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: модель, представление и контроллер — таким образом, что модификация каждого компонента может осуществляться независимо.’]MVC[/simple_tooltip].

Сегодня мы разберём только «Представления»:

  1. Как Jasonette выражает различные кросс-платформенные шаблоны UI в JSON.
  2. Как реализованы внутренние преобразования JSON-в-Native.

Базовая структура

Издалека можно подумать, что Jasonette работает подобно браузеру. Но вместо того, чтобы работать с HTML и отрисовывать web-view, Jasonette загружает JSON и «на лету» собирает нативное представление.

Разметка JSON здесь самая обычная, но при этом она строится на нескольких стандартах. Во-первых, структура начинается с ключа $jason, который имеет двух потомков: head и body. Выглядит она так:

{
  "$jason": {
    "head": {
      .. метаданные документа ...
    },
    "body": {
      .. содержимое, выводимое на экран ..
    }
  }
}

Философия проектирования

Если посмотреть на то, как построены большинство мобильных приложений, то можно увидеть, что они используют всего несколько видов представлений:

  1. Список с вертикальной прокруткой
  2. Список с горизонтальной прокруткой
  3. Абсолютное позиционирование
  4. Сетка

Взглянем на первые три, ибо они самые распространённые.

1. Секции — описание списка с прокруткой

Списки с прокруткой это самый популярный способ построения интерфейса приложений. В Jasonette мы называем их sections.

Они бывают двух видов: вертикальные и горизонтальные.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON


Реализация — Вертикальные секции

Под iOS Jasonette реализует их с помощью UITableView. Под Android — с RecyclerView.


{
  "body": {
    "sections": [{
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}

Под iOS эта JSON разметка создаст UITableView с тремя UITableViewCells, каждая из которых содержит UILabel, с соответствующими атрибутами.
Под Android будет создано RecyclerView с тремя элементами, каждый из которых это TextView, выводящий соответствующий элемент.
Всё это будет сконструировано программно, без какого-либо использования Storyboards (в iOS) или файлов макета XML (в Android).

Реализация — Горизонтальные секции

Синтаксически нет разницы с вертикальными секциями. Единственное, что мы изменили, это type в “horizontal”.


{
  "body": {
    "sections": [{
      "type": "horizontal",
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}
2. Элементы — Описываем макет каждого элементы прокрутки

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

Элемент может быть:

  • Единичным компонентом типа label (метка), image (картинка), button (кнопка), textarea (текстовое поле) и т.д.
  • Комбинацией любых этих элементов

К счастью, iOS и Android имеют похожие системы построения представлений, UIStackView и LinearLayout, соответственно. И эти системы в свою очередь похожи на CSS Flexbox, что облегчает нам работу. И вдобавок к этому, нативные системы представлений бесконечно компонуемы — как показано ниже, вы можете создать вертикальный макет, горизонтальный макет, а также скомпоновать и горизонтальный и вертикальный в одном, и так до бесконечности.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка кросс-платформенного мобильного приложения на голом JSON

Для создания вертикального макета, выставим type как vertical, затем настроим остальные компоненты:


{
  "items": [{
    "type": "vertical",
    "components": [
      {
        "type": "label",
        "text": "First"
      }, 
      {
        "type": "label",
        "text": "Second"
      }, 
      {
        "type": "label",
        "text": "Third"
      }
    ]
  }]
}

Здесь то же самое. Просто установим type в horizontal:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "label",
        "text": "Rick"
      }
    ]
  }]
}

Встроить один макет в другой так же просто:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "vertical",
        "components": [{
          "type": "label",
          "text": "User"
        }, {
          "type": "label",
          "text": "Rick"
        }]
      }
    ]
  }]
}

Чтобы не усложнять понимание, я пока не упоминал про стилизование элементов приложения, но это делается очень просто. Всё, что вам нужно для этого, это добавить объект style с описанием атрибутов font (шрифт), size (размер), width (ширина), height (высота), color (цвет), background (фон), corner_radius (угловой радиус), opacity (прозрачность) и т.п.

3. Слои — абсолютное позиционирование

Иногда вам может понадобиться разместить элементы в определённой части экрана без прокрутки и перемещений. Jasonette поддерживает такое размещение в layers.

На текущий момент в слое можно разместить только два типа дочерних объектов: image и label. Вы можете разместить их в любой части экрана. Ниже пример этого:

Разработка кросс-платформенного мобильного приложения на голом JSON

В этом примере у нас созданы две метки (для температуры и состояния погоды) и картинка (иконка камеры). Они размещены в своих координатах и не двигаются:


{
  "$jason": {
    "body": {
      "style": {
        "background": "camera"
      },
      "layers": [
        {
          "type": "label",
          "text": "22°C",
          "style": {
            "font": "HelveticaNeue-Light",
            "size": "20",
            "top": "50",
            "left": "50%-100",
            "width": "200",
            "align": "center"
          }
        },
        {
          "type": "label",
          "text": "few clouds",
          "style": {
            "font": "HelveticaNeue",
            "size": "15"
          }
        },
        {
          "type": "image",
          "url": "https://s3.amazonaws.com/.../camera%402x.png",
          "style": {
            "bottom": "100",
            "width": "30",
            "color": "#ffffff",
            "right": "30"
          }
        }
      ]
    }
  }
}

И этого достаточно для того, чтобы сделать практически любое приложение, какое вы только можете представить.

Вот ещё примеры того, что построено в Jasonette:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON
За пределами представлений

Прочитав всё это, вы можете подумать:

  • «Ух ты, круто! Я должен это попробовать!» или
  • «Да, я конечно могу сделать приложение на поиграться, но реальное — никогда!»

Ещё раз повторюсь, здесь мы говорили про самую лёгкую часть работы с Jasonette — представления. Но вы действительно можете построить приложение практически любой сложности в JSON.

Вы можете соединить действия с элементами UI и они сработают, как только пользователь кликнет на них. Также вы можете вызывать действия одно за другим, основываясь на возвращаемых ими успехах/ошибках. А можно даже автоматически вызывать действия на основе происходящих событий с устройством или приложением.

Что ещё возможно?

Вам обязательно нужен сервер, хранящий JSON, а в остальном Jasonette совершенно автономна. И этот JSON может прилетать отовсюду: с локального устройства, с удаленных серверов, да хоть с raspberry pi!

  1. У вас есть веб-приложение? Тогда вы с лёгкостью построите мобильное приложение, вызывая серверный API
  2. Вам можно вообще не думать о сервере. Храните JSON файл на Pastebin или Github!
  3. Сконвертируйте любой веб-сайт в приложение. В Jasonette есть мощный парсер HTML-в-JSON на базе библиотеки cheerio, которая позволяет преобразовать любой HTML в объект JSON. Ну и, конечно, вы можете сами сформировать нужный JSON.

Ещё немного примеров:

Разработка кросс-платформенного мобильного приложения на голом JSON

Фото-приложение, делающее снимок камерой устройства и выкладывающая его в S3, затем она создаёт запись в ленте новостей на своём сервере:

Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение Eliza Chatbot для iOS и Android на базе Node.js:

Приложение для микро-блоггинга:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение, конвертирующее сайт HTML в JSON структуру, а затем в мобильное приложение:

Разработка кросс-платформенного мобильного приложения на голом JSON
Заключение

Jasonette это пока молодой проект. Версия для iOS вышла в конце 2016 г, а для Android ещё немного позже.

Но уже сейчас у неё есть огромное сообщество разработчиков и она активно развивается.

Звучит круто? Тогда Jasonette будет очень рад вам!

Источник

Разработка кросс-платформенного мобильного приложения на голом JSON

Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.

Пишите: @ighar. Buy me a coffee, please :).