Skip to content

Sample Flutter apply Clean Architecture - Pure Rxdart BLoC pattern, SOLID, Get_it (DI), Dio, Dartz, RxDart 🚀🚀🚀 Functionalities: Search Keyword, Loadmore, Pull to Refresh

Notifications You must be signed in to change notification settings

LamNguyen17/flutter_clean_architecture

Repository files navigation

🚀 Project using Clean Architecture recommend by Google Developer

This guide encompasses best practices and recommended architecture for building robust, high-quality apps

Medium

🚀 Introduction

This sample demonstrates how one can

  • Setup base architecture of Flutter app using Clean Architecture
  • Use dependency injection for layers separation
  • Make api calls using Axios plugin.
├── common
|   └── helper
├── data
|   ├── config
|   ├── datasources
|   ├── gateway
|   ├── helper
|   ├── models
|   └── repositories
├── di (dependency injection)
├── domain 
|   ├── entities
|   ├── repositories
|   └── usecases
└── presentation
    ├── assests
    ├── components
    ├── contants
    ├── features
    ├── localizations
    ├── navigations
    └── utils

Dependencies

  • Dio : http client
  • Get_it : dependency injection
  • Build runner : The build_runner package provides a concrete way of generating files using Dart code. Files are always generated directly on disk, and rebuilds are incremental - inspired by tools such as Bazel
  • Rxdart : RxDart extends the capabilities of Dart Streams and StreamControllers.
  • Dartz : Functional programming in Dart.

🚀 Module Structure

Clean Architecture

There are 3 main modules to help separate the code. They are Data, Domain, and Presentaion.

  • Data contains Local Storage, APIs, Data objects (Request/Response object, DB objects), and the repository implementation.

  • Domain contains UseCases, Domain Objects/Models, and Repository Interfaces

  • Presentaion contains UI, View Objects, Widgets, etc. Can be split into separate modules itself if needed. For example, we could have a module called Device handling things like camera, location, etc.

🚀 Flutter version: channel stable

environment:
  sdk: '>=3.2.0-16.0.dev <4.0.0'
  dart: ">=3.2.0-16.0.dev <4.0.0"
  flutter: ">=3.10.0"
Flutter 3.19.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 54e66469a9 (5 days ago) • 2024-04-17 13:08:03 -0700
Engine • revision c4cd48e186
Tools • Dart 3.3.4 • DevTools 2.31.1

🚀 Detail overview

  • Using modular architecture to architect the app per feature to be easier and more readable and isolate the feature from each other

Repository

  • Bridge between Data layer and Domain layer
  • Connects to data sources and returns mapped data
  • Data sources include DB and Api

- DataSource:

class PhotoRemoteDataSourceImpl implements PhotoRemoteDataSource {
  final RestApiGateway _restApiGateway;

  PhotoRemoteDataSourceImpl(this._restApiGateway);

  @override
  Future<PhotosResponse> getPhoto(RequestPhoto? reqParams) async {
    final response = await _restApiGateway.dio.get(
        "?key=${API_KEY}&q=${reqParams?.query}&page=${reqParams?.page}&per_page=20");
    if (response.statusCode == 200) {
      return PhotosResponse.fromJson(response.data);
    } else {
      throw const ServerFailure('Lỗi xảy ra');
    }
  }
}

- RepositoryImpl:

class PhotoRepositoryImpl implements PhotoRepository {
  final PhotoRemoteDataSource _dataSource;

  PhotoRepositoryImpl(this._dataSource);

  @override
  Future<Either<Failure, Photos>> getPhoto(RequestPhoto? reqParams) async {
    try {
      var response = await _dataSource.getPhoto(reqParams);
      return Right(response.toEntity());
    } on DioException catch (e) {
      return Left(ServerFailure(e.message ?? 'Lỗi hệ thống'));
    }
  }
}

Domain

  • Responsible for connecting to repository to retrieve necessary data. returns a Stream that will emit each update.
  • This is where the business logic takes place.
  • Returns data downstream.
  • Single use.
  • Lives in Domain (No Platform dependencies. Very testable).

- UseCase:

class GetPhotoUseCase implements BaseUseCase<Photos, RequestPhoto> {
  final PhotoRepository repository;

  GetPhotoUseCase(this.repository);

  @override
  Future<Either<Failure, Photos>> execute(RequestPhoto? reqParams) async {
    return await repository.getPhoto(reqParams);
  }
}

Presentation (Holder)

  • Organizes data and holds View state.
  • Talks to use cases.

🚀🚀🚀 Case study 1 🚀🚀🚀

class PhotoViewAdapter {
  /// Input
  final Input input;

  /// Output
  final Output output;

  factory PhotoViewAdapter(final PhotoViewModel photoViewModel) {
    final loadMoreS = BehaviorSubject<void>();
    final refreshS = BehaviorSubject<void>();
    final textChangesS = BehaviorSubject<String>();

    var input = Input(
      search: textChangesS,
      onLoadMore: loadMoreS,
      onRefresh: refreshS,
    );
    var output = photoViewModel.transform(input);

    return PhotoViewAdapter._(
      input: input,
      output: output,
    );
  }

  PhotoViewAdapter._({
    required this.input,
    required this.output,
  });
}
class Input {
  final BehaviorSubject<String> search;
  final BehaviorSubject<void> onLoadMore;
  final BehaviorSubject<void> onRefresh;

