Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
walkerJung opened this issue Feb 23, 2024 · 53 comments
Closed

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

walkerJung opened this issue Feb 23, 2024 · 53 comments
Assignees
Labels
bug Something isn't working. It's clear that this does need to be fixed.

Comments

@walkerJung
Copy link

walkerJung commented Feb 23, 2024

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
Copy link
Author

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
Copy link
Contributor

ttypic commented Feb 23, 2024

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
Copy link
Author

@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
Copy link
Author

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
Copy link
Contributor

ttypic commented Feb 23, 2024

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

  • What platform do you use: iOS or Android?
  • Are you using an emulator or a physical device?
  • Could you share a code snippet to help us reproduce this behavior?

@sync-by-unito sync-by-unito bot closed this as completed Feb 23, 2024
@ttypic ttypic reopened this Feb 23, 2024
@walkerJung
Copy link
Author

walkerJung commented Feb 26, 2024

@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
Copy link
Author

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
Copy link
Author

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

@walkerJung
Copy link
Author

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

Fifteen seconds later, it's still disconnected

@walkerJung
Copy link
Author

스크린샷 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
Copy link
Author

스크린샷 2024-02-26 오후 2 26 48

@walkerJung
Copy link
Author

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
Copy link
Contributor

ttypic commented Feb 27, 2024

@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

@ttypic ttypic added the bug Something isn't working. It's clear that this does need to be fixed. label Feb 27, 2024
@walkerJung
Copy link
Author

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

@walkerJung
Copy link
Author

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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Author

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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Author

@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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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

@sacOO7
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Author

walkerJung commented Feb 28, 2024

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
Copy link
Author

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

@walkerJung
Copy link
Author

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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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

@walkerJung
Copy link
Author

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
Copy link
Author

스크린샷 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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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

@walkerJung
Copy link
Author

스크린샷 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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Author

So that method is not available now?

@walkerJung
Copy link
Author

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
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

createRecoveryKey method will be added as a part of #508

@sacOO7
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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

@sacOO7
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Author

Is this a bug that only happens to me?

@sacOO7
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Author

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

@sacOO7
Copy link
Collaborator

sacOO7 commented Feb 28, 2024

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
Copy link
Author

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

@sacOO7
Copy link
Collaborator

sacOO7 commented Feb 29, 2024

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

@walkerJung
Copy link
Author

@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
Copy link
Collaborator

sacOO7 commented Mar 4, 2024

@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

@sacOO7 sacOO7 self-assigned this Mar 4, 2024
@walkerJung
Copy link
Author

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
Copy link
Collaborator

sacOO7 commented Mar 4, 2024

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

@walkerJung
Copy link
Author

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

@sacOO7
Copy link
Collaborator

sacOO7 commented Mar 4, 2024

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

@walkerJung
Copy link
Author

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

@walkerJung
Copy link
Author

@sacOO7 Now I'm going to close this issue. It was my mistake using provider and thank you for your care.

@sacOO7
Copy link
Collaborator

sacOO7 commented Mar 4, 2024

Btw, @walkerJung if needed, you can post updated code here, so it's helpful for others. Also, about disconnectedRetryTimeoutand suspendedRetryTimeout, those options might not work on iOS for now. We are planning to address them soon as a part of #509

@sacOO7
Copy link
Collaborator

sacOO7 commented Mar 4, 2024

Feel free to raise new issues : )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working. It's clear that this does need to be fixed.
Development

No branches or pull requests

3 participants