Chat application based on flitter

Preface

                                 . The author is a web front-end development, and there are inevitably mistakes and omissions related to the original place. Welcome to criticize and correct. Project code base links are placed at the end of the article.

Function introduction

  1. Chat list
    This application supports direct point-to-point chat, and uses webSocket to realize message reminder and synchronization
    Friends list page:
Show all friends in the chat list, click to enter the chat details, and unread messages are indicated by a small red dot in the upper right corner of the friend's Avatar. Chat page:
  1. Search page
    Users can add friends by searching:
  1. Personal center page
    This page can modify personal information, including adjusting nicknames, avatars, changing passwords, etc., and log out at the same time.

Tool chain combing

Here are a few key third-party libraries used in this example. The specific details will be explained in the function implementation section.

  1. Message synchronization and sending
    In the project, webSocket is used to communicate with the server. My server is written in node, and webSocket is implemented in socket.io (see the link at the end of the article for details). Socket.io official also recently developed a matching client library based on dart socket ﹐ IO ﹐ client, which is used with the server. From this, message sending and receiving and server event notification can be realized.
  2. State management
  • Persistent state management
    Persistent state refers to the persistent state of user name, login state, avatar and so on. After the user exits the app, he does not need to log in to the app again, because the login state has been saved locally. Here, a lightweight package shared_preferences is used to save the persistent state locally through the way of writing a file. Each time the application is started, the file is read for recovery User status.
  • Non persistent state
    Here, we use the library provider widely used by the community to manage the non persistent state. The non persistent cache refers to controlling the relevant state displayed by the app, such as user list, message reading state, and various states depending on the interface. I also had a blog post to introduce the provider Using guide of Flutter Provider
  1. Network request
    Here we use dio for network request and simple encapsulation
  2. Other
  • The small red dot of mobile desktop message notification is realized through the flitter app gadget package, with the following effects:
  • When modifying the user's head picture, obtain the local photo album or call the camera, use the image ﹣ picker library to realize, and the image ﹣ clipper library to realize picture clipping
  • Use the cached network image to cache network pictures. Avoid calling http service repeatedly when using pictures

Function realization

  1. Application initialization
    When you open an app, you first need to initialize it, request related interfaces, and restore the persistent state. At the beginning of the main.dart file, do the following:

In order to avoid the impact of a large section of specific business code on the reading experience, the code part of this article will only list the core content, some common logic and style content will be omitted, and the complete code is detailed in the project warehouse

import 'global.dart';
...

//  Run initialization in global between running runApp
void main() => Global.init().then((e) => runApp(MyApp(info: e)));

Next let's look at the global.dart file

library global;

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
...
//  Space relation, omitting some package references

// To avoid the single file being too large, part is used here to split the file
part './model/User.dart';
part './model/FriendInfo.dart';
part './model/Message.dart';

//  Define a Profile, which is a class of persistent storage
class Profile {
  String user = '';
  bool isLogin = false;
  //  Friend application list
  List friendRequest = [];
  //  Head portrait
  String avatar = '';
  //  Nickname?
  String nickName = '';
  //  Friends list
  List friendsList = [];

  Profile();

  // Define the construction method of fromJson, and restore the Profile instance through json
  Profile.fromJson(Map json) {
    user = json['user'];
    isLogin = json['isLogin'];
    friendRequest = json['friendRequest'];
    avatar = json['avatar'];
    friendsList = json['friendsList'];
    nickName = json['nickName'];
  }
  //    Define the toJson method to convert the instance to json for convenient storage
  Map<String, dynamic> toJson() => {
    'user': user,
    'isLogin': isLogin,
    'friendRequest': friendRequest,
    'avatar': avatar,
    'friendsList': friendsList,
    'nickName': nickName
  };
}

//  Define global class to implement initialization
class Global {
  static SharedPreferences _prefs;
  static Profile profile = Profile();

