Кастомная нижняя панель в android

Настраиваемая форма нижней панели в android
Однажды меня попросили создать приложение с необычным дизайном нижней панели BottomNavigationView :
Кастомная нижняя панель в android
Изогнутый BottomNavigationView
Я никогда ранее не делал ничего подобного, поэтому начал гуглить на предмет создания custom view. После поисков и советов коллег, я понял, что это всего лишь разновидность кривой Безье.Рекомендую так же прочесть мою статью Разработка приложения-галереи под Android на Kotlin

Кривая Безье

Если вы когда-либо использовали компьютерные программы для рисования, вы скорее всего использовали кривые Безье, даже не подозревая об этом. 
Кастомная нижняя панель в android
пример кривой Безье в Adobe Illustrator
Я нашёл интересную статью, в которой был описан необходимый мне тип кривой, рекомендую её прочитать. Там же есть утилита для работы с кривыми.Кривая, отражающая нужный мне дизайн, оказалась кубической кривой Безье.
Кастомная нижняя панель в android
Кубические кривые определяются четырьмя опорными точками. Первая и последняя определяют положение концов кривой, а вторая и третья точки отвечают за направление кривой.Перейдём к коду и увидим, как создавать кривые Безье в android.Вначале создадим собственный view, расширяющий BottomNavigationView, чтобы взять весь его функционал и сконцентрироваться только на дизайне.
public class CurvedBottomNavigationView extends BottomNavigationView {
    public CurvedBottomNavigationView(Context context) {
        super(context);
        init();
    }
    public CurvedBottomNavigationView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public CurvedBottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setColor(Color.WHITE);
        setBackgroundColor(Color.TRANSPARENT);
    }
}
Метод init()в этом коде только инициализирует объекты path и paint.Роль path — позволить нам рисовать геометрические формы, в том числе кривые Безье. Path рисует линию от точки до точки. Обычный 
BottomNavigationView в форме прямоугольника, поэтому используя Path мы перерисуем его с помощью кривых.
Кастомная нижняя панель в android
Позиции точек
Сначала инициализируем позиции наших точек. Они зависят от ширины и высоты view. Здесь P1 — начальная точка рисования, P2 — начало первой кривой, P3 — конец первой кривой и начало второй, а P4 — окончание второй кривой. Никогда не делайте расчёты в методе onDraw()— он вызывается постоянно и это просто убьёт производительность приложения. Я делаю расчёты в методе onSizeChanged(), вызываемом при изменении размера view, поэтому на производительности это не должно сильно сказаться
 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // получаем ширину и высоту navigation bar
         mNavigationBarWidth = getWidth();
         mNavigationBarHeight = getHeight();
        // координаты (x,y) начальной точки до кривой
        mFirstCurveStartPoint.set((mNavigationBarWidth / 2) - (CURVE_CIRCLE_RADIUS * 2) - (CURVE_CIRCLE_RADIUS / 3), 0);
        // координаты (x,y) конечной точки после кривой
        mFirstCurveEndPoint.set(mNavigationBarWidth / 2, CURVE_CIRCLE_RADIUS + (CURVE_CIRCLE_RADIUS / 4));
        // то же самое для второй кривой
        mSecondCurveStartPoint = mFirstCurveEndPoint;
        mSecondCurveEndPoint.set((mNavigationBarWidth / 2) + (CURVE_CIRCLE_RADIUS * 2) + (CURVE_CIRCLE_RADIUS / 3), 0);

        // координаты (x,y) первой опорной точки на кубической кривой
        mFirstCurveControlPoint1.set(mFirstCurveStartPoint.x + CURVE_CIRCLE_RADIUS + (CURVE_CIRCLE_RADIUS / 4), mFirstCurveStartPoint.y);
        // координаты (x,y) второй опорной точки
        mFirstCurveControlPoint2.set(mFirstCurveEndPoint.x - (CURVE_CIRCLE_RADIUS * 2) + CURVE_CIRCLE_RADIUS, mFirstCurveEndPoint.y);

        mSecondCurveControlPoint1.set(mSecondCurveStartPoint.x + (CURVE_CIRCLE_RADIUS * 2) - CURVE_CIRCLE_RADIUS, mSecondCurveStartPoint.y);
        mSecondCurveControlPoint2.set(mSecondCurveEndPoint.x - (CURVE_CIRCLE_RADIUS + (CURVE_CIRCLE_RADIUS / 4)), mSecondCurveEndPoint.y);
    }
