Создание приложения для заметок на 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; } }