  static Future init() async {
    //  Here, we use the shared preferences library to assist in persistent state storage
    _prefs = await SharedPreferences.getInstance();

    String _profile = _prefs.getString('profile');
    Response message;
    if (_profile != null) {
      try {
        //  If there are users, pull chat record
        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';
    //  This is the key to message sending and receiving and communication between server and client
    IO.Socket socket = IO.io(socketIODomain, <String, dynamic>{
      'transports': ['websocket'],
      'path': '/mySocket'
    });
    //  Return socket instance and message list as result
    return {
      'messageArray': message != null ? message.data : [],
      'socketIO': socket
    };
  }
  //    Define static methods to update locally stored data when needed
  static saveProfile() => _prefs.setString('profile', jsonEncode(profile.toJson()));
}
...

The global.dart file defines the Profile class, which defines the user's persistent information, such as the avatar, user name, login status, etc. the Profile class also provides the methods to json it and restore the Profile instance according to the json data. The Global class defines the initialization method of the whole application. First, with the help of the shared preferences library, the stored json Profile data is read and restored, so as to restore the user status. The saveProfile method is also defined in Global for external applications to call in order to update the content of local storage. After the local state is restored, the init method also requests the necessary interface, creates a Global socket instance, and passes both as parameters to the runApp method in main.dart. There are too many contents in global.dart. Here, the part keyword is used to split the contents. The definitions of UserModel and other classes are split. For details, see another blog post of the author The reference export of dart flitter file and Library

  1. State management
    Next, we go back to main.dart and observe the implementation of MyApp class:
class MyApp extends StatelessWidget with CommonInterface {
  MyApp({Key key, this.info}) : super(key: key);
  final info;
  // This widget is the root of your application.
  //  Root container, used to initialize the provider
  @override
  Widget build(BuildContext context) {
    UserModle newUserModel = new UserModle();
    Message messList = Message.fromJson(info['messageArray']);
    IO.Socket mysocket = info['socketIO'];
    return MultiProvider(
      providers: [
        //  User information
        ListenableProvider<UserModle>.value(value: newUserModel),
        //  websocket instance
        Provider<MySocketIO>.value(value: new MySocketIO(mysocket)),
        //  Chat message
        ListenableProvider<Message>.value(value: messList)
      ],
      child: ContextContainer(),
    );
  }
}

The main work of MyApp class is to create the status instance of the whole application, including user information, webSocket instance and chat information. Through the MultiProvider in the provider library, according to the type of state, the state instances are exposed to the subcomponents in the form of key value pairs, which is convenient for the subcomponents to read and use. Its principle is similar to the Context in react, which can pass parameters across components. Here we continue to see the definition of usermode:

part of global;

class ProfileChangeNotifier extends ChangeNotifier {
  Profile get _profile => Global.profile;

  @override
  void notifyListeners() {
    Global.saveProfile(); //Save Profile changes
    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();
  }

  ...Omit similar code

  BuildContext toastContext;
}

In order to update the UI synchronously when changing the data, UserModel inherits the ProfileChangeNotifier class, which defines the notifyListeners method. The user model internally sets the set and get methods of each property, and proxies the read and write operations to the Global.profile. At the same time, it hijacks the set method, so that the notifyListeners function will be triggered automatically when updating the value of the model. This function Data is responsible for updating UI and synchronizing state changes to persistent state management. In the specific business code, if you want to change the state value of the model, you can refer to the following code:

    if (key == 'avatar') {
      Provider.of<UserModle>(context).avatar = 'picture url';
    }

Here, through the provider package, according to the provided component context, trace up the component tree to find the nearest usermode and modify its value. Here you may complain that it's just to read and write a value, but it's inconvenient to add such a long string of content in front. To solve this problem, we can simply encapsulate it. In the global.dart file, we have the following definitions:

//  An abstract class for other widget s to get data
abstract class CommonInterface {
  String cUser(BuildContext context) {
    return Provider.of<UserModle>(context).user;
  }
  UserModle cUsermodal(BuildContext context) {
    return Provider.of<UserModle>(context);
  }
  ...
}

Through an abstract class, the prefixes of parameters are encapsulated as follows:

class testComponent extends State<FriendList> with CommonInterface {
    ...
    if (key == 'avatar') {
      cUsermodal(context).avatar = 'picture url';
    }
}
  1. Route management
    Next, we will continue to comb the main.dart file:
class ContextContainer extends StatefulWidget {
  //    Similar code will be omitted later
  @override
  _ContextContainerState createState() => _ContextContainerState();
}

class _ContextContainerState extends State<ContextContainer> with CommonInterface {
  //  Context container, mainly used to register and pass root context
  @override
  Widget build(BuildContext context) {
    //  Send a message to the server that the user is logged in
    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 {
  //  Global key used to record whether chat component exists
  final GlobalKey<ChatState> myK = GlobalKey<ChatState>();
  //  Register the routing component. Deleting friends will come here every time you pop, and the context will refresh
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        //  Configure initial route
        initialRoute: '/',
        routes: {
          //    Main route  
          '/': (context) => Provider.of<UserModle>(context).isLogin ? MyHomePage(myK: myK, originCon: widget.rootContext, toastContext: context) : LogIn(),
          //    Chat page
          'chat': (context) => Chat(key: myK),
          //    Modify personal information page
          'modify': (context) => Modify(),
          //    Friends information page
          'friendInfo': (context) => FriendInfoRoute()
        }
      );
  }
}

Here, ContextContainer is used to package components once to ensure that the logics of registering users online to the server are triggered only once. In the MaterialApp of ListenContainer, all routing pages that will appear in the application are defined, / represents the root route. Under the root route, the rendered components are selected according to the user's LogIn state: MyHomePage is the main page of the application, which contains The friends list page, search page, personal center page and the page cutting tab at the bottom, and the LogIn page of the application

  • Login page:
The code is in the login.dart file:
class LogIn extends StatefulWidget {
    ...
}

class _LogInState extends State<LogIn> {
  //    Text input controller
  TextEditingController _unameController = new TextEditingController();
  TextEditingController _pwdController = new TextEditingController();
  //    Is the password visible
  bool pwdShow = false;
  GlobalKey _formKey = new GlobalKey<FormState>();
  bool _nameAutoFocus = true;

  @override
  void initState() {
    //  Initialize user name
    _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(
                  //    Auto focus or not
                  autofocus: _nameAutoFocus,
                  //    Define TextFormField controller
                  controller: _unameController,
                  //    Calibrator
                  validator: (v) {
                    return v.trim().isNotEmpty ? null : 'required userName';
                  },
                ),
                TextFormField(
                  controller: _pwdController,
                  autofocus: !_nameAutoFocus,
                  decoration: InputDecoration(
                      ...
                    //  Button to control whether the password is displayed
                    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(
                    ...
                    //  Login button
                    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 };
    //  validate logon
    if (await userVerify(_unameController.text, _pwdController.text)) {
      Response info = await Network.get('userInfo', name);
      globalStore.apiUpdate(info.data);
      globalStore.isLogin = true;
      //  When you log in again, you should also pull the chat record
      Response message = await Network.get('getAllMessage', name);
      globalMessage.assignFromJson(message.data);
    } else {
      showToast('Account password error', context);
    }
  }
}

After a simple disassembly of this routing page, we found that the backbone of this page consists of three components, two textformfields are used as the form fields of user name and password, and one RaisedButton is used as the login button. Here is the most typical TextFormField widget application. The controller of the component is used to obtain the filled value. The validator of TextFormField will automatically verify the filled content. However, it should be noted that as long as it is on this page, the validation of the validator will run every moment, which is not intelligent. After the login verification is passed, the chat record of the user will be pulled.

  • Project Home
    Continue to return to our main.dart file. The main page is drawn as follows:
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; 
    });
  }
  //  Register event response from server
  void registerNotification() {
    //  The context here must use the root context, because the listencontainer component itself will lose the context due to the route reconstruction, and the global listening event error will not find the component tree
    BuildContext rootContext = widget.originCon;
    UserModle newUserModel = cUsermodal(rootContext);
    Message mesArray = Provider.of<Message>(rootContext);
    //  Monitor chat messages
    if(!cMysocket(rootContext).hasListeners('chat message')) {
      cMysocket(rootContext).on('chat message', (msg) {
        ...
        SingleMesCollection mesC = mesArray.getUserMesCollection(owner);
        //  Insert a new message in the message list
        ...
        //  Update the number of unread messages based on your environment
        ...
        updateBadger(rootContext);
      });
    }
    //  System notification
    if(!cMysocket(rootContext).hasListeners('system notification')) {
      cMysocket(rootContext).on('system notification', (msg) {
        String type = msg['type'];
        Map message = msg['message'] == 'msg' ? {} : msg['message'];
        //  map of registered events
        Map notificationMap = {
          'NOT_YOUR_FRIEND': () { showToast('The message can't be delivered when the other party starts friend verification', 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];
  }
}

Looking at the parameters of MyHomePage, we can see that two BuildContext instances are passed from the parent component. Each component has its own context, which is the context of the component. As a starting point, we can traverse the child elements of the component or trace the parent component upward. Whenever the component is redrawn, the context will be destroyed and rebuilt. _MyHomePageState's build method first calls registerNotification to register the response to the event initiated by the server, such as when a friend sends a message, the message list is automatically updated; when someone initiates a friend application, a reminder is triggered, etc. The application status is synchronized through the provider library, and the principle of provider is to trace the status of components through context. The context used within registerNotification must use the context of the parent component, originCon. Because MyHomePage will be rebuilt due to status refresh, but event registration will only be called once. If MyHomePage's own context is used, the component will redraw after registration, and the error of unable to find the context will be reported when calling related events. registerNotification internally registers the logic of reminding to pop up the toast. The implementation of the toast here uses the context of the MaterialApp found from the upstream. originCon cannot be used here, because it is the context of the parent component of MyHomePage, and the MaterialApp cannot be found from the upstream. If it is used directly, an error will be reported.
We implement the bottom tab through the BottomNavigationBarItem. Each item is bound with a click event. When clicking, we switch the displayed components. The chat list, search and personal center are implemented through a single component, wrapped by MiddleContent, without changing the route.

  • Chat page
    Click any dialogue on the chat list page to enter the chat page:
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;
    //  Update Desktop 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)
        ],
      ),
    );
  }
  //    Click to jump to the friend details page
  void toFriendInfo() {
    Navigator.pushNamed(context, 'friendInfo');
  }

  void slideToEnd() {
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent + 40);
  }
}

