Future<void> checkConsent() async {
    ConsentManager.requestConsentInfoUpdate(Constants.kAppodealKey);

    ConsentManager.setConsentInfoUpdateListener(
        (onConsentInfoUpdated, consent) {
      print('PRINT: onConsentInfoUpdated $consent');
    }, (onFailedToUpdateConsentInfo, error) {
      print('PRINT: onFailedToUpdateConsentInfo $error');
    });

    var consentStatus = await ConsentManager.getConsentStatus();
    print('PRINT: consentStatus $consentStatus');
    if (consentStatus.toString() == 'Status.UNKNOWN') {
      var shouldShow = await ConsentManager.shouldShowConsentDialog();
      print('PRINT: shouldShow $shouldShow');

      if (shouldShow.toString() == 'ShouldShow.TRUE') {
        ConsentManager.loadConsentForm();

        var isLoaded = await ConsentManager.consentFormIsLoaded();
        print('PRINT: isLoaded $isLoaded');
        if (isLoaded == true) {
          ConsentManager.showAsDialogConsentForm();
          ConsentManager.showAsActivityConsentForm();

          ConsentManager.setConsentFormListener((onConsentFormLoaded) {
            print('PRINT: onConsentFormLoaded');
          }, (onConsentFormError, error) {
            print('PRINT: onConsentFormError $error');
          }, (onConsentFormOpened) {
            print('PRINT: onConsentFormOpened');
          }, (onConsentFormClosed, consent) {
            print('PRINT: onConsentFormClosed $consent');
          });
        }
      }
    }
  }

Constants.kAppodealKey is what I got in the Application key from here: https://app.appodeal.com/apps enter image description here

But this is what I got:

I/flutter ( 9497): PRINT: consentStatus Status.UNKNOWN
I/flutter ( 9497): PRINT: shouldShow ShouldShow.UNKNOWN

In the documentation ShouldShow.UKNOWN means this: https://wiki.appodeal.com/en/android/get-started/data-protection/gdpr-and-ccpa

UNKNOWN The value is undefined(the requestConsentInfoUpdate method was not called).

But I have called it on the first line of my method. May I know why it is having a problem?


Solution 1: luvzfootball

Important update 2022 07 18

less than 10 days after I posted this answer, a new Flutter Appodeal plugin (3.0.0) was made available which is claimed to make the handling of consent much easier.

Old answer

I was able to reproduce your issue with your code, my verified app key and those prints on the first attempt after app installation:

I/flutter (21755): PRINT: consentStatus Status.UNKNOWN
I/flutter (21755): PRINT: shouldShow ShouldShow.UNKNOWN
I/flutter (21755): PRINT: onConsentInfoUpdated {"createdAt":1655890242,"zone":"NONE","acceptedVendors":[],"iab":{"IABConsent_SubjectToGDPR":"0"},"updatedAt":1655890242,"status":"UNKNOWN"}

The main problem here is that you cannot rely on the end of the execution of a method call to consider its work completed. Even with await.

You have to either rely on retry and wait loops which I don't recommend as it can be intensive and it can fail easily. Or you can rely on the listeners.

What I did, is a mix of both because listeners are not always available.

I would love to get some feedback on this proposal as I am not certain I interpreted correctly the AppoDeal documentation.

I call AdManager.init(); once the app is loaded. This may also work if called earlier.

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stack_appodeal_flutter/stack_appodeal_flutter.dart';
import 'dart:io' show Platform;

class AdManager {
  static var consent = false;
  static Status consentStatus = Status.UNKNOWN;
  static int requestConsentInfoUpdateRetries = 20;
  static int requestLoadConsentFormRetries = 20;
  static int adType = Appodeal.INTERSTITIAL;

  static String get appKey =>
      Platform.isAndroid ?
      "your android app key"
          :
      "your ios app key"
  ;
  //app ids found here: https://app.appodeal.com/apps

  static initializeAfterConsent() {
    Appodeal.initialize(
      appKey,
      [
        adType,
      ],
      boolConsent: consent,
    ).whenComplete(() {
      Appodeal.isLoaded(adType).then((bool isLoaded) {
        if (!isLoaded) {
          Appodeal.cache(adType).whenComplete(() {
            debugPrint("Note: appodeal ad has been loaded");
          });
        }
        else {
          debugPrint("Note: appodeal ad was loaded");
        }
      });
    });
  }

