artflutter / reactive_forms_widgets

MIT License
127 stars 74 forks source link

Problem in using ReactiveImagePicker (in conversion of model object to/from form.value) #125

Open rsbichkar opened 1 year ago

rsbichkar commented 1 year ago

I am using ReactiveForms along with ReactiveImagePicker and several other Reactive packages for the implementation of an application involving several forms. I am also using Freezed, JsonSerialization, and Riverpod packages and AppWrite for backend.

In ReactiveForms using ReactiveImagePicker, I am unable to pass 'PersonName' model class object to Reactive Form widget's form.value (for the editing mode) due to use of different types in model class and the form group (String? type in model class and List\<SelectedFile> type in form group) for the field using ReactiveImagePicker.

Extract of related code in my app is given below to understand the exact problem:

The model class file (PersonName) contains other fields besides profilePic, of type String?, which is used to store the id or Url of image file stored in Appwrite Storage bucket:

class PersonName with _$PersonName {
  const factory PersonName({
    @JsonKey(name: '\$id') String? id,
    required String name,
    ... // other fields 
    String? profilePic,  // this field stores the id or Url of image stored in Appwrite Storage bucket
  }) = _PersonName;

The FormGroup for this class is like this:

final form = FormGroup({
    'name': FormControl<String>(
      validators: [Validators.required],
    ),
    ... // other entries
    'profilePic': FormControl<List<SelectedFile>>(),
  });
}

The main problem is how to convert PersonName object to form.value data (in form display widget) as the form.value has field of type List\<SelectedFile> whereas PersonName has the corresponding model class field is of type String?.

For this I am using PersonNameToFormValue (and corresponding PersonNameFromFormValue) functions:

Map<String, dynamic> PersonNameToFormValue(PersonName personName) {
  final map = <String, dynamic>{
    'id': personName.id,
    'name': personName.name,
      ... //other entries
    'profilePic': personName.profilePic != null
            ? [SelectedFileImage(file: File(personName.profilePic!))]   // File is from dart:io package
            : [SelectedFileImage(url: personName.profilePic!)],
  };
  return map;
}

'profilePic' field in above function uses condition to pass either a File or Url to display widget image in either read only or editing mode.

The line using 'url:' displays the image correctly in readonly mode. However, the line using 'file:' gives the following error:

════════ Exception caught by image resource service ════════════════════════════
The following PathNotFoundException was thrown resolving an image codec:
Cannot retrieve length of file, path = 'http://192.168.12.203:4003/v1/storage/buckets/65092e956108ccf06e17/files/6509e7cde12e9b138dc4/view?project=64f436798e981511b356&mode=admin' (OS Error: No such file or directory, errno = 2)

Will you please help in this regard?

vasilich6107 commented 1 year ago

replace the file path with some image from internet to check if everything works fine. As far as i can see from exception PathNotFoundException so it can't find a path

rsbichkar commented 1 year ago

Thanks a lot. However, as I mentioned already, the images from Appwrite are already displayed properly if we use the 'Url' in SelectedFile in FormGroup.

It should be possible to convert the image file data on Appwrite backend to an in-memory file and then pass this file in FormControl<List<SelectedFile>>. It is possible to use http package to download a file and then use File or XFile to create in-memory file for the image. I have not yet figured how to do this. However, I would like to avoid creating image file on disk for efficiency reasons as there will be a large number of images the app will be processing.

Can we try something like this for the conversion of Url to a file? It uses getImageUrlFromId local function to get the Appwrite storage Url for a given image id.

Future<File> getFileFromImageId(String id) async {
  Uri uri = Uri.parse(getImageUrlFromId(id));
  final http.Response responseData = await http.get(uri);
  final file = XFile.fromData(responseData.bodyBytes);
  return file as File;   // ??????? We need File. Will it work?`
}

However, this gives Future and we may not be able to use it in the FormGroup.

But in the mean time, I have continued to use the conversion approach between Form Value and model object and got a success to a great extent.

However, the logic becomes quite complex, particularly when reading multiple files and allowing users to change one or more of those images. In addition, I have been using Reactive forms in either read-only or edit mode. It complicates the things further.

We have to handle data from three locations:

  1. Image 'ids' from model class filed
  2. Image 'Urls' for the files stored on Appwrite backend
  3. Images 'Files' read from local machine while adding new images

The current implementation of personNameFromFormValue function looks like this:

PersonName personNameFromFormValue(FormGroup form) {
  Map<String, dynamic> formValue = form.value;
  return PersonName(
    id: formValue['id'] as String?,
    first: formValue['first'] as String,
    ... // other fields

    // profilePic gets a single image input
    profilePic: formValue['profilePic'] != null
        ? form.control('profilePic').dirty
            ? formValue['profilePic'].isNotEmpty
                ? (formValue['profilePic'] as List<SelectedFile>)[0].file!.path
                : null
            : getIdFromImageUrl((formValue['profilePic'] as List<SelectedFileImage>)[0].url!)
        : null,

    // Just for testing...
    // This friendPics field takes multiple images as input
    friendPics: formValue['friendPics'] != null
        ? form.control('friendPics').dirty
            ? formValue['friendPics'].isNotEmpty
                ? (formValue['friendPics'] as List<SelectedFile>)
                    .map((e) => e.file != null ? e.file!.path : getIdFromImageUrl(e.url!))
                    .toList()
                : null
            : (formValue['friendPics'] as List<SelectedFileImage>)
                .map((e) => getIdFromImageUrl(e.url!))
                .toList()
        : null,
  );
}
adar2378 commented 1 year ago

Since image_picker returns XFile, why are we using SelectedFile? On web, I can't access the picked image.

