Skip to content

Commit

Permalink
Merge pull request #244 from nimblehq/feature/223-update-di-config-2
Browse files Browse the repository at this point in the history
  • Loading branch information
luongvo authored Jul 27, 2023
2 parents ab5559c + 8a4a0f3 commit 2d3b116
Show file tree
Hide file tree
Showing 19 changed files with 297 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ All the templates can be used to kick off a new Flutter project quickly.
- Supports __Android__ and __iOS__ platforms *(Web and Desktop are not yet supported)*.
- [__Clean Architecture__](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) with `MVVM` and pre-built foundational components.
- [Pre-set environments](bricks/template/__brick__/%7B%7Bproject_name.snakeCase()%7D%7D#setup): `Staging` and `Production`. Environment variables are supplied through `.env` files through [flutter_config](https://pub.dev/packages/flutter_config).
- Dependency Injection (DI), State Management, and Navigating with [get_it](https://pub.dev/packages/get_it) and [go_router](https://pub.dev/packages/go_router).
- Dependency Injection (DI), State Management, and Navigating with [get_it](https://pub.dev/packages/get_it), [flutter_riverpod](https://pub.dev/packages/flutter_riverpod), and [go_router](https://pub.dev/packages/go_router).
- Networking with [dio](https://pub.dev/packages/dio) and [retrofit](https://pub.dev/packages/retrofit), JSON serializing with [json_serializable](https://pub.dev/packages/json_serializable).
- [Localization](https://docs.flutter.dev/accessibility-and-localization/internationalization) integrated in [3 initial languages](bricks/template/__brick__/%7B%7Bproject_name.snakeCase()%7D%7D/lib/l10n).
- [Testing](https://docs.flutter.dev/testing)-ready (unit, integration, and widget testing), [production and deployment](https://docs.flutter.dev/deployment)-ready (to Firebase, Play Store, TestFlight, and AppStore).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import 'package:dio/dio.dart';
import 'package:{{project_name.snakeCase()}}/model/response/user_response.dart';
import 'package:{{project_name.snakeCase()}}/api/response/user_response.dart';
import 'package:retrofit/retrofit.dart';

part 'api_service.g.dart';

abstract class BaseApiService {
Future<List<UserResponse>> getUsers();
}

@RestApi()
abstract class ApiService {
abstract class ApiService extends BaseApiService {
factory ApiService(Dio dio, {String baseUrl}) = _ApiService;

// TODO add API endpoint
@override
@GET('users')
Future<List<UserResponse>> getUsers();
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import 'package:{{project_name.snakeCase()}}/api/api_service.dart';
import 'package:{{project_name.snakeCase()}}/api/exception/network_exceptions.dart';
import 'package:{{project_name.snakeCase()}}/model/response/user_response.dart';
import 'package:{{project_name.snakeCase()}}/model/user.dart';
import 'package:injectable/injectable.dart';

abstract class CredentialRepository {
Future<List<UserResponse>> getUsers();
Future<List<User>> getUsers();
}

@LazySingleton(as: CredentialRepository)
class CredentialRepositoryImpl extends CredentialRepository {
final ApiService _apiService;
final BaseApiService _apiService;

CredentialRepositoryImpl(this._apiService);

@override
Future<List<UserResponse>> getUsers() async {
Future<List<User>> getUsers() async {
try {
return await _apiService.getUsers();
final userResponses = await _apiService.getUsers();
return userResponses
.map((userResponse) => User.fromUserResponse(userResponse))
.toList();
} catch (exception) {
throw NetworkExceptions.fromDioException(exception);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';

import 'di.config.dart';

final GetIt getIt = GetIt.instance;

@injectableInit
Future<void> configureInjection() async => getIt.init();
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:{{project_name.snakeCase()}}/api/api_service.dart';
import 'package:{{project_name.snakeCase()}}/env.dart';
import 'package:{{project_name.snakeCase()}}/di/provider/dio_provider.dart';
import 'package:injectable/injectable.dart';

@module
abstract class NetworkModule {
@Singleton(as: BaseApiService)
ApiService provideApiService(DioProvider dioProvider) {
return ApiService(
dioProvider.getDio(),
baseUrl: Env.restApiEndpoint,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:{{project_name.snakeCase()}}/di/interceptor/app_interceptor.dart';
import 'package:injectable/injectable.dart';

const String headerContentType = 'Content-Type';
const String defaultContentType = 'application/json; charset=utf-8';

@Singleton()
class DioProvider {
Dio? _dio;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:async';
import 'package:{{project_name.snakeCase()}}/home_view_state.dart';
import 'package:{{project_name.snakeCase()}}/usecases/base/base_use_case.dart';
import 'package:{{project_name.snakeCase()}}/usecases/user/get_users_use_case.dart';
import 'package:{{project_name.snakeCase()}}/model/user.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class HomeViewModel extends StateNotifier<HomeViewState> {
final GetUsersUseCase _getUsersUseCase;

HomeViewModel(
this._getUsersUseCase,
) : super(const HomeViewState.init());

final StreamController<List<User>> _usersStream = StreamController();

Stream<List<User>> get usersStream => _usersStream.stream;

Future<void> getUsers() async {
final result = await _getUsersUseCase.call();
if (result is Success<List<User>>) {
_usersStream.add(result.value);
} else {
// TODO handle error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'home_view_state.freezed.dart';

@freezed
class HomeViewState with _$HomeViewState {
const factory HomeViewState.init() = _Init;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_config/flutter_config.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:{{project_name.snakeCase()}}/gen/assets.gen.dart';
import 'package:{{project_name.snakeCase()}}/gen/colors.gen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:{{project_name.snakeCase()}}/di/di.dart';
import 'package:{{project_name.snakeCase()}}/gen/assets.gen.dart';
import 'package:{{project_name.snakeCase()}}/gen/colors.gen.dart';
import 'package:{{project_name.snakeCase()}}/usecases/user/get_users_use_case.dart';

import 'home_view_model.dart';
import 'home_view_state.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterConfig.loadEnvVariables();
runApp(MyApp());
await configureInjection();
runApp(
ProviderScope(
child: MyApp(),
),
);
}

const routePathRootScreen = '/';
Expand Down Expand Up @@ -52,8 +63,26 @@ class MyApp extends StatelessWidget {
}
}

class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
final homeViewModelProvider =
StateNotifierProvider.autoDispose<HomeViewModel, HomeViewState>((ref) {
return HomeViewModel(
getIt.get<GetUsersUseCase>(),
);
});

class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});

@override
HomeScreenState createState() => HomeScreenState();
}

class HomeScreenState extends ConsumerState<HomeScreen> {
@override
void initState() {
super.initState();
ref.read(homeViewModelProvider.notifier).getUsers();
}

@override
Widget build(BuildContext context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:equatable/equatable.dart';
import 'package:{{project_name.snakeCase()}}/api/response/user_response.dart';

class User extends Equatable {
final String email;
final String username;

const User({
required this.email,
required this.username,
});

factory User.fromUserResponse(UserResponse response) {
return User(
email: response.email,
username: response.username,
);
}

@override
bool? get stringify => true;

@override
List<Object?> get props => [
email,
username,
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:{{project_name.snakeCase()}}/api/exception/network_exceptions.dart';
import 'package:{{project_name.snakeCase()}}/api/repository/credential_repository.dart';
import 'package:{{project_name.snakeCase()}}/usecases/base/base_use_case.dart';
import 'package:{{project_name.snakeCase()}}/model/user.dart';
import 'package:injectable/injectable.dart';

@Injectable()
class GetUsersUseCase extends NoParamsUseCase<List<User>> {
final CredentialRepository _credentialRepository;

GetUsersUseCase(this._credentialRepository);

@override
Future<Result<List<User>>> call() async {
return _credentialRepository
.getUsers()
.then((value) =>
Success(value) as Result<List<User>>) // ignore: unnecessary_cast
.onError<NetworkExceptions>(
(err, stackTrace) => Failed(UseCaseException(err)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4
url: "https://pub.dev"
source: hosted
version: "2.3.6"
flutter_test:
dependency: "direct dev"
description: flutter
Expand Down Expand Up @@ -319,6 +327,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468"
url: "https://pub.dev"
source: hosted
version: "7.6.0"
glob:
dependency: transitive
description:
Expand Down Expand Up @@ -367,6 +383,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
injectable:
dependency: "direct main"
description:
name: injectable
sha256: fbe4b673f8c344e03eb4ab50fa853405872f401bbf6a7ba84b2b7447ac81ed1b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
integration_test:
dependency: "direct dev"
description: flutter
Expand Down Expand Up @@ -580,6 +604,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef"
url: "https://pub.dev"
source: hosted
version: "2.3.6"
shelf:
dependency: transitive
description:
Expand Down Expand Up @@ -633,6 +665,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.0"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289"
url: "https://pub.dev"
source: hosted
version: "0.7.2+1"
stream_channel:
dependency: transitive
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ environment:
# versions available, run `flutter pub outdated`.
dependencies:
dio: ^5.1.2
equatable: ^2.0.5
flutter:
sdk: flutter
flutter_config: ^2.0.2
flutter_localizations:
sdk: flutter
flutter_riverpod: ^2.3.6
flutter_svg: ^2.0.7
freezed_annotation: ^2.2.0
get_it: ^7.6.0
go_router: ^9.0.3
injectable: ^2.1.2
intl: ^0.18.0
json_annotation: ^4.8.1
package_info_plus: ^4.0.0{{#add_permission_handler}}{{{ _pubspec_dependencyyaml }}}{{/add_permission_handler}}
Expand All @@ -49,6 +53,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
freezed: ^2.3.4
injectable_generator: ^2.1.6
integration_test:
sdk: flutter
json_serializable: ^6.6.2
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:{{project_name.snakeCase()}}/api/exception/network_exceptions.dart';
import 'package:{{project_name.snakeCase()}}/api/repository/credential_repository.dart';
import 'package:{{project_name.snakeCase()}}/model/response/user_response.dart';
import 'package:{{project_name.snakeCase()}}/api/response/user_response.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import 'package:dio/dio.dart';
import 'package:{{project_name.snakeCase()}}/api/api_service.dart';
import 'package:{{project_name.snakeCase()}}/api/repository/credential_repository.dart';
import 'package:{{project_name.snakeCase()}}/usecases/user/get_users_use_case.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([
ApiService,
CredentialRepository,
DioError,
GetUsersUseCase,
])
main() {
// empty class to generate mock repository classes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:{{project_name.snakeCase()}}/api/response/user_response.dart';

class UserResponseMocks {
static UserResponse mock() {
return UserResponse(
"email",
"username",
);
}
}
Loading

0 comments on commit 2d3b116

Please sign in to comment.