  Input({
    required this.search,
    required this.onLoadMore,
    required this.onRefresh,
  });
}

class Output {
  final Stream<PhotoState?> results$;
  final Function0<void> dispose;

  Output({
    required this.results$,
    required this.dispose,
  });
}

class PhotoViewModel extends BaseViewModel<Input, Output> {
  final GetPhotoUseCase _getPhoto;

  PhotoViewModel(this._getPhoto);

  @override
  Output transform(Input input) {
    final currentPage = BehaviorSubject<int>.seeded(1);
    final List<Hits> appendPhotos = [];

    final loadMore$ = input.onLoadMore.doOnData((event) {
      var nextPage = currentPage.value + 1;
      currentPage.add(nextPage);
    }).withLatestFrom(input.search, (_, s) => input.search.value);

    final refresh$ = input.onRefresh.doOnData((event) {
      currentPage.add(1);
    }).withLatestFrom(input.search, (_, s) => input.search.value);

    final search$ = input.search.doOnData((event) {
      currentPage.add(1);
    });

    final results$ = Rx.merge([refresh$, search$, loadMore$])
        .debounceTime(const Duration(milliseconds: 350))
        .switchMap((String keyword) {
      if (keyword.isEmpty) {
        return Stream.value(null);
      } else {
        if (currentPage.value == 1) {
          const PhotoLoading();
        }
        return Stream.fromFuture(_getPhoto
                .execute(RequestPhoto(query: keyword, page: currentPage.value)))
            .exhaustMap((either) => either.fold((error) {
                  return Stream<PhotoState?>.value(
                      PhotoError(error.message.toString()));
                }, (data) {
                  FocusManager.instance.primaryFocus?.unfocus();
                  if (currentPage.value == 1) {
                    appendPhotos.clear();
                  }
                  appendPhotos.addAll(data.hits as List<Hits>);
                  return Stream<PhotoState?>.value(PhotoLoaded(
                      data: appendPhotos,
                      currentPage: currentPage.value,
                      hasReachedMax: appendPhotos.length < data.totalHits));
                }))
            .debug()
            .onErrorReturnWith(
                (error, _) => const PhotoError("Đã có lỗi xảy ra"));
      }
    });

    return Output(
      results$: results$,
      dispose: () {
        input.search.close();
        input.onLoadMore.close();
        input.onRefresh.close();
        currentPage.close();
      },
    );
  }
}

🚀🚀🚀 Case study 2 🚀🚀🚀

class PhotoBloc {
  /// Input
  final Sink<String?> search;
  final Sink<void> onLoadMore;
  final Function0<void> dispose;
  final Sink<void> onRefresh;

  /// Output
  final Stream<PhotoState?> results$;

  factory PhotoBloc(final GetPhotoUseCase getPhoto) {
    final currentPage = BehaviorSubject<int>.seeded(1);
    final loadMoreS = BehaviorSubject<void>();
    final refreshS = BehaviorSubject<void>();
    final textChangesS = BehaviorSubject<String>();
    final List<Hits> appendPhotos = [];

    final loadMore$ = loadMoreS.doOnData((event) {
      var nextPage = currentPage.value + 1;
      currentPage.add(nextPage);
    }).withLatestFrom(textChangesS, (_, s) => textChangesS.value);

    final refresh$ = refreshS.doOnData((event) {
      currentPage.add(1);
    }).withLatestFrom(textChangesS, (_, s) => textChangesS.value);

    final search$ = textChangesS.doOnData((event) {
      currentPage.add(1);
    });

    final results$ = Rx.merge([refresh$, search$, loadMore$])
            .debounceTime(const Duration(milliseconds: 350))
            .switchMap((String keyword) {
      if (keyword.isEmpty) {
        return Stream.value(null);
      } else {
        if (currentPage.value == 1) {
          const PhotoLoading();
        }
        return Stream.fromFuture(getPhoto
                .execute(RequestPhoto(query: keyword, page: currentPage.value)))
                .flatMap((either) => either.fold((error) {
          return Stream<PhotoState?>.value(
                  PhotoError(error.message.toString()));
        }, (data) {
          FocusManager.instance.primaryFocus?.unfocus();
          if (currentPage.value == 1) {
            appendPhotos.clear();
          }
          appendPhotos.addAll(data.hits as List<Hits>);
          return Stream<PhotoState?>.value(PhotoLoaded(
                  data: appendPhotos,
                  currentPage: currentPage.value,
                  hasReachedMax: appendPhotos.length < data.totalHits));
                }))
                .onErrorReturnWith(
                        (error, _) => const PhotoError("Đã có lỗi xảy ra"));
      }
    });
    return PhotoBloc._(
      search: textChangesS.sink,
      onLoadMore: loadMoreS,
      onRefresh: refreshS,
      results$: results$,
      dispose: () {
        textChangesS.close();
        currentPage.close();
        loadMoreS.close();
        refreshS.close();
      },
    );
  }

  PhotoBloc._({
    required this.search,
    required this.onRefresh,
    required this.onLoadMore,
    required this.results$,
    required this.dispose,
  });
}

Presentation (View)

  • View,updates UI

🚀 Screenshoots

Default Search Search keyword (ex: flo)

About

Sample Flutter apply Clean Architecture - Pure Rxdart BLoC pattern, SOLID, Get_it (DI), Dio, Dartz, RxDart 🚀🚀🚀 Functionalities: Search Keyword, Loadmore, Pull to Refresh

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published