1. Старт проекта
1. Создайте новый флаттер-проектПоначалу понятно, что на нативной стороне используется язык java или kotlin, objectC или swift, иначе изменить потом будет проблематично, если вы выберете не тот.
2. Выберите собственный пакет управления маршрутизацией и состоянием., определить структуру проекта В моем случае для первого проектафлюро и провайдерУправление маршрутами, управление состоянием, каталог проекта, новое хранилище и папки маршрутов, хранящиеся в поставщике файлов моделей и файлов конфигурации Fluro, для второго проекта найден Getx, набор инъекций зависимостей, управление маршрутами, пакет управления состоянием!Изменена структура каталогов проекта много с ним, в целом хорошо организовано и аккуратно
Первый
второй
3. Общая конфигурация пакета, Например, Getx нужно заменить внешнее MaterialApp на GetMaterialApp, flutter_screenutil нужно инициализировать масштаб дизайна, глобальный импорт провайдера, пакет Dio, перехватчик, сетевую подсказку и т. д.
2. Глобальная конфигурация
1. Повторное использование стиля
1. Поскольку некоторые небольшие виджеты флаттера можно многократно использовать, а приложение должно иметь единый стиль, предустановленные файлы, такие как стиль и цвет, помещаются в папку команд.
colors.dart, вы можете настроить статические классы для хранения общих цветов темы.
styles.dart, вы можете предустановить стили шрифта, стили разделительной линии, различные интервалы с фиксированными значениями.
2. Рекомендуется глобально управлять внутренним интерфейсом, который является чистым, простым в обслуживании и удобным.
3. папка с моделями
Папка моделей может не часто использоваться на веб-стороне, но я думаю, что она очень необходима в дарте.Строка Json, возвращаемая серверной частью, должна быть отформатирована как класс через класс модели, что может значительно уменьшить орфографические ошибки или ошибки типа. , , Синтаксис также более удобен в использовании, чем ['']. Порекомендовать веб-сайтquickTypeВведите объект json и выведите класс модели одним щелчком мыши!
4. Заставлять ли горизонтальный и вертикальный экран?
Нужно настроить его в main.dart
// 强制横屏
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
5. Вам нужно изменить макет и стиль верхней и нижней строк состояния?
использоватьSystemUiOverlayStyle
а такжеSystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
настроить
6. Установите шрифт, чтобы он не следовал системе
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Container(color: Colors.white),
builder: (context, widget) {
return MediaQuery(
//设置文字大小不随系统设置改变
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget,
);
},
);
}
}
7. Международная конфигурация
Использование некоторых виджетов будет отображать английский язык, например диалоговое окно в стиле IOS, и отображать китайский язык.Это необходимо установить. Сначала вам нужна поддержка пакета,
flutter_localizations:
sdk: flutter
Введите пакет, а затем добавьте его в элемент конфигурации main.dart MetrialApp.
// 设置本地化,部分原生内容显示中文,例如长按选中文本显示复制、剪切
localizationsDelegates: [
GlobalMaterialLocalizations.delegate, //国际化
GlobalWidgetsLocalizations.delegate,//国际化
const FallbackCupertinoLocalisationsDelegate() // 这里是为了解决一个报错 看第 8 条
],
//国际化
supportedLocales: [
const Locale('zh', 'CH'),
const Locale('en', 'US'),
],
8. Использование CupertinoAlertDialog для сообщения об ошибке: геттер 'alertDialogLabel' был вызван при нулевом значении
Решение: Добавьте следующий класс в main.dart, а затем создайте его экземпляр в localizationsDelegates of MetrialApp. См. пункт 7.
class FallbackCupertinoLocalisationsDelegate
extends LocalizationsDelegate<CupertinoLocalizations> {
const FallbackCupertinoLocalisationsDelegate();
@override
bool isSupported(Locale locale) => true;
@override
Future<CupertinoLocalizations> load(Locale locale) =>
DefaultCupertinoLocalizations.load(locale);
@override
bool shouldReload(FallbackCupertinoLocalisationsDelegate old) => false;
}
9. ImageCache
Последняя версия обновления флаттера ограничивает верхний предел catchImage, 100 1000 МБ, но для бизнеса необходимо кэшировать больше, установите эту потребность
class ChangeCatchImage extends WidgetsFlutterBinding {
@override
createImageCache() *{*
Global.myImageCatche = ImageCache()
..maximumSize = 1000
..maximumSizeBytes = 1000 << 20; // 1000MB
return Global.myImageCatche;
}
}
Затем создайте экземпляр ChangeCatchImage() в main.dart runApp.
3. Бизнес-модуль
常见的业务模块代码分析,比如登录页,闪屏页,首页,退出登录等
1. Во-первых, используйте Getx
Папка — это бизнес-модуль, который управляет данными независимо и совместно использует данные посредством внедрения зависимостей.
Нетдовольно частоКомфортный
Включая уровень логического управления состоянием уровня управления данными уровня представления уровня компонента, текущий виджет повторного использования бизнеса записывается в папке
2. Модуль входа
В качестве входного портала приложения необходимы классные и красивые, что требует внимания к оптимизации производительности, а место входа, логика проверки должна иметь безопасный дизайн.
- Прежде всего, об оптимизации производительности анимации, наиболее важным моментом является точное обновление компонентов, которые необходимо изменить, мы можем просмотреть диапазон обновлений через инструмент devtool.
- Во-вторых, дизайн безопасности простым способом ограничивает количество входов в систему, запрещает простые пароли, шифрует передачу, проверяет токены и т. д., например, расширенная версия предотвращает ввод параметров, фильтрует конфиденциальные символы и т. д.
- Перед логином, верификацией аккаунта, верификацией пароля, обязательными пунктами и т.д., а затем запросом на вход, нужно добавить загрузку, кнопка отключена, антишейк не нужен
- После входа в систему сохраните основную информацию локального пользователя (могут быть проблемы с безопасностью, которые еще не исследованы), а затем при следующем входе определите, есть ли основная информация по умолчанию, и проверите время истечения срока действия, и токен , а затем неявно войти на домашнюю страницу
3. модуль заставки заставки
Страница подготовки домашней страницы приложения, вы можете встроить рекламу или настроить анимацию продвижения программного обеспечения, пригласить пропустить через три секунды Как добавить экран всплеск приложения изящно? На самом деле, он должен установить страницу инициализации в качестве всплеск в Main.dart, а затем пройти логику прыжка Определите, следует ли перейти на домашнюю страницу или войти на страницу регистрации Например, здесь я использую GetX, чтобы просто настроить его
4. Модуль руководства по эксплуатации
При первом использовании приложения или после крупного обновления часто имеется руководство по эксплуатации. В моем проекте используются два типа руководств по эксплуатации
Карта результатов Первый
второй
Оба основаны наoverlayEntry()
а такжеOverlay.of(context).insert(overlayEntry)
осуществленный
Второй использует пакет操作引导 flutter_intro: ^2.2.1
Закрепите GlobalKey виджета, чтобы получить информацию элемента, получить размер позиции и убедитесь, что положение выбора кадра является правильным. Внешняя маска создана с помощью Ollyentry (), как первый.
После создания показатьOverlay.of(context).insert(your_overlayEntry)
Переключитесь на следующую кнопку на определенной кнопке, например, нажмите «Я знаю», «Следующая страница» и т. д.
onPressed: () {
// 执行 remove 方法销毁 第一个overlayEntry 实例
overlayEntryTap.remove();
// 第二个
Overlay.of(context).insert(overlayEntryScale);
},
Что касается пакета flutter_intro, задействованного во второй реализации, вставьте мой код, вы можете обратиться к пабу за подробностями.
final intro = Intro(
// 一共有几步,这里就会创建2个GlobalKey,一会用到
stepCount: 2,
// 点击遮罩下一个
maskClosable: true,
// 高亮区域与 widget 的内边距
padding: EdgeInsets.all(0),
// 高亮区域的圆角半径
borderRadius: BorderRadius.all(Radius.circular(4)),
// use defaultTheme
widgetBuilder: StepWidgetBuilder.useDefaultTheme(
texts: ["点击添加收藏", "下拉添加书签"],
buttonTextBuilder: (currPage, totalPage) {
return currPage < totalPage - 1
? '我知道了 ${currPage + 3}/${totalPage + 2}'
: '完成 ${currPage + 3}/${totalPage + 2}';
},
),
);
......
// 这里用到key来绑定任意Widget
Positioned(
key: intro.keys[1],
top: 0,
right: 20,
...
)
......
5. Модуль чертежной доски CustomPaint
Карта результатов
Сначала я выбрал флаттер, потому что есть много потребностей в рисовании, мне нравится встроенная лыжа, эффективность рисования высокая и плавная, и она имеет согласованность с платформой. Результат - много ям
Давайте сначала поговорим о свиных ногахCustomPaint
Как следует из названия, это персонализированный компонент рисования, его задача состоит в том, чтобы создать для вас холст, как рисовать так, как вы хотите, давайте посмотрим, как его использовать напрямую. сначала отформатировать
- Во-первых, его нужно прописать в дереве виджетов.
Глядя на список параметров, я обнаружил, что рисовальщик получает объект CustomePainter. Здесь можно обратить внимание на дочерний параметр. Очень странно, что интерфейс рисования весь помещается в рисовальщике. Какой смысл оставлять дочерний элемент? Дочерний виджет, но не участвует в обновлении отрисовки, популярный момент, что я рисую перетекающее облако, но есть стационарное солнце, положение облака перерисовывается в реальном времени, тогда виджет солнца можно разместить в ребенок для оптимизации производительностиContainer( child: CustomPaint( painter: myPainter(), ),
- Далее мы создаем myPainter()
class myPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 创建画笔
final Paint paint = Paint();
// 绘制一个圆
canvas.drawCircle(Offset(50, 50), 5, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
- Здесь нам нужно реализовать две важные функции (как в коде выше)
Первая функция paint() поставляется с холстом объекта холста и размером холста, так что мы можем использовать встроенную функцию рисования Canvas! Функция рисования должна получить объект Paint brush.
Этот объект кисти используется для установки цвета кисти, толщины, стиля, стиля соединения и т. д.
Paint paint = Paint();
//设置画笔
paint ..style = PaintingStyle.stroke
..color = Colors.red
..strokeWidth = 10;
Вторая функция shouldRepaint(), как видно из названия, оценивает необходимость перерисовки. Если она возвращает false, перерисовывать не нужно. Она выполняет paint() только один раз. Если возвращает true, перерисовывает всегда. в соответствии с реальными потребностями. Если вам нужно нарисовать что-то вроде анимации гистограммы, которая становится все выше и выше в зависимости от значения
Код выглядит следующим образом (вы можете использовать его при перемещении)
class BarChartPainter extends CustomPainter {
final List<double> datas;
final List<double> datasrc;
final List<String> xAxis;
final double max;
final Animation<double> animation;
BarChartPainter(
{@required this.xAxis,
@required this.datas,
this.max,
this.datasrc,
this.animation})
: super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
_darwBars(canvas, size);
_drawAxis(canvas, size);
}
@override
bool shouldRepaint(BarChartPainter oldDelegate) => true;
// 绘制坐标轴
void _drawAxis(Canvas canvas, Size size) {
final double sw = size.width;
final double sh = size.height;
// 使用 Paint 定义路径的样式
final Paint paint = Paint()
..color = Colors.grey
..style = PaintingStyle.stroke
..strokeWidth = 1
..strokeCap = StrokeCap.round;
// 使用 Path 定义绘制的路径,从画布的左上角到左下角在到右下角
final Path path = Path()
..moveTo(40, sh)
..lineTo(sw - 20, sh);
// 使用 drawPath 方法绘制路径
canvas.drawPath(path, paint);
}
// 绘制柱形
void _darwBars(Canvas canvas, Size size) {
final sh = size.height;
final paint = Paint()..style = PaintingStyle.fill;
final double _barWidth = size.width / 20;
final double _barGap = size.width / 25 * 2 + 18;
final double textFontSize = 14.0;
for (int i = 0; i < datas.length; i++) {
final double data = datas[i] * ((size.height - 15) / max);
final top = sh - data;
// 矩形的左边缘为当前索引值乘以矩形宽度加上矩形之间的间距
final double left = i * _barWidth + (i * _barGap) + _barGap;
// 使用 Rect.fromLTWH 方法创建要绘制的矩形
final rect = RRect.fromLTRBAndCorners(
left, top, left + _barWidth, top + data,
topLeft: Radius.circular(5), topRight: Radius.circular(3));
// 使用 drawRect 方法绘制矩形
final offset = Offset(
left + _barWidth / 2 - textFontSize / 2 - 8,
top - textFontSize - 5,
);
paint.color = Color(0xFF59C8FD);
//绘制bar
canvas.drawRRect(rect, paint);
// 使用 TextPainter 绘制矩形上放的数值
TextPainter(
text: TextSpan(
text: datas[i] == 0.0 ? '' : datas[i].toStringAsFixed(0) + " %",
style: TextStyle(
fontSize: textFontSize,
color: paint.color,
// color: Colours.gray_33,
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: textFontSize * data.toString().length,
)
..paint(canvas, offset);
final xData = xAxis[i];
final xOffset = Offset(left, sh + 6);
// 绘制横轴标识
TextPainter(
textAlign: TextAlign.center,
text: TextSpan(
text: '$xData' != ''
? '$xData'.substring(0, 4) + '-' + '$xData'.substring(4, 6)
: '',
style: TextStyle(
fontSize: 12,
color: Colors.black,
),
),
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: size.width,
)
..paint(canvas, xOffset);
}
}
}
Ладно, customPainter, в принципе так и используется, вернемся к теме, нарисуем чертежную доску На самом деле общая задача достаточно сложная, вот один анализ, а остальные интегрированы.
Возьмем, к примеру, самый классический карандашный рисунок. На самом деле очень просто просто реализовать карандашный рисунок, даже с краем пера, похожий на подпись, есть много онлайн-уроков
Общая идея состоит в том, чтобы добавить GestureDetector, в основном использовать событие onPanUpdate для запуска действия рисования в реальном времени и рисовать его с помощью холста. Рисование просто, но оптимизация производительности сложна
Вот оптимальное решение, которое я тестировал напрямую Сначала соедините новую точку координат с предыдущей точкой в линию, можно соединить еще несколько за раз, т.е.Похоже на дросселирование, Например, когда panUpate запускает пять обратных вызовов, сначала соедините эти пять точек в линию, а затем равномерно нарисуйте линию в шестой раз (если есть лучший способ, надеюсь, вы меня просветите!) Детально отдельный проект разберём позже
6. модуль обмена мгновенными сообщениями websocket
Карта результатов
Выполняйте только самые основные функции работы с текстом, изображениями и файлами.
简单把各项功能实现说一下,以后会详细整理,并加入音视频
-
О веб-сокетах
В первую очередь его нужно подключить к вебсокету, используя пакет
web_socket_channel
Затем инициализируйте веб-сокет
// 初始化websocket initWebsocket(){ Global.channel = IOWebSocketChannel.connect( WebsocketUrl, // websocket地址 //这个参数注意一下, 这里是每隔10000毫秒发送ping,如果间隔10000ms没收到pong,就默认断开连接 //所以收网速等影响,这个参数如果太小,比如100ms就会,出现过一阵子自己断开连接的问题,参考实际设置 pingInterval: Duration(milliseconds: 10000), ); // 监听服务端消息 Global.channel.stream.listen( (mes) => onMessage(mes),// 处理消息 onError: (error) => {onError(error)}, // 连接错误 onDone: () => {onDone()}, // 断开连接 cancelOnError: true //设置错误时取消订阅 ); }
-
обработать сообщение
При входе на страницу для загрузки сообщений чата вам все равно придется использовать ListView.build() для длинных списков.Опыт намного лучше, когда есть много сообщений.
Каждый раз, когда отслеживается новое сообщение, оно добавляется в массив, и представление обновляется.Этот шаг имеет разные методы управления состоянием.
Здесь трудно добавить сообщение
Первые подчетыре случая а. В нижней части своих волос и ListView, б. ListView собственные волосы, но не в нижней части, в. Нижние и исходящие сообщения других, г. Не в нижней части волос других.
a и b, c: прокрутите вниз, пока оно отправлено вами, медленно прокручивайте, когда оно находится внизу, такое ощущение, что сообщение подтягивается
// 这里要确保在LIstView中已经加入并渲染完成新消息 // 我的处理就是加了一个延迟,再滚动 // 直接滚动到ListView底部 scrollController.jumpTo(scrollController.position.maxScrollExtent); // 滚动到某个确定的元素 Scrollable.ensureVisible( // 给每一条消息对象加GlobalKey,获取到当前上下文 state.messageList[index].key.currentContext, duration: Duration(milliseconds: 100), curve: Curves.easeInOut, // 控制对齐方式 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
d : В этом случае создается напоминание, аналогичное WeChat.
Но нажмите, чтобы найти сообщение, есть яма, потому что используется listView.build, когда вы прокручиваете сообщение выше, сообщение ниже не загружается, поэтому currentContext не может быть получен, потому что элемент не отображается, а позиционирование неупорядочено В настоящее время идеальное решение — отображать все записи ниже при прокрутке вверх и очищать их последовательно при прокрутке вниз.
-
документы и фотографии
использовал несколько пакетов
file_picker, open_file, path_provider
file_picker, используется для выбора файлов и изображений, вы можете настроить множественный выбор, вам нужно добавить разрешения в файл конфигурации Android
Open_File, похоже на WECHAT, нажав на файл, сначала загрузите, а затем вызовите локальную программу по умолчанию, чтобы открыть файл
path_provider, предоставляет системный доступный путь, используемый для создания каталога файлов
Конкретное использование заключается в следующем
// 访问不到app私有目录 导致我卡了很久... // Directory dirloc = await getTemporaryDirectory(); // 访问外置存储目录 final dirPath = await getExternalStorageDirectory(); Directory file = Directory(dirPath.path + "/" + "temFile"); // 不存在就创建目录 try { bool exists = await file.exists(); if (!exists) { await file.create(); // 创建了temFile 目录 用于缓存文件 } } catch (e) { print(e); } // 下边就很关键了 可能不同的后端数据不同实现 // 请求存储权限 需要一个包 permission_handler: ^6.1.1 Permission.storage.request().then((value) async { //如果许可 if (value.isGranted) { // 判断文件是否存在 wjmc 就是一个变量存储着文件名 File _tempFile = File(file.path + '/' + wjmc); if (!await _tempFile.exists()) { try { //1、创建文件地址 带扩展 我用了getx cstate // final ChatState cState = Get.find<ChatLogic>().state; // 这是一个通用组件 不管理数据 从chatState里注入 cState.path = file.path + '/' + wjmc; //2、下载文件到本地 cState.downloading.value = nbbh; var response = await dio.get(fileUrl); Stream resp = response.data.stream; //4. 转为uint8类型 final Uint8List bytes = await consolidateHttpClientResponseBytes(resp); //5. 转为List<int> 并写入文件 final List<int> _filelist = List.from(bytes); final filePath = File(cState.path); await filePath.writeAsBytes(_filelist, mode: FileMode.append, flush: true); } catch (e) { print(e); } } cState.downloading.value = ''; // 6.这里可以记录位置,保存path到一个数组里,退出软件之后清除缓存 我没做 open(cState.path); } }); // 读取Stream 文件流 处理为Uint8List Future<Uint8List> consolidateHttpClientResponseBytes(Stream response) { final Completer<Uint8List> completer = Completer<Uint8List>.sync(); final List<List<int>> chunks = <List<int>>[]; int contentLength = 0; response.listen((chunk) { chunks.add(chunk); contentLength += chunk.length; }, onDone: () { final Uint8List bytes = Uint8List(contentLength); int offset = 0; for (List<int> chunk in chunks) { bytes.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; } completer.complete(bytes); }, onError: completer.completeError, cancelOnError: true); return completer.future; } void open(path) { // 下载完成 准备打开文件 showCupertinoDialog( context: Get.context,// 舒服 builder: (context) { return Material( color: Colors.transparent, child: CupertinoAlertDialog( title: Padding( padding: EdgeInsets.only(bottom: 10), child: Text("提示"), ), content: Padding( padding: EdgeInsets.only(left: 5), child: Text("是否打开文件?"), ), actions: <Widget>[ CupertinoButton( child: Text( "取消", style: TextStyle(color: Colours.gray_88), ), onPressed: () { Get.back(); }, ), CupertinoButton( child: Text("确定"), onPressed: () async { Get.back(); // 直接调用就能打开,会通过系统默认程序打开 比如.doc 默认用office等. await OpenFile.open( cState.path, ); }), ]), ); }, ); }
Xiaoyu для аудио и видео легко подключается, но нет Flutter SDK, его можно запаковать только на базе Android, и об этом я расскажу позже.