Создание приложения для заметок на Flutter/Dart

Хотите изучить новый язык – пожалуйста. Мы подготовили для вас туториал по созданию приложения для заметок на Dart и Flutter.

Flutter – это мобильный кроссплатформенный SDK с открытым исходным кодом от Google. Приложения, написанные на Dart и Flutter, по умолчанию включают в себя Material Design компоненты, что придает привлекательный внешний вид и юзабилити.

Инструкция по установке Flutter на официальном сайте.

Во-первых, давайте настроим проект:

  • создадим проект Flutter в Android Studio или в terminal/cmd с помощью команды "flutter create notes";
  • в dart удалим класс homePage и создадим новый файл с нашим собственным классом homePage, содержащий наш Scaffold (набор виджетов);
  • реализуем stateful класс StaggeredGridPage. Это позволит сделать макет приложения для заметок, содержащий элементы в шахматном порядке.

В приложении для создания шахматной сетки используем Staggered grid, а SQLite – для хранения данных.

Ниже приведен код из pubspec.yaml с необходимыми зависимостями. Добавьте их, сохранитесь и с помощью команды "flutter packages get" инициализируйте новые зависимости:

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2
  flutter_staggered_grid_view: ^0.2.7
  auto_size_text: ^1.1.2
  sqflite:
  path:
  intl: ^0.15.7
  share: ^0.6.1

Создайте класс для заметок. Вам нужна функция toMap для запросов к БД:

class Note {
  int id;
  String title;
  String content;
  DateTime date_created;
  DateTime date_last_edited;
  Color note_color;
  int is_archived = 0;

  Note(this.id, this.title, this.content, this.date_created, this.date_last_edited,
       this.note_color);

  Map<String, dynamic> toMap(bool forUpdate) {
    var data = {
      'title': utf8.encode(title),
      'content': utf8.encode( content ),
      'date_created': epochFromDate( date_created ),
      'date_last_edited': epochFromDate( date_last_edited ),
      'note_color': note_color.value,
      'is_archived': is_archived
    };
    if(forUpdate){  data["id"] = this.id;  }
    return data;
  }

// Преобразование даты и времени в секунды типа int
    after midnight 1st Jan, 1970 UTC int epochFromDate(DateTime dt) {  
        return dt.millisecondsSinceEpoch ~/ 1000; }

void archiveThisNote(){ is_archived = 1; }
}

В итоге получаем домашнюю страницу HomePage.dart с телом StaggeredGridView. В AppBar поместите кнопку, чтобы пользователь мог переключаться между шахматным и list представлением. А еще, оберните тело в SafeArea для "дружественности" к телефонам.

StaggeredGridView требует четкого указания количества заметок в ряду. В горизонтальном формате на экране телефона или планшета будет организовано по три заметки и две для телефона в портретном формате.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../Models/Note.dart';
import '../Models/SqliteHandler.dart';
import '../Models/Utility.dart';
import '../Views/StaggeredTiles.dart';
import 'HomePage.dart';

class StaggeredGridPage extends StatefulWidget {
  final notesViewType;
  const StaggeredGridPage({Key key, this.notesViewType}) : super(key: key);
  @override
  _StaggeredGridPageState createState() => _StaggeredGridPageState();
}

class _StaggeredGridPageState extends State<StaggeredGridPage> {

  var  noteDB = NotesDBHandler();
  List<Map<String, dynamic>> _allNotesInQueryResult = [];
  viewType notesViewType ;

@override
  void initState() {
    super.initState();
    this.notesViewType = widget.notesViewType;
  }

@override void setState(fn) {
    super.setState(fn);
    this.notesViewType = widget.notesViewType;
  }

  @override
  Widget build(BuildContext context) {
    GlobalKey _stagKey = GlobalKey();
    if(CentralStation.updateNeeded) {  retrieveAllNotesFromDatabase();  }
    return Container(child: Padding(padding:  _paddingForView(context) , child:
      new StaggeredGridView.count(key: _stagKey,
        crossAxisSpacing: 6, mainAxisSpacing: 6,
        crossAxisCount: _colForStaggeredView(context),
        children: List.generate(_allNotesInQueryResult.length, (i){ 
           return _tileGenerator(i); }), staggeredTiles: _tilesForView() ,
          ),
        )
      );
  }

