предисловие
Как кросс-энд фреймворк, который в настоящее время находится в центре внимания, флаттер стал полем, где нативные разработчики и фронтенд-разработчики соревнуются, чтобы проверить воду.Автор будет использовать приложение, которое имитирует чат WeChat, чтобы показать процесс разработки и связанные с ним tool chain of flutter, целью которых является знакомство с экологией развития флаттера и в то же время подведение итогов собственного процесса обучения. Автор является разработчиком веб-интерфейса, поэтому ошибки и упущения в соответствующих областях, связанных с оригиналом, неизбежны. Комментарии и исправления приветствуются. Ссылка на базу кода проекта размещается в конце текста.
Введение в функцию
- список чатов Это приложение поддерживает прямой одноранговый чат и использует веб-сокет для реализации напоминаний о сообщениях и синхронизации. Страница со списком друзей:
- страница поиска Пользователи могут добавлять друзей, выполнив поиск:
- Страница персонального центра
На этой странице можно изменять личную информацию, включая настройку псевдонима, аватара, изменение пароля и т. д., и в то же время можно выйти из системы.
Расчесывание цепи инструментов
Вот несколько ключевых сторонних библиотек, используемых в настоящем варианте осуществления, использование конкретных деталей реализации объяснит функциональную часть.
- Синхронизация и доставка сообщений
Проект использует webSocket для связи с сервером Мой сервер используетnode
Написано, webSocket используетsocket.io
добиться (подробнее см. ссылку в конце статьи),socket.io
Чиновник также недавно разработал вспомогательную клиентскую библиотеку на основе dart.socket_io_client
, который используется вместе с сервером. Это позволяет отправлять и получать сообщения, а также уведомления о событиях на стороне сервера. - государственное управление
- постоянное управление состоянием
Постоянное состояние относится к постоянному состоянию имени пользователя, состояния входа в систему, аватара и т. д. После того, как пользователь выходит из приложения, нет необходимости снова входить в приложение, поскольку состояние входа было сохранено локально, а облегченный пакет используется здесь.shared_preferences
, сохранять постоянное состояние локально, записывая файл, читать файл при каждом запуске приложения и восстанавливать состояние пользователя. - непостоянное состояние
Здесь используется библиотека, широко используемая сообществом
provider
Для непостоянного управления состоянием непостоянный кеш относится к управлению соответствующим состоянием, отображаемым приложением, таким как список пользователей, состояние чтения сообщений и различные состояния, которые зависят от интерфейса и т. д. У автора также есть запись в блоге доprovider
представилРуководство по использованию поставщика Flutter
- сетевой запрос
используется здесьdio
Делайте сетевые запросы с простой инкапсуляцией - разное
- Уведомление о сообщении мобильного рабочего стола с маленькой красной точкой
flutter_app_badger
пакет для достижения, эффект выглядит следующим образом:
- При изменении аватара пользователя, получить локальный альбом или вызвать камеру, использовать
image_picker
Реализована библиотека, а обрезка изображения производитсяimage_cropper
Библиотека для реализации - кеш веб-изображений, используя
cached_network_image
Для завершения избегайте повторных вызовов службы http при использовании изображений.
Реализация функции
- Инициализация приложения При открытии приложения сначала необходимо инициализировать, запросить соответствующий интерфейс, восстановить постоянное состояние и т. д. В начале файла main.dart сделайте следующее:
Во избежание переполнения статьи большим количеством конкретных бизнес-кодов, влияющих на удобство чтения, в части кода этой статьи будет перечислено только основное содержание, а некоторая общая логика и стиль содержания будут опущены. , обратитесь к репозиторию проекта
import 'global.dart';
...
// 在运行runApp,之间,运行global中的初始化操作
void main() => Global.init().then((e) => runApp(MyApp(info: e)));
Далее мы видимglobal.dart
документ
library global;
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
...
// 篇幅关系,省略部分包引用
// 为了避免单文件过大,这里使用part将文件拆分
part './model/User.dart';
part './model/FriendInfo.dart';
part './model/Message.dart';
// 定义Profile,其为持久化存储的类
class Profile {
String user = '';
bool isLogin = false;
// 好友申请列表
List friendRequest = [];
// 头像
String avatar = '';
// 昵称
String nickName = '';
// 好友列表
List friendsList = [];
Profile();
// 定义fromJson的构造方法,通过json还原Profile实例
Profile.fromJson(Map json) {
user = json['user'];
isLogin = json['isLogin'];
friendRequest = json['friendRequest'];
avatar = json['avatar'];
friendsList = json['friendsList'];
nickName = json['nickName'];
}
// 定义toJson方法,将实例转化为json方便存储
Map<String, dynamic> toJson() => {
'user': user,
'isLogin': isLogin,
'friendRequest': friendRequest,
'avatar': avatar,
'friendsList': friendsList,
'nickName': nickName
};
}
// 定义全局类,实现初始化操作
class Global {
static SharedPreferences _prefs;
static Profile profile = Profile();
static Future init() async {
// 这里使用了shared_preferences这个库辅助持久化状态存储
_prefs = await SharedPreferences.getInstance();
String _profile = _prefs.getString('profile');
Response message;
if (_profile != null) {
try {
// 如果存在用户,则拉取聊天记录
Map decodeContent = jsonDecode(_profile != null ? _profile : '');
profile = Profile.fromJson(decodeContent);
message = await Network.get('getAllMessage', { 'userName' : decodeContent['user'] });
} catch (e) {
print(e);
}
}
String socketIODomain = 'http://testDomain';
// 生成全局通用的socket实例,这个是消息收发和server与客户端通信的关键
IO.Socket socket = IO.io(socketIODomain, <String, dynamic>{
'transports': ['websocket'],
'path': '/mySocket'
});
// 将socket实例和消息列表作为结果返回
return {
'messageArray': message != null ? message.data : [],
'socketIO': socket
};
}
// 定义静态方法,在需要的时候更新本地存储的数据
static saveProfile() => _prefs.setString('profile', jsonEncode(profile.toJson()));
}
...
Класс Profile определен в файле global.dart.Этот класс определяет постоянную информацию о пользователе, такую как аватар, имя пользователя, статус входа в систему и т. д. Класс Profilet также предоставляет методы для его преобразования в формат json и восстановления экземпляра профиля на основе данных в формате json. Метод инициализации всего приложения определяется в классе Global сначала с помощьюshared_preferences
Библиотека, которая считывает сохраненные данные профиля в формате json и восстанавливает их, тем самым восстанавливая состояние пользователя. В Global также определен метод saveProfile, который может вызываться внешними приложениями для обновления содержимого локального хранилища. После восстановления локального состояния метод init также запрашивает необходимые интерфейсы, создает экземпляр глобального сокета и передает их в качестве параметров методу runApp в main.dart. Слишком много контента в global.dart, который используется здесьpart
Контент разделен по ключевым словам, а определения классов, таких как UserModel, разделены.Подробности см. в другом сообщении в блоге автора.Экспорт файла dart flutter и ссылки на библиотеку
- государственное управление Далее мы возвращаемся к main.dart и наблюдаем за реализацией класса MyApp:
class MyApp extends StatelessWidget with CommonInterface {
MyApp({Key key, this.info}) : super(key: key);
final info;
// This widget is the root of your application.
// 根容器,用来初始化provider
@override
Widget build(BuildContext context) {
UserModle newUserModel = new UserModle();
Message messList = Message.fromJson(info['messageArray']);
IO.Socket mysocket = info['socketIO'];
return MultiProvider(
providers: [
// 用户信息
ListenableProvider<UserModle>.value(value: newUserModel),
// websocket 实例
Provider<MySocketIO>.value(value: new MySocketIO(mysocket)),
// 聊天信息
ListenableProvider<Message>.value(value: messList)
],
child: ContextContainer(),
);
}
}
Основная задача класса MyApp — создание экземпляров состояния всего приложения, включая информацию о пользователе, экземпляры веб-сокетов и информацию о чате. пройти черезprovider
MultiProvider в библиотеке, в зависимости от типа состояния, выставляет экземпляр состояния дочернему компоненту в виде пары ключ-значение, что дочернему компоненту удобно читать и использовать. Его принцип чем-то похож на Context во внешнем интерфейсе React, который может передавать параметры между компонентами. Здесь мы продолжаем рассматривать определение UserModle:
part of global;
class ProfileChangeNotifier extends ChangeNotifier {
Profile get _profile => Global.profile;
@override
void notifyListeners() {
Global.saveProfile(); //保存Profile变更
super.notifyListeners();
}
}
class UserModle extends ProfileChangeNotifier {
String get user => _profile.user;
set user(String user) {
_profile.user = user;
notifyListeners();
}
bool get isLogin => _profile.isLogin;
set isLogin(bool value) {
_profile.isLogin = value;
notifyListeners();
}
...省略类似代码
BuildContext toastContext;
}
Чтобы синхронно обновлять пользовательский интерфейс при изменении данных, UserModel наследует класс ProfileChangeNotifier, который определяет метод notifyListeners.UserModel устанавливает методы set и get каждого атрибута внутри UserModel для передачи операций чтения и записи в Global.profile и hijacks. это в то же время.Метод set автоматически запускает функцию notifyListeners при обновлении значения модели.Эта функция отвечает за обновление пользовательского интерфейса и синхронизацию изменений состояния с постоянным управлением состоянием. В конкретном бизнес-коде, если вы хотите изменить значение состояния модели, вы можете обратиться к следующему коду:
if (key == 'avatar') {
Provider.of<UserModle>(context).avatar = '图片url';
}
Здесь через пакет провайдера в соответствии с предоставленным контекстом компонента найдите ближайший UserModle в дереве компонентов и измените его значение. Вы можете пожаловаться здесь, что просто читать и записывать значение, слишком неудобно добавлять перед ним такую длинную строку содержимого.Чтобы решить эту проблему, мы можем просто инкапсулировать его.В файле global.dart , мы Есть следующие определения:
// 给其他widget做的抽象类,用来获取数据
abstract class CommonInterface {
String cUser(BuildContext context) {
return Provider.of<UserModle>(context).user;
}
UserModle cUsermodal(BuildContext context) {
return Provider.of<UserModle>(context);
}
...
}
Через абстрактный класс инкапсулируется префиксная часть параметра, и конкретное использование выглядит следующим образом:
class testComponent extends State<FriendList> with CommonInterface {
...
if (key == 'avatar') {
cUsermodal(context).avatar = '图片url';
}
}
- управление маршрутом
Далее продолжаем прочесывать файл main.dart:
class ContextContainer extends StatefulWidget {
// 后文中类似代码将省略
@override
_ContextContainerState createState() => _ContextContainerState();
}
class _ContextContainerState extends State<ContextContainer> with CommonInterface {
// 上下文容器,主要用来注册登记和传递根上下文
@override
Widget build(BuildContext context) {
// 向服务器发送消息,表示该用户已登录
cMysocket(context).emit('register', cUser(context));
return ListenContainer(rootContext: context);
}
}
class ListenContainer extends StatefulWidget {
ListenContainer({Key key, this.rootContext})
: super(key: key);
final BuildContext rootContext;
@override
_ListenContainerState createState() => _ListenContainerState();
}
class _ListenContainerState extends State<ListenContainer> with CommonInterface {
// 用来记录chat组件是否存在的全局key
final GlobalKey<ChatState> myK = GlobalKey<ChatState>();
// 注册路由的组件,删好友每次pop的时候都会到这里,上下文都会刷新
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 配置初始路由
initialRoute: '/',
routes: {
// 主路由
'/': (context) => Provider.of<UserModle>(context).isLogin ? MyHomePage(myK: myK, originCon: widget.rootContext, toastContext: context) : LogIn(),
// 聊天页
'chat': (context) => Chat(key: myK),
// 修改个人信息页
'modify': (context) => Modify(),
// 好友信息页
'friendInfo': (context) => FriendInfoRoute()
}
);
}
}
Здесь ContextContainer используется для пакета компонентов, чтобы гарантировать, что логика онлайн-регистрации пользователя на сервере запускается только один раз.В MaterialApp из ListenContainer определяются все страницы маршрутизации, которые будут отображаться в приложении./
Представляет корневой маршрут. При корневом маршруте компоненты для отображения выбираются в соответствии с состоянием входа пользователя: MyHomePage — это главная страница приложения, которая включает страницу списка друзей, страницу поиска, страницу личного центра и страницу вкладка вырезания страниц внизу, а LogIn представляет собой страницу входа в приложение
- Страница авторизации:
class LogIn extends StatefulWidget {
...
}
class _LogInState extends State<LogIn> {
// 文字输入控制器
TextEditingController _unameController = new TextEditingController();
TextEditingController _pwdController = new TextEditingController();
// 密码是否可见
bool pwdShow = false;
GlobalKey _formKey = new GlobalKey<FormState>();
bool _nameAutoFocus = true;
@override
void initState() {
// 初始化用户名
_unameController.text = Global.profile.user;
if (_unameController.text != null) {
_nameAutoFocus = false;
}
super.initState();
}
@override
Widget build(BuildContext context){
return Scaffold(
appBar: ...
body: SingleChildScrollView(
child: Padding(
child: Form(
key: _formKey,
autovalidate: true,
child: Column(
children: <Widget>[
TextFormField(
// 是否自动聚焦
autofocus: _nameAutoFocus,
// 定义TextFormField控制器
controller: _unameController,
// 校验器
validator: (v) {
return v.trim().isNotEmpty ? null : 'required userName';
},
),
TextFormField(
controller: _pwdController,
autofocus: !_nameAutoFocus,
decoration: InputDecoration(
...
// 控制密码是否展示的按钮
suffixIcon: IconButton(
icon: Icon(pwdShow ? Icons.visibility_off : Icons.visibility),
onPressed: () {
setState(() {
pwdShow = !pwdShow;
});
},
)
),
obscureText: !pwdShow,
validator: (v) {
return v.trim().isNotEmpty ? null : 'required passWord';
},
),
Padding(
child: ConstrainedBox(
...
// 登录按钮
child: RaisedButton(
...
onPressed: _onLogin,
child: Text('Login'),
),
),
)
],
),
),
)
)
);
}
void _onLogin () async {
String userName = _unameController.text;
UserModle globalStore = Provider.of<UserModle>(context);
Message globalMessage = Provider.of<Message>(context);
globalStore.user = userName;
Map<String, String> name = { 'userName' : userName };
// 登录验证
if (await userVerify(_unameController.text, _pwdController.text)) {
Response info = await Network.get('userInfo', name);
globalStore.apiUpdate(info.data);
globalStore.isLogin = true;
// 重新登录的时候也要拉取聊天记录
Response message = await Network.get('getAllMessage', name);
globalMessage.assignFromJson(message.data);
} else {
showToast('账号密码错误', context);
}
}
}
После простой разборки этой страницы маршрутизации мы обнаружили, что костяк страницы состоит из трех компонентов, два TextFormField используются как поля формы для имени пользователя и пароля, а один RaisedButton используется как кнопка входа. Вот наиболее типичное приложение виджета TextFormField.Заполняемое значение получается через контроллер компонента.Валидатор TextFormField автоматически проверяет заполненное содержимое, но следует отметить, что пока оно находится на этой странице, проверка валидатора будет выполняться каждый раз, он будет работать все время, и это выглядит очень неразумно. После того, как проверка входа будет пройдена, история чата пользователя будет извлечена.
- Домашняя страница проекта
Возвращаясь к нашему файлу main.dart, содержимое отрисовки домашней страницы выглядит следующим образом:
class MyHomePage extends StatefulWidget {
...
}
class _MyHomePageState extends State<MyHomePage> with CommonInterface{
int _selectedIndex = 1;
@override
Widget build(BuildContext context) {
registerNotification();
return Scaffold(
appBar: ...
body: MiddleContent(index: _selectedIndex),
bottomNavigationBar: BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.chat), title: Text('Friends')),
BottomNavigationBarItem(
icon: Stack(
overflow: Overflow.visible,
children: <Widget>[
Icon(Icons.find_in_page),
cUsermodal(context).friendRequest.length > 0 ? Positioned(
child: Container(
...
),
) : null,
].where((item) => item != null).toList()
),
title: Text('Contacts')),
BottomNavigationBarItem(icon: Icon(Icons.my_location), title: Text('Me')),
],
currentIndex: _selectedIndex,
fixedColor: Colors.green,
onTap: _onItemTapped,
),
);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
// 注册来自服务器端的事件响应
void registerNotification() {
// 这里的上下文必须要用根上下文,因为listencontainer组件本身会因为路由重建,导致上下文丢失,全局监听事件报错找不到组件树
BuildContext rootContext = widget.originCon;
UserModle newUserModel = cUsermodal(rootContext);
Message mesArray = Provider.of<Message>(rootContext);
// 监听聊天信息
if(!cMysocket(rootContext).hasListeners('chat message')) {
cMysocket(rootContext).on('chat message', (msg) {
...
SingleMesCollection mesC = mesArray.getUserMesCollection(owner);
// 在消息列表中插入新的消息
...
// 根据所处环境更新未读消息数
...
updateBadger(rootContext);
});
}
// 系统通知
if(!cMysocket(rootContext).hasListeners('system notification')) {
cMysocket(rootContext).on('system notification', (msg) {
String type = msg['type'];
Map message = msg['message'] == 'msg' ? {} : msg['message'];
// 注册事件的映射map
Map notificationMap = {
'NOT_YOUR_FRIEND': () { showToast('对方开启好友验证,本消息无法送达', cUsermodal(rootContext).toastContext); },
...
};
notificationMap[type]();
});
}
}
}
class MiddleContent extends StatelessWidget {
MiddleContent({Key key, this.index}) : super(key: key);
final int index;
@override
Widget build(BuildContext context) {
final contentMap = {
0: FriendList(),
1: FindFriend(),
2: MyAccount()
};
return contentMap[index];
}
}
Глядя на параметры MyHomePage, мы можем обнаружить, что два экземпляра BuildContext передаются из компонента верхнего уровня. Каждый компонент имеет свой собственный контекст. Контекст — это контекст компонента. В качестве отправной точки мы можем пройтись по дочерним элементам компонента, а также мы можем проследить родительский компонент вверх. Всякий раз, когда компонент перерисовывается, контекст будет быть разрушены и восстановлены. Метод сборки _MyHomePageState сначала вызывает registerNotification, чтобы зарегистрировать ответ на событие, инициированное сервером, например, когда друг отправляет сообщение, список сообщений автоматически обновляется, когда кто-то инициирует приложение друга, срабатывает напоминание и т. д. через которыйprovider
библиотека для синхронизации состояния приложения,provider
Принцип также заключается в отслеживании состояния компонента через контекст. Контекст, используемый внутри registerNotification, должен использовать контекст родительского компонента, а именно originCon. Поскольку MyHomePage будет перестроен из-за обновления состояния, но регистрация события будет вызвана только один раз.Если используется собственный контекст MyHomePage, компонент будет перерисован после регистрации, и будет сообщено об ошибке, что контекст не может быть найден когда вызывается связанное событие. RegisterNotification внутренне регистрирует логику всплывающего уведомления-напоминания. Реализация всплывающего уведомления здесь использует контекст MaterialApp, найденный в резервной копии. Здесь нельзя использовать originCon, поскольку это контекст родительского компонента MyHomePage, а MaterialApp не может быть найден задним числом. , и при прямом использовании будет сообщено об ошибке.
Нижняя вкладка реализована BottomNavigationBarItem.Каждый элемент привязан к событию клика, а отображаемые компоненты переключаются при клике.Список чата, поиск и личный центр реализованы одним компонентом, который обернут MiddleContent и не изменить маршрут.
- страница чата
Щелкните любой разговор на странице списка чатов, чтобы перейти на страницу чата:
class ChatState extends State<Chat> with CommonInterface {
ScrollController _scrollController = ScrollController(initialScrollOffset: 18000);
@override
Widget build(BuildContext context) {
UserModle myInfo = Provider.of<UserModle>(context);
String sayTo = myInfo.sayTo;
cUsermodal(context).toastContext = context;
// 更新桌面icon
updateBadger(context);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(cFriendInfo(context, sayTo).nickName),
actions: <Widget>[
IconButton(
icon: Icon(Icons.attach_file, color: Colors.white),
onPressed: toFriendInfo,
)
],
),
body: Column(children: <Widget>[
TalkList(scrollController: _scrollController),
ChatInputForm(scrollController: _scrollController)
],
),
);
}
// 点击跳转好友详情页
void toFriendInfo() {
Navigator.pushNamed(context, 'friendInfo');
}
void slideToEnd() {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent + 40);
}
}
Структура здесь относительно проста.Страница чата и поле ввода состоят из TalkList и ChatInputForm соответственно.Периферия обёрнута в Scaffold для реализации отображения имени пользователя и щелчка значка в правом верхнем углу.Далее , давайте взглянем на компонент TalkList:
class _TalkLitState extends State<TalkList> with CommonInterface {
bool isLoading = false;
// 计算请求的长度
int get acculateReqLength {
// 省略业务代码
...
}
// 拉取更多消息
_getMoreMessage() async {
// 省略业务代码
...
}
@override
Widget build(BuildContext context) {
SingleMesCollection mesCol = cTalkingCol(context);
return Expanded(
child: Container(
color: Color(0xfff5f5f5),
// 通过NotificationListener实现下拉操作拉取更多消息
child: NotificationListener<OverscrollNotification>(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
// 滚动的菊花
if (index == 0) {
// 根据数据状态控制显示标志 没有更多或正在加载
...
}
return MessageContent(mesList: mesCol.message, rank:index);
},
itemCount: mesCol.message.length + 1,
controller: widget.scrollController,
),
// 注册通知函数
onNotification: (OverscrollNotification notification) {
if (widget.scrollController.position.pixels <= 10) {
_getMoreMessage();
}
return true;
},
)
)
);
}
}
Ключевым моментом здесь является использование NotificationListener, чтобы пользователи могли получать больше информации о чате во время операции раскрывающегося списка, то есть для поэтапной загрузки. Прочитайте значение смещения текущего списка прокрутки с помощью widget.scrollController.position.pixels. Когда оно меньше 10, считается, что он скользит вверх. В это время выполняется _getMoreMessage, чтобы получить больше сообщений. Вот подробное объяснение реализации функции чата: передача сообщений очень частая, и использовать обычные http-запросы для реализации обмена сообщениями нереально, когда вы отправляете сообщение, он сначала обновит локальный список сообщений , и в то же время отправить сообщение на сервер через экземпляр сокета, и сервер перешлет полученное сообщение целевому пользователю после получения сообщения. Когда целевой пользователь инициализирует приложение, оно будет прослушивать соответствующие события сокета и обновлять локальный список сообщений после получения уведомления о сообщении с сервера. Конкретный процесс относительно громоздкий, и есть много деталей реализации, которые здесь пока опущены, а полная реализация находится в исходном коде.
Далее мы рассмотрим компонент ChatInputForm.
class _ChatInputFormState extends State<ChatInputForm> with CommonInterface {
TextEditingController _messController = new TextEditingController();
GlobalKey _formKey = new GlobalKey<FormState>();
bool canSend = false;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Container(
color: Color(0xfff5f5f5),
child: TextFormField(
...
controller: _messController,
onChanged: validateInput,
// 发送摁钮
decoration: InputDecoration(
...
suffixIcon: IconButton(
icon: Icon(Icons.message, color: canSend ? Colors.blue : Colors.grey),
onPressed: sendMess,
)
),
)
)
);
}
void validateInput(String test) {
setState(() {
canSend = test.length > 0;
});
}
void sendMess() {
if (!canSend) {
return;
}
// 想服务器发送消息,更新未读消息,并更新本地消息列表
...
// 保证在组件build的第一帧时才去触发取消清空内容
WidgetsBinding.instance.addPostFrameCallback((_) {
_messController.clear();
});
// 键盘自动收起
//FocusScope.of(context).requestFocus(FocusNode());
widget.scrollController.jumpTo(widget.scrollController.position.maxScrollExtent + 50);
setState(() {
canSend = false;
});
}
}
Здесь компонент TextFormField обернут с Form, а входное содержимое проверяется путем регистрации метода onChanged, чтобы он не был пустым.После нажатия кнопки отправки сообщение отправляется через экземпляр сокета, список прокручивается вниз , и текущее поле ввода очищается.
- Страница персонального центра
class _MyAccountState extends State<MyAccount> with CommonInterface{
@override
Widget build(BuildContext context) {
String me = cUser(context);
return SingleChildScrollView(
child: Container(
...
child: Column(
...
children: <Widget>[
Container(
// 通用组件,展现用户信息
child: PersonInfoBar(infoMap: cUsermodal(context)),
...
),
// 展示昵称,头像,密码三个配置项
Container(
margin: EdgeInsets.only(top: 15),
child: Column(
children: <Widget>[
ModifyItem(text: 'Nickname', keyName: 'nickName', owner: me),
ModifyItem(text: 'Avatar', keyName: 'avatar', owner: me),
ModifyItem(text: 'Password', keyName: 'passWord', owner: me, useBottomBorder: true)
],
),
),
// 退出摁钮
Container(
child: GestureDetector(
child: Container(
...
child: Text('Log Out', style: TextStyle(color: Colors.red)),
),
onTap: quit,
)
)
],
)
)
);
}
void quit() {
Provider.of<UserModle>(context).isLogin = false;
}
}
var borderStyle = BorderSide(color: Color(0xffd4d4d4), width: 1.0);
class ModifyItem extends StatelessWidget {
ModifyItem({this.text, this.keyName, this.owner, this.useBottomBorder = false, });
...
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(
...
child: Text(text),
),
onTap: () => modify(context, text, keyName, owner),
);
}
}
void modify(BuildContext context, String text, String keyName, String owner) {
Navigator.pushNamed(context, 'modify', arguments: {'text': text, 'keyName': keyName, 'owner': owner });
}
Голова — это общий компонент отображения, используемый для отображения имени пользователя и аватара, а затем три элемента ModifyItem используются для отображения элементов модификации псевдонима, аватара и пароля.GestureDetector
Привяжите событие клика и переключите маршрут для входа на страницу модификации.
- Страница модификации личной информации (никнейм)
Схема эффекта выглядит следующим образом:
class NickName extends StatefulWidget {
NickName({Key key, @required this.handler, @required this.modifyFunc, @required this.target})
: super(key: key);
...
@override
_NickNameState createState() => _NickNameState();
}
class _NickNameState extends State<NickName> with CommonInterface{
TextEditingController _nickNameController = new TextEditingController();
GlobalKey _formKey = new GlobalKey<FormState>();
bool _nameAutoFocus = true;
@override
Widget build(BuildContext context) {
String oldNickname = widget.target == cUser(context) ? cUsermodal(context).nickName : cFriendInfo(context, widget.target).nickName;
return Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
autovalidate: true,
child: Column(
children: <Widget>[
TextFormField(
...
validator: (v) {
var result = v.trim().isNotEmpty ? (_nickNameController.text != oldNickname ? null : 'please enter another nickname') : 'required nickname';
widget.handler(result == null);
widget.modifyFunc('nickName', _nickNameController.text);
return result;
},
),
],
),
),
);
}
}
Логика здесь относительно проста, простое TextFormfield, используйте валидатор, чтобы проверить, будет ли вход пустой, соответствует ли он с исходным контентом и так далее. Логика изменения пароля похожа здесь и не будет повторяться здесь.
- Страница модификации личной информации (аватар)
Конкретная схема эффекта выглядит следующим образом:
Код реализован следующим образом:
import 'package:image_picker/image_picker.dart';
import 'package:image_cropper/image_cropper.dart';
import '../../tools/base64.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class Avatar extends StatefulWidget {
Avatar({Key key, @required this.handler, @required this.modifyFunc})
: super(key: key);
final ValueChanged<bool> handler;
final modifyFunc;
@override
_AvatarState createState() => _AvatarState();
}
class _AvatarState extends State<Avatar> {
var _imgPath;
var baseImg;
bool showCircle = false;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SingleChildScrollView(child: imageView(context),) ,
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
RaisedButton(
onPressed: () => pickImg('takePhote'),
child: Text('拍照')
),
RaisedButton(
onPressed: () => pickImg('gallery'),
child: Text('选择相册')
),
],
)
],
);
}
Widget imageView(BuildContext context) {
if (_imgPath == null && !showCircle) {
return Center(
child: Text('请选择图片或拍照'),
);
} else if (_imgPath != null) {
return Center(
child:
// 渐进的图片加载
FadeInImage(
placeholder: AssetImage("images/loading.gif"),
image: FileImage(_imgPath),
height: 375,
width: 375,
)
);
} else {
return Center(
child: Image.asset("images/loading.gif",
width: 375.0,
height: 375,
)
);
}
}
Future<String> getBase64() async {
// 生成图片实体
final img.Image image = img.decodeImage(File(_imgPath.path).readAsBytesSync());
// 缓存文件夹
Directory tempDir = await getTemporaryDirectory();
String tempPath = tempDir.path; // 临时文件夹
// 创建文件
final File imageFile = File(path.join(tempPath, 'dart.png')); // 保存在应用文件夹内
await imageFile.writeAsBytes(img.encodePng(image));
return 'data:image/png;base64,' + await Util.imageFile2Base64(imageFile);
}
void pickImg(String action) async{
setState(() {
_imgPath = null;
showCircle = true;
});
File image = await (action == 'gallery' ? ImagePicker.pickImage(source: ImageSource.gallery) : ImagePicker.pickImage(source: ImageSource.camera));
File croppedFile = await ImageCropper.cropImage(
// cropper的相关配置
...
);
setState(() {
showCircle = false;
_imgPath = croppedFile;
});
widget.handler(true);
widget.modifyFunc('avatar', await getBase64());
}
}
На этой странице сначала нарисуйте две кнопки, и привяжите к ним разные события, чтобы управлять выбором локальных альбомов или делать новые снимки (используяimage_picker
), в частности,ImagePicker.pickImage(source: ImageSource.gallery)
а такжеImagePicker.pickImage(source: ImageSource.camera))
для этого вызов вернет файл file, а затем передастImageCropper.cropImage
чтобы войти в операцию обрезки, после завершения обрезки пропустите готовое изображение черезgetBase64
Преобразуйте его в строку base64 и отправьте на сервер через почтовый запрос для завершения модификации аватара.
постскриптум
Этот проект включает в себя только соответствующую логику на стороне приложения. Для нормальной работы он должен взаимодействовать с серверной службой. Конкретную логику см. в собственной статье автора.узловой сервер, который содержит соответствующую логическую реализацию обычных HTTP-запросов и серверов веб-сокетов.
Репозиторий кода проекта
Если у вас есть какие-либо вопросы, пожалуйста, оставьте сообщение~