inal images = formGroup.control('image').value
                                  as List<SelectedFile>?;

                              MultipartFile? multiPartImageFile;
                              if (images != null && images.isNotEmpty) {
                                final selectedImageFile = images.first;
                                print(selectedImageFile);
                                XFile? file;
                                if (kIsWeb) {
                                  Uint8List? imageBytes =
                                      await selectedImageFile.file
                                          ?.readAsBytes(); // Throw error `Unsupported operation: _Namespace,`
adar2378 commented 1 year ago

@rsbichkar You could probably try this https://github.com/djangoflow/reactive_forms_widgets It returns XFile inside SelectedFile, from which you should be able to read image bytes.

For your case, I suppose what you can do is download the image to temp directory and get the image bytes and pass those bytes to ReactiveImagePicker as initial value?

adar2378 commented 1 year ago

@vasilich6107 Could you check this PR? https://github.com/artflutter/reactive_forms_widgets/pull/128

After that I will create the PR for File -> XFile change if that is okay.

rsbichkar commented 1 year ago

@adar2378 Thanks for your help.

However, I could get the code working (for Linux platform at least). I am not converting image URLs and image Files anymore. Instead I am handling them separately. It is not necessary to download the file to temporary storage either.

First I have used the conversion functions fromFormValue and toFormValue as follows:

Map<String, dynamic> personNameToFormValue(PersonName personName) {
  final map = <String, dynamic>{
    'id': personName.id,
    'name': personName.name,
   ...
    'profilePic': personName.profilePic != null
        ? [
            SelectedFileImage(
                url: AppwriteConstants.getImageUrlFromId(personName.profilePic!))
          ]
        : null,
    'friendPics': personName.friendPics != null
        ? personName.friendPics!
            .map((e) => SelectedFileImage(url: AppwriteConstants.getImageUrlFromId(e)))
            .toList()
        : null,
  };
  return map;
}
// Uses any valid value(s) for image fields. Their actual values are obtained in controller code
PersonName personNameFromFormValue(FormGroup form) {
  Map<String, dynamic> formValue = form.value;
  return PersonName(
    id: formValue['id'] as String?,
    name: formValue['name'] as String,
    ...
    // use null values for these image fields
    profilePic: null,
    friendPics: null,
  );
}

These functions are used in PersonNameReactiveFormView class for conversion between PersonName object and formValue.

The controller uses the createPersonName and updatePersonName functions as follows:

  void createPersonName(
      BuildContext context, PersonName personName, FormGroup form) async {

    Map<String, dynamic> formValue = form.value;
    state = true;

    // Create image for profilePic field, if an image is entered by user
    String? profilePicIds;
    if (form.control('profilePic').dirty) {
      // store new images, if any, to appwrite storage and create newPersonName with new image id
      profilePicIds = formValue['profilePic'].isNotEmpty
          ? (await _storageRepo
              .uploadImages([formValue['profilePic'][0].file]))[0]
          : null;
    }

    // Create images for friendPics field, if one or more images are entered by user
    List<String> friendPicsIds = [];
    if (form.control('friendPics').dirty) {
      // first create a list of image files added
      final newImageFiles = <io.File>[];        // we can use XFile here for Platform Independence
      formValue['friendPics'].forEach((e) {
        if (e.file != null) newImageFiles.add(e.file);
      });

      // upload these files to Appwrite storage and get image ids
      friendPicsIds = formValue['friendPics'].isNotEmpty
          ? await _storageRepo.uploadImages(newImageFiles)
          : [];
    }

    // set new image ids for image field
    final newPersonName = personName.copyWith(
      profilePic: profilePicIds,
      friendPics: friendPicsIds,
    );

    final res = await _personNameRepo.createPersonName(newPersonName);
    state = false;
    ...
void updatePersonName(BuildContext context, PersonName personName,
      PersonName prevPersonName, FormGroup form) async {
    Map<String, dynamic> formValue = form.value;
    state = true;

    // Update image for profilePic field, if an image is changed by user
    String? profilePicIds = formValue['profilePic'] != null
        ? form.control('profilePic').dirty
            ? formValue['profilePic'].isNotEmpty
                ? (await _storageRepo.uploadImages([formValue['profilePic'][0].file]))[0]
                : null
            : AppwriteConstants.getIdFromImageUrl(
                (formValue['profilePic'] as List<SelectedFileImage>)[0].url!)
        : null;

    // delete previous image, if any
    if (prevPersonName.profilePic != null &&
        prevPersonName.profilePic != profilePicIds) {
      await _storageRepo.deleteImage(prevPersonName.profilePic!);
    }

    // Update images for friendPics field, if one or more images are changed by user
    List<String>? friendPicsIds;

    if (formValue['friendPics'] != null) {
      if (form.control('friendPics').dirty) {
        if (formValue['friendPics'].isNotEmpty) {

          // get set of ids of previous images, if any, in friendPics field
          final idSet = prevPersonName.friendPics?.toSet();

          // get a set of ids of images available in formValue urls after the update operation
          final urlIds = <String>[];
          formValue['friendPics']!.forEach((e) {
            if (e.url != null) {
              urlIds.add(AppwriteConstants.getIdFromImageUrl(e.url));
            }
          });
          final urlIdsSet = urlIds.toSet();

          // difference, idSet - urlSet, gives deleted images
          if (idSet != null) {
            Set deletedIdsSet = idSet.difference(urlIdsSet);
            // remove these images from Appwrite storage
            deletedIdsSet
                .forEach((e) async => await _storageRepo.deleteImage(e));
          }

          // now add new files, if any, in formValue 'files' to Appwrite storage
          // first create a list of files added
          final newImageFiles = <io.File>[];       // we can use XFile here as well
          formValue['friendPics'].forEach((e) {
            if (e.file != null) newImageFiles.add(e.file);
          });

          // upload these files to Appwrite storage and get image ids
          final List<String> newImageIds =
              await _storageRepo.uploadImages(newImageFiles);

          // prepare list of ids of all images (previous images not yet deleted and new images added)
          friendPicsIds = [...urlIds, ...newImageIds];
        } else {
          // delete images, if any, present earlier
          prevPersonName.friendPics!
              .forEach((e) => _storageRepo.deleteImage(e));
          friendPicsIds = [];
        }
      } else {
        friendPicsIds = prevPersonName.friendPics;
      }
    } else {
      friendPicsIds = null;
    }

    // set new image ids for image field
    final newPersonName = personName.copyWith(
      profilePic: profilePicIds,
      friendPics: friendPicsIds,
    );

    final res = await _personNameRepo.updatePersonName(newPersonName);
    state = false;

This code works correctly for adding new files, removing exising files, and any combination as well.

rsbichkar commented 1 year ago

'You could probably try this https://github.com/djangoflow/reactive_forms_widgets'

@adar2378: Will you please guide me how can I use reactive_image_picker from this repo?
I have tried to add entire repo as:

  reactive_forms_widgets:
    git:
      url: https://github.com/djangoflow/reactive_forms_widgets

However, I am unable to use reactive_image_picker from this repo.

On the other hand, if I use

  reactive_image_picker:
    git:
      url: https://github.com/djangoflow/reactive_forms_widgets/tree/master/packages/reactive_image_picker

it results in github error

fatal: repository 'https://github.com/djangoflow/reactive_forms_widgets/tree/master/packages/reactive_image_picker/' not found
exit code: 128
exit code 69