  static init(bool testing) async {
    try {
      await handleATT();
      await Appodeal.setTesting(true);
      await Appodeal.setLogLevel(Appodeal.LogLevelVerbose);
      await Appodeal.disableNetwork("admob");
      await Appodeal.setAutoCache(adType, true);
      await Appodeal.setAutoCache(Appodeal.REWARDED_VIDEO, false);
      await Appodeal.setAutoCache(Appodeal.BANNER, false);
      await Appodeal.setAutoCache(Appodeal.MREC, false);
      await Appodeal.setChildDirectedTreatment(false);
      await Appodeal.setUseSafeArea(true);
      await Appodeal.muteVideosIfCallsMuted(true);
      await doTheConsentStuff();
    }
    catch (e, st) {
      debugPrint("Error initializing ads: ${e.toString()} ${st.toString()}");
    }
  }

  static Future<String> showInterstitialAd(
      {Function doWhenComplete, int retries = 10}) async {
    try {
      Map<String, Function> callsForAd = {
        "not initialized": () async {
          return await Appodeal.isInitialized(adType);
        },
        "not loaded": () async {
          return await Appodeal.isLoaded(adType);
        },
        "cannot show": () async {
          return await Appodeal.canShow(adType);
        },
        "not shown": () async {
          return await Appodeal.show(adType);
        },
      };
      for (int i = 0; i < callsForAd.length; i++) {
        if (!await callsForAd.values.toList()[i]()) {
          String res = callsForAd.keys.toList()[i];
          switch (res) {
            case "not initialized":
              await initializeAfterConsent();
              break;
            case "not loaded":
              await Appodeal.cache(adType);
              break;
            case "cannot show":
            case "not shown":
              if (retries > 0) {
                await Future.delayed(Duration(milliseconds: 100));
              }
              break;
            default:
              return (res);
              break;
          }
          if (retries > 0) {
            debugPrint(
                "Warning from ads: " + res + "; with retries: $retries");
            return (await showInterstitialAd(
                doWhenComplete: doWhenComplete, retries: retries - 1));
          }
          return (res);
        }
        else {
          if (doWhenComplete != null) {
            doWhenComplete();
          }
        }
      }
      return (null);
    }
    catch (e, st) {
      return ("${e.toString()} ${st.toString()}");
    }
  }

  static retryMaybeConsentInfoUpdate() {
    if (requestConsentInfoUpdateRetries > 0) {
      Future.delayed(Duration(milliseconds: 2000)).whenComplete(() {
        requestConsentInfoUpdateRetries--;
        ConsentManager.requestConsentInfoUpdate(appKey);
      });
    }
    else {
      debugPrint(
          "Error: exhausted all retries on retryMaybeConsentInfoUpdate, aborting consent handling");
      initializeAfterConsent();
    }
  }

  static retryMaybeLoadConsentForm() {
    if (requestLoadConsentFormRetries > 0) {
      Future.delayed(Duration(milliseconds: 2000)).whenComplete(() {
        requestLoadConsentFormRetries--;
        ConsentManager.loadConsentForm();
      });
    }
    else {
      debugPrint(
          "Error: exhausted all retries on requestLoadConsentFormRetries, aborting consent handling");
      initializeAfterConsent();
    }
  }