  int _colForStaggeredView(BuildContext context) {
      if (widget.notesViewType == viewType.List) { return 1; }
      // для ширины больше 600 размещаем 3 заметки по горизонтали
      return MediaQuery.of(context).size.width > 600 ? 3 : 2 ;
  }

 List<StaggeredTile> _tilesForView() { // Создание шахматного представления 
  return List.generate(_allNotesInQueryResult.length,(index){ return StaggeredTile.fit( 1 ); }
  ) ;
}

EdgeInsets _paddingForView(BuildContext context){
  double width = MediaQuery.of(context).size.width;
  double padding ;
  double top_bottom = 8;
  if (width > 500) {
    padding = ( width ) * 0.05 ; // 5% ширины с двух сторон
  } else {
    padding = 8;
  }
  return EdgeInsets.only(left: padding, right: padding, top: top_bottom, bottom: top_bottom);
}


 MyStaggeredTile _tileGenerator(int i){
 return MyStaggeredTile(  Note(
      _allNotesInQueryResult[i]["id"],
      _allNotesInQueryResult[i]["title"] == null ? "" : utf8.decode(_allNotesInQueryResult[i]
         ["title"]),
      _allNotesInQueryResult[i]["content"] == null ? "" : utf8.decode(_allNotesInQueryResult[i]
         ["content"]),
     DateTime.fromMillisecondsSinceEpoch(_allNotesInQueryResult[i]["date_created"] * 1000),
     DateTime.fromMillisecondsSinceEpoch(_allNotesInQueryResult[i]["date_last_edited"] * 1000),
      Color(_allNotesInQueryResult[i]["note_color"] ))
  );
  }

  void retrieveAllNotesFromDatabase() {
  // запрос всех заметок из БД, упорядоченных по последнему редактированию. 
  // Архивные заметки исключаются.
    var _testData = noteDB.testSelect();
    _testData.then((value){
        setState(() {
          this._allNotesInQueryResult = value;
          CentralStation.updateNeeded = false;
        });
    });
  }
}

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

Для постраничной навигации у Flutter есть Navigator (как segue в iOS или Intent в Android).

import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import '../ViewControllers/NotePage.dart';
import '../Models/Note.dart';
import '../Models/Utility.dart';

class MyStaggeredTile extends StatefulWidget {
  final Note note;
  MyStaggeredTile(this.note);
  @override
  _MyStaggeredTileState createState() => _MyStaggeredTileState();
}

class _MyStaggeredTileState extends State<MyStaggeredTile> {

  String _content ;
  double _fontSize ;
  Color tileColor ;
  String title;

  @override
  Widget build(BuildContext context) {

    _content = widget.note.content;
    _fontSize = _determineFontSizeForContent();
    tileColor = widget.note.note_color;
    title = widget.note.title;

    return GestureDetector(
      onTap: ()=> _noteTapped(context),
      child: Container(
      decoration: BoxDecoration(
        border: tileColor == Colors.white ?   Border.all(color: CentralStation.borderColor) : 
            null,
          color: tileColor,
          borderRadius: BorderRadius.all(Radius.circular(8))),
      padding: EdgeInsets.all(8),
      child:  constructChild(),) ,
    );
  }

  void _noteTapped(BuildContext ctx) {
    CentralStation.updateNeeded = false;
    Navigator.push(ctx, MaterialPageRoute(builder: (ctx) => NotePage(widget.note)));
  }