The structure here is relatively simple. The TalkList and ChatInputForm form constitute the chat page and input box respectively. They are wrapped with Scaffold to display the user name and click icon in the upper right corner. Next, let's look at the TalkList component:

class _TalkLitState extends State<TalkList> with CommonInterface {
  bool isLoading = false;

  //    Calculate the length of the request
  int get acculateReqLength {
      //    Omit business code
      ...
  }
  //    Pull more messages
  _getMoreMessage() async {
      //    Omit business code
      ...
  }

  @override
  Widget build(BuildContext context) {
    SingleMesCollection mesCol = cTalkingCol(context);
    return Expanded(
            child: Container(
              color: Color(0xfff5f5f5),
              //    Pull more messages through pull-down operation through notification listener
              child: NotificationListener<OverscrollNotification>(
                child: ListView.builder(
                  itemBuilder: (BuildContext context, int index) {
                    //  Rolling Chrysanthemum
                    if (index == 0) {
                        //  Control display flags according to data status no more or loading
                        ...
                    }
                    return MessageContent(mesList: mesCol.message, rank:index);
                  },
                  itemCount: mesCol.message.length + 1,
                  controller: widget.scrollController,
                ),
                //  Register notification function
                onNotification: (OverscrollNotification notification) {
                  if (widget.scrollController.position.pixels <= 10) {
                    _getMoreMessage();
                  }
                  return true;
                },
              )
            )
          );
  }
}

