6. Flutterr chat interface & network request

1. Prepare network data

1.1 Data preparation

  1. Go to the website of network data manufacturing , register and log in, and create a new warehouse named WeChat_flutter;
  2. Click to enter the warehouse, delete the example interface on the left, and create a new interface.

3. Then click the "Edit" button in the upper right corner, create a new response content, the type is Array, and generate 50 items at a time

4. Click the Add button on the left side of the chat_list to create new data content in the chat_list. At this time, a website for obtaining random avatars is used . Go to the website and copy a random image address, assuming: https://randomuser.me/api /portraits/women/35.jpg . Fill in the data and save it.

5. Next, we want the obtained image to be a random value, so refer to the generation rules in the Mock.js website.

6. Then go back to the response content, and generate a random image address by setting the initial value rule.

  • Next populate name, random value for message.

  • In summary: the data preparation of the server is completed, and the requested link address

2. Chat interface navigation bar

  • First set the private variable _currentIndex of the NavigationBar selected by default in the App in _RootPageState = 0, that is, the WeChat interface is selected by default.
  • Then according to the UI effect of the WeChat chat interface, we first realize the plus sign in the upper right corner.

1. The actions of AppBar are where we need to add operations.

  • To continue with this idea, you need to implement a pop-up menu component yourself. However, at this time, Flutter has actually provided a set of mature controls to achieve the effect

2. PopupMenuButton component

  • The PopupMenuButton component is used to pop up a menu, and the required parameter is itemBuilder, which is used to realize the content it needs to display. PopupMenuItem is the class used to display the content. PopupMenuButton has an onSelected attribute, which is a closure, which means to select a PopupMenuItem , the closure will be called. But there is a premise that the onSelected closure will be executed only when the value of each PopupMenuItem must not be null.
  • The specific internal implementation in AppBar is .
AppBar(
  //去除导航条黑线
  elevation: 0.0,
  backgroundColor: WeChatThemeColor,
  //设置标题默认居中、否则双端默认方式不一致
  centerTitle: true,
  title: const Text("微信", style: TextStyle(color: Colors.black),),
  actions: [
    Container(
      margin: EdgeInsets.only(right: 10),
      child: PopupMenuButton(
        onSelected: (item){
          print(item);
        },
        onCanceled: (){
          print('onCanceled');
        },
        //PopupMenuButton的背景颜色
        color: Colors.black,
        offset: Offset(0,60),
        child: Image(image: AssetImage('images/圆加.png'),width: 25,height: 25,),
        itemBuilder: (BuildContext context){
          return <PopupMenuItem>[
            _buildMenuItem('images/发起群聊.png','发起群聊'),
            _buildMenuItem('images/添加朋友.png','添加朋友'),
            _buildMenuItem('images/扫一扫1.png','扫一扫'),
            _buildMenuItem('images/收付款.png','收付款'),
          ];
        },
      ),
    )
  ],
),

3. Among them, _buildMenuItem is a method of creating components encapsulated by us, and the internal implementation is

PopupMenuItem _buildMenuItem(String imageName,String title){
  return PopupMenuItem(
    value: {
      'imageName' : imageName,
      'title' : title,
    },
      child: Row(
        children: [
          Image(image: AssetImage(imageName), width: 25,),
          SizedBox(width: 10,),
          Text(title, style: TextStyle(color: Colors.white),),
        ],
      )
  );
}

4. Regarding the setting of the PopupMenuButton background color. You can set the background color directly inside it. You can also set the cardColor of the app in ThemeData. But the priority is not directly set the PopupMenuButton high. If you do not set the black background color, the pop-up view display is uniform It is white, no UI effect can be seen.

3. Request network data

  1. Through the Dart packages website, you can search for the packages used by flutter. We use the http package to request our network data. This package is officially provided by flutter. The actual project development may not use the http package, most of which are Use dio to request network data. Here we only introduce how to use the official http package.
  2. Import the http package. After the name, you can click to copy the package name.

3. Paste the copied package name in the project's pubspec.yaml.

4. After pasting, you need to get it through Pub get to get the code corresponding to the package.

  • You can also enter flutter packages get in the terminal to get it.

5. Import the http package in chat_page.dart and take an alias

import 'package:http/http.dart' as http;

6. Initiate a network request when rendering a state component, that is, initiate a network request in initState. After getData, async is used to indicate asynchronous execution. Async needs to be used with await, and await is followed by time-consuming code, so the call will be executed asynchronously .

class _ChatPageState extends State<ChatPage> {
    .....
    @override
    void initState() {
      super.initState();
      getDatas();
    }
    getDatas() async {
      var response = await http.get(Uri.parse('http://rap2api.taobao.org/app/mock/311243/api/chat/list'),);
      print(response.statusCode);//200
      print(response.body);//这里就是我们自定义的网络数据了
    }
    .....
}
  • Click on other interfaces and return to the chat interface again, and you will find that the initState method has gone through again and called the network request. This is because we have not saved the state. We will describe how to save the state of the Widget later.

7. Process the returned data

  • First, let me introduce how to convert the JSON data returned by the request into a Map in flutter. In our iOS development, it is converted into a dictionary. However, there is no dictionary type in flutter, and the corresponding type is Map. And how to convert Map into JSON. In iOS, we will use an NSJSONSerialization class to process JSON data. Similarly, there will be a special class JsonCode in flutter to process.

Convert between JSON and Map

  • First, you need to import the convert component in dart.
  • Then we write some test cases to get familiar with how it is used.
void initState() {
  super.initState();
  getDatas();
  final chat = {
    'name': '张三',
    'message': '在干嘛?',
  };
  //Map转JSON
  final jsonChat = json.encode(chat);
  print(jsonChat);
  //JSON转Map
  final mapChat = json.decode(jsonChat);
  print(mapChat);
  print(mapChat is Map);
}
  • The returned results are as follows:
flutter: {"name":"张三","message":"在干嘛?"}
flutter: {name: 张三, message: 在干嘛?}
flutter: true
flutter: 200
  • The json is an instance of JsonCodec. 'is' is used to judge whether it is a certain type.

8. Create a new chat model

  • Because the data from the network may be empty, then you need to use? to modify the defined attributes;
class Chat {
      final String? name;
      final String? message;
      final String? imageUrl;
      Chat(this.name,this.message,this.imageUrl);
      //工厂方法,用来初始化对象.
     factory Chat.fromJson(Map json){
       return Chat(json['name'],json['message'],json['imageUrl']);
     }
}
  • The factory keyword is used to mark the current factory method, which is a kind of design pattern and is used to initialize objects. In addition to the default construction method, you can also use this factory method to instantiate a Chat object. After the model is established, it can be processed the response data.
    • As follows: The model data is converted successfully.
//将json转为Chat模型
final chatModule = Chat.fromJson(mapChat);
print('name:${chatModule.name} message:${chatModule.message}');// name:张三 message:在干嘛? 

9. Process the response data

  • First of all, we will get the list data obtained through the network interface, but there is no guarantee that the network request will be sent successfully. So we need to deal with some error conditions. Introduce Future in flutter. It means that the next requested data may or may not have a value , generally used in conjunction with network requests.
  • So we can set the return value as .
Future<List<Chat>?> getDatas() async {}
  • For the handling of abnormal conditions, it can be in the form of throw Exception.
Future<List<Chat>?> getDatas() async {
  final response = await http.get(Uri.parse('http://rap2api.taobao.org/app/mock/311243/api/chat/list'));
  print(response.statusCode);
  if (response.statusCode == 200) {

  } else {
    throw Exception('statusCode: ${response.statusCode}');
  }
}
  • Next we process the data in the returned body
    • Get the response data and convert it to Map type
//获取响应数据,并且转换成Map类型
final responseBody = json.decode(response.body);
//转换模型数组
responseBody['chat_list'].map(
    (item) {
      print(item);
      return item;
    }
);
  • In this way, you can see the traversal data of the item.
