1

I am trying to get user avatar from firebase storage, however, my current code only returns Instance of 'Future<String>' even I am using async/await as below. How is it possible to get actual download URL as String, rather Instance of Future so I can access the data from CachedNewtworkImage?

this is the function that calls getAvatarDownloadUrl with current passed firebase user instance. myViewModel

  FutureOr<String> getAvatarUrl(User user) async {
    var snapshot = await _ref
        .read(firebaseStoreRepositoryProvider)
        .getAvatarDownloadUrl(user.code);
    if (snapshot != null) {
      print("avatar url: $snapshot");
    }
    return snapshot;
  }

getAvatarURL is basically first calling firebase firestore reference then try to access to the downloadURL, if there is no user data, simply returns null.

  Future<String> getAvatarDownloadUrl(String code) async {
    Reference _ref =
        storage.ref().child("users").child(code).child("asset.jpeg");
    try {
      String url = await _ref.getDownloadURL();
      return url;
    } on FirebaseException catch (e) {
      print(e.code);
      return null;
    }
}

I am calling these function from HookWidget called ShowAvatar. To show current user avatar, I use useProvider and useFuture to actually use the data from the database, and this code works with no problem. However, once I want to get downloardURL from list of users (inside of ListView using index),

class ShowAvatar extends HookWidget {
// some constructors...

 @override
  Widget build(BuildContext context) {
    // get firebase user instance
    final user = useProvider(accountProvider.state).user;
    // get user avatar data as Future<String>
    final userLogo = useProvider(firebaseStoreRepositoryProvider)
        .getAvatarDownloadUrl(user.code);
    // get actual user data as String
    final snapshot = useFuture(userLogo);
    // to access above functions inside of ListView
    final viewModel = useProvider(myViewModel);

    return SingleChildScrollView(
      physics: AlwaysScrollableScrollPhysics(),
      child: Container(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            SizedBox(
              height: 100,
              width: 100,
              child: Avatar(
                avatarUrl: snapshot.data, // **this avatar works!!!** so useProvider & useFuture is working
              ),
            ),
            SizedBox(height: 32),
            ListView.builder(
              shrinkWrap: true,
              physics: NeverScrollableScrollPhysics(),
              itemBuilder: (context, index) {
                return Center(
                  child: Column(
                    children: [
                      SizedBox(
                        height: 100,
                        width: 100,
                         child: Avatar(
                           avatarUrl: viewModel
                               .getAvatarUrl(goldWinners[index].user)
                               .toString(), // ** this avatar data is not String but Instance of Future<String>
                         ),
                      ),
                      
                      ),
                    ],
                  ),
                );
              },
              itemCount: goldWinners.length,
            ),

Avatar() is simple statelesswidget which returns ClipRRect if avatarURL is not existed (null), it returns simplace placeholder otherwise returns user avatar that we just get from firebase storage. However, since users from ListView's avatarUrl is Instance of Future<String> I can't correctly show user avatar.

I tried to convert the instance to String multiple times by adding .toString(), but it didn't work.

class Avatar extends StatelessWidget {
  final String avatarUrl;
  final double radius;
  final BoxFit fit;

  Avatar({Key key, this.avatarUrl, this.radius = 16, this.fit})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('this is avatar url :   ' + avatarUrl.toString());
    return avatarUrl == null
        ? ClipRRect(
            borderRadius: BorderRadius.circular(radius),
            child: Image.asset(
              "assets/images/avatar_placeholder.png", 
              fit: fit,
            ),
          )
        : ClipRRect(
            borderRadius: BorderRadius.circular(radius),
            child: CachedNetworkImage(
              imageUrl: avatarUrl.toString(),
              placeholder: (_, url) => Skeleton(radius: radius),
              errorWidget: (_, url, error) => Icon(Icons.error),
              fit: fit,
            ));
  }
}

husky
  • 831
  • 3
  • 10
  • 24
  • Have you tried future builder for the same ? – 50_Seconds _Of_Coding May 15 '21 at 06:42
  • 1
    not yet - should I use future builder inside of HookWidget? I believe `ListView.builder()` should work... – husky May 15 '21 at 06:43
  • ``` FutureOr getAvatarUrl(User user) async { var snapshot = await _ref .read(firebaseStoreRepositoryProvider) .getAvatarDownloadUrl(user.code); if (snapshot != null) { print("avatar url: $snapshot"); } return snapshot; } ``` Cause the function you mentioned is returning a future itself thats why you are not getting your url as a string so you will have to use future builder for your image or if you want to listen to the real time changes you will use strem builder – 50_Seconds _Of_Coding May 15 '21 at 06:46
  • No list view builder does not wait for the results to build your widget that is it is not an async widget. on other hand future builder and sttream builder waits for the data response and until then they replace the widget with another widget like progress indicators. – 50_Seconds _Of_Coding May 15 '21 at 06:49
  • Future builder and Stream builder both are different, future builder is valid for activity life cycle, (correct me if I am wrong cause I am might be wrong on this statement only *activity life cycle*). So future builder only updates once when you open your activity. Where stream builder works in realtime, if the database changes it will imdiately get updated in your applications without need of refreshing the screen. – 50_Seconds _Of_Coding May 15 '21 at 06:52
  • everything you have mentioned are correct. I might have to consider using/wrapping FutureBuilder with ListView.builder to actually async/await. But one thing I noticed is that I might be able to get List of user avatar data by saying like this` final userNum = goldWinners.length - 1; final goldUser = useProvider(firebaseStoreRepositoryProvider) .getAvatarDownloadUrl(goldWinners[goldUserNum].user.code); final goldSnap = useFuture(goldUser);` – husky May 15 '21 at 08:14
  • but this can't get list of users, and only returns one user since it doesn't loop through index... let me know if there is a way to get list of users from this riverpod/hookwidget method, thanks anyways. – husky May 15 '21 at 08:16

2 Answers2

0

Since the download URL is asynchronously determined, it is returned as Future<String> from your getAvatarUrl method. To display a value from a Future, use a FutureBuilder widget like this:

child: FutureBuilder<String>(
  future: viewModel.getAvatarUrl(goldWinners[index].user),
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    return snapshot.hashData
      ? Avatar(avatarUrl: snapshot.data)
      : Text("Loading URL...")
  }
)
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thank you. To use index in the future, I first needed to declare ListView, then FutureBuilder - after that I tried the way you provided, but it still shows `Instance of Future` then endlessly showing Text widget ('Loading URL...'). my future sometimes return `null` if user's logo is not stored in the firebase, and it is causing some problems..? But I am not sure why it still shows Instance of Future even I am using FutureBuilder... – husky May 15 '21 at 21:52
  • looks like FutureBuilder never receives snapshot data from my `getAvatarUrl` method for some reasons (possibly because of null value). but hooks `useProvider` & `useFuture` combination actually works to get data from future using same methods. appreciate if you could update some possible fixes if any. – husky May 15 '21 at 22:55
0

Frank actually you gave an good start but there are some improvements we can do to handle the errors properly,

new FutureBuilder(
            future: //future you need to pass,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return new ListView.builder(
                    itemCount: snapshot.data.docs.length,
                    itemBuilder: (context, i) {
                      DocumentSnapshot ds = snapshot.data.docs[i];
                      return //the data you need to return using /*ds.data()['field value of doc']*/
                    });
              } else if (snapshot.hasError) {

                // Handle the error and stop rendering
                GToast(
                        message:
                            'Error while fetching data : ${snapshot.error}',
                        type: true)
                    .toast();
                return new Center(
                  child: new CircularProgressIndicator(),
                );
              } else {
                // Wait for the data to fecth
                return new Center(
                  child: new CircularProgressIndicator(),
                );
              }
            }),

Now if you are using a text widget as a return statement in case of errors it will be rendered forever. Incase of Progress Indicators, you will exactly know if it is an error it will show the progress indicator and then stop the widget rendering.

else if (snapshot.hasError) {
}
else {

}

above statement renders until, if there is an error or the builder finished fetching the results and ready to show the result widget.

  • noticed that my snapshot future data sometimes with `null` value and I think it is making some error here since FutureBuilder never expect null value? And my user key is not future, listview is needed to list all of users! Plus, I noticed the FutureBuilder was called like forever and the UI is rendering for some reason... – husky May 17 '21 at 09:36
  • In case of null you use future builder. Now if you need to fetch list of users you should use first future builder and then the list view inside it and pass the snapshot to the future builder. Here I am updating the code with stream builder where implementation of future builder and stream builde is almost same. – 50_Seconds _Of_Coding May 18 '21 at 04:20