{"id":364686,"date":"2024-05-21T02:22:15","date_gmt":"2024-05-21T02:22:15","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=364686"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=364686","title":{"rendered":"<span>WebSocket Reconnection in Flutter<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Continuous socket connection can be crucial to ensure correct app behavior. Whether it\u2019s delivering real-time chat updates, stock prices, or in-app indicators, a reliable connection is vital.<\/p>\n<p>One of the irritating problems with sockets is a sudden loss of connection. If the true cause is not visibly evident, i.e., unstable internet connection, then the disruption cause is often well hidden. To tackle this issue we can implement an automatic socket reconnection strategy. Let\u2019s see what options we have in the industry-standard socket library for Dart \u2014\u00a0<a href=\"https:\/\/pub.dev\/packages\/web_socket_channel\" rel=\"noopener noreferrer nofollow\"><u>web_socket_channel<\/u><\/a>.<\/p>\n<h3>The Classic Approach<\/h3>\n<p>The example from the library is pretty much straightforward:<\/p>\n<pre><code class=\"dart\">import 'package:web_socket_channel\/web_socket_channel.dart'; import 'package:web_socket_channel\/status.dart' as status;  main() async {   final wsUrl = Uri.parse('ws:\/\/localhost:1234')   var channel = WebSocketChannel.connect(wsUrl);    channel.stream.listen((message) {     channel.sink.add('received!');     channel.sink.close(status.goingAway);   }); }<\/code><\/pre>\n<p>Unfortunately, <code>WebSocketChannel<\/code><em> <\/em>doesn\u2019t offer built-in configuration options for handling reconnection. Hence, we need to manually react to the stream errors. Let\u2019s imitate the sudden error from the WebSocket. Here\u2019s how you can catch errors in the stream listener:<\/p>\n<pre><code class=\"dart\">channel.stream.listen(    (message) {      channel.sink.add('received!');    },    onError: (error) {      \/\/ Handle error here    },    onDone: () {      \/\/ Handle socket disruption    }, );<\/code><\/pre>\n<p>The typical solution would be to call <code>WebSocketChannel.connect<\/code><em> <\/em>again and override the stream in the callback.<\/p>\n<pre><code class=\"dart\">onDone: () {   channel = WebSocketChannel.connect(Uri.parse(url));   stream = channel.stream.listen(     ...   ); },<\/code><\/pre>\n<p>While this approach works, it can become cumbersome in a production application with a well-structured architecture.<\/p>\n<h3>The Clean Architecture Solution<\/h3>\n<p>A typical app architecture consists of different layers, classes, and zones of responsibility. Let\u2019s look at the clean architecture <a href=\"https:\/\/verygood.ventures\/blog\/very-good-flutter-architecture\" rel=\"noopener noreferrer nofollow\">example<\/a>:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9eb\/6c0\/05d\/9eb6c005d6a68c9a31dcc8bd385ecf31.png\" width=\"800\" height=\"641\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/9eb\/6c0\/05d\/9eb6c005d6a68c9a31dcc8bd385ecf31.png\"\/><\/figure>\n<p>Sockets are often initialized in the data layer and used in the presentation layer. Ideally, the presentation layer shouldn\u2019t know about the socket&#8217;s inner work including whether the socket is trying to reconnect.<\/p>\n<p>However, the previous approach forces us to manage reconnection logic in the presentation layer. So, instead of overwhelming the UI layer with data responsibilities, we can consolidate the reconnection logic to the data layer, and even better, within a single class.<\/p>\n<h3>The Two-Stream Strategy<\/h3>\n<p>The idea is to utilize two streams: an inner stream that maintains a connection to the socket and an outer stream that serves as the entry point for other classes, all while preserving the connection. The inner stream is responsible for handling errors and reconnections, while the outer stream remains untouched, waiting for data from the inner stream.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d82\/72f\/577\/d8272f5774a4295f587324b1736e9966.png\" width=\"1200\" height=\"302\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/d82\/72f\/577\/d8272f5774a4295f587324b1736e9966.png\"\/><\/figure>\n<h3>Implementation: The SocketChannel Class<\/h3>\n<p>Let\u2019s dive into the implementation of the <code>SocketChannel<\/code> class, which will handle our reconnection logic. We\u2019ll start by passing the socket configuration to this class:<\/p>\n<pre><code class=\"dart\">SocketChannel getChannel() {     return SocketChannel(       () => IOWebSocketChannel.connect(         'ws:\/\/localhost:1234',       ),     ); }<\/code><\/pre>\n<p>The <code>SocketChannel<\/code> class will handle subscriptions, reconnection, message sending, and data streaming. We also need a sink to pass messages and <code>IOWebSocketChannel<\/code><em> <\/em>itself, which will be extracted from the constructor parameter.<\/p>\n<p>As discussed before we are going to implement an inner stream and an outer stream, and the latter will be presented by <code>BehaviorSubject<\/code> from the <a href=\"https:\/\/pub.dev\/packages\/rxdart\" rel=\"noopener noreferrer nofollow\">rxdart<\/a> library. Thus, every time someone connects to our socket class, they will get the latest data from the socket.<\/p>\n<pre><code class=\"dart\">class SocketChannel {   SocketChannel(this._getIOWebSocketChannel) {     _startConnection();   }    final IOWebSocketChannel Function() _getIOWebSocketChannel;    late IOWebSocketChannel _ioWebSocketChannel;    WebSocketSink get _sink => _ioWebSocketChannel.sink;    late Stream&lt;dynamic> _innerStream;    final _outerStreamSubject = BehaviorSubject&lt;dynamic>();    Stream&lt;dynamic> get stream => _outerStreamSubject.stream; }<\/code><\/pre>\n<p>Now, let\u2019s add the <code>_startConnection()<\/code> method to initiate the socket connection from the constructor:<\/p>\n<pre><code class=\"dart\">void _startConnection() {   _ioWebSocketChannel = _getIOWebSocketChannel();   _innerStream = _ioWebSocketChannel.stream;   _innerStream.listen(     (event) {       \/\/ Forward data to outer stream       _outerStreamSubject.add(event);     },     onError: (error) {       \/\/ Handle web socket connection error       _handleLostConnection();     },     onDone: () {       \/\/ Handle web socket connection break       _handleLostConnection();     },   ); }  void _handleLostConnection() {   _startConnection(); }<\/code><\/pre>\n<h3>Improved Reconnection Logic<\/h3>\n<p>To enhance our solution, let\u2019s address the scenario where a socket fails to reconnect immediately. For instance, if the internet connection is lost for a few minutes, we can implement a ping mechanism to check the server\u2019s status periodically. The first reconnection attempt should occur immediately after the initial connection break, with subsequent attempts being delayed.<\/p>\n<pre><code class=\"dart\">bool _isFirstRestart = false; bool _isFollowingRestart = false;  void _handleLostConnection() {   if (_isFirstRestart &amp;&amp; !_isFollowingRestart) {     Future.delayed(const Duration(seconds: 3), () {       _isFollowingRestart = false;       _startConnection();     });     _isFollowingRestart = true;   } else {     _isFirstRestart = true;     _startConnection();   } }<\/code><\/pre>\n<p>Finally, we can add a <code>close()<\/code> method to close the socket. Closing the <em>sink<\/em> will trigger the <code>onDone<\/code><em> <\/em>callback, so we need to set the flag <code>_isManuallyClose = true<\/code> inside the method and check it in the callback.<\/p>\n<pre><code class=\"dart\">bool _isManuallyClosed = false;  void _startConnection() { ...    onDone: () {     if (!_isManuallyClosed) {       _handleLostConnection();     }   },  ... }    void close() {   _isManuallyClosed = true;   _sink.close(); }<\/code><\/pre>\n<p>Final result:<\/p>\n<pre><code class=\"dart\">import 'package:rxdart\/rxdart.dart'; import 'package:web_socket_channel\/io.dart'; import 'package:web_socket_channel\/web_socket_channel.dart';  class SocketChannel {   SocketChannel(this._getIOWebSocketChannel) {     _startConnection();   }    final IOWebSocketChannel Function() _getIOWebSocketChannel;    late IOWebSocketChannel _ioWebSocketChannel;    WebSocketSink get _sink => _ioWebSocketChannel.sink;    late Stream&lt;dynamic> _innerStream;    final _outerStreamSubject = BehaviorSubject&lt;dynamic>();    Stream&lt;dynamic> get stream => _outerStreamSubject.stream;    bool _isFirstRestart = false;   bool _isFollowingRestart = false;   bool _isManuallyClosed = false;    void _handleLostConnection() {     if (_isFirstRestart &amp;&amp; !_isFollowingRestart) {       Future.delayed(const Duration(seconds: 3), () {         _isFollowingRestart = false;         _startConnection();       });       _isFollowingRestart = true;     } else {       _isFirstRestart = true;       _startConnection();     }   }    void _startConnection() {     _ioWebSocketChannel = _getIOWebSocketChannel();     _innerStream = _ioWebSocketChannel.stream;     _innerStream.listen(       (event) {         _isFirstRestart = false;         _outerStreamSubject.add(event);       },       onError: (error) {         _handleLostConnection();       },       onDone: () {         if (!_isManuallyClosed) {           _handleLostConnection();         }       },     );   }    void sendMessage(String message) => _sink.add(message);    void close() {     _isManuallyClosed = true;     _sink.close();   } }<\/code><\/pre>\n<h3>Conclusion<\/h3>\n<p>In this article, we explored socket reconnection in Flutter applications and implemented a clean and efficient solution using the <code>SocketChannel<\/code> class. By encapsulating reconnection logic within the data layer, we can keep our presentation layer clean. With the added feature of delayed reconnections, we&#8217;ve built a foundation for maintaining continuous socket connections.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/784872\/\"> https:\/\/habr.com\/ru\/articles\/784872\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Continuous socket connection can be crucial to ensure correct app behavior. Whether it\u2019s delivering real-time chat updates, stock prices, or in-app indicators, a reliable connection is vital.<\/p>\n<p>One of the irritating problems with sockets is a sudden loss of connection. If the true cause is not visibly evident, i.e., unstable internet connection, then the disruption cause is often well hidden. To tackle this issue we can implement an automatic socket reconnection strategy. Let\u2019s see what options we have in the industry-standard socket library for Dart \u2014\u00a0<a href=\"https:\/\/pub.dev\/packages\/web_socket_channel\" rel=\"noopener noreferrer nofollow\"><u>web_socket_channel<\/u><\/a>.<\/p>\n<h3>The Classic Approach<\/h3>\n<p>The example from the library is pretty much straightforward:<\/p>\n<pre><code class=\"dart\">import 'package:web_socket_channel\/web_socket_channel.dart'; import 'package:web_socket_channel\/status.dart' as status;  main() async {   final wsUrl = Uri.parse('ws:\/\/localhost:1234')   var channel = WebSocketChannel.connect(wsUrl);    channel.stream.listen((message) {     channel.sink.add('received!');     channel.sink.close(status.goingAway);   }); }<\/code><\/pre>\n<p>Unfortunately, <code>WebSocketChannel<\/code><em> <\/em>doesn\u2019t offer built-in configuration options for handling reconnection. Hence, we need to manually react to the stream errors. Let\u2019s imitate the sudden error from the WebSocket. Here\u2019s how you can catch errors in the stream listener:<\/p>\n<pre><code class=\"dart\">channel.stream.listen(    (message) {      channel.sink.add('received!');    },    onError: (error) {      \/\/ Handle error here    },    onDone: () {      \/\/ Handle socket disruption    }, );<\/code><\/pre>\n<p>The typical solution would be to call <code>WebSocketChannel.connect<\/code><em> <\/em>again and override the stream in the callback.<\/p>\n<pre><code class=\"dart\">onDone: () {   channel = WebSocketChannel.connect(Uri.parse(url));   stream = channel.stream.listen(     ...   ); },<\/code><\/pre>\n<p>While this approach works, it can become cumbersome in a production application with a well-structured architecture.<\/p>\n<h3>The Clean Architecture Solution<\/h3>\n<p>A typical app architecture consists of different layers, classes, and zones of responsibility. Let\u2019s look at the clean architecture <a href=\"https:\/\/verygood.ventures\/blog\/very-good-flutter-architecture\" rel=\"noopener noreferrer nofollow\">example<\/a>:<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>Sockets are often initialized in the data layer and used in the presentation layer. Ideally, the presentation layer shouldn\u2019t know about the socket&#8217;s inner work including whether the socket is trying to reconnect.<\/p>\n<p>However, the previous approach forces us to manage reconnection logic in the presentation layer. So, instead of overwhelming the UI layer with data responsibilities, we can consolidate the reconnection logic to the data layer, and even better, within a single class.<\/p>\n<h3>The Two-Stream Strategy<\/h3>\n<p>The idea is to utilize two streams: an inner stream that maintains a connection to the socket and an outer stream that serves as the entry point for other classes, all while preserving the connection. The inner stream is responsible for handling errors and reconnections, while the outer stream remains untouched, waiting for data from the inner stream.<\/p>\n<figure class=\"full-width\"><\/figure>\n<h3>Implementation: The SocketChannel Class<\/h3>\n<p>Let\u2019s dive into the implementation of the <code>SocketChannel<\/code> class, which will handle our reconnection logic. We\u2019ll start by passing the socket configuration to this class:<\/p>\n<pre><code class=\"dart\">SocketChannel getChannel() {     return SocketChannel(       () => IOWebSocketChannel.connect(         'ws:\/\/localhost:1234',       ),     ); }<\/code><\/pre>\n<p>The <code>SocketChannel<\/code> class will handle subscriptions, reconnection, message sending, and data streaming. We also need a sink to pass messages and <code>IOWebSocketChannel<\/code><em> <\/em>itself, which will be extracted from the constructor parameter.<\/p>\n<p>As discussed before we are going to implement an inner stream and an outer stream, and the latter will be presented by <code>BehaviorSubject<\/code> from the <a href=\"https:\/\/pub.dev\/packages\/rxdart\" rel=\"noopener noreferrer nofollow\">rxdart<\/a> library. Thus, every time someone connects to our socket class, they will get the latest data from the socket.<\/p>\n<pre><code class=\"dart\">class SocketChannel {   SocketChannel(this._getIOWebSocketChannel) {     _startConnection();   }    final IOWebSocketChannel Function() _getIOWebSocketChannel;    late IOWebSocketChannel _ioWebSocketChannel;    WebSocketSink get _sink => _ioWebSocketChannel.sink;    late Stream&lt;dynamic> _innerStream;    final _outerStreamSubject = BehaviorSubject&lt;dynamic>();    Stream&lt;dynamic> get stream => _outerStreamSubject.stream; }<\/code><\/pre>\n<p>Now, let\u2019s add the <code>_startConnection()<\/code> method to initiate the socket connection from the constructor:<\/p>\n<pre><code class=\"dart\">void _startConnection() {   _ioWebSocketChannel = _getIOWebSocketChannel();   _innerStream = _ioWebSocketChannel.stream;   _innerStream.listen(     (event) {       \/\/ Forward data to outer stream       _outerStreamSubject.add(event);     },     onError: (error) {       \/\/ Handle web socket connection error       _handleLostConnection();     },     onDone: () {       \/\/ Handle web socket connection break       _handleLostConnection();     },   ); }  void _handleLostConnection() {   _startConnection(); }<\/code><\/pre>\n<h3>Improved Reconnection Logic<\/h3>\n<p>To enhance our solution, let\u2019s address the scenario where a socket fails to reconnect immediately. For instance, if the internet connection is lost for a few minutes, we can implement a ping mechanism to check the server\u2019s status periodically. The first reconnection attempt should occur immediately after the initial connection break, with subsequent attempts being delayed.<\/p>\n<pre><code class=\"dart\">bool _isFirstRestart = false; bool _isFollowingRestart = false;  void _handleLostConnection() {   if (_isFirstRestart &amp;&amp; !_isFollowingRestart) {     Future.delayed(const Duration(seconds: 3), () {       _isFollowingRestart = false;       _startConnection();     });     _isFollowingRestart = true;   } else {     _isFirstRestart = true;     _startConnection();   } }<\/code><\/pre>\n<p>Finally, we can add a <code>close()<\/code> method to close the socket. Closing the <em>sink<\/em> will trigger the <code>onDone<\/code><em> <\/em>callback, so we need to set the flag <code>_isManuallyClose = true<\/code> inside the method and check it in the callback.<\/p>\n<pre><code class=\"dart\">bool _isManuallyClosed = false;  void _startConnection() { ...    onDone: () {     if (!_isManuallyClosed) {       _handleLostConnection();     }   },  ... }    void close() {   _isManuallyClosed = true;   _sink.close(); }<\/code><\/pre>\n<p>Final result:<\/p>\n<pre><code class=\"dart\">import 'package:rxdart\/rxdart.dart'; import 'package:web_socket_channel\/io.dart'; import 'package:web_socket_channel\/web_socket_channel.dart';  class SocketChannel {   SocketChannel(this._getIOWebSocketChannel) {     _startConnection();   }    final IOWebSocketChannel Function() _getIOWebSocketChannel;    late IOWebSocketChannel _ioWebSocketChannel;    WebSocketSink get _sink => _ioWebSocketChannel.sink;    late Stream&lt;dynamic> _innerStream;    final _outerStreamSubject = BehaviorSubject&lt;dynamic>();    Stream&lt;dynamic> get stream => _outerStreamSubject.stream;    bool _isFirstRestart = false;   bool _isFollowingRestart = false;   bool _isManuallyClosed = false;    void _handleLostConnection() {     if (_isFirstRestart &amp;&amp; !_isFollowingRestart) {       Future.delayed(const Duration(seconds: 3), () {         _isFollowingRestart = false;         _startConnection();       });       _isFollowingRestart = true;     } else {       _isFirstRestart = true;       _startConnection();     }   }    void _startConnection() {     _ioWebSocketChannel = _getIOWebSocketChannel();     _innerStream = _ioWebSocketChannel.stream;     _innerStream.listen(       (event) {         _isFirstRestart = false;         _outerStreamSubject.add(event);       },       onError: (error) {         _handleLostConnection();       },       onDone: () {         if (!_isManuallyClosed) {           _handleLostConnection();         }       },     );   }    void sendMessage(String message) => _sink.add(message);    void close() {     _isManuallyClosed = true;     _sink.close();   } }<\/code><\/pre>\n<h3>Conclusion<\/h3>\n<p>In this article, we explored socket reconnection in Flutter applications and implemented a clean and efficient solution using the <code>SocketChannel<\/code> class. By encapsulating reconnection logic within the data layer, we can keep our presentation layer clean. With the added feature of delayed reconnections, we&#8217;ve built a foundation for maintaining continuous socket connections.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/784872\/\"> https:\/\/habr.com\/ru\/articles\/784872\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-364686","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/364686","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=364686"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/364686\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=364686"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=364686"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=364686"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}