Перейдём к более интересному. Для начала обнулим (reset) path для удаления линий и кривых из неё, затем переместим её в начальную точку P1, используя метод moveTo(x,y), и нарисуем первую линию между P1 и P2, используя lineTo(x,y), это добавит линию с последней точки до заданной точки. Так как у нас пока нет последней точки, укажем moveTo(0,0).Для рисования кубических кривых мы должны использовать метод cubicTo(x1,y1,x2,y2,x3,y3), где первая и вторая точки (x1,y1) (x2,y2) контролируют наклон. Для проверки корректности точек можно использовать эту утилиту. Так же отрисуем вторую кривую и так далее всю панель.
 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        
        //расчет точки 
        
        mPath.reset();
        mPath.moveTo(0, 0);
        mPath.lineTo(mFirstCurveStartPoint.x, mFirstCurveStartPoint.y);

        mPath.cubicTo(mFirstCurveControlPoint1.x, mFirstCurveControlPoint1.y,
                mFirstCurveControlPoint2.x, mFirstCurveControlPoint2.y,
                mFirstCurveEndPoint.x, mFirstCurveEndPoint.y);

        mPath.cubicTo(mSecondCurveControlPoint1.x, mSecondCurveControlPoint1.y,
                mSecondCurveControlPoint2.x, mSecondCurveControlPoint2.y,
                mSecondCurveEndPoint.x, mSecondCurveEndPoint.y);

        mPath.lineTo(mNavigationBarWidth, 0);
        mPath.lineTo(mNavigationBarWidth, mNavigationBarHeight);
        mPath.lineTo(0, mNavigationBarHeight);
        mPath.close();
    }
Наконец, мы можем рисовать с помощью объекта canvas (канва). Канва содержит несколько методов рисования, и нам подходит canvas.drawPath(Path path, Paint paint).
  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    }
Хорошей разработки!Код примеров вы сможете найти в моём Github и на Java и на Kotlin. Только выберите правильную ветку.По материалам: «How I drew custom shapes in bottom bar»Также по разработке под android рекомендую почитать «Разработка приложения-галереи под Android на Kotlin»
Кастомная нижняя панель в android

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

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

Аутентификация в приложении 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 :).

Lepton — менеджер сниппетов

Lepton — бесплатный менеджер сниппетов

Сегодня мы рассмотрим Lepton — бесплатный менеджер сниппетов на базе Github Gist с поддержкой markdown и встроенным поиском. Так как он поддерживает весь функционал Gist и GitHub, вы сможете создавать сниппеты прямо из приложения и добавлять к ним неограниченное количество тегов. Вы можете выбрать типа сниппета — публичный или приватный. Lepton автоматически распознаёт язык программирования и может фильтровать по языку список ваших сниппетов. Он так же поддерживает разметку markdown, имеет встроенный поиск по наименованию, тегам и содержимому сниппетов.

Также есть возможность прикрепить сверху списка наиболее важные или любимые сниппеты, чтобы они были всегда на виду. Сниппеты автоматически (и практически мгновенно) синхронизируются между вашим ПК и учётной записью GitHub. Есть возможность поделиться сниппетом по его URL прямо из приложения.

Lepton — менеджер сниппетов

Lepton — кроссплатформенное ПО с открытым исходным кодом. Для работы с ним нужно всего лишь быть авторизованным в GitHub.

Как работать с Lepton:

1: Скачайте файл с программой для Windows отсюда. Запустите его. Сразу вам будет предложено авторизоваться через GitHub.

Lepton — менеджер сниппетов

2: После успешного входа, в приложение будут загружены все ваши сниппеты, а если у них были указаны теги, то они также будут загружены в программу, а сниппеты автоматически будут рассортированы по тегам. Так же и с языками программирования — при автоматическом распознавании сниппеты будут организованы по ним:

Lepton — менеджер сниппетов

3: Создать новый сниппет можно нажав на кнопку New. Затем укажите его название, имя файла и поместите сам код. Теги можно поместить сразу после названия. Для создания приватного сниппета отметьте галочку “Private” в нижнем правом углу окна.

Lepton — менеджер сниппетов

4: Поиск нужного сниппета можно начать с фильтра по языкам программирования или тегам. Или же воспользоваться полнотекстовым поиском в приложении: нажмите Shift + Space и в появившемся окне поиска начните вводить нужный запрос.

Lepton — менеджер сниппетов

5: Просмотреть сниппет на GitHub можно кликнув на иконку в виде глаза на панели инструментов. А кликнув на кнопку Share, вы скопируете URL сниппета в буфер обмена.

Lepton — менеджер сниппетов

Альтернативы

Несмотря на удобство Lepton, есть и немало альтернативных клиентов для Github Gist. К примеру, Gisto -тоже менеджер сниппетов с открытым исходным кодом, обладающий таким же функционалом.

Lepton — менеджер сниппетов
Lepton — менеджер сниппетов

Однако, если сравнить оба этих приложения, Lepton выигрывает за счёт большего коммьюнити, более удобного интерфейса и стабильности приложения. Хотя я продолжаю наблюдать за развитием обоих приложений, так как у каждого из них есть свои преимущества.

А каким менеджером сниппетов пользуетесь вы?

Lepton — менеджер сниппетов

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

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

Adblock detector