jamiewest / signalr_core

ASP.NET Core SignalR Dart Client
https://pub.dev/packages/signalr_core
MIT License
91 stars 58 forks source link

Recommendations and best practices? #53

Closed RCSandberg closed 7 months ago

RCSandberg commented 3 years ago

Hi,

Awesome job providing this!

This is obviously not an bug/issue, and can of course be answered with the phrase "it depends..." :) But I'm wondering if you have any implementation best practices on the Flutter side of things working with this? Signal-R is new to me so I'm not sure about common patterns.

For instance, is it best practice to keep a connection to the server open for the entire app and use that as a (lazy)singleton? How and when do you close the connection? Do you e.g. usually close the connection when the app is minimized? Or do you setup a new HubConnection for each route in the app when needed and close each connection "as soon as possible"?

In my case, and I guess this is a common vanilla case:

If you have any open source code that I could get inspired by I would happily check it out.

If this is too vague, feel free to close this :)

BR, Robert

janjoosse commented 3 years ago

I am not a pro on SignalR either, so take it with a pinch of salt, but this is how I use the package:

Here's the code for my HubConnectionService which handles (drumroll) the connection and handling the different messages. Probably I will rewrite the stuff a little bit and instead of letting the HubConnectionService calling methods on ChatService and EventService, I will then expose the content of the messages to services/widgets interested in them through Streams.

Feel free to comment/question/suggest.

class HubConnectionService {
  final _storageService = locator.get<StorageService>();
  final _chatService = locator.get<ChatService>();
  final _eventService = locator.get<EventService>();

  late HubConnection _connection;
  StreamController<HubConnectionState> _connectionStarted = StreamController<HubConnectionState>.broadcast();
  Stream<HubConnectionState> get connectionStarted => _connectionStarted.stream;

  Future<void> init() async {
    _connection = HubConnectionBuilder()
      .withUrl('${ApiRoutes.apiBaseUrl}${ApiRoutes.negotiate}', 
        HttpConnectionOptions(accessTokenFactory: () => _storageService.getAccessToken()))
      .withAutomaticReconnect()
      .build();

    await _startConnection();
    if (_connection.state == HubConnectionState.connected) {
      _connectionStarted.sink.add(HubConnectionState.connected);
    }
  }

  Future<void> closeConnection() async {
    if (_connection.state == HubConnectionState.connected) {
      _connection.off(HubClient.receiveConversation);
      _connection.off(HubClient.receiveMessage);
      _connection.off(HubClient.receiveEvent);
      _connection.stop();
    } 
  }

  Future<void> restartConnection() async {
    await _startConnection();
  }

  Future<void> _startConnection() async {
    if (_connection.state == HubConnectionState.disconnected) {
      await _connection.start();
      _connection.on(HubClient.receiveConversation, (args) => _chatService.onConversation(args!));
      _connection.on(HubClient.receiveMessage, (args) => _chatService.onMessage(args!));
      _connection.on(HubClient.receiveEvent, (args) => _eventService.onEvent(args!));
    }
  }

  void dispose() {
    _connectionStarted.close();
  }
}
RCSandberg commented 3 years ago

Sweet! Many thanks for your reply!

tl;dr; Okey, yeah I kind of went for the same pattern. In fact, extremely close with regards to implementation as well :) I established the hubconnection when it is first required then keep it for use in the entire app until the user close/minimize the app.

In my case I join different groups and register different callbacks on different routes and on different widgets on same route, and then when the routes/widgets are disposed I leave said groups and de-register the callbacks.

I think the only real difference is when I register the callbacks, and how I close my connections. I have to specify callback using the method parameter when I close a connection because I have several callbacks methods listening to the same signal r method, so I'm extremely glad that is implemented :)

  @override
  void membershipApproved(CallbackFunc callback) {
    connection.on(HubMethod.membershipApproved, callback);
  }

  @override
  void closeMembershipApproved(CallbackFunc callback) {
    connection.off(HubMethod.membershipApproved, method: callback);
  }

Yet again, great that you provided this!