Refresh page if data isn't shown on screen

Issue

I have a Future in my initState function that gets jwt from cache and uses it to get the logged in user’s details. The initState function is:

  @override
  void initState() {
    super.initState();
    Future.delayed(Duration.zero, () async {
      final token = await CacheService().readCache(key: "jwt");
      if (token != null) {
        await Provider.of<ProfileNotifier>(context, listen: false)
            .decodeUserData(
          context: context,
          token: token,
          option: 'home',
        );
      }
    });
  }

Now, it does work and I do get the data, but not on the first run. I have to either hot reload the emulator or navigate to another page and come back for the page to rebuild itself and show the data on screen. I don’t understand why it doesn’t show the data on the first run itself.

ProfileNotifier class:

class ProfileNotifier extends ChangeNotifier {
  final ProfileAPI _profileAPI = ProfileAPI();
  final CacheService _cacheService = CacheService();

  ProfileModel _profile = ProfileModel(
    profileImage: "",
    profileName: "",
    profileBio: "",
  );

  AccountModel _account = AccountModel(
    userId: "",
    userEmail: "",
    userPassword: "",
  );

  ProfileModel get profile => _profile;
  AccountModel get account => _account;

  Future decodeUserData({
    required BuildContext context,
    required String token,
    required String option,
  }) async {
    try {
      _profileAPI.decodeUserData(token: token).then((value) async {
        final Map<String, dynamic> parsedData = await jsonDecode(value);
        var userData = parsedData['data'];
        if (userData != null) {
          List<String>? userProfileData = await _cacheService.readProfileCache(
            key: userData['userData']['id'],
          );
          if (userProfileData == null) {
            final isProfileAvailable =
                await Provider.of<ProfileNotifier>(context, listen: false)
                    .getProfile(
              context: context,
              userEmail: userData['userData']['userEmail'],
            );
            if (isProfileAvailable is ProfileModel) {
              _profile = isProfileAvailable;
            } else {
              _account = AccountModel(
                userId: userData['userData']['id'],
                userEmail: userData['userData']['userEmail'],
                userPassword: userData['userData']['userPassword'],
              );
              _profile = ProfileModel(
                profileImage: '',
                profileName: '',
              );
            }
            if (option != 'profileCreation' && isProfileAvailable == false) {
              Navigator.of(context).pushReplacementNamed(ProfileCreationRoute);
            }
          } else {
            _account = AccountModel(
              userId: userData['userData']['id'],
              userEmail: userData['userData']['userEmail'],
              userPassword: userData['userData']['userPassword'],
            );
            _profile = ProfileModel(
              profileName: userProfileData[3],
              profileImage: userProfileData[4],
              profileBio: userProfileData[5],
            );
          }
        } else {
          Navigator.of(context).pushReplacementNamed(AuthRoute);
        }
        notifyListeners();
      });
    } catch (e) {
      debugPrint('account/profileNotifier decode error: ' + e.toString());
    }
  }

  Future getProfile({
    required BuildContext context,
    required String userEmail,
  }) async {
    try {
      var getProfileData = await _profileAPI.getProfile(
        userEmail: userEmail,
      );
      final Map<String, dynamic> parsedProfileData =
          await jsonDecode(getProfileData);
      bool isReceived = parsedProfileData["received"];
      dynamic profileData = parsedProfileData["data"];
      if (isReceived && profileData != 'Fill some info') {
        Map<String, dynamic> data = {
          'id': (profileData['account']['id']).toString(),
          'userEmail': profileData['account']['userEmail'],
          'userPassword': profileData['account']['userPassword'],
          'profile': {
            'profileName': profileData['profileName'],
            'profileImage': profileData['profileImage'],
            'profileBio': profileData['profileBio'],
          }
        };
        AccountModel accountModel = AccountModel.fromJson(
          map: data,
        );
        return accountModel;
      } else {
        return false;
      }
    } catch (e) {
      debugPrint('profileNotifier getProfile error: ' + e.toString());
    }
  }

  Future setProfile({
    required String profileName,
    required String profileImage,
    required String profileBio,
  }) async {
    _profile.profileName = profileName;
    _profile.profileImage = profileImage;
    _profile.profileBio = profileBio;
    await _cacheService.writeProfileCache(
      key: _account.userId,
      value: [
        _account.userId,
        _account.userEmail,
        _account.userPassword as String,
        profileName,
        profileImage,
        profileBio,
      ],
    );
    notifyListeners();
  }
}