flutter: {imageUrl: https://randomuser.mflutter: {imageUrl: https://randomuser.me/api/portraits/women/12.jpg, name: 黎超, message: 音和委起度明条部过们放省。们区以号还九保把王之候包与先件能议清。江知天能能五开比点别增石次米五平。极养提立手专把示低率号容眼组是石。离维照联子象派三热始受构参元离还。相电构次色影件力计面进东把。}
flutter: {imageUrl: https://randomuser.me/api/portraits/women/23.jpg, name: 傅秀兰, message: 但保写太满果此力少合反压色生太个图。制社并更个构北不张需国些清不。没八你或况铁员三时划志有改题头感。值年改你要变程新但八传织。进化林号中不按亲天张原美多。}e/api/portraits/women/37.jpg, name: 李丽, message: 可组品且发铁直报表状传素安小全。器音天石别数业局装共习清。加然处进派变装你农速约部族利音次层。毛得理状主质所局等工型即天研走机段。}
    • Next, directly traverse the returned result as a model and return it as a List
final responseBody = json.decode(response.body);
//转换模型数组
List<Chat>chatList = responseBody['chat_list'].map<Chat>(
    (item) {
      return Chat.fromJson(item);
    }
).toList();
print(chatList);
return chatList;
  • At this point we can see that the output is all instance objects
[Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', Instance of 'Chat',...] 
  • Arrow functions can be used directly in the above. In this way, we have realized the model conversion of the response data.
List<Chat>chatList = responseBody['chat_list'].map<Chat>((item) => Chat.fromJson(item)).toList();
return chatList;

10. Handle the result of the network request

  • Next we have to handle the asynchronous network request results that return the Future type.
    • You can use try...catch
    • It can also be combined with then
  • Here we use then to process the result and output the value.
loadData(){
  getDatas().then((value) {
    print(value);
  });
}
    • Here you can see that the result of value is our chatList data.
  • In fact, we can also use a simpler way to process network requests. It will be explained in the next chapter.

4. Use FutureBuilder to render the WeChat interface

  • There is a special control called FutureBuilder for rendering network data in flutter. When there is no data, the default interface is displayed, and when there is data, continue to render the data requested by the network.

  • At this point, you can see that the returned result is first null, and then several consecutive data.

  1. At this point we can view it through the asynchronous connection status of the snapshot.
  • That is snapshot.connectionState
    • When in the waiting state, data will return null
    • When in the done state, data will return the normal parsing result.
flutter: data: null
flutter: state:ConnectionState.waiting
flutter: data: [Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', ...]
flutter: state:ConnectionState.done

    2. So at this time we can use the ConnectionState state to determine the current interface to be rendered.

    • When waiting state, display a Loading...
    • When the done state, render the interface.
  • So the rendering implementation part of our FutureBuilder is
FutureBuilder(
  future: getDatas(),
  builder: (BuildContext context, AsyncSnapshot snapshot){
     //无数据时渲染默认界面,有数据时显示网络数据
    print('state:${snapshot.connectionState}');
    if (snapshot.connectionState == ConnectionState.waiting) {
      return Center(child: Text('Loading...'),);
    } else {
      return ListView(
        children: snapshot.data.map<Widget>((item){
            return ListTile(
              //右侧 标题
              title: Text(item.name),
              //右侧 子标题
              subtitle: Container(
                height: 20,width: 20,
                //TextOverflow.ellipsis 展示不下的时候省略号
                child: Text(item.message,overflow: TextOverflow.ellipsis,),
              ),
              //左侧:圆型头像
              leading: CircleAvatar(
                backgroundImage: NetworkImage(item.imageUrl),
              ),
            );
        }).toList(),
      );
    }
  },
)
  • The effect picture displayed is as follows:

  • But this rendering method is not the best. Because every time you enter the WeChat interface, you need to send a network request. Every time you have to re-render.
    • Then this way of FutureBilder directly rendering the layout can only be applied to interfaces with relatively simple data.
  • It is also possible to put the requested data into a cache model array, and use it from the model array when the builder is running.

5. Processing of network requests

  1. Here we process the data requested by the network. First come to _ChatPageState and create a cache array
List<Chat> _datas = [];

      2. Process the returned data during loadData.

  • Assign the normally returned data to the cache through then, and then implement the assignment operation in the setState method, which will cause the rendering of the interface.
  • Output the wrong return result through the log.
  • There will be a type mismatch error: set datas to a nullable type, and then empty the empty case when assigning a value.
loadData(){
  //当数据正常返回的时候
  getDatas().then((List<Chat>? datas) {
     setState(() {
       _datas = datas ?? [];
     });
  }).catchError((err){
    print(err);
  });
}

3. At this time, use the cached data to render the interface

  • The FutureBuilder of the body in the Scaffold is replaced back to the Container. Use the ternary operator to determine whether there is a value in the current cache array. If not, set Loading. If there is a value, render the interface
Container(
  child: _datas.length == 0 ?
  Center(child: Text('Loading...'),)
      : ListView.builder(
      itemCount: _datas.length,
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text(_datas[index].name ?? ""),
          subtitle: Container(height: 20,width: 20,child: Text(_datas[index].message ?? ""),),
          leading: CircleAvatar(
            backgroundImage: NetworkImage(_datas[index].imageUrl ?? ""),
          ),
        );
    }),
),
  • Click other NavigationBar interfaces, and then click back to the WeChat interface, you will find the rendering process of the interface.

4. Improve the processing of Future request results

  • In terms of the processing of Future request results, in addition to catchError, there are also whenComplete and timeout
loadData(){
  //当数据正常返回的时候
  getDatas().then((List<Chat>? datas) {
     setState(() {
       _datas = datas ?? [];
     });
  }).catchError((err){
    print(err);
  }).whenComplete(() {
    //数据处理完毕
    print("完毕");
  }).timeout(Duration(milliseconds: 10)).catchError((timeout){
    print("加载超时 ${timeout}");//flutter: 加载超时 TimeoutException after 0:00:00.010000: Future not completed
  });
}
  • Here, timeout does not mean the end of the request, when the request result is returned, the end of the request is still called.
  • Here, we need to deal with: Once the request is sent, unless there is an exception, other data should not be returned after the timeout to call our setState rendering interface.

5. Exception handling of request timeout/multiple refreshes

  • Set a private bool variable _cancelConnect, if the current flag is not true, then go to setState to render the interface. Avoid data pollution.

  • At this time, we found a phenomenon in both the interface and the address book interface: when we click on other interfaces and return to the current interface, the data will be reloaded. This phenomenon shows that we need to save the state to avoid repeated refresh. Next This phenomenon will be dealt with.

Sixth, save the state of the Widget

Regarding the preservation of state, we introduce another concept at this time: Mixins, which are used to add functions to classes, are a kind of code reuse in the multi-inheritance mode. Use the with keyword to achieve mixing in one or more classes.

  1. Because the rendering efficiency of flutter is very high, when the control is not displayed on the interface, it will be destroyed, and it will be re-rendered when it is displayed again.
  2. If our state needs to be saved, we need to mix in a class: AutomaticKeepAliveClientMixin, which is an extension of ChatPage. Rewrite the wantKeepAlive attribute and call the rendering method of the parent class in the build rendering method.
//AutomaticKeepAliveClientMixin让当前界面保存状态
class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin<ChatPage>{
.....
//保留setState状态
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
  //6.3 重写父类渲染方法
  super.build(context);
  ...
}
...
}

3. In the same way, let's save the state of the address book interface.

4. At this time, we went to check the effect of the settings, and found that the settings did not seem to be successful. The corresponding interface will still be re-rendered. Is the setting invalid?

  • In fact, it is not. At this time, we need to consider the problem of our root view settings. Because the pits were pre-buried before.

5. Going back to rootpage.dart, we found that when setting the current display body, the corresponding interface is obtained from the _pages array. In iOS, there is no problem with this setting.

  • But in flutter, if it is set like this, the view display object obtained according to _currentIndex every time is not the object set in the array.
    • Because in flutter, the objects created in the array are just a bunch of data for flutter, which are rendered to the interface through the build method.
    • There is a widget tree in the build. This widget tree is built and rendered from MyApp, and then the RootPage object contained in its home attribute, and then the ChatPage, FriendsPage, etc. contained in the Container set in the RootPage.
MyApp => RootPage => Container => ChatPage/FriendsPage/DiscoverPage/MinePage
  • Once the current _currentIndex changes, the set body gets the corresponding interface object from _pages. However, this operation causes the Page contained in the current Container to switch from one to another, and the previous interface is not in the rendering tree. That is, it was destroyed.

6. Back to our purpose of mixing the AutomaticKeepAliveClientMixin class in the WeChat interface/address book interface:

    • Let the specified interfaces outside the widget tree not be destroyed. That is, we want to keep the state of the four interfaces under the root view. When one of them is rendered in the widget tree, the other three should not be destroyed.
  • To solve this problem, we introduce PageController, another control in flutter.

7. PageController sets the root page root view interface.

  • First create a private variable PageController _controller, and set the initial interface page as the first interface.
final PageController _controller = PageController(
   //初始显示的界面索引
   initialPage: 0,
);
  • Secondly, set the body of Scaffold in Container to PageView
    • onPageChanged is where we need to set the callback for index changes
    • NeverScrollableScrollPhysics can remove the default left and right scrolling effects.
PageView(
  onPageChanged: (int index ){
    _currentIndex = index;
    setState(() {
    });
  },
  //如果不设置这个属性,那么根视图事件可以左右滚动切换.
  physics: NeverScrollableScrollPhysics(),
  controller: _controller,
  children: [
    ChatPage(),
    FriendsPage(),
    DiscoverPage(),
    MinePage()
  ],
),
  • To sum up: After finishing these operations, we click on any interface, and the previous state will be preserved.

Guess you like

Origin blog.csdn.net/SharkToping/article/details/130515037