Как второй частью серии статей, эта статья подробно покажет вам: как создать универсальную структуру приложения на Flutter с часто используемыми функциями, чтобы быстро разрабатывать полноценное приложение на Flutter.
Примечание: все коды в этой статье доступны в GSYGithubAppFlutter. Примеры кода можно найти там. После прочтения данной статьи вы сможете легко достичь следующего результата. Основные знания рекомендуется получить из первой части.
Структура содержимого данной статьи представлена ниже. Она основана на трёх частях: основные компоненты, данные модули, другие функции. Каждый маленький модуль внутри больших блоков включает реализацию функциональности и описание проблем, с которыми столкнулся автор во время работы над ними. Конечной целью этого сериала является: дать вам возможность ощутить радость от использования Flutter! Так что давайте начнём наш путь вместе!
Основные компоненты — это своего рода базовая практика.### 1. Реализация компонента Tabbar
Требование создания страницы с Tabbar довольно распространено. В Flutter: Scaffold + AppBar + Tabbar + TabbarView являются простым способом создания такой страницы. Однако после добавления AutomaticKeepAliveClientMixin
, который используется для поддержания активности страницы, ранние проблемы, такие как #11895, стали причиной аварийного завершения программы. Только после исправления этих проблем в версии flutter v0.5.7 sdk, они не были полностью решены, поэтому было принято решение изменить подход реализации. (Проблема была окончательно исправлена в версии Yöntem 1.9.1 stable).
На данный момент автор использует Scaffold + Appbar + Tabbar + PageView для создания требуемого эффекта, тем самым решив вышеупомянутую проблему. Давайте сразу приступим к коду. Как видно, Tabbar Widget является StatefulWidget
. Поэтому мы сначала реализуем его State
:
class _GSYTabBarState extends State<GSYTabBarWidget> with SingleTickerProviderStateMixin {
// ... упущенные несущественные строки кода
@override
void initState() {
super.initState();
// При инициализации создаем контроллер
// Используем with SingleTickerProviderStateMixin для создания анимационного эффекта.
_tabController = new TabController(vsync: this, length: _tabItems.length);
}
@override
void dispose() {
/// При уничтожении страницы, уничтожаем контроллер
_tabController.dispose();
super.dispose();
}
``` @override
Widget build(BuildContext context) {
/// Кнопка "Bottom TabBar" режима
return new Scaffold(
/// Установите боковое выдвижное меню drawer, если не требуется, можно не указывать
drawer: _drawer,
/// Установите плавающую кнопку действия, если не требуется, можно не указывать
floatingActionButton: _floatingActionButton,
/// Шапка
appBar: new AppBar(
backgroundColor: _backgroundColor,
title: _title,
),
/// Основной контент страницы, PageView, используется для отображения страниц, соответствующих каждому Tab
body: new PageView(
/// Обязательный контроллер, синхронизируется с контроллером tabBar
controller: _pageController,
/// Каждый Tab соответствует основному контенту страницы, это List<Widget>
children: _tabViews,
onPageChanged: (index) {
/// Возврат при касании страницы, используемый для синхронизации состояния выбранного Tab
_tabController.animateTo(index);
},
),
/// Нижняя навигационная панель, то есть TabBar
bottomNavigationBar: new Material(
color: _backgroundColor,
/// Элемент управления TabBar
child: new TabBar(
/// Обязательный контроллер, синхронизируется с контроллером pageView
controller: _tabController,
/// Каждый TabItem, это List<Widget>
tabs: _tabItems,
/// Цвет нижней линии выбора Tab
indicatorColor: _indicatorColor,
),
));
}
}Как показано в приведённом выше коде, это страница с режимом "Bottom TabBar". TabBar и PageView между собой синхронизируются через `_pageController` и `_tabController`, а эффект анимации Tab реализуется с помощью `SingleTickerProviderStateMixin`. Из кода видно:* При горизонтальном свайпе `PageView`, вызывается метод `_tabController.animateTo(index)` через `onPageChanged` для синхронизации состояния `TabBar`.
* В списке `_tabItems`, каждый клик по `TabBarItem` синхронизирует состояние `PageView` через `_pageController`. Вышеуказанный код все еще не содержит события нажатия кнопки `TabBarItem`, поскольку эта часть была реализована внешним образом. Конечно, можно также упаковать управление прямо внутри него и передать конфигурационные данные для прямого отображения, в зависимости от ваших потребностей.
Код внешнего вызова представлен ниже: при каждом нажатии на `Tabbar` происходит переход на конкретную страницу через `pageController.jumpTo`. Координаты, которые требуется пройти, равны: **текущий размер экрана, умноженный на номер индекса**.
```markdown
class _TabBarBottomPageWidgetState extends State<TabBarBottomPageWidget> {
final PageController pageController = new PageController();
final List<String> tab = ["Динамика", "Тренды", "Мои"];
// Рender нижней части Tab
_renderTab() {
List<Widget> list = new List();
for (int i = 0; i < tab.length; i++) {
list.add(new FlatButton(onPressed: () {
// При каждом нажатии на Tabbar происходит переход на конкретную страницу через jumpTo
// Координаты, которые требуется пройти, равны: текущий размер экрана * номер индекса.
topPageControl.jumpTo(MediaQuery.of(context).size.width * i);
}, child: new Text(
tab[i],
maxLines: 1,
)));
}
return list;
}
// Рender страниц, соответствующих Tab
_renderPage() {
return [
new TabBarPageFirst(),
new TabBarPageSecond(),
new TabBarPageThree(),
];
}
@override
Widget build(BuildContext context) {
// Страница с `Tabbar` и `Scaffold`
return new GSYTabBarWidget(
type: GSYTabBarWidget.BOTTOM_TAB,
// Рender табов
tabItems: _renderTab(),
// Рender страниц
tabViews: _renderPage(),
topPageControl: pageController,
backgroundColor: Colors.black45,
indicatorColor: Colors.white,
title: new Text("GSYGithubFlutter"));
}
}
Если вы остановитесь здесь, вы заметите, что при переключении между страницами путем нажатия, подстраницы StatefulWidget
будут вызывать initState()
каждый раз. Это, конечно, не то, что мы хотели бы видеть, поэтому на этом этапе вам потребуется AutomaticKeepAliveClientMixin
.
Каждый лист Tab
должен использовать состояние своего соответствующего StatefulWidget
с with AutomaticKeepAliveClientMixin
, затем переопределить @override bool get wantKeepAlive => true;
для достижения эффекта, при котором виджет не перестраивается при навигации. Результат представлен на следующем изображении.
Так как нижняя панель навигации уже реализована, давайте завершим верхнюю панель навигации также. Различия между нижней и верхней панелями навигации следующие:
bottomNavigationBar
компонента Scaffold
.bottom
компонента AppBar
, то есть ниже строки заголовка.Для реализации обычного верхнего меню с панелями мы добавляем атрибут isScrollable: true
к верхнему TabBar
. Пример приведён ниже:```dart
return new Scaffold(
/// Установка выдвигающегося меню drawer; если не требуется, можно не указывать
drawer: _drawer,
/// Установка кнопки действия; если не требуется, можно не указывать
floatingActionButton: _floatingActionButton,
/// Заголовок
appBar: new AppBar(
backgroundColor: _backgroundColor,
title: _title,
/// Контроллер панели навигации
bottom: new TabBar(
/// При вертикальной ориентации, панель навигации может прокручиваться
isScrollable: true,
/// Обязательный контроллер, синхронизируется со страницей PageView
controller: _tabController,
/// Каждый элемент панели навигации — это список виджетов List
tabs: _tabItems,
/// Цвет линии выбора элемента панели навигации
indicatorColor: _indicatorColor,
),
),
/// Основное содержимое страницы, PageView, используется для отображения страниц, связанных с панелями навигации
body: new PageView(
/// Обязательный контроллер, синхронизируется с контроллером панели навигации
controller: _pageController,
/// Связанные страницы, это список виджетов List
children: _tabViews,
/// Обработчик события изменения страницы, используется для синхронизации состояния выбора панели навигации
onPageChanged: (index) {
_tabController.animateTo(index);
},
),
);
В приложении с панелями навигации часто возникает необходимость управления дочерними страницами из родительского окна. Для этого используются глобальные ключи (`GlobalKey`). Например, `GlobalKey<PageOneState> stateOne = new GlobalKey<PageOneState>();`. Через объект `globalKey.currentState` можно вызвать открытые методы из `PageOneState`. Важно отметить, что экземпляр `GlobalKey` должен быть уникален во всем приложении.
### 2. Вертикальное и горизонтальное свайпы для обновления данных
*Необходимый компонент.*
В Flutter существует встроенный компонент `RefreshIndicator` для выполнения нижнего свайпа для обновления данных; одновременно мы можем использовать `ScrollController` для слушания событий свайпов и добавить один элемент в конец списка `ListView`, чтобы показать загрузку новых данных при вертикальном свайпе вниз. Как показано ниже в коде, можно легко реализовать возможность прокрутки для обновления с помощью компонента `RefreshIndicator`. Важно отметить, что **можно использовать `GlobalKey<RefreshIndicatorState>` для предоставления состояния `RefreshIndicatorState`, таким образом внешний код может вызвать метод `globalKey.currentState.show()` через `GlobalKey`, чтобы активировать состояние обновления и запустить `onRefresh`**.**Загрузка большего количества данных при прокрутке вниз** осуществляется методом `_getListCount()`, который увеличивает количество элементов, необходимых для отображения, на основе существующих данных, тем самым обеспечивая это для `ListView`. Наконец, **состояние прокрутки контролируется с помощью `ScrollController`, который слушает достижение нижней границы экрана и запускает `onLoadMore`**.Как показано ниже в коде, метод `_getListCount()` также позволяет настроить пустую страницу, заголовок и другие часто используемые эффекты. Это достигается путём изменения фактического количества элементов и рендера элементов внутри компонента.
```markdown
Класс `_GSYPullLoadWidgetState` расширяет состояние `GSYPullLoadWidget`.
```dart
class _GSYPullLoadWidgetState extends State<GSYPullLoadWidget> {
// ...
final ScrollController _scrollController = new ScrollController();
@override
void initState() {
// Добавление прослушивания событий прокрутки
_scrollController.addListener(() {
// Определение, находится ли текущее положение прокрутки в конце списка
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
if (this.onLoadMore != null && this.control.needLoadMore) {
this.onLoadMore();
}
}
});
super.initState();
}
// Возврат фактического количества элементов в списке согласно конфигурационному состоянию
_getListCount() {
// Определение необходимости наличия заголовка
if (control.needHeader) {
// Если требуется заголовок, используется первый элемент как заголовок ListView
// Когда список содержит более одного элемента, общее количество элементов увеличивается на два
return (control.dataList.length > 0) ? control.dataList.length + 2 : control.dataList.length + 1;
} else {
// Если заголовок не требуется, в случае отсутствия данных всегда возвращается одно значение для отображения пустой страницы
if (control.dataList.length == 0) {
return 1;
}
}
}
}
``````markdown
### 3. Кнопка загрузки
В предыдущем разделе мы реализовали эффект прокрутки вниз для загрузки новых данных, что требует отображения состояния загрузки. По умолчанию система предоставляет компоненты `CircularProgressIndicator`, но нам с высокими требованиями этого может оказаться недостаточно. В этом месте рекомендуется использовать стороннюю библиотеку загрузки [flutter_spinkit](https://pub.flutter-io.cn/packages/flutter_spinkit), которая позволяет легко конфигурировать множество различных стилей загрузки.
Продолжим реализацию метода `_buildProgressIndicator`, используя библиотеку `flutter_spinkit` для создания уникального стиля загрузки.
```dart
/// Виджет для отображения прогресса загрузки
Widget _buildProgressIndicator() {
/// Определяет, требуется ли отображение загрузки при прокрутке вниз
Widget bottomWidget = control.needLoadMore
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
/// Контейнер с анимацией загрузки
SpinKitRotatingCircle(color: Color(0xFF24292E)),
SizedBox(width: 5.0),
/// Текстовое сообщение о загрузке
Text(
'Загрузка...',
style: TextStyle(
color: Color(0xFF121917),
fontSize: 14.0,
fontWeight: FontWeight.bold),
),
],
)
/// Если загрузка не требуется
: Container();
}
``` return Padding(
padding: const EdgeInsets.all(20.0),
child: Center(child: bottomWidget),
);
}
Векторные иконки являются незаменимыми для меня, так как они позволяют легко менять цвет и масштабировать до любого размера без потери качества. Векторные иконки обычно используются путем импорта TTF файла шрифтов. В Flutter это можно сделать через компонент Icon
.
По умолчанию Flutter предоставляет множество иконок через класс Icons
. Вы можете использовать эти иконки напрямую, но также рекомендую использовать библиотеку iconfont
от Alibaba.
Далее представлен пример использования этих иконок:
# pubspec.yaml
fonts:
- family: wxcIconFont
fonts:
- asset: static/font/iconfont.ttf
// Пример использования иконок
Tab(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.list, size: 16.0),
Text('Тренд'),
],
),
),
Tab(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
IconData(0xe6d0, fontFamily: 'wxcIconFont'),
size: 16.0,
),
Text('Моё'),
],
),
),
Это позволяет вам использовать как встроенные иконки Flutter, так и пользовательские иконки из вашего TTF файла.### 5. Переход между маршрутамиПереход между страницами в Flutter осуществляется с помощью Navigator
. Маршрутные переходы могут быть как с передачей параметров, так и без передачи параметров. Безпараметрический переход проще всего реализуется через таблицу маршрутов MaterialApp
; а при передаче параметров они передаются через конструктор целевой страницы. Вот несколько наиболее часто используемых методов:> Начиная с новых версий, можно использовать параметр arguments
вместе с pushNamed
, чтобы передать данные, а затем получить их на новой странице через ModalRoute.of(context).settings.arguments
.
/// Переход без передачи параметров
Navigator.pushNamed(context, routeName);
/// Переход на новую страницу и замена текущей, например, переход с экрана входа на главный экран
Navigator.pushReplacementNamed(context, routeName);
/// Переход на новый маршрут и закрытие всех предыдущих страниц данного маршрута
Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/'));
/// Переход с передачей параметров и слушанием ответа
Navigator.push(context, new MaterialPageRoute(builder: (context) => new NotifyPage())).then((res) {
/// Обработка полученного ответа
});
При этом видно, что push
возвращает объект типа Future
, который вызывается при возврате страницы. То есть вы можете передать параметры при использовании pop
, а затем обработать результат возвращения в блоке Future
.
@optionalTypeArgs
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
return Navigator.of(context).push(route);
}
Данные — это король, но, возможно, не тот самый соседний старик?
/// Создаем объект сетевого запроса, лучше всего сделать это глобальным одиночкой для dio
Dio dio = new Dio();
Response response;
try {
/// Отправляем запрос
/// URL адрес, данные запроса, обычно Map или FormData
/// options дополнительные конфигурации, такие как настройка времени ожидания, заголовков, типа запроса, типа данных ответа, хоста и т.д.
response = await dio.request(url, data: params, options: option);
} on DioError catch (e) {
/// Ошибка HTTP возвращается через объект, переданный в catch для DioError
}
В Flutter сериализация JSON имеет некоторые особенности, отличающиеся от JS. Например, при использовании вышеупомянутого Dio для сетевых запросов, если конфигурация указывает возврат данных в формате JSON, фактический ответ будет представлять собой объект типа Map. Однако использование ключей и значений этого Map может вызвать трудности в процессе разработки, поэтому вам потребуется преобразовать этот Map еще раз в реальный объект модели.
Поэтому был создан плагин json_serializable
. На сайте FlutterChina есть подробное руководство по его использованию, а здесь мы рассмотрим конкретные шаги использования.
dependencies:
# Ваши другие зависимости здесь
json_annotation: ^0.2.2
``````markdown
dev_dependencies:
# Ваши другие dev_dependencies здесь
build_runner: ^0.7.6
json_serializable: ^0.3.2
Как показано ниже:
После создания вашей модели, расширяйте её от Object
и пометьте класс с помощью @JsonSerializable()
.
Используйте with _$TemplateSerializerMixin
, чтобы передать метод fromJson
в реализацию файла Template.g.dart
.
Наконец, используйте команду flutter packages pub run build_runner build
для компиляции и автоматического генерирования объектов сериализации. (Лично предпочитаю после выполнения команды делать ручную компиляцию)
import 'package:json_annotation/json_annotation.dart';
/// Связь с файлом, позволяющая Template доступ к приватным методам в Template.g.dart
/// Template.g.dart - это файл, созданный командой. Его имя xx.g.dart, где xx - текущее имя dart файла
/// В Template.g.dart создаётся абстрактный класс _$TemplateSerializerMixin, который реализует _$TemplateFromJson метод
part 'Template.g.dart';
/// Отмечает класс как требующий реализации функциональности сериализации JSON
@JsonSerializable()
/// В файле 'xx.g.dart', по умолчанию, будет создан _$TemplateSerializerMixin, основываясь на имени класса Template
/// Поэтому имя текущего класса Template, и абстрактный класс будет _$TemplateSerializerMixin
class Template extends Object with _$TemplateSerializerMixin {
String name;
int id;
/// Переопределяет имя параметра с помощью JsonKey
@JsonKey(name: "push_id")
int pushId;
Template(this.name, this.id, this.pushId);
/// В файле 'xx.g.dart', по умолчанию, будет создан _$TemplateFromJson, основываясь на имени класса Template
factory Template.fromJson(Map<String, dynamic> json) => _$TemplateFromJson(json);
}
```
Вышеупомянутые действия создали следующий код в файле `Template.g.dart`, который позволяет нам использовать методы `Template.fromJson` и `toJson` для преобразования между сущностью и картой. В сочетании с `json.decode` и `json.encode` вы можете легко осуществлять взаимообмен между **строками, картами и сущностями**.
```dart
part of 'Template.dart';
Template _$TemplateFromJson(Map<String, dynamic> json) => new Template(
json['name'] as String, json['id'] as int, json['push_id'] as int);
abstract class _$TemplateSerializerMixin {
String get name;
int get id;
int get pushId;
Map<String, dynamic> toJson() =>
<String, dynamic>{'name': name, 'id': id, 'push_id': pushId};
}
Уверены, что в области фронтенд-разработки концепция Redux вам знакома. Как глобальный механизм управления состоянием, он идеально подходит для использования в Flutter. Если вы еще не слышали о нем, не волнуйтесь — это просто способ передачи данных между компонентами и синхронизации состояния. Поэтому если вы хотите узнать больше, то пакет flutter_redux ждет вас.
В Flutter состояние компонента хранится в объектах State
и изменяется через вызов setState
. Если вы используете flutter_redux
, как будет работать этот процесс?Например, можно хранить информацию о пользователе в хранилище Redux, и тогда при изменении информации о пользователе на одной странице все связанные компоненты будут автоматически обновлены благодаря Redux. Таким образом, состояние может быть использовано на нескольких страницах одновременно.Дополнительные детали о Redux мы рассмотрим более подробно в последующих разделах. Давайте теперь обратимся к использованию flutter_redux
.
В Redux используются такие понятия, как действие, редуктор и хранение:
Итак, ниже приведен пример кода, где мы создаем объект состояния для хранения необходимых данных, ключевой частью которого является UserReducer
.
/// Глобальное хранилище Redux, содержащее данные состояния
class GSYState {
/// Информация пользователя
User userInfo;
/// Конструктор
GSYState({this.userInfo});
}
/// Создание редуктора для использования в хранилище GSYState appReducer(GSYState state, action) { return GSYState( /// Применение UserReducer для связи userInfo внутри GSYState с действием userInfo: UserReducer(state.userInfo, action), ); }
Далее приведена реализация `UserReducer`, которая связывает логику обработки редьюсеров с определёнными действиями через `TypedReducer`. В конце используется `combineReducers` для создания объекта типа `Reducer<State>`, который затем применяется к хранилищу.
/// Объединение всех редьюсеров в одном, где TypedReducer связывает UpdateUserAction с редьюсерами
final UserReducer = combineReducers([
TypedReducer<User, UpdateUserAction>(_updateLoaded),
]);
```/// При получении действия UpdateUserAction вызывается _update_loaded
/// `_update_loaded` принимает новое значение `user_info` и возвращает его
User _update_loaded(User user, action) {
user = action.user_info;
return user;
}
/// Определение действия UpdateUserAction
для изменения состояния пользователя
/// Вы можете назвать этот класс как вам угодно, главное чтобы он был связан с TypedReducer
class UpdateUserAction {
final User user_info;
UpdateUserAction(this.user_info);
}
Ниже показано, как использовать store в приложении Flutter, используя `StoreProvider`.
void main() { runApp(FlutterReduxApp()); }
class FlutterReduxApp extends StatelessWidget {
/// Создание Store, использование app_reducer
, созданного из GSYState
/// initial_state
- начальное состояние
final store = Store(app_reducer, initial_state: GSYState(user_info: User.empty()));
FlutterReduxApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// Применение StoreProvider
для применения store
return StoreProvider(
store: store,
child: MaterialApp(
home: DemoUseStorePage(),
),
);
}
}
```В компоненте DemoUseStorePage
используются `StoreConnector` для привязки состояния к виджету; `StoreProvider.of` для получения объекта состояния; а также `dispatch` для отправки действия и обновления состояния.
class DemoUseStorePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
/// Используем StoreConnector для связи с User в GSYState
return new StoreConnector<GSYState, User>(
/// Преобразуем store.state.userInfo через конвертер
converter: (store) => store.state.userInfo,
/// Возвращаем контроллер для отображения имени пользователя
builder: (context, userInfo) {
return new Text(
userInfo.name,
style: Theme.of(context).textTheme.display1,
);
},
);
}
}
///
/// С помощью StoreProvider.of(context) (с контекстом внутри StoreProvider)
/// можно получить доступ к данным состояния в любом месте
StoreProvider.of(context).state.userInfo;
```///
/// С помощью `dispatch` метода `UpdateUserAction` можно обновить состояние
```dart
StoreProvider.of(context).dispatch(new UpdateUserAction(newUserInfo));
Неужели после всего этого хочется просто "побыть в одиночестве"? Независимо от того, кто такой "静静", полезность Redux должна привлекать больше внимания, чем любое желание побыть в одиночестве. Как программист с амбициями, что ещё нельзя завоевать? Для более подробной реализации см.: [GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter).
В приложении GSYGithubAppFlutter база данных используется через sqflite, что фактически представляет собой использование SQL-синтаксиса. Подробнее можно посмотреть полный код DemoDb.dart. Здесь представлен один подход, основанный на методах, предоставленных документацией sqflite, с некоторыми изменениями. Через определение Provider для работы с базой данных:
В Provider определяются название таблицы и константы полей базы данных, используемые для создания таблиц и операций над полями.
Предоставление отображения между объектами базы данных и моделями данных, например, преобразование объектов базы данных в объекты User.
При вызове Provider проверяется наличие таблицы, затем возвращаются объекты базы данных для выполнения запросов пользователя.
Если объединить это с сетевым запросом, то можно использовать замыкания для выполнения действий. Когда требуется работа с базой данных, она возвращается первым, а затем метод next
возвращает сетевой запрос. Внешний код может вызвать метод next
, чтобы выполнить сетевой запрос. Пример ниже:
UserDao.getUserInfo(userName, needDb: true).then((res) {
/// Результат базы данных
if (res != null && res.result) {
setState(() {
userInfo = res.data;
});
}
return res.next;
}).then((res) {
/// Результат сети
if (res != null && res.result) {
setState(() {
userInfo = res.data;
});
}
});
Другие возможности, поскольку не могу придумать заголовок.
В Flutter, используя WillPopScope
, можно встроиться для прослушивания логики обработки кнопки Back Android. На самом деле, WillPopScope
не слушает нажатия кнопки Back, как следует из его имени; это callback, который вызывается, когда текущий экран будет закрыт pop-операцией.
```Функция Future<bool> _dialogExitApp(BuildContext context)
возвращает диалоговое окно с запросом подтверждения выхода из приложения.``````dart
class HomePage extends StatelessWidget {
/// Отображение диалогового окна для подтверждения выхода
Future _dialogExitApp(BuildContext context) {
return showDialog(
context: context,
builder: (context) => new AlertDialog(
content: new Text("Вы действительно хотите выйти?"),
actions: [
new FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: new Text("Отмена")),
new FlatButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: new Text("Подтвердить"))
],
));
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
/// Если вернуть new Future.value(false), то событие pop не будет обработано
/// Если вернуть new Future.value(true), то событие pop будет активировано
/// Здесь можно использовать showDialog для отображения диалогового окна,
/// а затем использовать Navigator.of(context).pop(true) для подтверждения выхода
return _dialogExitApp(context);
},
child: new Container(),
);
}
}
WidgetsBindingObserver
sınıfı, uygulamanın yaşam döngüsündeki değişiklikleri izlemek için çeşitli bildirimler sağlar. Bu sınıf içindeki didChangeAppLifecycleState
metodunu kullanarak uygulamanın durumunu (arka planda / ön plana geçerken) izleyebilirsiniz.
/// Klasör WidgetsBindingObserver, uygulamanın yaşam döngüsünde değişiklikleri izlemek için bildirimler sunar
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
/// `didChangeAppLifecycleState` metodunu WidgetsBindingObserver'dan devralıyoruz
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
/// Uygulamanın durumu üzerinden belirleniyor
if (state == AppLifecycleState.resumed) {
}
}
@override
Widget build(BuildContext context) {
return new Container();
}
}
Genellikle ekranı tıkladığınızda klavyeyi gizlemeniz gerekecektir. Aşağıdaki örnek, GestureDetector
ve FocusScope
'u bu amaçla kullanır.
/// Genellikle ekranı tıklatıldığında klavyeyi gizlemek gereklidir.
/// Aşağıdaki örnek, GestureDetector ve FocusScope'u bu amaçla kullanır.
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: new Container(),
);
}
}
``````dart
// Пример использования GestureDetector и FocusScope для скрытия клавиатуры
class _LoginPageState extends State {
@override
Widget build(BuildContext context) {
/// Определяем слой для касаний
return new GestureDetector(
/// Прозрачность также реагирует на события
behavior: HitTestBehavior.translucent,
onTap: () {
/// При касании скрываем клавиатуру
FocusScope.of(context).requestFocus(new FocusNode());
},
child: new Container(),
);
}
}
### 4. Стартовая страница
Стартовая страница iOS находится в директории ios/Runner/Assets.xcassets/LaunchImage.imageset/
. В этой директории находятся файл Contents.json и стартовое изображение. Разместите вашу стартовую страницу в этом каталоге и отредактируйте файл Contents.json, чтобы указать размеры.
Для Android стартовой страницы уже есть готовый шаблон в файле android/app/src/main/res/drawable/launch_background.xml
. Часть <item><bitmap>
закомментирована. Вам нужно будет раскомментировать этот участок и заменить стартовое изображение на launch_image
, а затем поместить его в соответствующие папки mipmap с учетом различных размеров экрана.
Так закончилась вторая часть! (///▽///)
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )