ably / ably-flutter

A wrapper around our Cocoa and Java client library SDKs, providing iOS and Android support for those using Flutter and Dart.
https://ably.com/download
Apache License 2.0
60 stars 16 forks source link

When the app comes back to the foreground from the background #504

Closed walkerJung closed 8 months ago

walkerJung commented 8 months ago

Hi, I'm using flutter ably sdk. When testing on Android and IOS devices, when the app goes to the background and comes back to the foreground, there are times when the channel is not attached, but when the app comes back to the foreground, should I manage the realtime and channel?

┆Issue is synchronized with this Jira Bug by Unito

walkerJung commented 8 months ago

When I move the flutter app to the background and come back after about 15 minutes, I am logged as 80003 error. Connection temporarily unavailable code= 80003 Do I have to explicitly let you connect at times like this?

ttypic commented 8 months ago

Hey @walkerJung,

Thank you for choosing Ably! You don't need to explicitly reconnect after your app comes back from the background, but you need to check the resumed flag on the channel's attached event. When resumed is false, some messages may have been lost while the app was in the background, and you probably need to handle this in your business logic (e.g., refetch data from the backend).

Here is the quote from our FAQ docs about connection recovery:

Once the connection is reestablished, the client library will reattach the suspended channels automatically and emit an attached event with the resumed flag set to false. This ensures that as a developer, you can listen for attached events and check the resumed flag to see if a channel resumed fully and no messages were lost (when resumed is true), or the channel attached but could not resume (when resumed is false).

walkerJung commented 8 months ago

@ttypic Thank you for your answer, but the status of my realtime provider is not changing from disconnected to connected and all the connections are still disconnected.

walkerJung commented 8 months ago

On my app, I'm using it by declaring it as a provider to create only one realtime object because they're subscribing to different channels on different screens, is this part wrong?

ttypic commented 8 months ago

@walkerJung, it doesn't look like you're doing anything wrong. Can you share some details that can help us understand better:

walkerJung commented 8 months ago

@ttypic Thank you for your answer,

platform : IOS, Android i use physical device

// ably realtime
final ablyRealtimeProvider = Provider<Realtime>(
  (ref) {
    final realtime = Realtime(
      options: ClientOptions(
        key: ABLYAPIKEY,
        autoConnect: true,
        disconnectedRetryTimeout: 5000,
        suspendedRetryTimeout: 5000,
        fallbackRetryTimeout: 5000,
        channelRetryTimeout: 5000,
        logLevel: LogLevel.verbose,
      ),
    );

    return realtime;
  },
);

The above source code is my realtime object

My app has multiple channels, or chat screens, so I made this to create and write only one realtime object. When I send the app to the background on the screen where the entire user can gather and chat, and after a certain period of time, the realtime.connection.state does not change from disconnected to connected.

walkerJung commented 8 months ago

It seems to mainly happen on Android devices. After leaving the Android app in the background for more than 30 minutes to an hour, when I move to the foreground, the realtime connection seems to be disconnected and I don't try to reconnect

walkerJung commented 8 months ago

스크린샷 2024-02-26 오후 2 00 10

walkerJung commented 8 months ago

https://faqs.ably.com/error-code-80003

Fifteen seconds later, it's still disconnected

walkerJung commented 8 months ago
스크린샷 2024-02-26 오후 2 17 33

If you bring the app to the foreground when the above log is no longer taken, the connection does not change in disconnected.

walkerJung commented 8 months ago
스크린샷 2024-02-26 오후 2 26 48
walkerJung commented 8 months ago
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ably_flutter/ably_flutter.dart';
import 'package:vs_score_app/common/const/data.dart';

// ably realtime
final ablyRealtimeProvider = Provider<Realtime>(
  (ref) {
    final realtime = Realtime(
      options: ClientOptions(
        key: ABLYAPIKEY,
        autoConnect: true,
        disconnectedRetryTimeout: 5000,
        suspendedRetryTimeout: 5000,
        fallbackRetryTimeout: 5000,
        channelRetryTimeout: 5000,
        logLevel: LogLevel.verbose,
      ),
    );

    realtime.connection.on(ConnectionEvent.disconnected).listen((event) {
      realtime.connection.connect();
    });

    realtime.connection.on(ConnectionEvent.suspended).listen((event) {
      realtime.connection.connect();
    });

    realtime.connection.on(ConnectionEvent.failed).listen((event) {
      realtime.connection.connect();
    });

    realtime.connection.on(ConnectionEvent.closed).listen((event) {
      realtime.connection.connect();
    });

    return realtime;
  },
);

This is my source code, but it doesn't happen on ios, but on android the app doesn't connect ably when it comes back to foreground after about 5 hours in the background. I think it only happens on android, is there anyone similar to me?

ttypic commented 8 months ago

@walkerJung thank you very much for the details. We are looking into this. Right now, we still have difficulties reproducing the issue. Could you also share the Android version that you are using? We'll keep you posted as soon as we find something

walkerJung commented 8 months ago

@ttypic Thank you so much for answering, my test android phone's Android version is 13

walkerJung commented 8 months ago

Should I deliver the id value (realtime.connection.id ) of the realtime object to the recoverKey value? There were times when Error code : 80008 was shown. Maybe it's because the new connection is connected, but my app still refers to the last connection?

sacOO7 commented 8 months ago

Hi @walkerJung you don't need to set realtime.connection.id. Also, you don't need to handle reconnection explicitly. Our SDK clients automatically detects and handles reconnection. You can provide clientOption to provide reconnect timeout using disconnectedRetryTimeout and suspendedRetryTimeout.

walkerJung commented 8 months ago

hi @sacOO7 Thank you for your answer, then I don't need this part in my source code?

realtime.connection.on(ConnectionEvent.disconnected).listen((event) {
    realtime.connection.connect();
  });

  realtime.connection.on(ConnectionEvent.suspended).listen((event) {
    realtime.connection.connect();
  });

  realtime.connection.on(ConnectionEvent.failed).listen((event) {
    realtime.connection.connect();
  });

  realtime.connection.on(ConnectionEvent.closed).listen((event) {
    realtime.connection.connect();
  });
sacOO7 commented 8 months ago

Yes, you don't need to do it as such! Failed state is reached, when server closes connection due to some type of auth error or when invalid token is provided. Closed state is reached when you explicitly close the connection using realtime.close() method call. Both of those states will never reach, if auth mechanism is correct and no explicit close call is provided. Let me go through the whole thread first, I might be able to help you a bit. There is auto retry mechanism for disconnected and suspended states, you just need to provide timeout values as a part of clientOptions.

walkerJung commented 8 months ago

@sacOO7 Then I guess there will be an error in the part where I use realtime as a provider, but how should I create a realtime object if I use about realtime on multiple screens and attach different channels on each screen? Now, I have made it a provider so that I can use it like singleton to create only one realtime object on one app

sacOO7 commented 8 months ago

Yeah, I am familiar with a concept of providers in android. I can give you few suggestions on this. You should check for

  1. How does flutter providers match with service provider in android
  2. What is a lifecycle of service providers/providers, are they active when app is in background? Seems they work just fine when app is in the foreground
  3. Some of those providers are automatically killed when app goes background, you can check if you can set a priority for the same. Also, check if we can provide a callback to a provider when it is killed.
  4. Behaviour of auto-killing may differ in android or iOS platforms. You should check https://riverpod.dev/docs/concepts/provider_lifecycles. Also, look for paused state. https://medium.com/@madampitige90/provider-state-management-in-flutter-3bc555a1eafb
sacOO7 commented 8 months ago

You can also check official doc. on state management https://docs.flutter.dev/data-and-backend/state-mgmt/simple

sacOO7 commented 8 months ago

I feel problem with using Providers is that they are tightly bound to app lifecycle and hence app going foreground or background can affect them depending on the android or ios platform. If Providers doesn't work as expected, I would recommend using https://pub.dev/packages/flutter_background_service which runs in a different isolate. It might need extra permissions depending on the platform but will make sure your app will have a dedicated background service which doesn't depend on app lifecycle as such. Your app being a chat app, it will be necessary to receive notifications even if app is closed Or you can choose to run background service only when app is foreground/background and kill the service when app is closed.

walkerJung commented 8 months ago

I think your advice will be very helpful. I will refer to the linked content carefully and modify the realtime object I made as a provider. Thank you very much! @sacOO7

walkerJung commented 8 months ago

I changed the realtime object from provider to global variable and it seems the same thing happens on Android

walkerJung commented 8 months ago
import 'package:ably_flutter/ably_flutter.dart';
import 'package:vs_score_app/common/const/data.dart';

Realtime? globalRealtime;

void initializeGlobalRealtime() {
  globalRealtime = Realtime(
    options: ClientOptions(
      key: ABLYAPIKEY,
      autoConnect: true,
      disconnectedRetryTimeout: 5000,
      suspendedRetryTimeout: 5000,
      fallbackRetryTimeout: 5000,
      channelRetryTimeout: 5000,
      logLevel: LogLevel.verbose,
    ),
  );
}
sacOO7 commented 8 months ago

Providers are supposed to be global object tied to global application context ( at least in android ). Can you check how it transpiles in case of android. It should ideally be bound at application state level. A subclass of Application is responsible for maintaining all global objects. If your provider lives inside Activity context ( given visible layout ), then it will not work when u switch to other layouts or app goes background.

sacOO7 commented 8 months ago

Can you check if you can initialize Provider inside FlutterApplication class as per -> https://stackoverflow.com/a/49084400/7363205

walkerJung commented 8 months ago

I'm not using a provider right now,

// realtime.dart

import 'package:ably_flutter/ably_flutter.dart';
import 'package:vs_score_app/common/const/data.dart';

Realtime? globalRealtime;

void initializeGlobalRealtime() {
  globalRealtime = Realtime(
    options: ClientOptions(
      key: ABLYAPIKEY,
      autoConnect: true,
      disconnectedRetryTimeout: 5000,
      suspendedRetryTimeout: 5000,
      fallbackRetryTimeout: 5000,
      channelRetryTimeout: 5000,
      logLevel: LogLevel.verbose,
    ),
  );
}
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  initializeGlobalRealtime();
  await initializeAppSettings();
  await initFCM();

  SystemChrome.setPreferredOrientations(
      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]).then(
    (_) {
      initializeDateFormatting().then(
        (_) => runApp(
          const ProviderScope(
            child: _App(),
          ),
        ),
      );
    },
  );
}

Create a realtime object by calling initializeGlobalRealtime() within the main method

void initializeAbly() async {
    final realtime = globalRealtime;
    chatChannel = realtime!.channels.get('SCORE:CHAT:MAIN');
    subscription = chatChannel
        .subscribe(names: ['system', 'punishment', 'chat', 'change']).listen(
            (ably.Message message) {
      if (message.data is Map) {
        var chat = (message.data as Map).cast<String, dynamic>();
        print("전체 채팅 메세지 도착: ${chat['chat']['message']}");
        final meState = ref.read(meProvider);
        // 채팅 삭제
        if (chat['type'] == 'deleted_message') {
          setState(() {
            _messages.removeWhere(
                (item) => item['chat']?['chatId'] == chat['chat']['chatId']);
          });
          return;
        }

        // 채팅 신고 5회시
        if (chat['type'] == 'report5') {
          for (var item in _messages) {
            if (item['chat']?['chatId'] == chat['chat']['chatId']) {
              setState(() {
                item['chat']['message'] = chat['chat']['message'];
              });
              break;
            }
          }
          return;
        }

        // 차단한 사람의 메세지 (방장은 차단 여부와 별개로 모두 볼수있음)
        if (meState is ResponseModel &&
            meState.data!['myBlocks']
                .contains(chat['chat']?['profile']?['uid'])) {
          return;
        }

        // 스크롤이 0~80 사이에 메세지가 오는 경우
        if (_scrollController.hasClients) {
          double maxOffset = _scrollController.position.maxScrollExtent;
          double currentOffset = _scrollController.offset;
          double eightyPercentOfMaxOffset = maxOffset * 0.2;

          if (chat['type'] == 'normal' &&
              currentOffset >= eightyPercentOfMaxOffset &&
              _scrollController.position.maxScrollExtent > 0) {
            _bottomMessage = message.data;
          }
        }

        // 메세지 렌더링
        setState(() {
          _messages.insert(0, message.data);
        });
      }
    });
  }

I'm using it like this where I need realtime

walkerJung commented 8 months ago

스크린샷 2024-02-28 오후 6 42 58

How can I know? It's connected to a new connection. What should I do if I get a new connection?

sacOO7 commented 8 months ago

You can check the documentation -> https://ably.com/docs/connect/states?lang=java

walkerJung commented 8 months ago

스크린샷 2024-02-28 오후 7 32 02

final ablyRealtime = AblyRealtimeSingleton().realtime;
print(ablyRealtime.connection.key);

Do I have to transfer the key value here to recover to keep the connection? It comes out as a key value null.

sacOO7 commented 8 months ago

As a part of new flutter release, we will have a method called createRecoveryKey on connection. It returns string value. You can pass it as a recover option to recover the connection.

walkerJung commented 8 months ago

So that method is not available now?

walkerJung commented 8 months ago

I make and use one realtime on the source code, but I don't understand that the app is disconnected when it goes to the background only on Android and then comes back to the foreground after a certain amount of time.

My app doesn't need to be connected even in the background. When the app returns to the foreground, there is also a logic to add observer to bring back data, so for me, only about connection needs to be connected and operated normally, but I don't know why that part isn't working

sacOO7 commented 8 months ago

createRecoveryKey method will be added as a part of https://github.com/ably/ably-flutter/pull/508

sacOO7 commented 8 months ago

Interesting, you might like to take a look at https://developer.android.com/training/monitoring-device-state/doze-standby#understand_doze.

sacOO7 commented 8 months ago

You might like to test your app in doze mode https://developer.android.com/training/monitoring-device-state/doze-standby#assessing_your_app. It will be helpful if we can reproduce the issue.

walkerJung commented 8 months ago

Is this a bug that only happens to me?

sacOO7 commented 8 months ago

Not sure, it seems @ttypic is not able to reproduce the same from his side. Maybe, it has do with the way flutter_riverpod.Provider handles realtime object when app goes background. But, if system is dropping network connection because it goes into doze mode, then you need to reconnect after it comes online. I think there might be cases where client will not retry when system abruptly closes the connection ( it would be great, if we are able to find the case by reproducing the issue )

walkerJung commented 8 months ago

I have now changed the source code to disable the riversepod provider, but the same thing is still happening

sacOO7 commented 8 months ago

Hey, can you do one thing? Upload this buggy flutter source code as a example github repo. I can clone and will try to reproduce the same from my side. Also are you using emulator for this ?

walkerJung commented 8 months ago

I'm testing with an actual Android device, you're saying I need a full source code that I can build right away?

sacOO7 commented 8 months ago

Yes, are you able to reproduce the same on android emulator?

walkerJung commented 8 months ago

@sacOO7

Thanks to your help, I think using provider was a big problem. Apart from this, I have another question, and I wonder why the above source doesn't work. When the app goes to the background and comes back, it shows loader if it's not connected, and when it's connected, it clears loader, but the loader keeps showing up even after it's actually connected

final ablyRealtime = AblyRealtimeSingleton().realtime;
isAblyConnected =
    ablyRealtime.connection.state == ably.ConnectionState.connected;
ablyRealtime.connection.on().listen((event) {
  print("1111:::$event");
  if (event.current == ably.ConnectionState.connected) {
     isAblyConnected = true;
  } else {
    isAblyConnected = false;
  }
  setState(() {});
});
sacOO7 commented 8 months ago

@walkerJung is your issue resolved ( apart from loader issue ) ? What did you do differently to implement the same ? About loader issue, can you try declaring _isAblyConnected as a state variable, that is updated inside

setState(() {
_isAblyConnected = isAblyConnected;
});

https://api.flutter.dev/flutter/widgets/State/setState.html https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html

walkerJung commented 8 months ago

The part surrounding the realtime with a provider was simply transformed into a single tone class and used.

How many seconds do you usually set the values of disconnected RetryTimeout, suspendedRetryTimeout, fallbackRetryTimeout, and channelRetryTimeout? Is it okay to set it to 3000ms?

sacOO7 commented 8 months ago

You can set it according to your requirement. But, default values from disconnectedRetryTimeout and suspendedRetryTimeout are 15s and 30s respectivly

walkerJung commented 8 months ago

Are there any problems when you set it shorter than 3 seconds or 3 seconds?

sacOO7 commented 8 months ago

Yes, you can set and test the same. Btw, ably-client working as expected now ?

walkerJung commented 8 months ago

Yes, it's working normally. Your help is really good. I think the problem was with using the provider