  static doTheConsentStuff() async {
    try {
      ConsentManager.setConsentInfoUpdateListener(
              (onConsentInfoUpdated, consent) {
            debugPrint(
                "Note: Appodeal consent onConsentInfoUpdated: $onConsentInfoUpdated : $consent");
            if (onConsentInfoUpdated == "onConsentInfoUpdated") {
              try {
                Map<String, dynamic> result = jsonDecode(consent);
                switch (result["status"]) {
                  case "UNKNOWN":
                    debugPrint("Note: got a consent info status of unknown");
                    updateConsentStatus().whenComplete(() {
                      getShouldShow().then((shouldShow) {
                        switch (shouldShow) {
                          case ShouldShow.TRUE:
                            ConsentManager.setConsentFormListener(
                                    (onConsentFormLoaded) {
                                  debugPrint(
                                      "Note: consent form loaded: $onConsentFormLoaded");
                                  if (Platform.isIOS) {
                                    ConsentManager.showAsActivityConsentForm();
                                  }
                                  else {
                                    ConsentManager.showAsDialogConsentForm();
                                  }
                                },
                                    (onConsentFormError, error) {
                                  debugPrint(
                                      "Error: consent form error: $onConsentFormError: $error");
                                  if (error == "Nothing to load") {
                                    updateConsentStatus().whenComplete(() {
                                      initializeAfterConsent();
                                    });
                                  }
                                  else {
                                    retryMaybeLoadConsentForm();
                                  }
                                },
                                    (onConsentFormOpened) {
                                  debugPrint(
                                      "Note: consent form opened: $onConsentFormOpened");
                                },
                                    (onConsentFormClosed, consent) {
                                  debugPrint(
                                      "Note: consent form closed: $onConsentFormClosed: $consent");
                                  updateConsentStatus().whenComplete(() {
                                    initializeAfterConsent();
                                  });
                                }
                            );
                            ConsentManager.loadConsentForm();
                            break;
                          case ShouldShow.FALSE:
                            debugPrint("Note: no need to show consent form");
                            updateConsentStatus().whenComplete(() {
                              initializeAfterConsent();
                            });
                            break;
                          case ShouldShow.UNKNOWN:
                            retryMaybeConsentInfoUpdate();
                            break;
                          default:
                            debugPrint(
                                "Error: undefined consent shouldShow value, aborting consent handling");
                            initializeAfterConsent();
                            break;
                        }
                      });
                    });
                    break;
                  case "PARTLY_PERSONALIZED":
                    debugPrint(
                        "Note: got a consent info status of PARTLY_PERSONALIZED");
                    updateConsentStatus().whenComplete(() {
                      initializeAfterConsent();
                    });
                    break;
                  case "PERSONALIZED":
                    debugPrint(
                        "Note: got a consent info status of PERSONALIZED");
                    updateConsentStatus().whenComplete(() {
                      initializeAfterConsent();
                    });
                    break;
                  case "NON_PERSONALIZED":
                    debugPrint(
                        "Note: got a consent info status of NON_PERSONALIZED");
                    updateConsentStatus().whenComplete(() {
                      initializeAfterConsent();
                    });
                    break;
                  default:
                    debugPrint("Error: got an unknown consent info status");
                    retryMaybeConsentInfoUpdate();
                    break;
                }
              }
              catch (e, st) {
                debugPrint("Error reading onConsentInfoUpdated data");
                retryMaybeConsentInfoUpdate();
              }
            }
          },
              (onFailedToUpdateConsentInfo, error) {
            debugPrint(
                "Note: Appodeal consent onFailedToUpdateConsentInfo: $onFailedToUpdateConsentInfo : $error with $requestConsentInfoUpdateRetries retries");
            retryMaybeConsentInfoUpdate();
          });
      await ConsentManager.requestConsentInfoUpdate(appKey);
    }
    catch (e, st) {
      debugPrint("Error: consent form general error: ${e.toString()} ${st
          .toString()}");
    }
  }

  static Future<void> updateConsentStatus() async {
    consentStatus = await ConsentManager.getConsentStatus();
    switch (consentStatus) {
      case Status.UNKNOWN:
        debugPrint("Warning: consent status is unknown");
        consent = false;
        break;
      case Status.NON_PERSONALIZED:
      case Status.PARTLY_PERSONALIZED:
        consent = false;
        break;
      case Status.PERSONALIZED:
        debugPrint("Note: consent status is known");
        consent = true;
        break;
      default:
        consent = false;
        debugPrint("Warning: undefined consent status");
        break;
    }
  }

  static Future<ShouldShow> getShouldShow([int retries = 20]) async {
    var shouldShow = await ConsentManager.shouldShowConsentDialog();
    if (shouldShow == ShouldShow.UNKNOWN) {
      if (retries > 0) {
        await Future.delayed(Duration(milliseconds: 500));
        return (getShouldShow(retries - 1));
      }
    }
    return (shouldShow);
  }

  static Future<void> handleATT({int retries = 20}) async {
    if (Platform.isIOS) {
      Permission appTrackingTransparencyPermission = Permission
          .appTrackingTransparency;
      PermissionStatus aTTPermissionStatus = await appTrackingTransparencyPermission
          .status;
      if (aTTPermissionStatus == null ||
          aTTPermissionStatus == PermissionStatus.denied) {
        debugPrint("ATT is null or denied, requesting with retries: $retries");
        aTTPermissionStatus = await appTrackingTransparencyPermission.request();
        if (aTTPermissionStatus == null ||
            aTTPermissionStatus == PermissionStatus.denied) {
          if (retries > 0) {
            await Future.delayed(Duration(milliseconds: 500));
            await handleATT(retries: retries - 1);
          }
          else {
            debugPrint(
                "Warning: ATT permission request retries exhausted, aborting");
          }
        }
        return;
      }
      debugPrint(
          "ATT is either granted, permanently denied, limited or restricted, thus, not requesting");
    }
  }
}

With this code, I get this edited consent form:

Edited AppoDeal consent form

Then, the following is shown when I request to display an ad:

enter image description here

So, to me, apart from the deprecation warnings, everything seems to work fine.

Edit 2022 06 23: Running on iOS, I realized that the shouldShow answer is enough to know whether to show the consent form or not. Thus, the consent form is only displayed when my VPN is set to Europe or California. Also for iOS, I added ATT handling. I have made some other general improvements.

I do not work for Appodeal and I am not a legal expert. I just provide this code based on my personal understanding which may not lead to the right way to handle those ads and consent questions.