Table of Contents
In the first part of creating an AI Image Generator in Flutter With Clean Architecture, we talked about and understood the clean architecture in Flutter, the data flow, and the call flow and we finished the Domain Layer, In this part 2 of our series we’re going to continue from we left, and start working on the Data Layer.
Please ensure you complete the first part, as doing so will provide you with the foundation to follow along with what we will cover in this article.
Part 1: Create an AI Image Generator In Flutter With Clean Architecture
So with the context properly set, let’s proceed to the work ahead!
01
of 04
Data Layer
Models
In the models’ folder create an image_model.dart file with the ImageModel class extending the ImageEntity with a construction.
import '../../domain/entities/icon.dart'; class ImageModel extends ImageEntity{ ImageModel(super.imageUrl); }
Data Sources
In the data source folder create a remote_datasource.dart file with two classes:
the first is an abstract class, BaseRemoteDataSource with two methods getGeneratedImage and downloadImageFromUrl.
import '../../domain/entities/image.dart'; import 'dart:io'; abstract class BaseRemoteDataSource{ Future<List<ImageEntity>> getGeneratedImage(String prompt); Future<File> downloadImageFromUrl(String url); }
and the second class, RemoteDataSource which is implementing the one we’ve just created.
inside this class, we override the method and implement them, to generate images giving a prompt, and to download the image by its URL.
import '../model/image_model.dart'; import 'package:dart_openai/dart_openai.dart'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; class RemoteDataSource implements BaseRemoteDataSource{ @override Future<List<ImageEntity>> getGeneratedImage(String prompt) async{ final generatedImages = await OpenAI.instance.image.create( prompt: prompt, n: 5,//number of images to generate. ); List<ImageEntity> imagesData=generatedImages.data.map((eachImage) =>ImageModel(eachImage.url!) ).toList(); return imagesData; } @override Future<File> downloadImageFromUrl(String url) async { var response = await Dio().get( url, options: Options(responseType: ResponseType.bytes), ); final appDir = await getTemporaryDirectory(); final file = File('${appDir.path}/image.jpg'); await file.writeAsBytes(response.data); return file; } }
the first method is getGeneratedImage returns a list of imageEntity and takes in a prompt.
inside this method, we create the generatedImages variable and call the create method from the OpenAI library passing in the prompt and the number of images we want to generate.
then we create a list of ImageEntity, called imagesData, and assign it the list of ImageEntity after we map through the generatedImages data and convert them to a list.
finally, we return the list.
for the second method which is responsible for downloading the image, we used the Dio package to get the image, then we use the path_provider package to get the temporary directory, then we create a file object appending the name “image.jpg”
and then write the response.data to the file object created earlier and finally we return it.
02
of 04
Repositories
Now let’s work in the concrete image repository, for that, in the repositories folder, create a concrete_image_repository.dart file that will have ConcreteImageRepository implementing the ImageRepository abstract class.
inside this class, we create a BaseRemoteDataSource instance injecting it into the ConcreteImageRepository constructor.
override the two methods, getImageByPrompt, and downloadImage.
inside the getImageByPrompt method, we call getGeneratedImage from the baseRemoteDataSource instance passing in the prompt.
and inside the downloadImage we call the downloadImageFromUrl from the baseRemoteDataSource instance passing in the URL.
import '../data_source/remote_datasource.dart'; import '../../domain/entities/image.dart'; import '../../domain/repository/image_repository.dart'; import 'dart:io'; class ConcreteImageRepository implements ImageRepository{ final BaseRemoteDataSource baseRemoteDataSource; ConcreteImageRepository(this.baseRemoteDataSource); @override Future<List<ImageEntity>> getImageByPrompt(String prompt) async{ return await baseRemoteDataSource.getGeneratedImage(prompt); } @override Future<File> downloadImage(String url) async{ return await baseRemoteDataSource.downloadImageFromUrl(url); } }
03
of 04
Presentation Layer: Controllers
we’ve done with the domain and data layer, now we’re going to work on the presentation layer, the UI.
Controllers
so inside the controller’s folder create another folder: get_image that will have two files, image_state.dart and image_cubit.dart.
Let’s first know what a cubit is.
Cubit is a lightweight state management solution. It is a subset of the bloc package.
Cubit does not rely on events and instead uses methods to emit new states.
Every cubit requires an initial state which will be the state of the cubit before emit has been called.
in the image_state.dart file create the different state the image can has:
import '../../../domain/entities/image.dart'; class ImageState{} class ImageInitial extends ImageState{} class ImageLoaded extends ImageState{ final List<ImageEntity> loadedImages; ImageLoaded(this.loadedImages); } class ImageIsLoading extends ImageState{} class ImageError extends ImageState{ final String errorMessage; ImageError(this.errorMessage); }
for the imageLoaded state class we create a list of the loaded images and pass them through the constructor so when we emit the state we emit it with the loaded images.
same thing if an error occurred, we pass through to the ImageError constructor the error message.
Now, let’s work on the image_cubit.dart file, for that go ahead and create an ImageCubit class that extend Cubit of type ImageState state: Cubit<ImageState>.
then we create ImageRepository instance and pass it to the ImageCubit constructor, as we said, every cubit need an initial state so we call the super (ImageInitial( )).
inside this class we class the getImage method that will emit the image states as follows:
import '../../../domain/entities/image.dart'; import '../../../domain/usecases/get_image_by_prompt.dart'; import 'package:dart_openai/dart_openai.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../domain/repository/image_repository.dart'; import 'image_state.dart'; class ImageCubit extends Cubit<ImageState>{ final ImageRepository imageRepository; ImageCubit(this.imageRepository):super (ImageInitial()); Future<void> getImage(String prompt)async{ emit(ImageInitial()); try{ emit(ImageIsLoading()); List<ImageEntity> gottenImage= await GetImageByPrompt(imageRepository).getTheImageByPrompt(prompt); emit(ImageLoaded(gottenImage)); }catch (e) { if (e is RequestFailedException) { emit(ImageError(e.message)); } else { emit(ImageError("Unknown error occurred.")); } }} }
we emit the ImageInnial first, in the try block, we emit the ImageIsLoading then we call the getTheImageByPrompt from GetItmageByPrompt use case passing the imageRepository instance.
and then we emit the ImageLoaded state.
and if an error happened we emit the error message.
Let’s work now on the downloading the images, for that, inside the controllers folder create another folder, download_image and create two files.
download_image_state.dart and download_image_cubit.dart.
for the download image state create the necessary states as follows:
import 'dart:io'; class DownloadImageState{} class DownloadImageInitial extends DownloadImageState{} class DownloadImageDone extends DownloadImageState{} class Downloading extends DownloadImageState {} class DownloadError extends DownloadImageState{ final String errorMessage; DownloadError(this.errorMessage); }
import 'download_image_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../domain/repository/image_repository.dart'; import '../../../domain/usecases/download_image.dart'; import 'dart:io'; class DownloadImageCubit extends Cubit<DownloadImageState> { final ImageRepository imageRepository; DownloadImageCubit(this.imageRepository) : super(DownloadImageInitial()); Future<File> downloadImage(String url) async { emit(DownloadImageInitial()); try { emit(Downloading()); File downloadedImage = await DownloadImage(imageRepository).downloadImageByItsUrl(url); emit(DownloadImageDone()); return downloadedImage; } catch (e) { emit(DownloadError("Unable to download the image")); } throw("cant download image"); } }
In the downloadImageCubit we have the downloadImage method where we emit the different states.
we call downloadImageByItsUrl method form the DownloadImage use cases that that take the imageRepository instance.
and we catch errors if any.
04
of 04
Conclusion of Part 2
By now our data is ready and we can generate images and download them, we finished the data layer and finished the controllers from the presentation layer.
In the next article we will work on the UI part where we will leverage the controllers so we can call its methods to generate and download images.
If you found this article helpful or have any thoughts, questions, or additional insights, I’d love to hear from you! Your feedback is incredibly important in helping me improve the quality of my content.
Found an error or want to add to the discussion? Please don’t hesitate to leave a comment below or reach out to me on LinkedIn.
Comments 2