The key here is to use NotificationListener to pull more chat information when users pull down, that is, load it in batches. Read the offset value of the current scroll list through widget.scrollController.position.pixels. When it is less than 10, it will be judged as sliding to the top. At this time, execute "getMoreMessage" to pull more messages. The realization of chat function is explained in detail here: the delivery of messages is very frequent, and it is unrealistic to use ordinary http requests. Here, the message exchange is realized through socket.io of dart end (similar to webSocket of web end, The server is implemented by socket.io server on node). When you send a message, you will first update the local message list, and send a message to the server through the instance of socket. The server will forward the received message to the target user after receiving the message. When the target user initializes the app, they will listen for socket related events, and update the local message list after receiving the server's message notification. The specific process is rather tedious, there are many implementation details, which are omitted here temporarily, and the complete implementation is in the source code.
Next let's look at the ChatInputForm component

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,
                //  Send button
                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;
    }
    //  Send messages to the server, update unread messages, and update the local message list
    ...
    // Make sure that the content is not cleared until the first frame of component build
    WidgetsBinding.instance.addPostFrameCallback((_) {
        _messController.clear();
    });
    //  Keyboard auto Stow
    //FocusScope.of(context).requestFocus(FocusNode());
    widget.scrollController.jumpTo(widget.scrollController.position.maxScrollExtent + 50);
    setState(() {
      canSend = false;
    });
  }
}