CacheService class:

class CacheService {
  Future<String?> readCache({
    required String key,
  }) async {
    final SharedPreferences sharedPreferences =
        await SharedPreferences.getInstance();
    String? cache = await sharedPreferences.getString(key);
    return cache;
  }

  Future<List<String>?> readProfileCache({
    required String key,
  }) async {
    final SharedPreferences sharedPreferences =
        await SharedPreferences.getInstance();
    List<String>? cachedData = await sharedPreferences.getStringList(key);
    return cachedData;
  }

  Future writeCache({required String key, required String value}) async {
    final SharedPreferences sharedPreferences =
        await SharedPreferences.getInstance();
    await sharedPreferences.setString(key, value);
  }

  Future writeProfileCache(
      {required String key, required List<String> value}) async {
    final SharedPreferences sharedPreferences =
        await SharedPreferences.getInstance();
    await sharedPreferences.setStringList(key, value);
  }

  Future deleteCache({
    required BuildContext context,
    required String key,
  }) async {
    final SharedPreferences sharedPreferences =
        await SharedPreferences.getInstance();
    await sharedPreferences.remove(key).whenComplete(() {
      Navigator.of(context).pushReplacementNamed(AuthRoute);
    });
  }
}

I can’t seem to figure out the problem here. Please help.

EDIT: The data is used to show profileImage of user in CircleAvatar like this:

 @override
  Widget build(BuildContext context) {
    ProfileModel profile =
        Provider.of<ProfileNotifier>(context, listen: false).profile;
    return GestureDetector(
      onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
      child: Scaffold(
        drawer: const ProfileDrawer(),
        appBar: AppBar(
          backgroundColor: Colors.white,
          leading: Row(children: [
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 9),
              child: Builder(builder: (BuildContext context) {
                return InkWell(
                  onTap: () => Scaffold.of(context).openDrawer(),
                  child: CircleAvatar(
                      maxRadius: 20.0,
                      backgroundImage: profile.profileImage.isNotEmpty
                          ? NetworkImage(profile.profileImage)
                          : null,
                      child: profile.profileImage.isEmpty
                          ? SvgPicture.asset(
                              'assets/images/profile-default.svg') 
                          : null),
                );
              }),
            ), ....

This CircleAvatar in the appBar shows the image only after the page is rebuilt. There’s nothing else on the page except the appbar for now.

Solution

When we use ChangeNotifier, it provides two options to access the data. These are:

  1. Read the data – You read the data, it doesn’t act as a Stream or State and only one time. This is what you’re doing in your case.

    Advantage – Whenever the data is needed only one time, for example – Mathematical calculation, you use this.

    Disadvantage – It doesn’t listen to the changes and the data returned is static.

  2. Watch the data – What you need. It provides the data in a state manner, wherever you access the data using Watch, it (or the widget in the data is used) will be updated whenever the underlying data is updated, even from other Screens/Widgets.

    Advantage – The data result is dynamic and the widget is updated whenever the data is updated.

    Disadvantage – In case where static data works, it is unnecessary plus it may affect any operations dependent on the data.

There are two ways to use Read and Watch.

  1. The normal functions provided by the Author of the package

    //For reading the data
    var yourData = Provider.of<YourNotifier>(context, listen: false);
    
    //For watching the data
    var yourData = Provider.of<ProfileNotifier>(context, listen: true);
    
  2. The extension functions on BuildContext provided by the Author:

    //For reading the data
    var yourData = context.read<YourNotifier>();
    
    //For watching the data
    var yourData = context.watch<YourNotifier>();
    

So, what you need to do is:

Change

ProfileModel profile =
        Provider.of<ProfileNotifier>(context, listen: false).profile;

to

ProfileModel profile =
        Provider.of<ProfileNotifier>(context, listen: true).profile;

//Or

ProfileModel profile = context.watch<ProfileNotifier>().profile;

Edit: Also, considering good UX, you can use a bool flag to update the UI whenever the data is loaded and if it’s loading, show a CircularProgressIndicator.

Answered By – Lalit Fauzdar

Answer Checked By – Katrina (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.