  Widget constructChild() {
    List<Widget> contentsOfTiles = [];

    if(widget.note.title.length != 0) {
      contentsOfTiles.add(
        AutoSizeText(title,
          style: TextStyle(fontSize: _fontSize,fontWeight: FontWeight.bold),
          maxLines: widget.note.title.length == 0 ? 1 : 3,
          textScaleFactor: 1.5,
        ),
      );
      contentsOfTiles.add(Divider(color: Colors.transparent,height: 6,),);
    }

    contentsOfTiles.add(
        AutoSizeText(
          _content,
          style: TextStyle(fontSize: _fontSize),
          maxLines: 10,
          textScaleFactor: 1.5,)
    );
    return Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.start,
        children:     contentsOfTiles
    );
  }

 double _determineFontSizeForContent() {
    int charCount = _content.length + widget.note.title.length ;
    double fontSize = 20 ;
    if (charCount > 110 ) { fontSize = 12; }
    else if (charCount > 80) {  fontSize = 14;  }
    else if (charCount > 50) {  fontSize = 16;  }
    else if (charCount > 20) {  fontSize = 18;  }
    return fontSize;
  }
}

Плитка выглядит примерно так:

Теперь реализуйте вьюху для редактирования/создания заметки, обладающую различными функциями в AppBar, например: отмена, архивирование и т. д. Больше дополнительных действий можно вызвать в модальном блоке: поделиться, копировать, удалить и вызов горизонтально-прокручиваемого выбора цвета для смены фона конкретной заметки.

Виджеты NotePage, BottomSheet и ColorSlider разнесите по разным классам и файлам для чистого управляемого кода. Чтобы изменить цвет, выбранный пользователем из ColorSlider, нужно обновить состояние объекта. Подключите виджеты через callback-функции, чтобы они реагировали на изменения, и могли самостоятельно обновляться.

import 'package:flutter/material.dart';

class ColorSlider extends StatefulWidget {
  final void Function(Color)  callBackColorTapped ;
  final Color noteColor ;
  ColorSlider({@required this.callBackColorTapped, @required this.noteColor});
  @override
  _ColorSliderState createState() => _ColorSliderState();
}

class _ColorSliderState extends State<ColorSlider> {

  final colors = [
    Color(0xffffffff), // classic white
    Color(0xfff28b81), // light pink
    Color(0xfff7bd02), // yellow
    Color(0xfffbf476), // light yellow
    Color(0xffcdff90), // light green
    Color(0xffa7feeb), // turquoise
    Color(0xffcbf0f8), // light cyan
    Color(0xffafcbfa), // light blue
    Color(0xffd7aefc), // plum
    Color(0xfffbcfe9), // misty rose
    Color(0xffe6c9a9), // light brown
    Color(0xffe9eaee)  // light gray
  ];

   final Color borderColor = Color(0xffd3d3d3);
   final Color foregroundColor = Color(0xff595959);

  final _check = Icon(Icons.check);
  Color noteColor;
  
  int indexOfCurrentColor;
  @override void initState() {
    super.initState();
    this.noteColor = widget.noteColor;
    indexOfCurrentColor = colors.indexOf(noteColor);
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      scrollDirection: Axis.horizontal,
      children:
      List.generate(colors.length, (index)
      {
        return
          GestureDetector(
              onTap: ()=> _colorChangeTapped(index),
              child: Padding(
                  padding: EdgeInsets.only(left: 6, right: 6),
                  child:Container(
                  child: new CircleAvatar(
                    child: _checkOrNot(index),
                    foregroundColor: foregroundColor,
                    backgroundColor: colors[index],
                  ),
                  width: 38.0,
                  height: 38.0,
                  padding: const EdgeInsets.all(1.0), // border width
                  decoration: new BoxDecoration(
                    color: borderColor,
                    shape: BoxShape.circle,
                  )
              ) )
          );
      })
      ,);
  }

  void _colorChangeTapped(int indexOfColor) {
    setState(() {
      noteColor = colors[indexOfColor];
      indexOfCurrentColor = indexOfColor;
      widget.callBackColorTapped(colors[indexOfColor]);
    });
  }

  Widget _checkOrNot(int index){
    if (indexOfCurrentColor == index) {
      return _check;
    }
    return null;
  }

}

Понравилось приложение? Поделитесь своими впечатлениями ;)

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