Skip to content

Commit

Permalink
Merge pull request #262 from nimblehq/feature/258-update-clean-archit…
Browse files Browse the repository at this point in the history
…ecture
  • Loading branch information
luongvo authored Aug 28, 2023
2 parents 17471b3 + 4832e58 commit 6b4cd23
Show file tree
Hide file tree
Showing 46 changed files with 264 additions and 250 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:{{project_name.snakeCase()}}/main.dart';
import 'package:{{project_name.snakeCase()}}/app/screens/home/home_screen.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'utils/test_util.dart';
import '../utils/test_util.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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()}}/main.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:{{project_name.snakeCase()}}/main.dart';

class TestUtil {
/// This is useful when we test the whole app with the real configs(styling,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:{{project_name.snakeCase()}}/app/screens/home/home_view_model.dart';
import 'package:{{project_name.snakeCase()}}/app/screens/home/home_view_state.dart';
import 'package:{{project_name.snakeCase()}}/domain/usecases/get_users_use_case.dart';
import 'package:{{project_name.snakeCase()}}/main.dart';
import 'package:{{project_name.snakeCase()}}/di/di.dart';
import 'package:{{project_name.snakeCase()}}/gen/assets.gen.dart';
import 'package:{{project_name.snakeCase()}}/app/resources/app_colors.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter_config/flutter_config.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';

final homeViewModelProvider =
StateNotifierProvider.autoDispose<HomeViewModel, HomeViewState>((ref) {
return HomeViewModel(
getIt.get<GetUsersUseCase>(),
);
});

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

@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}

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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
return snapshot.hasData
? Text(snapshot.data?.appName ?? "")
: const SizedBox.shrink();
}),
),
body: Center(
child: Column(
children: [
const SizedBox(height: 24),
FractionallySizedBox(
widthFactor: 0.5,
child: Image.asset(
Assets.images.nimbleLogo.path,
fit: BoxFit.fitWidth,
),
),
const SizedBox(height: 8),
Assets.svg.flutterLogo.svg(
width: 32,
height: 32,
),
const SizedBox(height: 24),
Text(AppLocalizations.of(context)!.hello),
Text(
FlutterConfig.get('SECRET'),
style: const TextStyle(
color: AppColors.nimblePrimaryBlue,
fontSize: 24,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/$routePathSecondScreen'),
child: const Text("Navigate to Second Screen"),
),
],
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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:{{project_name.snakeCase()}}/app/screens/home/home_view_state.dart';
import 'package:{{project_name.snakeCase()}}/domain/usecases/base/base_use_case.dart';
import 'package:{{project_name.snakeCase()}}/domain/usecases/get_users_use_case.dart';
import 'package:{{project_name.snakeCase()}}/domain/models/user.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class HomeViewModel extends StateNotifier<HomeViewState> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:{{project_name.snakeCase()}}/api/response/user_response.dart';
import 'package:{{project_name.snakeCase()}}/data/remote/models/responses/user_response.dart';
import 'package:retrofit/retrofit.dart';

part 'api_service.g.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:{{project_name.snakeCase()}}/domain/models/user.dart';

part 'user_response.g.dart';

Expand All @@ -13,4 +14,9 @@ class UserResponse {
_$UserResponseFromJson(json);

Map<String, dynamic> toJson() => _$UserResponseToJson(this);

User toUser() => User(
email: email,
username: username,
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
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/user.dart';
import 'package:{{project_name.snakeCase()}}/data/remote/datasources/api_service.dart';
import 'package:{{project_name.snakeCase()}}/domain/exceptions/network_exceptions.dart';
import 'package:{{project_name.snakeCase()}}/domain/models/user.dart';
import 'package:{{project_name.snakeCase()}}/domain/repositories/credential_repository.dart';
import 'package:injectable/injectable.dart';

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

@LazySingleton(as: CredentialRepository)
class CredentialRepositoryImpl extends CredentialRepository {
final BaseApiService _apiService;
Expand All @@ -18,7 +15,7 @@ class CredentialRepositoryImpl extends CredentialRepository {
try {
final userResponses = await _apiService.getUsers();
return userResponses
.map((userResponse) => User.fromUserResponse(userResponse))
.map((userResponse) => userResponse.toUser())
.toList();
} catch (exception) {
throw NetworkExceptions.fromDioException(exception);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:{{project_name.snakeCase()}}/api/api_service.dart';
import 'package:{{project_name.snakeCase()}}/data/remote/datasources/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';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:sample/api/response/user_response.dart';

class User extends Equatable {
final String email;
Expand All @@ -10,13 +9,6 @@ class User extends Equatable {
required this.username,
});

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

@override
bool? get stringify => true;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:{{project_name.snakeCase()}}/domain/models/user.dart';

abstract class CredentialRepository {
Future<List<User>> getUsers();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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:{{project_name.snakeCase()}}/domain/exceptions/network_exceptions.dart';
import 'package:{{project_name.snakeCase()}}/domain/repositories/credential_repository.dart';
import 'package:{{project_name.snakeCase()}}/domain/usecases/base/base_use_case.dart';
import 'package:{{project_name.snakeCase()}}/domain/models/user.dart';
import 'package:injectable/injectable.dart';

@Injectable()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_config/flutter_config.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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:flutter_config/flutter_config.dart';
import 'package:{{project_name.snakeCase()}}/di/di.dart';
import 'package:{{project_name.snakeCase()}}/gen/assets.gen.dart';
import 'package:{{project_name.snakeCase()}}/resources/app_colors.dart';
import 'package:{{project_name.snakeCase()}}/usecases/user/get_users_use_case.dart';

import 'home_view_model.dart';
import 'home_view_state.dart';
import 'package:{{project_name.snakeCase()}}/app/screens/home/home_screen.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -63,76 +58,6 @@ class MyApp extends StatelessWidget {
}
}

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) {
return Scaffold(
appBar: AppBar(
title: FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
return snapshot.hasData
? Text(snapshot.data?.appName ?? "")
: const SizedBox.shrink();
}),
),
body: Center(
child: Column(
children: [
const SizedBox(height: 24),
FractionallySizedBox(
widthFactor: 0.5,
child: Image.asset(
Assets.images.nimbleLogo.path,
fit: BoxFit.fitWidth,
),
),
const SizedBox(height: 8),
Assets.svg.flutterLogo.svg(
width: 32,
height: 32,
),
const SizedBox(height: 24),
Text(AppLocalizations.of(context)!.hello),
Text(
FlutterConfig.get('SECRET'),
style: const TextStyle(
color: AppColors.nimblePrimaryBlue,
fontSize: 24,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/$routePathSecondScreen'),
child: const Text("Navigate to Second Screen"),
),
],
),
),
);
}
}

class SecondScreen extends StatelessWidget {
const SecondScreen({
Key? key,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:{{project_name.snakeCase()}}/usecases/base/base_use_case.dart';
import 'package:{{project_name.snakeCase()}}/home_view_model.dart';
import 'package:{{project_name.snakeCase()}}/main.dart';
import 'package:{{project_name.snakeCase()}}/model/user.dart';
import 'package:{{project_name.snakeCase()}}/app/screens/home/home_view_model.dart';
import 'package:{{project_name.snakeCase()}}/domain/usecases/base/base_use_case.dart';
import 'package:{{project_name.snakeCase()}}/app/screens/home/home_screen.dart';

import '../mocks/generate_mocks.mocks.dart';
import '../mocks/response/user_response_mocks.dart';
import '../../../mocks/generate_mocks.mocks.dart';
import '../../../mocks/data/remote/models/responses/user_response_mocks.dart';

void main() {
group("HomeViewModelTest", () {
Expand All @@ -30,7 +29,7 @@ void main() {

test('When calling get user list successfully, it returns correctly',
() async {
final expectedResult = [User.fromUserResponse(UserResponseMocks.mock())];
final expectedResult = [UserResponseMocks.mock().toUser()];
when(mockGetUsersUseCase.call())
.thenAnswer((_) async => Success(expectedResult));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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()}}/api/response/user_response.dart';
import 'package:{{project_name.snakeCase()}}/domain/exceptions/network_exceptions.dart';
import 'package:{{project_name.snakeCase()}}/domain/repositories/credential_repository.dart';
import 'package:{{project_name.snakeCase()}}/data/repositories/credential_repository_impl.dart';
import 'package:{{project_name.snakeCase()}}/data/remote/models/responses/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,11 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.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:{{project_name.snakeCase()}}/domain/usecases/base/base_use_case.dart';
import 'package:{{project_name.snakeCase()}}/domain/usecases/get_users_use_case.dart';

import '../../mocks/generate_mocks.mocks.dart';
import '../../mocks/response/user_response_mocks.dart';
import '../../mocks/data/remote/models/responses/user_response_mocks.dart';

void main() {
group('GetUsersUseCase', () {
Expand All @@ -19,7 +18,7 @@ void main() {

test('When getting users successfully, it returns Success result',
() async {
final expectedResult = [User.fromUserResponse(UserResponseMocks.mock())];
final expectedResult = [UserResponseMocks.mock().toUser()];
when(mockRepository.getUsers()).thenAnswer((_) async => expectedResult);
final result = await getUsersUseCase.call();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:{{project_name.snakeCase()}}/api/response/user_response.dart';
import 'package:{{project_name.snakeCase()}}/data/remote/models/responses/user_response.dart';

class UserResponseMocks {
static UserResponse mock() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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:{{project_name.snakeCase()}}/data/remote/datasources/api_service.dart';
import 'package:{{project_name.snakeCase()}}/domain/repositories/credential_repository.dart';
import 'package:{{project_name.snakeCase()}}/domain/usecases/get_users_use_case.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:sample/main.dart';
import 'package:sample/app/screens/home/home_screen.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'utils/test_util.dart';
import '../utils/test_util.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand Down
Loading

0 comments on commit 6b4cd23

Please sign in to comment.