Code With Bisky

Boost Your Flutter Chat App Performance: Master Image and Video Storage Techniques! Episode [17]

Key Topics covered:

  • Save videos and images on local device
  • View Images by clicking it
  • Messages optimization by using HookConsumerWidget

Description:

In this tutorial, we will explore the best practices for saving images and videos in local storage to enhance chat message performance. If you're developing a chat application using Flutter, you may have wondered how to efficiently handle images and videos shared within the chat. Optimizing the storage and retrieval of these media files locally can significantly boost your app's performance, resulting in a seamless user experience.

In this comprehensive tutorial, we'll guide you through the implementation of a robust solution for saving and retrieving images and videos in the local storage of your Flutter application. We'll cover popular techniques such as caching, compression, and asynchronous operations to ensure fast and efficient handling of media files without consuming excessive device storage.

Code Snippet(DirectoryProvider.dart):

  • lib/core/providers/DirectoryProvider.dart

A provider to get application directory which we are going to save images and videos


import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path_provider/path_provider.dart';

final directoryProvider = FutureProvider((ref) => getApplicationDocumentsDirectory());
        
        

Code Snippet(FileProvider.dart):

  • lib/core/providers/FileProvider.dart

A provider to get a file from device if exists or download it from Appwrite Storage


import 'dart:io';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/DirectoryProvider.dart';
import 'package:chat_with_bisky/core/providers/StorageProvider.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'FileProvider.g.dart';
@riverpod
Future<File> file(FileRef ref,
String bucketId,
String id,
String fileName) async{

  ref.keepAlive();
  final dir =await ref.read(directoryProvider.future);
  final file = File('${dir.path}/$fileName');
  if(await file.exists()){
    return file;
  }
  final dataResponse = await ref.read(storageProvider).getFileDownload(bucketId: Strings.messagesBucketId, fileId: id);
  await file.writeAsBytes(dataResponse);
  return file;
}

Don't forget to run the following command in your terminal to generate FileProvider.g.dart flutter packages pub run build_runner build

Code Snippet(ThumbnailProvider.dart):

  • lib/core/providers/ThumbnailProvider.dart

A provider to create a thumbnail from the video or image


import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'ThumbnailProvider.g.dart';

@riverpod
Future<String?> thumbnail(ThumbnailRef ref, String path) async {
  ref.keepAlive();
  return await VideoThumbnail.thumbnailFile(
      video: path,
      thumbnailPath: (await getTemporaryDirectory()).path,
      imageFormat: ImageFormat.PNG,
      quality: 100);
}

Don't forget to run the following command in your terminal to generate ThumbnailProvider.g.dart flutter packages pub run build_runner build

Code Snippet(AttachmentType.dart):

  • lib/model/AttachmentType.dart

Lets create constants of messages type


class AttachmentType{

  static const video = 'VIDEO';
  static const image = 'IMAGE';
  static const text = 'TEXT';
  static const voice = 'VOICE';
}   
        

Code Snippet(MessageRealm.dart):

  • lib/model/db/MessageRealm.dart

Add fileName property


String? fileName;
        
        

Code Snippet(RealmProvider.dart) modification:

Increase schema version since we are updated Realm model => MessageRealm

  • lib/core/providers/RealmProvider.dart

final config = Configuration.local(...., schemaVersion:2);
        
        

Don't forget to run the following command in your terminal to regenerate MessageRealm.g.dartdart run realm generate

Code Snippet(AsynWidget.dart):

Create AsynWidget to load images and videos asynchronously

  • lib/widget/AsynWidget.dart

import 'package:flutter/cupertino.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart';

class AsyncWidget<T> extends StatelessWidget{

  final AsyncValue<T> value;
  final Widget Function(T data)  data;
  final VoidCallback? retryAgain;

  const AsyncWidget({super.key, required this.value, required this.data, this.retryAgain});

  @override
  Widget build(BuildContext context) {

    return value.when(data: data, error: (error, stackTrace) {
      return Padding(padding: const EdgeInsets.all(10),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('$error'),
            if (retryAgain != null) ...[
              const SizedBox(
                height: 14,

              ),
              TextButton(onPressed: retryAgain, child: const Text('Retry'))
            ]
          ],
        ),
      ),);
    }, loading:() =>  const Center(
      child: Padding(padding: const EdgeInsets.all(10),
      child: CircularProgressIndicator(),)
    ),);
  }
} 
        

Update an extensions file.

  • lib/core/extensions/extensions.dart

In OnBuildContext , implement the logic to open an image


MediaQueryData get media => MediaQuery.of(this);

  void openImage(String path){
    Navigator.of(this).push(MaterialPageRoute(builder: (context) => Scaffold(
      appBar: AppBar(title: const Text("Image"),),
      body: Center(
        child: SafeArea(
          child: Image.file(File(path)),
        ),
      ),
    ),));
  }
        
        