Here, the TextFormField component is wrapped by Form. The input content is verified by registering onChanged method to prevent it from being empty. Click Send button and send message through socket instance. The list will scroll to the bottom and clear the current input box.

  • Personal center page
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(
              //    General components to show user information
              child: PersonInfoBar(infoMap: cUsermodal(context)),
              ...
            ),
            //  Display three configuration items: nickname, avatar and password
            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)
                ],
              ),
            ),
            //  Exit button
            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 });
}

The header is a general display component, which is used to display the user name and the avatar. After that, three modifyitems are used to display the nickname, the avatar and the password modification item. On the header, the GestureDetector binds the click event and switches the route to enter the modification page.

  • Personal information modification page (nickname)
    The renderings are as follows:
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;
              },
            ),
          ],
        ),
      ),
    );
  }
}

The logic here is relatively simple. A simple TextFormField uses validator to check whether the input is empty, whether it is consistent with the original content, and so on. The logic of password modification is similar here, and will not be repeated.

  • Personal information modification page (picture)
    The specific renderings are as follows:
After selecting the picture, enter the clipping logic:

The code implementation is as follows:

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('Photograph')
            ),
            RaisedButton(
              onPressed: () => pickImg('gallery'),
              child: Text('Choosing albums')
            ),
          ],
        )
      ],
    );
  }

  Widget imageView(BuildContext context) {
    if (_imgPath == null && !showCircle) {
      return Center(
        child: Text('Please select a picture or take a picture'),
      );
    } else if (_imgPath != null) {
      return Center(
          child: 
          //    Progressive picture loading
          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 {
    //  Generate image entities
    final img.Image image = img.decodeImage(File(_imgPath.path).readAsBytesSync());
    //  Cache folder
    Directory tempDir = await getTemporaryDirectory();
    String tempPath = tempDir.path; // Temporary folder
    //  create a file
    final File imageFile = File(path.join(tempPath, 'dart.png')); // Save in app folder
    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(
        //  Relevant configuration of clipper
        ...
    );
    setState(() {
      showCircle = false;
      _imgPath = croppedFile;
    });
    widget.handler(true);
    widget.modifyFunc('avatar', await getBase64());
  }
}

In this page, first draw two buttons, bind them with different events, respectively control and select the local album or take a new picture (using image ﹐ picker), specifically through ImagePicker.pickImage(source: ImageSource.gallery) and ImagePicker.pickImage(source: ImageSource.camera)), the call will return a file file, and then through ImageCropper.cropImage To enter the trimming operation, after the trimming is completed, the finished image will be converted into base64 string through getBase64, and sent to the server through post request, so as to complete the modification of the head image.

Epilogue

This project only involves the relevant logic of app side, and needs to cooperate with back-end services to run normally. For specific logic, please refer to the author's own node server , including the normal http request and the related logic implementation of websocket server.
Code warehouse of this project
If you have any questions, please leave a message~

Published 12 original articles, won praise and 874 visitors
Private letter follow

Tags: socket JSON network Mobile

Posted on Wed, 12 Feb 2020 21:39:34 -0500 by eaglelegend