Code Snippet(ChatMessageItem.dart) modification:

Create ChatMessageItem widget to load our messages based ton the message type [IMAGE,VIDEO,TEXT]

  • lib/widget/ChatMessageItem.dart

import 'dart:io';

import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/extensions/extensions.dart';
import 'package:chat_with_bisky/core/providers/FileProvider.dart';
import 'package:chat_with_bisky/core/providers/ThumbnailProvider.dart';
import 'package:chat_with_bisky/model/AttachmentType.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:chat_with_bisky/widget/AsynWidget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class ChatMessageItem extends HookConsumerWidget {
  final MessageRealm message;
  final bool myMessage;
  final String displayName;

  const ChatMessageItem(
      {Key? key,
      required this.message,
      required this.myMessage,
      required this.displayName})
      : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    Widget fileView(File file, String type) {
      return Stack(
        fit: StackFit.passthrough,
        children: [
          ConstrainedBox(
              constraints: BoxConstraints(
                minHeight: 0,
                maxHeight:
                    [AttachmentType.image, AttachmentType.video].contains(type)
                        ? double.infinity
                        : (context.media.size.width * 3 / 4),
                minWidth: context.media.size.width / 2,
                maxWidth: context.media.size.width * 2 / 3,
              ),
              child: Stack(
                children: [
                  if (type == AttachmentType.video)
                    GestureDetector(
                      onTap: () {
                        // todo open video
                      },
                      child: AsyncWidget(
                        value: ref.watch(thumbnailProvider(file.path)),
                        data: (data) => data != null
                            ? Stack(
                                fit: StackFit.passthrough,
                                alignment: Alignment.center,
                                children: [
                                  ClipRRect(
                                    borderRadius: BorderRadius.circular(12),
                                    child: Image.file(
                                      File(data as String),
                                      fit: BoxFit.cover,
                                    ),
                                  ),
                                  const Icon(
                                    Icons.play_arrow_rounded,
                                    size: 40,
                                    color: Colors.green,
                                  ),
                                ],
                              )
                            : const SizedBox(),
                      ),
                    ),
                  if (type == AttachmentType.image)
                    GestureDetector(
                      onTap: () {
                        context.openImage(file.path);
                      },
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(12),
                        child: Image.file(
                          file,
                          fit: BoxFit.cover,
                        ),
                      ),
                    )
                ],
              )),
        ],
      );
    }

    Widget messageWidget(MessageRealm messageAppwrite) {
      if (messageAppwrite.type == AttachmentType.text) {
        return Text(
          messageAppwrite.message ?? "",
          style: const TextStyle(color: Colors.black, fontSize: 14.0),
        );
      } else {
        return Consumer(
          builder: (context, ref, child) {
            final fileAsync = ref.watch(fileProvider(Strings.messagesBucketId,
                messageAppwrite.message ?? "", messageAppwrite.fileName ?? ""));
            return fileAsync.when(
              data: (file) => fileView(file, messageAppwrite.type ?? ""),
              error: (error, stackTrace) {
                print(error);
                return Center(
                  child: Text('$error'),
                );
              },
              loading: () =>
                  const SizedBox(width: 20, child: LinearProgressIndicator()),
            );
          },
        );
      }
    }

    Widget buildChatLayout(MessageRealm snapshot) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(12.0),
            child: Row(
              mainAxisAlignment:
                  myMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
              children: <Widget>[
                const SizedBox(
                  width: 10.0,
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    myMessage
                        ? const Text(
                            "",
                            style: TextStyle(
                                color: Colors.black,
                                fontSize: 16.0,
                                fontWeight: FontWeight.bold),
                          )
                        : Text(
                            displayName,
                            style: const TextStyle(
                                color: Colors.black,
                                fontSize: 16.0,
                                fontWeight: FontWeight.bold),
                          ),
                    messageWidget(snapshot)
                  ],
                )
              ],
            ),
          ),
        ],
      );
    }
    return buildChatLayout(message);
  }
}
        
        

Code Snippet(MessageScreen.dart) modification:

Let's use ChatMessageItem widget that created instead of using the old logic which was not optimized

Delete buildChatLayout() and messageWidget() methods

  • lib/pages/dashboard/chat/MessageScreen.dart

import 'package:chat_with_bisky/widget/ChatMessageItem.dart';

Widget chatMessageItem(MessageRealm documentSnapshot) {
    return ChatMessageItem(message: documentSnapshot,
        displayName: displayName,
        myMessage: documentSnapshot.senderUserId == myUserId,);
  }

        
        

Conclusion:

We managed to optimize the way we read images and videos on the Chat Screen. The application can now display thumbnails and view the clicked image. In the next tutorial, we are going to implement Chat Message Seen by showing 2 green double ticks. Don't forget to share and join our Discord Channel. May you please subscribe to our YouTube Channel.